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

22
Cargo.lock generated
View file

@ -1068,6 +1068,7 @@ dependencies = [
"tracing-journald", "tracing-journald",
"tracing-opentelemetry", "tracing-opentelemetry",
"tracing-subscriber", "tracing-subscriber",
"trc",
"unicode-security", "unicode-security",
"utils", "utils",
"whatlang", "whatlang",
@ -1655,7 +1656,7 @@ dependencies = [
"tokio", "tokio",
"tokio-rustls 0.26.0", "tokio-rustls 0.26.0",
"totp-rs", "totp-rs",
"tracing", "trc",
"utils", "utils",
] ]
@ -2959,6 +2960,7 @@ dependencies = [
"tokio", "tokio",
"tokio-rustls 0.26.0", "tokio-rustls 0.26.0",
"tracing", "tracing",
"trc",
"utils", "utils",
] ]
@ -3191,6 +3193,7 @@ dependencies = [
"tokio", "tokio",
"tokio-tungstenite 0.23.1", "tokio-tungstenite 0.23.1",
"tracing", "tracing",
"trc",
"tungstenite 0.23.0", "tungstenite 0.23.0",
"utils", "utils",
"x509-parser 0.16.0", "x509-parser 0.16.0",
@ -3230,6 +3233,7 @@ dependencies = [
"store", "store",
"tokio", "tokio",
"tracing", "tracing",
"trc",
"utils", "utils",
] ]
@ -3591,6 +3595,7 @@ dependencies = [
"store", "store",
"tokio", "tokio",
"tracing", "tracing",
"trc",
"utils", "utils",
] ]
@ -4468,6 +4473,7 @@ dependencies = [
"tokio", "tokio",
"tokio-rustls 0.26.0", "tokio-rustls 0.26.0",
"tracing", "tracing",
"trc",
"utils", "utils",
] ]
@ -6038,6 +6044,7 @@ dependencies = [
"tokio", "tokio",
"tokio-rustls 0.26.0", "tokio-rustls 0.26.0",
"tracing", "tracing",
"trc",
"utils", "utils",
"webpki-roots 0.26.3", "webpki-roots 0.26.3",
"x509-parser 0.16.0", "x509-parser 0.16.0",
@ -6186,7 +6193,7 @@ dependencies = [
"tokio", "tokio",
"tokio-postgres", "tokio-postgres",
"tokio-rustls 0.26.0", "tokio-rustls 0.26.0",
"tracing", "trc",
"utils", "utils",
"xxhash-rust", "xxhash-rust",
] ]
@ -6859,6 +6866,16 @@ dependencies = [
"tracing-log", "tracing-log",
] ]
[[package]]
name = "trc"
version = "0.8.5"
dependencies = [
"base64 0.22.1",
"bincode",
"reqwest 0.12.5",
"serde_json",
]
[[package]] [[package]]
name = "trim-in-place" name = "trim-in-place"
version = "0.1.7" version = "0.1.7"
@ -7117,6 +7134,7 @@ dependencies = [
"tokio-rustls 0.26.0", "tokio-rustls 0.26.0",
"tracing", "tracing",
"tracing-journald", "tracing-journald",
"trc",
"webpki-roots 0.26.3", "webpki-roots 0.26.3",
"x509-parser 0.16.0", "x509-parser 0.16.0",
] ]

View file

@ -14,6 +14,7 @@ members = [
"crates/directory", "crates/directory",
"crates/utils", "crates/utils",
"crates/common", "crates/common",
"crates/trc",
"crates/cli", "crates/cli",
"tests", "tests",
] ]

View file

@ -8,6 +8,7 @@ resolver = "2"
utils = { path = "../utils" } utils = { path = "../utils" }
nlp = { path = "../nlp" } nlp = { path = "../nlp" }
store = { path = "../store" } store = { path = "../store" }
trc = { path = "../trc" }
directory = { path = "../directory" } directory = { path = "../directory" }
jmap_proto = { path = "../jmap-proto" } jmap_proto = { path = "../jmap-proto" }
sieve-rs = { version = "0.5" } sieve-rs = { version = "0.5" }

View file

@ -18,11 +18,7 @@ use crate::{
}; };
impl Core { impl Core {
pub async fn email_to_ids( pub async fn email_to_ids(&self, directory: &Directory, email: &str) -> trc::Result<Vec<u32>> {
&self,
directory: &Directory,
email: &str,
) -> directory::Result<Vec<u32>> {
let mut address = self let mut address = self
.smtp .smtp
.session .session
@ -53,7 +49,7 @@ impl Core {
Ok(vec![]) Ok(vec![])
} }
pub async fn rcpt(&self, directory: &Directory, email: &str) -> directory::Result<bool> { pub async fn rcpt(&self, directory: &Directory, email: &str) -> trc::Result<bool> {
// Expand subaddress // Expand subaddress
let mut address = self let mut address = self
.smtp .smtp
@ -83,11 +79,7 @@ impl Core {
Ok(false) Ok(false)
} }
pub async fn vrfy( pub async fn vrfy(&self, directory: &Directory, address: &str) -> trc::Result<Vec<String>> {
&self,
directory: &Directory,
address: &str,
) -> directory::Result<Vec<String>> {
directory directory
.vrfy( .vrfy(
self.smtp self.smtp
@ -101,11 +93,7 @@ impl Core {
.await .await
} }
pub async fn expn( pub async fn expn(&self, directory: &Directory, address: &str) -> trc::Result<Vec<String>> {
&self,
directory: &Directory,
address: &str,
) -> directory::Result<Vec<String>> {
directory directory
.expn( .expn(
self.smtp self.smtp

View file

@ -442,7 +442,7 @@ impl JmapConfig {
} }
impl ParseValue for SpecialUse { impl ParseValue for SpecialUse {
fn parse_value(value: &str) -> utils::config::Result<Self> { fn parse_value(value: &str) -> Result<Self, String> {
match value { match value {
"inbox" => Ok(SpecialUse::Inbox), "inbox" => Ok(SpecialUse::Inbox),
"trash" => Ok(SpecialUse::Trash), "trash" => Ok(SpecialUse::Trash),

View file

@ -304,7 +304,7 @@ impl Servers {
} }
impl ParseValue for ServerProtocol { impl ParseValue for ServerProtocol {
fn parse_value(value: &str) -> utils::config::Result<Self> { fn parse_value(value: &str) -> Result<Self, String> {
if value.eq_ignore_ascii_case("smtp") { if value.eq_ignore_ascii_case("smtp") {
Ok(Self::Smtp) Ok(Self::Smtp)
} else if value.eq_ignore_ascii_case("lmtp") { } else if value.eq_ignore_ascii_case("lmtp") {

View file

@ -157,7 +157,7 @@ impl TlsManager {
acme_providers.insert(acme_id.to_string(), acme_provider); acme_providers.insert(acme_id.to_string(), acme_provider);
} }
Err(err) => { Err(err) => {
config.new_build_error(format!("acme.{acme_id}"), err); config.new_build_error(format!("acme.{acme_id}"), err.to_string());
} }
} }
} }
@ -359,10 +359,7 @@ pub(crate) fn parse_certificates(
} }
} }
pub(crate) fn build_certified_key( pub(crate) fn build_certified_key(cert: Vec<u8>, pk: Vec<u8>) -> Result<CertifiedKey, String> {
cert: Vec<u8>,
pk: Vec<u8>,
) -> utils::config::Result<CertifiedKey> {
let cert = certs(&mut Cursor::new(cert)) let cert = certs(&mut Cursor::new(cert))
.collect::<Result<Vec<_>, _>>() .collect::<Result<Vec<_>, _>>()
.map_err(|err| format!("Failed to read certificates: {err}"))?; .map_err(|err| format!("Failed to read certificates: {err}"))?;
@ -391,7 +388,7 @@ pub(crate) fn build_certified_key(
pub(crate) fn build_self_signed_cert( pub(crate) fn build_self_signed_cert(
domains: impl Into<Vec<String>>, domains: impl Into<Vec<String>>,
) -> utils::config::Result<CertifiedKey> { ) -> Result<CertifiedKey, String> {
let cert = generate_simple_self_signed(domains) let cert = generate_simple_self_signed(domains)
.map_err(|err| format!("Failed to generate self-signed certificate: {err}",))?; .map_err(|err| format!("Failed to generate self-signed certificate: {err}",))?;
build_certified_key( build_certified_key(

View file

@ -456,7 +456,7 @@ impl ConstantValue for VerifyStrategy {
} }
impl ParseValue for DkimCanonicalization { impl ParseValue for DkimCanonicalization {
fn parse_value(value: &str) -> utils::config::Result<Self> { fn parse_value(value: &str) -> Result<Self, String> {
if let Some((headers, body)) = value.split_once('/') { if let Some((headers, body)) = value.split_once('/') {
Ok(DkimCanonicalization { Ok(DkimCanonicalization {
headers: Canonicalization::parse_value(headers.trim())?, headers: Canonicalization::parse_value(headers.trim())?,

View file

@ -534,7 +534,7 @@ fn parse_queue_quota_item(config: &mut Config, prefix: impl AsKey) -> Option<Que
} }
impl ParseValue for RequireOptional { impl ParseValue for RequireOptional {
fn parse_value(value: &str) -> utils::config::Result<Self> { fn parse_value(value: &str) -> Result<Self, String> {
match value { match value {
"optional" => Ok(RequireOptional::Optional), "optional" => Ok(RequireOptional::Optional),
"require" | "required" => Ok(RequireOptional::Require), "require" | "required" => Ok(RequireOptional::Require),

View file

@ -214,7 +214,7 @@ impl Default for ReportConfig {
} }
impl ParseValue for AggregateFrequency { impl ParseValue for AggregateFrequency {
fn parse_value(value: &str) -> utils::config::Result<Self> { fn parse_value(value: &str) -> Result<Self, String> {
match value { match value {
"daily" | "day" => Ok(AggregateFrequency::Daily), "daily" | "day" => Ok(AggregateFrequency::Daily),
"hourly" | "hour" => Ok(AggregateFrequency::Hourly), "hourly" | "hour" => Ok(AggregateFrequency::Hourly),
@ -267,7 +267,7 @@ impl ConstantValue for AggregateFrequency {
} }
impl ParseValue for AddressMatch { impl ParseValue for AddressMatch {
fn parse_value(value: &str) -> utils::config::Result<Self> { fn parse_value(value: &str) -> Result<Self, String> {
if let Some(value) = value.strip_prefix('*').map(|v| v.trim()) { if let Some(value) = value.strip_prefix('*').map(|v| v.trim()) {
if !value.is_empty() { if !value.is_empty() {
return Ok(AddressMatch::EndsWith(value.to_lowercase())); return Ok(AddressMatch::EndsWith(value.to_lowercase()));

View file

@ -325,7 +325,7 @@ impl Core {
} }
impl ParseValue for Mode { impl ParseValue for Mode {
fn parse_value(value: &str) -> utils::config::Result<Self> { fn parse_value(value: &str) -> Result<Self, String> {
match value { match value {
"enforce" => Ok(Self::Enforce), "enforce" => Ok(Self::Enforce),
"testing" | "test" => Ok(Self::Testing), "testing" | "test" => Ok(Self::Testing),

View file

@ -875,7 +875,7 @@ impl Default for SessionConfig {
pub struct Mechanism(u64); pub struct Mechanism(u64);
impl ParseValue for Mechanism { impl ParseValue for Mechanism {
fn parse_value(value: &str) -> utils::config::Result<Self> { fn parse_value(value: &str) -> Result<Self, String> {
Ok(Mechanism(match value.to_ascii_uppercase().as_str() { Ok(Mechanism(match value.to_ascii_uppercase().as_str() {
"LOGIN" => AUTH_LOGIN, "LOGIN" => AUTH_LOGIN,
"PLAIN" => AUTH_PLAIN, "PLAIN" => AUTH_PLAIN,

View file

@ -104,7 +104,7 @@ fn parse_throttle_item(
} }
} }
pub(crate) fn parse_throttle_key(value: &str) -> utils::config::Result<u16> { pub(crate) fn parse_throttle_key(value: &str) -> Result<u16, String> {
match value { match value {
"rcpt" => Ok(THROTTLE_RCPT), "rcpt" => Ok(THROTTLE_RCPT),
"rcpt_domain" => Ok(THROTTLE_RCPT_DOMAIN), "rcpt_domain" => Ok(THROTTLE_RCPT_DOMAIN),

View file

@ -16,6 +16,7 @@ use store::{
}, },
IterateParams, ValueKey, U32_LEN, U64_LEN, IterateParams, ValueKey, U32_LEN, U64_LEN,
}; };
use trc::AddContext;
use utils::{BlobHash, BLOB_HASH_LEN}; use utils::{BlobHash, BLOB_HASH_LEN};
use crate::Core; use crate::Core;
@ -59,7 +60,7 @@ impl Core {
pub async fn list_deleted( pub async fn list_deleted(
&self, &self,
account_id: u32, account_id: u32,
) -> store::Result<Vec<DeletedBlob<BlobHash, u64, u8>>> { ) -> trc::Result<Vec<DeletedBlob<BlobHash, u64, u8>>> {
let from_key = ValueKey { let from_key = ValueKey {
account_id, account_id,
collection: 0, collection: 0,
@ -92,9 +93,7 @@ impl Core {
results.push(DeletedBlob { results.push(DeletedBlob {
hash: BlobHash::try_from_hash_slice( hash: BlobHash::try_from_hash_slice(
key.get(U32_LEN..U32_LEN + BLOB_HASH_LEN).ok_or_else(|| { key.get(U32_LEN..U32_LEN + BLOB_HASH_LEN).ok_or_else(|| {
store::Error::InternalError(format!( trc::Error::corrupted_key(key, value.into(), trc::location!())
"Invalid key {key:?} in blob hash tables"
))
})?, })?,
) )
.unwrap(), .unwrap(),
@ -107,7 +106,8 @@ impl Core {
Ok(true) Ok(true)
}, },
) )
.await?; .await
.caused_by(trc::location!())?;
Ok(results) Ok(results)
} }

View file

@ -342,7 +342,7 @@ impl From<i64> for VariableWrapper {
} }
impl Deserialize for VariableWrapper { impl Deserialize for VariableWrapper {
fn deserialize(bytes: &[u8]) -> store::Result<Self> { fn deserialize(bytes: &[u8]) -> trc::Result<Self> {
String::deserialize(bytes).map(|v| VariableWrapper(Variable::String(v.into()))) String::deserialize(bytes).map(|v| VariableWrapper(Variable::String(v.into())))
} }
} }

View file

@ -20,9 +20,7 @@ use config::{
storage::Storage, storage::Storage,
tracers::{OtelTracer, Tracer, Tracers}, tracers::{OtelTracer, Tracer, Tracers},
}; };
use directory::{ use directory::{core::secret::verify_secret_hash, Directory, Principal, QueryBy, Type};
core::secret::verify_secret_hash, Directory, DirectoryError, Principal, QueryBy, Type,
};
use expr::if_block::IfBlock; use expr::if_block::IfBlock;
use listener::{ use listener::{
blocked::{AllowedIps, BlockedIps}, blocked::{AllowedIps, BlockedIps},
@ -92,7 +90,7 @@ pub enum AuthFailureReason {
InvalidCredentials, InvalidCredentials,
MissingTotp, MissingTotp,
Banned, Banned,
InternalError(DirectoryError), InternalError(trc::Error),
} }
#[derive(Debug)] #[derive(Debug)]
@ -241,7 +239,7 @@ impl Core {
remote_ip: IpAddr, remote_ip: IpAddr,
protocol: ServerProtocol, protocol: ServerProtocol,
return_member_of: bool, return_member_of: bool,
) -> directory::Result<AuthResult<Principal<u32>>> { ) -> trc::Result<AuthResult<Principal<u32>>> {
// First try to authenticate the user against the default directory // First try to authenticate the user against the default directory
let result = match directory let result = match directory
.query(QueryBy::Credentials(credentials), return_member_of) .query(QueryBy::Credentials(credentials), return_member_of)
@ -266,10 +264,13 @@ impl Core {
return Ok(AuthResult::Success(principal)); return Ok(AuthResult::Success(principal));
} }
Ok(None) => Ok(()), Ok(None) => Ok(()),
Err(DirectoryError::MissingTotpCode) => { Err(err) => {
return Ok(AuthResult::Failure(AuthFailureReason::MissingTotp)) if err.matches(trc::Cause::MissingParameter) {
return Ok(AuthResult::Failure(AuthFailureReason::MissingTotp));
} else {
Err(err)
}
} }
Err(err) => Err(err),
}; };
// Then check if the credentials match the fallback admin or master user // Then check if the credentials match the fallback admin or master user
@ -281,7 +282,7 @@ impl Core {
(Some((fallback_admin, fallback_pass)), _, Credentials::Plain { username, secret }) (Some((fallback_admin, fallback_pass)), _, Credentials::Plain { username, secret })
if username == fallback_admin => if username == fallback_admin =>
{ {
if verify_secret_hash(fallback_pass, secret).await { if verify_secret_hash(fallback_pass, secret).await? {
// Send webhook event // Send webhook event
if self.has_webhook_subscribers(WebhookType::AuthSuccess) { if self.has_webhook_subscribers(WebhookType::AuthSuccess) {
ipc.send_webhook( ipc.send_webhook(
@ -304,7 +305,7 @@ impl Core {
(_, Some((master_user, master_pass)), Credentials::Plain { username, secret }) (_, Some((master_user, master_pass)), Credentials::Plain { username, secret })
if username.ends_with(master_user) => if username.ends_with(master_user) =>
{ {
if verify_secret_hash(master_pass, secret).await { if verify_secret_hash(master_pass, secret).await? {
let username = username.strip_suffix(master_user).unwrap(); let username = username.strip_suffix(master_user).unwrap();
let username = username.strip_suffix('%').unwrap_or(username); let username = username.strip_suffix('%').unwrap_or(username);
return Ok( return Ok(

View file

@ -4,49 +4,50 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/ */
use std::io::ErrorKind;
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use trc::AddContext;
use utils::config::ConfigKey; use utils::config::ConfigKey;
use crate::Core; use crate::Core;
use super::{AcmeError, AcmeProvider}; use super::AcmeProvider;
impl Core { impl Core {
pub(crate) async fn load_cert( pub(crate) async fn load_cert(&self, provider: &AcmeProvider) -> trc::Result<Option<Vec<u8>>> {
&self,
provider: &AcmeProvider,
) -> Result<Option<Vec<u8>>, AcmeError> {
self.read_if_exists(provider, "cert", provider.domains.as_slice()) self.read_if_exists(provider, "cert", provider.domains.as_slice())
.await .await
.map_err(AcmeError::CertCacheLoad) .add_context(|err| {
err.caused_by(trc::location!())
.details("Failed to load certificates")
})
} }
pub(crate) async fn store_cert( pub(crate) async fn store_cert(&self, provider: &AcmeProvider, cert: &[u8]) -> trc::Result<()> {
&self,
provider: &AcmeProvider,
cert: &[u8],
) -> Result<(), AcmeError> {
self.write(provider, "cert", provider.domains.as_slice(), cert) self.write(provider, "cert", provider.domains.as_slice(), cert)
.await .await
.map_err(AcmeError::CertCacheStore) .add_context(|err| {
err.caused_by(trc::location!())
.details("Failed to store certificate")
})
} }
pub(crate) async fn load_account( pub(crate) async fn load_account(
&self, &self,
provider: &AcmeProvider, provider: &AcmeProvider,
) -> Result<Option<Vec<u8>>, AcmeError> { ) -> trc::Result<Option<Vec<u8>>> {
self.read_if_exists(provider, "account-key", provider.contact.as_slice()) self.read_if_exists(provider, "account-key", provider.contact.as_slice())
.await .await
.map_err(AcmeError::AccountCacheLoad) .add_context(|err| {
err.caused_by(trc::location!())
.details("Failed to load account")
})
} }
pub(crate) async fn store_account( pub(crate) async fn store_account(
&self, &self,
provider: &AcmeProvider, provider: &AcmeProvider,
account: &[u8], account: &[u8],
) -> Result<(), AcmeError> { ) -> trc::Result<()> {
self.write( self.write(
provider, provider,
"account-key", "account-key",
@ -54,7 +55,10 @@ impl Core {
account, account,
) )
.await .await
.map_err(AcmeError::AccountCacheStore) .add_context(|err| {
err.caused_by(trc::location!())
.details("Failed to store account")
})
} }
async fn read_if_exists( async fn read_if_exists(
@ -62,19 +66,19 @@ impl Core {
provider: &AcmeProvider, provider: &AcmeProvider,
class: &str, class: &str,
items: &[String], items: &[String],
) -> Result<Option<Vec<u8>>, std::io::Error> { ) -> trc::Result<Option<Vec<u8>>> {
match self if let Some(content) = self
.storage .storage
.config .config
.get(self.build_key(provider, class, items)) .get(self.build_key(provider, class, items))
.await .await?
{ {
Ok(Some(content)) => match URL_SAFE_NO_PAD.decode(content.as_bytes()) { URL_SAFE_NO_PAD
Ok(contents) => Ok(Some(contents)), .decode(content.as_bytes())
Err(err) => Err(std::io::Error::new(ErrorKind::Other, err)), .map_err(Into::into)
}, .map(Some)
Ok(None) => Ok(None), } else {
Err(err) => Err(std::io::Error::new(ErrorKind::Other, err)), Ok(None)
} }
} }
@ -84,7 +88,7 @@ impl Core {
class: &str, class: &str,
items: &[String], items: &[String],
contents: impl AsRef<[u8]>, contents: impl AsRef<[u8]>,
) -> Result<(), std::io::Error> { ) -> trc::Result<()> {
self.storage self.storage
.config .config
.set([ConfigKey { .set([ConfigKey {
@ -92,7 +96,6 @@ impl Core {
value: URL_SAFE_NO_PAD.encode(contents.as_ref()), value: URL_SAFE_NO_PAD.encode(contents.as_ref()),
}]) }])
.await .await
.map_err(|err| std::io::Error::new(ErrorKind::Other, err))
} }
fn build_key(&self, provider: &AcmeProvider, class: &str, _: &[String]) -> String { fn build_key(&self, provider: &AcmeProvider, class: &str, _: &[String]) -> String {

View file

@ -5,18 +5,18 @@ use std::time::Duration;
use base64::engine::general_purpose::URL_SAFE_NO_PAD; use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use base64::Engine; use base64::Engine;
use rcgen::{Certificate, CustomExtension, PKCS_ECDSA_P256_SHA256}; use rcgen::{Certificate, CustomExtension, PKCS_ECDSA_P256_SHA256};
use reqwest::header::{ToStrError, CONTENT_TYPE}; use reqwest::header::CONTENT_TYPE;
use reqwest::{Method, Response, StatusCode}; use reqwest::{Method, Response};
use ring::error::{KeyRejected, Unspecified};
use ring::rand::SystemRandom; use ring::rand::SystemRandom;
use ring::signature::{EcdsaKeyPair, EcdsaSigningAlgorithm, ECDSA_P256_SHA256_FIXED_SIGNING}; use ring::signature::{EcdsaKeyPair, EcdsaSigningAlgorithm, ECDSA_P256_SHA256_FIXED_SIGNING};
use serde::Deserialize; use serde::Deserialize;
use serde_json::json; use serde_json::json;
use store::write::Bincode; use store::write::Bincode;
use store::Serialize; use store::Serialize;
use trc::conv::AssertSuccess;
use super::jose::{ use super::jose::{
key_authorization, key_authorization_sha256, key_authorization_sha256_base64, sign, JoseError, key_authorization, key_authorization_sha256, key_authorization_sha256_base64, sign,
}; };
pub const LETS_ENCRYPT_STAGING_DIRECTORY: &str = pub const LETS_ENCRYPT_STAGING_DIRECTORY: &str =
@ -42,7 +42,7 @@ impl Account {
.to_vec() .to_vec()
} }
pub async fn create<'a, S, I>(directory: Directory, contact: I) -> Result<Self, DirectoryError> pub async fn create<'a, S, I>(directory: Directory, contact: I) -> trc::Result<Self>
where where
S: AsRef<str> + 'a, S: AsRef<str> + 'a,
I: IntoIterator<Item = &'a S>, I: IntoIterator<Item = &'a S>,
@ -54,12 +54,13 @@ impl Account {
directory: Directory, directory: Directory,
contact: I, contact: I,
key_pair: &[u8], key_pair: &[u8],
) -> Result<Self, DirectoryError> ) -> trc::Result<Self>
where where
S: AsRef<str> + 'a, S: AsRef<str> + 'a,
I: IntoIterator<Item = &'a S>, I: IntoIterator<Item = &'a S>,
{ {
let key_pair = EcdsaKeyPair::from_pkcs8(ALG, key_pair, &SystemRandom::new())?; let key_pair = EcdsaKeyPair::from_pkcs8(ALG, key_pair, &SystemRandom::new())
.map_err(|err| trc::Cause::Crypto.reason(err).caused_by(trc::location!()))?;
let contact: Vec<&'a str> = contact.into_iter().map(AsRef::<str>::as_ref).collect(); let contact: Vec<&'a str> = contact.into_iter().map(AsRef::<str>::as_ref).collect();
let payload = json!({ let payload = json!({
"termsOfServiceAgreed": true, "termsOfServiceAgreed": true,
@ -86,7 +87,7 @@ impl Account {
&self, &self,
url: impl AsRef<str>, url: impl AsRef<str>,
payload: &str, payload: &str,
) -> Result<(Option<String>, String), DirectoryError> { ) -> trc::Result<(Option<String>, String)> {
let body = sign( let body = sign(
&self.key_pair, &self.key_pair,
Some(&self.kid), Some(&self.kid),
@ -100,68 +101,66 @@ impl Account {
Ok((location, body)) Ok((location, body))
} }
pub async fn new_order(&self, domains: Vec<String>) -> Result<(String, Order), DirectoryError> { pub async fn new_order(&self, domains: Vec<String>) -> trc::Result<(String, Order)> {
let domains: Vec<Identifier> = domains.into_iter().map(Identifier::Dns).collect(); let domains: Vec<Identifier> = domains.into_iter().map(Identifier::Dns).collect();
let payload = format!("{{\"identifiers\":{}}}", serde_json::to_string(&domains)?); let payload = format!("{{\"identifiers\":{}}}", serde_json::to_string(&domains)?);
let response = self.request(&self.directory.new_order, &payload).await?; let response = self.request(&self.directory.new_order, &payload).await?;
let url = response let url = response.0.ok_or(
.0 trc::Cause::Acme
.ok_or(DirectoryError::MissingHeader("Location"))?; .caused_by(trc::location!())
.details("Missing header")
.ctx(trc::Key::Id, "Location"),
)?;
let order = serde_json::from_str(&response.1)?; let order = serde_json::from_str(&response.1)?;
Ok((url, order)) Ok((url, order))
} }
pub async fn auth(&self, url: impl AsRef<str>) -> Result<Auth, DirectoryError> { pub async fn auth(&self, url: impl AsRef<str>) -> trc::Result<Auth> {
let response = self.request(url, "").await?; let response = self.request(url, "").await?;
serde_json::from_str(&response.1).map_err(Into::into) serde_json::from_str(&response.1).map_err(Into::into)
} }
pub async fn challenge(&self, url: impl AsRef<str>) -> Result<(), DirectoryError> { pub async fn challenge(&self, url: impl AsRef<str>) -> trc::Result<()> {
self.request(&url, "{}").await.map(|_| ()) self.request(&url, "{}").await.map(|_| ())
} }
pub async fn order(&self, url: impl AsRef<str>) -> Result<Order, DirectoryError> { pub async fn order(&self, url: impl AsRef<str>) -> trc::Result<Order> {
let response = self.request(&url, "").await?; let response = self.request(&url, "").await?;
serde_json::from_str(&response.1).map_err(Into::into) serde_json::from_str(&response.1).map_err(Into::into)
} }
pub async fn finalize( pub async fn finalize(&self, url: impl AsRef<str>, csr: Vec<u8>) -> trc::Result<Order> {
&self,
url: impl AsRef<str>,
csr: Vec<u8>,
) -> Result<Order, DirectoryError> {
let payload = format!("{{\"csr\":\"{}\"}}", URL_SAFE_NO_PAD.encode(csr)); let payload = format!("{{\"csr\":\"{}\"}}", URL_SAFE_NO_PAD.encode(csr));
let response = self.request(&url, &payload).await?; let response = self.request(&url, &payload).await?;
serde_json::from_str(&response.1).map_err(Into::into) serde_json::from_str(&response.1).map_err(Into::into)
} }
pub async fn certificate(&self, url: impl AsRef<str>) -> Result<String, DirectoryError> { pub async fn certificate(&self, url: impl AsRef<str>) -> trc::Result<String> {
Ok(self.request(&url, "").await?.1) Ok(self.request(&url, "").await?.1)
} }
pub fn http_proof(&self, challenge: &Challenge) -> Result<Vec<u8>, DirectoryError> { pub fn http_proof(&self, challenge: &Challenge) -> trc::Result<Vec<u8>> {
key_authorization(&self.key_pair, &challenge.token) key_authorization(&self.key_pair, &challenge.token)
.map(|key| key.into_bytes()) .map(|key| key.into_bytes())
.map_err(Into::into) .map_err(Into::into)
} }
pub fn dns_proof(&self, challenge: &Challenge) -> Result<String, DirectoryError> { pub fn dns_proof(&self, challenge: &Challenge) -> trc::Result<String> {
key_authorization_sha256_base64(&self.key_pair, &challenge.token).map_err(Into::into) key_authorization_sha256_base64(&self.key_pair, &challenge.token).map_err(Into::into)
} }
pub fn tls_alpn_key( pub fn tls_alpn_key(&self, challenge: &Challenge, domain: String) -> trc::Result<Vec<u8>> {
&self,
challenge: &Challenge,
domain: String,
) -> Result<Vec<u8>, DirectoryError> {
let mut params = rcgen::CertificateParams::new(vec![domain]); let mut params = rcgen::CertificateParams::new(vec![domain]);
let key_auth = key_authorization_sha256(&self.key_pair, &challenge.token)?; let key_auth = key_authorization_sha256(&self.key_pair, &challenge.token)?;
params.alg = &PKCS_ECDSA_P256_SHA256; params.alg = &PKCS_ECDSA_P256_SHA256;
params.custom_extensions = vec![CustomExtension::new_acme_identifier(key_auth.as_ref())]; params.custom_extensions = vec![CustomExtension::new_acme_identifier(key_auth.as_ref())];
let cert = Certificate::from_params(params)?; let cert = Certificate::from_params(params)
.map_err(|err| trc::Cause::Crypto.caused_by(trc::location!()).reason(err))?;
Ok(Bincode::new(SerializedCert { Ok(Bincode::new(SerializedCert {
certificate: cert.serialize_der()?, certificate: cert
.serialize_der()
.map_err(|err| trc::Cause::Crypto.caused_by(trc::location!()).reason(err))?,
private_key: cert.serialize_private_key_der(), private_key: cert.serialize_private_key_der(),
}) })
.serialize()) .serialize())
@ -183,12 +182,12 @@ pub struct Directory {
} }
impl Directory { impl Directory {
pub async fn discover(url: impl AsRef<str>) -> Result<Self, DirectoryError> { pub async fn discover(url: impl AsRef<str>) -> trc::Result<Self> {
Ok(serde_json::from_str( Ok(serde_json::from_str(
&https(url, Method::GET, None).await?.text().await?, &https(url, Method::GET, None).await?.text().await?,
)?) )?)
} }
pub async fn nonce(&self) -> Result<String, DirectoryError> { pub async fn nonce(&self) -> trc::Result<String> {
get_header( get_header(
&https(&self.new_nonce.as_str(), Method::HEAD, None).await?, &https(&self.new_nonce.as_str(), Method::HEAD, None).await?,
"replay-nonce", "replay-nonce",
@ -269,27 +268,12 @@ pub struct Problem {
pub detail: Option<String>, pub detail: Option<String>,
} }
#[derive(Debug)]
pub enum DirectoryError {
Io(std::io::Error),
Rcgen(rcgen::Error),
Jose(JoseError),
Json(serde_json::Error),
HttpRequest(reqwest::Error),
HttpRequestCode { code: StatusCode, reason: String },
HttpResponseNonStringHeader(ToStrError),
KeyRejected(KeyRejected),
Crypto(Unspecified),
MissingHeader(&'static str),
NoChallenge(ChallengeType),
}
#[allow(unused_mut)] #[allow(unused_mut)]
async fn https( async fn https(
url: impl AsRef<str>, url: impl AsRef<str>,
method: Method, method: Method,
body: Option<String>, body: Option<String>,
) -> Result<Response, DirectoryError> { ) -> trc::Result<Response> {
let url = url.as_ref(); let url = url.as_ref();
let mut builder = reqwest::Client::builder() let mut builder = reqwest::Client::builder()
.timeout(Duration::from_secs(30)) .timeout(Duration::from_secs(30))
@ -310,68 +294,38 @@ async fn https(
.body(body); .body(body);
} }
let response = request.send().await?; request.send().await?.assert_success().await
if response.status().is_success() {
Ok(response)
} else {
Err(DirectoryError::HttpRequestCode {
code: response.status(),
reason: response.text().await?,
})
}
} }
fn get_header(response: &Response, header: &'static str) -> Result<String, DirectoryError> { fn get_header(response: &Response, header: &'static str) -> trc::Result<String> {
match response.headers().get_all(header).iter().last() { match response.headers().get_all(header).iter().last() {
Some(value) => Ok(value.to_str()?.to_string()), Some(value) => Ok(value.to_str()?.to_string()),
None => Err(DirectoryError::MissingHeader(header)), None => Err(trc::Cause::Acme
.caused_by(trc::location!())
.details("Missing header")
.ctx(trc::Key::Id, header)),
} }
} }
impl From<std::io::Error> for DirectoryError { impl ChallengeType {
fn from(err: std::io::Error) -> Self { pub fn as_str(&self) -> &'static str {
Self::Io(err) match self {
Self::Http01 => "http-01",
Self::Dns01 => "dns-01",
Self::TlsAlpn01 => "tls-alpn-01",
}
} }
} }
impl From<rcgen::Error> for DirectoryError { impl AuthStatus {
fn from(err: rcgen::Error) -> Self { pub fn as_str(&self) -> &'static str {
Self::Rcgen(err) match self {
} Self::Pending => "pending",
} Self::Valid => "valid",
Self::Invalid => "invalid",
impl From<JoseError> for DirectoryError { Self::Revoked => "revoked",
fn from(err: JoseError) -> Self { Self::Expired => "expired",
Self::Jose(err) Self::Deactivated => "deactivated",
} }
}
impl From<serde_json::Error> for DirectoryError {
fn from(err: serde_json::Error) -> Self {
Self::Json(err)
}
}
impl From<reqwest::Error> for DirectoryError {
fn from(err: reqwest::Error) -> Self {
Self::HttpRequest(err)
}
}
impl From<KeyRejected> for DirectoryError {
fn from(err: KeyRejected) -> Self {
Self::KeyRejected(err)
}
}
impl From<Unspecified> for DirectoryError {
fn from(err: Unspecified) -> Self {
Self::Crypto(err)
}
}
impl From<ToStrError> for DirectoryError {
fn from(err: ToStrError) -> Self {
Self::HttpResponseNonStringHeader(err)
} }
} }

View file

@ -13,7 +13,7 @@ pub(crate) fn sign(
nonce: String, nonce: String,
url: &str, url: &str,
payload: &str, payload: &str,
) -> Result<String, JoseError> { ) -> trc::Result<String> {
let jwk = match kid { let jwk = match kid {
None => Some(Jwk::new(key)), None => Some(Jwk::new(key)),
Some(_) => None, Some(_) => None,
@ -21,7 +21,9 @@ pub(crate) fn sign(
let protected = Protected::base64(jwk, kid, nonce, url)?; let protected = Protected::base64(jwk, kid, nonce, url)?;
let payload = URL_SAFE_NO_PAD.encode(payload); let payload = URL_SAFE_NO_PAD.encode(payload);
let combined = format!("{}.{}", &protected, &payload); let combined = format!("{}.{}", &protected, &payload);
let signature = key.sign(&SystemRandom::new(), combined.as_bytes())?; let signature = key
.sign(&SystemRandom::new(), combined.as_bytes())
.map_err(|err| trc::Cause::Crypto.caused_by(trc::location!()).reason(err))?;
let signature = URL_SAFE_NO_PAD.encode(signature.as_ref()); let signature = URL_SAFE_NO_PAD.encode(signature.as_ref());
let body = Body { let body = Body {
protected, protected,
@ -31,7 +33,7 @@ pub(crate) fn sign(
Ok(serde_json::to_string(&body)?) Ok(serde_json::to_string(&body)?)
} }
pub(crate) fn key_authorization(key: &EcdsaKeyPair, token: &str) -> Result<String, JoseError> { pub(crate) fn key_authorization(key: &EcdsaKeyPair, token: &str) -> trc::Result<String> {
Ok(format!( Ok(format!(
"{}.{}", "{}.{}",
token, token,
@ -39,17 +41,14 @@ pub(crate) fn key_authorization(key: &EcdsaKeyPair, token: &str) -> Result<Strin
)) ))
} }
pub(crate) fn key_authorization_sha256( pub(crate) fn key_authorization_sha256(key: &EcdsaKeyPair, token: &str) -> trc::Result<Digest> {
key: &EcdsaKeyPair,
token: &str,
) -> Result<Digest, JoseError> {
key_authorization(key, token).map(|s| digest(&SHA256, s.as_bytes())) key_authorization(key, token).map(|s| digest(&SHA256, s.as_bytes()))
} }
pub(crate) fn key_authorization_sha256_base64( pub(crate) fn key_authorization_sha256_base64(
key: &EcdsaKeyPair, key: &EcdsaKeyPair,
token: &str, token: &str,
) -> Result<String, JoseError> { ) -> trc::Result<String> {
key_authorization_sha256(key, token).map(|s| URL_SAFE_NO_PAD.encode(s.as_ref())) key_authorization_sha256(key, token).map(|s| URL_SAFE_NO_PAD.encode(s.as_ref()))
} }
@ -77,7 +76,7 @@ impl<'a> Protected<'a> {
kid: Option<&'a str>, kid: Option<&'a str>,
nonce: String, nonce: String,
url: &'a str, url: &'a str,
) -> Result<String, JoseError> { ) -> trc::Result<String> {
let protected = Self { let protected = Self {
alg: "ES256", alg: "ES256",
jwk, jwk,
@ -113,7 +112,7 @@ impl Jwk {
y: URL_SAFE_NO_PAD.encode(y), y: URL_SAFE_NO_PAD.encode(y),
} }
} }
pub(crate) fn thumb_sha256_base64(&self) -> Result<String, JoseError> { pub(crate) fn thumb_sha256_base64(&self) -> trc::Result<String> {
let jwk_thumb = JwkThumb { let jwk_thumb = JwkThumb {
crv: self.crv, crv: self.crv,
kty: self.kty, kty: self.kty,
@ -133,21 +132,3 @@ struct JwkThumb<'a> {
x: &'a str, x: &'a str,
y: &'a str, y: &'a str,
} }
#[derive(Debug)]
pub enum JoseError {
Json(serde_json::Error),
Crypto(ring::error::Unspecified),
}
impl From<serde_json::Error> for JoseError {
fn from(err: serde_json::Error) -> Self {
Self::Json(err)
}
}
impl From<ring::error::Unspecified> for JoseError {
fn from(err: ring::error::Unspecified) -> Self {
Self::Crypto(err)
}
}

View file

@ -18,10 +18,7 @@ use rustls::sign::CertifiedKey;
use crate::Core; use crate::Core;
use self::{ use self::directory::{Account, ChallengeType};
directory::{Account, ChallengeType},
order::{CertParseError, OrderError},
};
pub struct AcmeProvider { pub struct AcmeProvider {
pub id: String, pub id: String,
@ -51,17 +48,6 @@ pub struct StaticResolver {
pub key: Option<Arc<CertifiedKey>>, pub key: Option<Arc<CertifiedKey>>,
} }
#[derive(Debug)]
pub enum AcmeError {
CertCacheLoad(std::io::Error),
AccountCacheLoad(std::io::Error),
CertCacheStore(std::io::Error),
AccountCacheStore(std::io::Error),
CachedCertParse(CertParseError),
Order(OrderError),
NewCertParse(CertParseError),
}
impl AcmeProvider { impl AcmeProvider {
pub fn new( pub fn new(
id: String, id: String,
@ -71,7 +57,7 @@ impl AcmeProvider {
challenge: ChallengeSettings, challenge: ChallengeSettings,
renew_before: Duration, renew_before: Duration,
default: bool, default: bool,
) -> utils::config::Result<Self> { ) -> trc::Result<Self> {
Ok(AcmeProvider { Ok(AcmeProvider {
id, id,
directory_url, directory_url,
@ -95,7 +81,7 @@ impl AcmeProvider {
} }
impl Core { impl Core {
pub async fn init_acme(&self, provider: &AcmeProvider) -> Result<Duration, AcmeError> { pub async fn init_acme(&self, provider: &AcmeProvider) -> trc::Result<Duration> {
// Load account key from cache or generate a new one // Load account key from cache or generate a new one
if let Some(account_key) = self.load_account(provider).await? { if let Some(account_key) = self.load_account(provider).await? {
provider.account_key.store(Arc::new(account_key)); provider.account_key.store(Arc::new(account_key));

View file

@ -7,7 +7,6 @@ use rcgen::{CertificateParams, DistinguishedName, PKCS_ECDSA_P256_SHA256};
use rustls::crypto::ring::sign::any_ecdsa_type; use rustls::crypto::ring::sign::any_ecdsa_type;
use rustls::sign::CertifiedKey; use rustls::sign::CertifiedKey;
use rustls_pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer}; use rustls_pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer};
use std::fmt::Debug;
use std::sync::Arc; use std::sync::Arc;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use utils::suffixlist::DomainPart; use utils::suffixlist::DomainPart;
@ -17,29 +16,8 @@ use crate::listener::acme::directory::Identifier;
use crate::listener::acme::ChallengeSettings; use crate::listener::acme::ChallengeSettings;
use crate::Core; use crate::Core;
use super::directory::{Account, Auth, AuthStatus, Directory, DirectoryError, Order, OrderStatus}; use super::directory::{Account, AuthStatus, Directory, OrderStatus};
use super::jose::JoseError; use super::AcmeProvider;
use super::{AcmeError, AcmeProvider};
#[derive(Debug)]
pub enum OrderError {
Acme(DirectoryError),
Rcgen(rcgen::Error),
BadOrder(Order),
BadAuth(Auth),
TooManyAttemptsAuth(String),
ProcessingTimeout(Order),
Store(store::Error),
Dns(dns_update::Error),
}
#[derive(Debug)]
pub enum CertParseError {
X509(x509_parser::nom::Err<x509_parser::error::X509Error>),
Pem(pem::PemError),
TooFewPem(usize),
InvalidPrivateKey,
}
impl Core { impl Core {
pub(crate) async fn process_cert( pub(crate) async fn process_cert(
@ -47,16 +25,8 @@ impl Core {
provider: &AcmeProvider, provider: &AcmeProvider,
pem: Vec<u8>, pem: Vec<u8>,
cached: bool, cached: bool,
) -> Result<Duration, AcmeError> { ) -> trc::Result<Duration> {
let (cert, validity) = match (parse_cert(&pem), cached) { let (cert, validity) = parse_cert(&pem)?;
(Ok(r), _) => r,
(Err(err), cached) => {
return match cached {
true => Err(AcmeError::CachedCertParse(err)),
false => Err(AcmeError::NewCertParse(err)),
}
}
};
self.set_cert(provider, Arc::new(cert)); self.set_cert(provider, Arc::new(cert));
@ -82,7 +52,7 @@ impl Core {
Ok(renew_at) Ok(renew_at)
} }
pub async fn renew(&self, provider: &AcmeProvider) -> Result<Duration, AcmeError> { pub async fn renew(&self, provider: &AcmeProvider) -> trc::Result<Duration> {
let mut backoff = 0; let mut backoff = 0;
loop { loop {
match self.order(provider).await { match self.order(provider).await {
@ -99,12 +69,12 @@ impl Core {
backoff = (backoff + 1).min(16); backoff = (backoff + 1).min(16);
tokio::time::sleep(Duration::from_secs(1 << backoff)).await; tokio::time::sleep(Duration::from_secs(1 << backoff)).await;
} }
Err(err) => return Err(AcmeError::Order(err)), Err(err) => return Err(err.details("Failed to renew certificate")),
} }
} }
} }
async fn order(&self, provider: &AcmeProvider) -> Result<Vec<u8>, OrderError> { async fn order(&self, provider: &AcmeProvider) -> trc::Result<Vec<u8>> {
let directory = Directory::discover(&provider.directory_url).await?; let directory = Directory::discover(&provider.directory_url).await?;
let account = Account::create_with_keypair( let account = Account::create_with_keypair(
directory, directory,
@ -116,7 +86,8 @@ impl Core {
let mut params = CertificateParams::new(provider.domains.clone()); let mut params = CertificateParams::new(provider.domains.clone());
params.distinguished_name = DistinguishedName::new(); params.distinguished_name = DistinguishedName::new();
params.alg = &PKCS_ECDSA_P256_SHA256; params.alg = &PKCS_ECDSA_P256_SHA256;
let cert = rcgen::Certificate::from_params(params)?; let cert = rcgen::Certificate::from_params(params)
.map_err(|err| trc::Cause::Crypto.caused_by(trc::location!()).reason(err))?;
let (order_url, mut order) = account.new_order(provider.domains.clone()).await?; let (order_url, mut order) = account.new_order(provider.domains.clone()).await?;
loop { loop {
@ -151,7 +122,9 @@ impl Core {
} }
} }
if order.status == OrderStatus::Processing { if order.status == OrderStatus::Processing {
return Err(OrderError::ProcessingTimeout(order)); return Err(trc::Cause::Timeout
.caused_by(trc::location!())
.details("Order processing timed out"));
} }
} }
OrderStatus::Ready => { OrderStatus::Ready => {
@ -162,7 +135,9 @@ impl Core {
"Sending CSR" "Sending CSR"
); );
let csr = cert.serialize_request_der()?; let csr = cert.serialize_request_der().map_err(|err| {
trc::Cause::Crypto.caused_by(trc::location!()).reason(err)
})?;
order = account.finalize(order.finalize, csr).await? order = account.finalize(order.finalize, csr).await?
} }
OrderStatus::Valid { certificate } => { OrderStatus::Valid { certificate } => {
@ -190,7 +165,7 @@ impl Core {
"Invalid order" "Invalid order"
); );
return Err(OrderError::BadOrder(order)); return Err(trc::Cause::Invalid.into_err().details("Invalid ACME order"));
} }
} }
} }
@ -201,7 +176,7 @@ impl Core {
provider: &AcmeProvider, provider: &AcmeProvider,
account: &Account, account: &Account,
url: &String, url: &String,
) -> Result<(), OrderError> { ) -> trc::Result<()> {
let auth = account.auth(url).await?; let auth = account.auth(url).await?;
let (domain, challenge_url) = match auth.status { let (domain, challenge_url) = match auth.status {
AuthStatus::Pending => { AuthStatus::Pending => {
@ -218,7 +193,11 @@ impl Core {
.challenges .challenges
.iter() .iter()
.find(|c| c.typ == challenge_type) .find(|c| c.typ == challenge_type)
.ok_or(DirectoryError::NoChallenge(challenge_type))?; .ok_or(
trc::Cause::MissingParameter
.into_err()
.ctx(trc::Key::Id, challenge_type.as_str()),
)?;
match &provider.challenge { match &provider.challenge {
ChallengeSettings::TlsAlpn01 => { ChallengeSettings::TlsAlpn01 => {
@ -290,7 +269,7 @@ impl Core {
error = ?err, error = ?err,
"Failed to create DNS record.", "Failed to create DNS record.",
); );
return Err(OrderError::Dns(err)); return Err(trc::Cause::Dns.caused_by(trc::location!()).reason(err));
} }
tracing::info!( tracing::info!(
@ -362,7 +341,11 @@ impl Core {
(domain, challenge.url.clone()) (domain, challenge.url.clone())
} }
AuthStatus::Valid => return Ok(()), AuthStatus::Valid => return Ok(()),
_ => return Err(OrderError::BadAuth(auth)), _ => {
return Err(trc::Cause::Authentication
.into_err()
.ctx(trc::Key::Status, auth.status.as_str()))
}
}; };
for i in 0u64..5 { for i in 0u64..5 {
@ -389,23 +372,34 @@ impl Core {
return Ok(()); return Ok(());
} }
_ => return Err(OrderError::BadAuth(auth)), _ => {
return Err(trc::Cause::Authentication
.into_err()
.ctx(trc::Key::Status, auth.status.as_str()))
}
} }
} }
Err(OrderError::TooManyAttemptsAuth(domain)) Err(trc::Cause::Authentication
.into_err()
.details("Too many attempts")
.ctx(trc::Key::Id, domain))
} }
} }
fn parse_cert(pem: &[u8]) -> Result<(CertifiedKey, [DateTime<Utc>; 2]), CertParseError> { fn parse_cert(pem: &[u8]) -> trc::Result<(CertifiedKey, [DateTime<Utc>; 2])> {
let mut pems = pem::parse_many(pem)?; let mut pems = pem::parse_many(pem)
.map_err(|err| trc::Cause::Crypto.reason(err).caused_by(trc::location!()))?;
if pems.len() < 2 { if pems.len() < 2 {
return Err(CertParseError::TooFewPem(pems.len())); return Err(trc::Cause::Crypto
.caused_by(trc::location!())
.ctx(trc::Key::Size, pems.len())
.details("Too few PEMs"));
} }
let pk = match any_ecdsa_type(&PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from( let pk = match any_ecdsa_type(&PrivateKeyDer::Pkcs8(PrivatePkcs8KeyDer::from(
pems.remove(0).contents(), pems.remove(0).contents(),
))) { ))) {
Ok(pk) => pk, Ok(pk) => pk,
Err(_) => return Err(CertParseError::InvalidPrivateKey), Err(err) => return Err(trc::Cause::Crypto.reason(err).caused_by(trc::location!())),
}; };
let cert_chain: Vec<CertificateDer> = pems let cert_chain: Vec<CertificateDer> = pems
.into_iter() .into_iter()
@ -420,50 +414,8 @@ fn parse_cert(pem: &[u8]) -> Result<(CertifiedKey, [DateTime<Utc>; 2]), CertPars
.unwrap_or_default() .unwrap_or_default()
}) })
} }
Err(err) => return Err(CertParseError::X509(err)), Err(err) => return Err(trc::Cause::Crypto.reason(err).caused_by(trc::location!())),
}; };
let cert = CertifiedKey::new(cert_chain, pk); let cert = CertifiedKey::new(cert_chain, pk);
Ok((cert, validity)) Ok((cert, validity))
} }
impl From<DirectoryError> for OrderError {
fn from(err: DirectoryError) -> Self {
Self::Acme(err)
}
}
impl From<rcgen::Error> for OrderError {
fn from(err: rcgen::Error) -> Self {
Self::Rcgen(err)
}
}
impl From<x509_parser::nom::Err<x509_parser::error::X509Error>> for CertParseError {
fn from(err: x509_parser::nom::Err<x509_parser::error::X509Error>) -> Self {
Self::X509(err)
}
}
impl From<pem::PemError> for CertParseError {
fn from(err: pem::PemError) -> Self {
Self::Pem(err)
}
}
impl From<JoseError> for OrderError {
fn from(err: JoseError) -> Self {
Self::Acme(DirectoryError::Jose(err))
}
}
impl From<JoseError> for AcmeError {
fn from(err: JoseError) -> Self {
Self::Order(OrderError::from(err))
}
}
impl From<store::Error> for OrderError {
fn from(value: store::Error) -> Self {
Self::Store(value)
}
}

View file

@ -108,7 +108,7 @@ impl AllowedIps {
} }
impl Core { impl Core {
pub async fn is_fail2banned(&self, ip: IpAddr, login: String) -> store::Result<bool> { pub async fn is_fail2banned(&self, ip: IpAddr, login: String) -> trc::Result<bool> {
if let Some(rate) = &self.network.blocked_ips.limiter_rate { if let Some(rate) = &self.network.blocked_ips.limiter_rate {
let is_allowed = self.is_ip_allowed(&ip) let is_allowed = self.is_ip_allowed(&ip)
|| (self || (self

View file

@ -900,11 +900,12 @@ impl Core {
); );
(hash, len as u8) (hash, len as u8)
} }
invalid => { _ => {
return Err(format!( return Err(trc::Error::corrupted_key(
"Invalid text bitmap key length {invalid}" key,
) None,
.into()) trc::location!(),
));
} }
}; };
@ -1108,34 +1109,34 @@ fn spawn_writer(path: PathBuf) -> (std::thread::JoinHandle<()>, SyncSender<Op>)
} }
pub(super) trait DeserializeBytes { pub(super) trait DeserializeBytes {
fn range(&self, range: Range<usize>) -> store::Result<&[u8]>; fn range(&self, range: Range<usize>) -> trc::Result<&[u8]>;
fn deserialize_u8(&self, offset: usize) -> store::Result<u8>; fn deserialize_u8(&self, offset: usize) -> trc::Result<u8>;
fn deserialize_leb128<U: Leb128_>(&self) -> store::Result<U>; fn deserialize_leb128<U: Leb128_>(&self) -> trc::Result<U>;
} }
impl DeserializeBytes for &[u8] { impl DeserializeBytes for &[u8] {
fn range(&self, range: Range<usize>) -> store::Result<&[u8]> { fn range(&self, range: Range<usize>) -> trc::Result<&[u8]> {
self.get(range.start..std::cmp::min(range.end, self.len())) self.get(range.start..std::cmp::min(range.end, self.len()))
.ok_or_else(|| store::Error::InternalError("Failed to read range".to_string())) .ok_or_else(|| trc::Cause::DataCorruption.caused_by(trc::location!()))
} }
fn deserialize_u8(&self, offset: usize) -> store::Result<u8> { fn deserialize_u8(&self, offset: usize) -> trc::Result<u8> {
self.get(offset) self.get(offset)
.copied() .copied()
.ok_or_else(|| store::Error::InternalError("Failed to read u8".to_string())) .ok_or_else(|| trc::Cause::DataCorruption.caused_by(trc::location!()))
} }
fn deserialize_leb128<U: Leb128_>(&self) -> store::Result<U> { fn deserialize_leb128<U: Leb128_>(&self) -> trc::Result<U> {
self.read_leb128::<U>() self.read_leb128::<U>()
.map(|(v, _)| v) .map(|(v, _)| v)
.ok_or_else(|| store::Error::InternalError("Failed to read leb128".to_string())) .ok_or_else(|| trc::Cause::DataCorruption.caused_by(trc::location!()))
} }
} }
struct RawBytes(Vec<u8>); struct RawBytes(Vec<u8>);
impl Deserialize for RawBytes { impl Deserialize for RawBytes {
fn deserialize(bytes: &[u8]) -> store::Result<Self> { fn deserialize(bytes: &[u8]) -> trc::Result<Self> {
Ok(Self(bytes.to_vec())) Ok(Self(bytes.to_vec()))
} }
} }

View file

@ -54,7 +54,7 @@ pub(crate) struct ExternalConfig {
} }
impl ConfigManager { impl ConfigManager {
pub async fn build_config(&self, prefix: &str) -> store::Result<Config> { pub async fn build_config(&self, prefix: &str) -> trc::Result<Config> {
let mut config = Config { let mut config = Config {
keys: self.cfg_local.load().as_ref().clone(), keys: self.cfg_local.load().as_ref().clone(),
..Default::default() ..Default::default()
@ -65,11 +65,7 @@ impl ConfigManager {
.map(|_| config) .map(|_| config)
} }
pub(crate) async fn extend_config( pub(crate) async fn extend_config(&self, config: &mut Config, prefix: &str) -> trc::Result<()> {
&self,
config: &mut Config,
prefix: &str,
) -> store::Result<()> {
for (key, value) in self.db_list(prefix, false).await? { for (key, value) in self.db_list(prefix, false).await? {
config.keys.entry(key).or_insert(value); config.keys.entry(key).or_insert(value);
} }
@ -77,7 +73,7 @@ impl ConfigManager {
Ok(()) Ok(())
} }
pub async fn get(&self, key: impl AsRef<str>) -> store::Result<Option<String>> { pub async fn get(&self, key: impl AsRef<str>) -> trc::Result<Option<String>> {
let key = key.as_ref(); let key = key.as_ref();
match self.cfg_local.load().get(key) { match self.cfg_local.load().get(key) {
Some(value) => Ok(Some(value.to_string())), Some(value) => Ok(Some(value.to_string())),
@ -95,7 +91,7 @@ impl ConfigManager {
&self, &self,
prefix: &str, prefix: &str,
strip_prefix: bool, strip_prefix: bool,
) -> store::Result<Vec<(String, String)>> { ) -> trc::Result<Vec<(String, String)>> {
let mut results = self.db_list(prefix, strip_prefix).await?; let mut results = self.db_list(prefix, strip_prefix).await?;
for (key, value) in self.cfg_local.load().iter() { for (key, value) in self.cfg_local.load().iter() {
if prefix.is_empty() || (!strip_prefix && key.starts_with(prefix)) { if prefix.is_empty() || (!strip_prefix && key.starts_with(prefix)) {
@ -112,7 +108,7 @@ impl ConfigManager {
&self, &self,
prefix: &str, prefix: &str,
suffix: &str, suffix: &str,
) -> store::Result<AHashMap<String, AHashMap<String, String>>> { ) -> trc::Result<AHashMap<String, AHashMap<String, String>>> {
let mut grouped = AHashMap::new(); let mut grouped = AHashMap::new();
let mut list = self.list(prefix, true).await?; let mut list = self.list(prefix, true).await?;
@ -138,7 +134,7 @@ impl ConfigManager {
&self, &self,
prefix: &str, prefix: &str,
strip_prefix: bool, strip_prefix: bool,
) -> store::Result<Vec<(String, String)>> { ) -> trc::Result<Vec<(String, String)>> {
let key = prefix.as_bytes(); let key = prefix.as_bytes();
let from_key = ValueKey::from(ValueClass::Config(key.to_vec())); let from_key = ValueKey::from(ValueClass::Config(key.to_vec()));
let to_key = ValueKey::from(ValueClass::Config( let to_key = ValueKey::from(ValueClass::Config(
@ -154,7 +150,7 @@ impl ConfigManager {
IterateParams::new(from_key, to_key).ascending(), IterateParams::new(from_key, to_key).ascending(),
|key, value| { |key, value| {
let mut key = std::str::from_utf8(key).map_err(|_| { let mut key = std::str::from_utf8(key).map_err(|_| {
store::Error::InternalError("Failed to deserialize config key".to_string()) trc::Error::corrupted_key(key, value.into(), trc::location!())
})?; })?;
if !patterns.is_local_key(key) { if !patterns.is_local_key(key) {
@ -173,7 +169,7 @@ impl ConfigManager {
Ok(results) Ok(results)
} }
pub async fn set<I, T>(&self, keys: I) -> store::Result<()> pub async fn set<I, T>(&self, keys: I) -> trc::Result<()>
where where
I: IntoIterator<Item = T>, I: IntoIterator<Item = T>,
T: Into<ConfigKey>, T: Into<ConfigKey>,
@ -220,7 +216,7 @@ impl ConfigManager {
Ok(()) Ok(())
} }
pub async fn clear(&self, key: impl AsRef<str>) -> store::Result<()> { pub async fn clear(&self, key: impl AsRef<str>) -> trc::Result<()> {
let key = key.as_ref(); let key = key.as_ref();
if self.cfg_local_patterns.is_local_key(key) { if self.cfg_local_patterns.is_local_key(key) {
@ -237,7 +233,7 @@ impl ConfigManager {
} }
} }
pub async fn clear_prefix(&self, key: impl AsRef<str>) -> store::Result<()> { pub async fn clear_prefix(&self, key: impl AsRef<str>) -> trc::Result<()> {
let key = key.as_ref(); let key = key.as_ref();
// Delete local keys // Delete local keys
@ -263,7 +259,7 @@ impl ConfigManager {
.await .await
} }
async fn update_local(&self, map: BTreeMap<String, String>) -> store::Result<()> { async fn update_local(&self, map: BTreeMap<String, String>) -> trc::Result<()> {
let mut cfg_text = String::with_capacity(1024); let mut cfg_text = String::with_capacity(1024);
for (key, value) in &map { for (key, value) in &map {
cfg_text.push_str(key); cfg_text.push_str(key);
@ -319,17 +315,21 @@ impl ConfigManager {
tokio::fs::write(&self.cfg_local_path, cfg_text) tokio::fs::write(&self.cfg_local_path, cfg_text)
.await .await
.map_err(|err| { .map_err(|err| {
store::Error::InternalError(format!( trc::Cause::Configuration
"Failed to write local configuration file: {err}" .caused_by(trc::Error::from(err))
)) .ctx(trc::Key::Path, self.cfg_local_path.display().to_string())
}) })
} }
pub async fn update_config_resource(&self, resource_id: &str) -> store::Result<Option<String>> { pub async fn update_config_resource(&self, resource_id: &str) -> trc::Result<Option<String>> {
let external = self let external = self
.fetch_config_resource(resource_id) .fetch_config_resource(resource_id)
.await .await
.map_err(store::Error::InternalError)?; .map_err(|reason| {
trc::Cause::Fetch
.caused_by(trc::location!())
.ctx(trc::Key::Reason, reason)
})?;
if self if self
.get(&external.id) .get(&external.id)
@ -396,7 +396,7 @@ impl ConfigManager {
} }
} }
pub async fn get_services(&self) -> store::Result<Vec<(String, u16, bool)>> { pub async fn get_services(&self) -> trc::Result<Vec<(String, u16, bool)>> {
let mut result = Vec::new(); let mut result = Vec::new();
for listener in self for listener in self

View file

@ -26,7 +26,7 @@ pub struct ReloadResult {
} }
impl Core { impl Core {
pub async fn reload_blocked_ips(&self) -> store::Result<ReloadResult> { pub async fn reload_blocked_ips(&self) -> trc::Result<ReloadResult> {
let mut ip_addresses = AHashSet::new(); let mut ip_addresses = AHashSet::new();
let mut config = self.storage.config.build_config(BLOCKED_IP_KEY).await?; let mut config = self.storage.config.build_config(BLOCKED_IP_KEY).await?;
@ -51,7 +51,7 @@ impl Core {
Ok(config.into()) Ok(config.into())
} }
pub async fn reload_certificates(&self) -> store::Result<ReloadResult> { pub async fn reload_certificates(&self) -> trc::Result<ReloadResult> {
let mut config = self.storage.config.build_config("certificate").await?; let mut config = self.storage.config.build_config("certificate").await?;
let mut certificates = self.tls.certificates.load().as_ref().clone(); let mut certificates = self.tls.certificates.load().as_ref().clone();
@ -62,7 +62,7 @@ impl Core {
Ok(config.into()) Ok(config.into())
} }
pub async fn reload_lookups(&self) -> store::Result<ReloadResult> { pub async fn reload_lookups(&self) -> trc::Result<ReloadResult> {
let mut config = self.storage.config.build_config("certificate").await?; let mut config = self.storage.config.build_config("certificate").await?;
let mut stores = Stores::default(); let mut stores = Stores::default();
stores.parse_memory_stores(&mut config); stores.parse_memory_stores(&mut config);
@ -78,7 +78,7 @@ impl Core {
}) })
} }
pub async fn reload(&self) -> store::Result<ReloadResult> { pub async fn reload(&self) -> trc::Result<ReloadResult> {
let mut config = self.storage.config.build_config("").await?; let mut config = self.storage.config.build_config("").await?;
// Parse tracers // Parse tracers

View file

@ -50,7 +50,7 @@ impl WebAdminManager {
} }
} }
pub async fn unpack(&self, blob_store: &BlobStore) -> store::Result<()> { pub async fn unpack(&self, blob_store: &BlobStore) -> trc::Result<()> {
// Delete any existing bundles // Delete any existing bundles
self.bundle_path.clean().await?; self.bundle_path.clean().await?;
@ -58,17 +58,26 @@ impl WebAdminManager {
let bundle = blob_store let bundle = blob_store
.get_blob(WEBADMIN_KEY, 0..usize::MAX) .get_blob(WEBADMIN_KEY, 0..usize::MAX)
.await? .await?
.ok_or_else(|| store::Error::InternalError("WebAdmin bundle not found".to_string()))?; .ok_or_else(|| {
trc::Cause::NotFound
.caused_by(trc::location!())
.details("Webadmin bundle not found")
})?;
// Uncompress // Uncompress
let mut bundle = zip::ZipArchive::new(Cursor::new(bundle)) let mut bundle = zip::ZipArchive::new(Cursor::new(bundle)).map_err(|err| {
.map_err(|err| store::Error::InternalError(format!("Unzip error: {err}")))?; trc::Cause::Decompress
.caused_by(trc::location!())
.reason(err)
})?;
let mut routes = AHashMap::new(); let mut routes = AHashMap::new();
for i in 0..bundle.len() { for i in 0..bundle.len() {
let (file_name, contents) = { let (file_name, contents) = {
let mut file = bundle let mut file = bundle.by_index(i).map_err(|err| {
.by_index(i) trc::Cause::Decompress
.map_err(|err| store::Error::InternalError(format!("Unzip error: {err}")))?; .caused_by(trc::location!())
.reason(err)
})?;
if file.is_dir() { if file.is_dir() {
continue; continue;
} }
@ -113,14 +122,17 @@ impl WebAdminManager {
Ok(()) Ok(())
} }
pub async fn update_and_unpack(&self, core: &Core) -> store::Result<()> { pub async fn update_and_unpack(&self, core: &Core) -> trc::Result<()> {
let bytes = core let bytes = core
.storage .storage
.config .config
.fetch_resource("webadmin") .fetch_resource("webadmin")
.await .await
.map_err(|err| { .map_err(|err| {
store::Error::InternalError(format!("Failed to download webadmin: {err}")) trc::Cause::Fetch
.caused_by(trc::location!())
.reason(err)
.details("Failed to download webadmin")
})?; })?;
core.storage.blob.put_blob(WEBADMIN_KEY, &bytes).await?; core.storage.blob.put_blob(WEBADMIN_KEY, &bytes).await?;
self.unpack(&core.storage.blob).await self.unpack(&core.storage.blob).await

View file

@ -400,7 +400,7 @@ pub async fn exec_local_domain(ctx: PluginContext<'_>) -> Variable {
pub struct VariableWrapper(Variable); pub struct VariableWrapper(Variable);
impl Deserialize for VariableWrapper { impl Deserialize for VariableWrapper {
fn deserialize(bytes: &[u8]) -> store::Result<Self> { fn deserialize(bytes: &[u8]) -> trc::Result<Self> {
Ok(VariableWrapper( Ok(VariableWrapper(
bincode::deserialize::<Variable>(bytes).unwrap_or_else(|_| { bincode::deserialize::<Variable>(bytes).unwrap_or_else(|_| {
Variable::String(String::from_utf8_lossy(bytes).into_owned().into()) Variable::String(String::from_utf8_lossy(bytes).into_owned().into())

View file

@ -7,6 +7,7 @@ resolver = "2"
[dependencies] [dependencies]
utils = { path = "../utils" } utils = { path = "../utils" }
store = { path = "../store" } store = { path = "../store" }
trc = { path = "../trc" }
jmap_proto = { path = "../jmap-proto" } jmap_proto = { path = "../jmap-proto" }
smtp-proto = { version = "0.1" } smtp-proto = { version = "0.1" }
mail-parser = { version = "0.9", features = ["full_encoding", "serde_support", "ludicrous_mode"] } mail-parser = { version = "0.9", features = ["full_encoding", "serde_support", "ludicrous_mode"] }
@ -21,7 +22,6 @@ deadpool = { version = "0.10", features = ["managed", "rt_tokio_1"] }
async-trait = "0.1.68" async-trait = "0.1.68"
parking_lot = "0.12" parking_lot = "0.12"
ahash = { version = "0.8" } ahash = { version = "0.8" }
tracing = "0.1"
lru-cache = "0.1.2" lru-cache = "0.1.2"
pwhash = "1" pwhash = "1"
password-hash = "0.5.0" password-hash = "0.5.0"

View file

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

View file

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

View file

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

View file

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

View file

@ -11,7 +11,7 @@ use crate::{Principal, QueryBy};
use super::{EmailType, MemoryDirectory}; use super::{EmailType, MemoryDirectory};
impl 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 { match by {
QueryBy::Name(name) => { QueryBy::Name(name) => {
for principal in &self.principals { for principal in &self.principals {
@ -48,7 +48,7 @@ impl MemoryDirectory {
Ok(None) 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 Ok(self
.emails_to_ids .emails_to_ids
.get(address) .get(address)
@ -65,11 +65,11 @@ impl MemoryDirectory {
.unwrap_or_default()) .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)) 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(); let mut result = Vec::new();
for (key, value) in &self.emails_to_ids { for (key, value) in &self.emails_to_ids {
if key.contains(address) && value.iter().any(|t| matches!(t, EmailType::Primary(_))) { if key.contains(address) && value.iter().any(|t| matches!(t, EmailType::Primary(_))) {
@ -79,7 +79,7 @@ impl MemoryDirectory {
Ok(result) 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(); let mut result = Vec::new();
for (key, value) in &self.emails_to_ids { for (key, value) in &self.emails_to_ids {
if key == address { if key == address {
@ -100,7 +100,7 @@ impl MemoryDirectory {
Ok(result) 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)) Ok(self.domains.contains(domain))
} }
} }

View file

@ -7,36 +7,52 @@
use mail_send::{smtp::AssertReply, Credentials}; use mail_send::{smtp::AssertReply, Credentials};
use smtp_proto::Severity; use smtp_proto::Severity;
use crate::{DirectoryError, Principal, QueryBy}; use crate::{IntoError, Principal, QueryBy};
use super::{SmtpClient, SmtpDirectory}; use super::{SmtpClient, SmtpDirectory};
impl 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 { 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 { } 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>> { pub async fn email_to_ids(&self, _address: &str) -> trc::Result<Vec<u32>> {
Err(DirectoryError::unsupported("smtp", "email_to_ids")) Err(trc::Cause::Unsupported
.caused_by(trc::location!())
.protocol(trc::Protocol::Smtp))
} }
pub async fn rcpt(&self, address: &str) -> crate::Result<bool> { pub async fn rcpt(&self, address: &str) -> trc::Result<bool> {
let mut conn = self.pool.get().await?; let mut conn = self
.pool
.get()
.await
.map_err(|err| err.into_error().caused_by(trc::location!()))?;
if !conn.sent_mail_from { if !conn.sent_mail_from {
conn.client conn.client
.cmd(b"MAIL FROM:<>\r\n") .cmd(b"MAIL FROM:<>\r\n")
.await? .await
.assert_positive_completion()?; .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; conn.sent_mail_from = true;
} }
let reply = conn let reply = conn
.client .client
.cmd(format!("RCPT TO:<{address}>\r\n").as_bytes()) .cmd(format!("RCPT TO:<{address}>\r\n").as_bytes())
.await?; .await
.map_err(|err| err.into_error().caused_by(trc::location!()))?;
match reply.severity() { match reply.severity() {
Severity::PositiveCompletion => { Severity::PositiveCompletion => {
conn.num_rcpts += 1; conn.num_rcpts += 1;
@ -48,27 +64,32 @@ impl SmtpDirectory {
Ok(true) Ok(true)
} }
Severity::PermanentNegativeCompletion => Ok(false), 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 self.pool
.get() .get()
.await? .await
.map_err(|err| err.into_error().caused_by(trc::location!()))?
.expand(&format!("VRFY {address}\r\n")) .expand(&format!("VRFY {address}\r\n"))
.await .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 self.pool
.get() .get()
.await? .await
.map_err(|err| err.into_error().caused_by(trc::location!()))?
.expand(&format!("EXPN {address}\r\n")) .expand(&format!("EXPN {address}\r\n"))
.await .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)) Ok(self.domains.contains(domain))
} }
} }
@ -77,7 +98,7 @@ impl SmtpClient {
async fn authenticate( async fn authenticate(
&mut self, &mut self,
credentials: &Credentials<String>, credentials: &Credentials<String>,
) -> crate::Result<Option<Principal<u32>>> { ) -> trc::Result<Option<Principal<u32>>> {
match self match self
.client .client
.authenticate(credentials, &self.capabilities) .authenticate(credentials, &self.capabilities)
@ -89,21 +110,30 @@ impl SmtpClient {
self.num_auth_failures += 1; self.num_auth_failures += 1;
Ok(None) Ok(None)
} }
_ => Err(err.into()), _ => Err(err.into_error()),
}, },
} }
} }
async fn expand(&mut self, command: &str) -> crate::Result<Vec<String>> { async fn expand(&mut self, command: &str) -> trc::Result<Vec<String>> {
let reply = self.client.cmd(command.as_bytes()).await?; let reply = self
.client
.cmd(command.as_bytes())
.await
.map_err(|err| err.into_error().caused_by(trc::location!()))?;
match reply.code() { match reply.code() {
250 | 251 => Ok(reply 250 | 251 => Ok(reply
.message() .message()
.split('\n') .split('\n')
.map(|p| p.to_string()) .map(|p| p.to_string())
.collect::<Vec<String>>()), .collect::<Vec<String>>()),
550 | 551 | 553 | 500 | 502 => Err(DirectoryError::Unsupported), code @ (550 | 551 | 553 | 500 | 502) => Err(trc::Cause::Unsupported
_ => Err(mail_send::Error::UnexpectedReply(reply).into()), .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 mail_send::Credentials;
use store::{NamedRows, Rows, Value}; use store::{NamedRows, Rows, Value};
use trc::AddContext;
use crate::{backend::internal::manage::ManageDirectory, Principal, QueryBy, Type}; use crate::{backend::internal::manage::ManageDirectory, Principal, QueryBy, Type};
@ -16,7 +17,7 @@ impl SqlDirectory {
&self, &self,
by: QueryBy<'_>, by: QueryBy<'_>,
return_member_of: bool, return_member_of: bool,
) -> crate::Result<Option<Principal<u32>>> { ) -> trc::Result<Option<Principal<u32>>> {
let mut account_id = None; let mut account_id = None;
let account_name; let account_name;
let mut secret = None; let mut secret = None;
@ -27,10 +28,16 @@ impl SqlDirectory {
self.store self.store
.query::<NamedRows>(&self.mappings.query_name, vec![username.into()]) .query::<NamedRows>(&self.mappings.query_name, vec![username.into()])
.await? .await
.caused_by(trc::location!())?
} }
QueryBy::Id(uid) => { 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; account_name = username;
} else { } else {
return Ok(None); return Ok(None);
@ -42,7 +49,8 @@ impl SqlDirectory {
&self.mappings.query_name, &self.mappings.query_name,
vec![account_name.clone().into()], vec![account_name.clone().into()],
) )
.await? .await
.caused_by(trc::location!())?
} }
QueryBy::Credentials(credentials) => { QueryBy::Credentials(credentials) => {
let (username, secret_) = match credentials { let (username, secret_) = match credentials {
@ -55,7 +63,8 @@ impl SqlDirectory {
self.store self.store
.query::<NamedRows>(&self.mappings.query_name, vec![username.into()]) .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 // 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 // Validate password
if let Some(secret) = secret { if let Some(secret) = secret {
if !principal.verify_secret(secret).await? { if !principal
tracing::debug!( .verify_secret(secret)
context = "directory", .await
event = "invalid_password", .caused_by(trc::location!())?
protocol = "sql", {
account = account_name,
"Invalid password for account"
);
return Ok(None); return Ok(None);
} }
} }
@ -87,7 +96,8 @@ impl SqlDirectory {
principal.id = self principal.id = self
.data_store .data_store
.get_or_create_account_id(&account_name) .get_or_create_account_id(&account_name)
.await?; .await
.caused_by(trc::location!())?;
} }
principal.name = account_name; principal.name = account_name;
@ -99,13 +109,17 @@ impl SqlDirectory {
&self.mappings.query_members, &self.mappings.query_members,
vec![principal.name.clone().into()], vec![principal.name.clone().into()],
) )
.await? .await
.caused_by(trc::location!())?
.rows .rows
{ {
if let Some(Value::Text(account_id)) = row.values.first() { if let Some(Value::Text(account_id)) = row.values.first() {
principal principal.member_of.push(
.member_of self.data_store
.push(self.data_store.get_or_create_account_id(account_id).await?); .get_or_create_account_id(account_id)
.await
.caused_by(trc::location!())?,
);
} }
} }
} }
@ -118,31 +132,38 @@ impl SqlDirectory {
&self.mappings.query_emails, &self.mappings.query_emails,
vec![principal.name.clone().into()], vec![principal.name.clone().into()],
) )
.await? .await
.caused_by(trc::location!())?
.into(); .into();
} }
Ok(Some(principal)) 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 let names = self
.store .store
.query::<Rows>(&self.mappings.query_recipients, vec![address.into()]) .query::<Rows>(&self.mappings.query_recipients, vec![address.into()])
.await?; .await
.caused_by(trc::location!())?;
let mut ids = Vec::with_capacity(names.rows.len()); let mut ids = Vec::with_capacity(names.rows.len());
for row in names.rows { for row in names.rows {
if let Some(Value::Text(name)) = row.values.first() { 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) Ok(ids)
} }
pub async fn rcpt(&self, address: &str) -> crate::Result<bool> { pub async fn rcpt(&self, address: &str) -> trc::Result<bool> {
self.store self.store
.query::<bool>( .query::<bool>(
&self.mappings.query_recipients, &self.mappings.query_recipients,
@ -152,7 +173,7 @@ impl SqlDirectory {
.map_err(Into::into) .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 self.store
.query::<Rows>( .query::<Rows>(
&self.mappings.query_verify, &self.mappings.query_verify,
@ -163,7 +184,7 @@ impl SqlDirectory {
.map_err(Into::into) .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 self.store
.query::<Rows>( .query::<Rows>(
&self.mappings.query_expand, &self.mappings.query_expand,
@ -174,7 +195,7 @@ impl SqlDirectory {
.map_err(Into::into) .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 self.store
.query::<bool>(&self.mappings.query_domains, vec![domain.into()]) .query::<bool>(&self.mappings.query_domains, vec![domain.into()])
.await .await
@ -183,7 +204,7 @@ impl SqlDirectory {
} }
impl SqlMappings { 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(); let mut principal = Principal::default();
if let Some(row) = rows.rows.into_iter().next() { 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") .property_or_default::<bool>(("directory", id, "disable"), "false")
.unwrap_or(false) .unwrap_or(false)
{ {
tracing::debug!("Skipping disabled directory {id:?}.");
continue; continue;
} }
} }
@ -104,7 +103,7 @@ pub(crate) fn build_pool<M: Manager>(
config: &mut Config, config: &mut Config,
prefix: &str, prefix: &str,
manager: M, manager: M,
) -> utils::config::Result<Pool<M>> { ) -> Result<Pool<M>, String> {
Pool::builder(manager) Pool::builder(manager)
.runtime(Runtime::Tokio1) .runtime(Runtime::Tokio1)
.max_size( .max_size(

View file

@ -4,6 +4,8 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/ */
use trc::AddContext;
use crate::{ use crate::{
backend::internal::lookup::DirectoryStore, Directory, DirectoryInner, Principal, QueryBy, backend::internal::lookup::DirectoryStore, Directory, DirectoryInner, Principal, QueryBy,
}; };
@ -13,7 +15,7 @@ impl Directory {
&self, &self,
by: QueryBy<'_>, by: QueryBy<'_>,
return_member_of: bool, return_member_of: bool,
) -> crate::Result<Option<Principal<u32>>> { ) -> trc::Result<Option<Principal<u32>>> {
match &self.store { match &self.store {
DirectoryInner::Internal(store) => store.query(by, return_member_of).await, DirectoryInner::Internal(store) => store.query(by, return_member_of).await,
DirectoryInner::Ldap(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::Smtp(store) => store.query(by).await,
DirectoryInner::Memory(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 { match &self.store {
DirectoryInner::Internal(store) => store.email_to_ids(email).await, DirectoryInner::Internal(store) => store.email_to_ids(email).await,
DirectoryInner::Ldap(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::Smtp(store) => store.email_to_ids(email).await,
DirectoryInner::Memory(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 // Check cache
if let Some(cache) = &self.cache { if let Some(cache) = &self.cache {
if let Some(result) = cache.get_domain(domain) { if let Some(result) = cache.get_domain(domain) {
@ -50,7 +54,8 @@ impl Directory {
DirectoryInner::Imap(store) => store.is_local_domain(domain).await, DirectoryInner::Imap(store) => store.is_local_domain(domain).await,
DirectoryInner::Smtp(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, DirectoryInner::Memory(store) => store.is_local_domain(domain).await,
}?; }
.caused_by( trc::location!())?;
// Update cache // Update cache
if let Some(cache) = &self.cache { if let Some(cache) = &self.cache {
@ -60,7 +65,7 @@ impl Directory {
Ok(result) Ok(result)
} }
pub async fn rcpt(&self, email: &str) -> crate::Result<bool> { pub async fn rcpt(&self, email: &str) -> trc::Result<bool> {
// Check cache // Check cache
if let Some(cache) = &self.cache { if let Some(cache) = &self.cache {
if let Some(result) = cache.get_rcpt(email) { if let Some(result) = cache.get_rcpt(email) {
@ -75,7 +80,8 @@ impl Directory {
DirectoryInner::Imap(store) => store.rcpt(email).await, DirectoryInner::Imap(store) => store.rcpt(email).await,
DirectoryInner::Smtp(store) => store.rcpt(email).await, DirectoryInner::Smtp(store) => store.rcpt(email).await,
DirectoryInner::Memory(store) => store.rcpt(email).await, DirectoryInner::Memory(store) => store.rcpt(email).await,
}?; }
.caused_by( trc::location!())?;
// Update cache // Update cache
if let Some(cache) = &self.cache { if let Some(cache) = &self.cache {
@ -85,7 +91,7 @@ impl Directory {
Ok(result) 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 { match &self.store {
DirectoryInner::Internal(store) => store.vrfy(address).await, DirectoryInner::Internal(store) => store.vrfy(address).await,
DirectoryInner::Ldap(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::Smtp(store) => store.vrfy(address).await,
DirectoryInner::Memory(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 { match &self.store {
DirectoryInner::Internal(store) => store.expn(address).await, DirectoryInner::Internal(store) => store.expn(address).await,
DirectoryInner::Ldap(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::Smtp(store) => store.expn(address).await,
DirectoryInner::Memory(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 totp_rs::TOTP;
use crate::backend::internal::SpecialSecrets; use crate::backend::internal::SpecialSecrets;
use crate::DirectoryError;
use crate::Principal; use crate::Principal;
impl<T: serde::Serialize + serde::de::DeserializeOwned> Principal<T> { 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 totp_token = None;
let mut is_totp_token_missing = false; let mut is_totp_token_missing = false;
let mut is_totp_required = 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 // Token needs to validate with at least one of the TOTP secrets
is_totp_verified = TOTP::from_url(secret) 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) .check_current(totp_token)
.unwrap_or(false); .unwrap_or(false);
} }
@ -67,9 +66,9 @@ impl<T: serde::Serialize + serde::de::DeserializeOwned> Principal<T> {
if let Some((_, app_secret)) = if let Some((_, app_secret)) =
secret.strip_prefix("$app$").and_then(|s| s.split_once('$')) 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 { } 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 // Only let the client know if the TOTP code is missing
// if the password is correct // if the password is correct
Err(DirectoryError::MissingTotpCode) Err(trc::Cause::MissingParameter.into_err())
} else { } else {
// Return the TOTP verification status // Return the TOTP verification status
@ -97,7 +96,7 @@ impl<T: serde::Serialize + serde::de::DeserializeOwned> Principal<T> {
if is_totp_verified { if is_totp_verified {
// TOTP URL appeared after password hash in secrets list // TOTP URL appeared after password hash in secrets list
for secret in &self.secrets { 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); 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") if hashed_secret.starts_with("$argon2")
|| hashed_secret.starts_with("$pbkdf2") || hashed_secret.starts_with("$pbkdf2")
|| hashed_secret.starts_with("$scrypt") || 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) { tokio::task::spawn_blocking(move || match PasswordHash::new(&hashed_secret) {
Ok(hash) => { Ok(hash) => {
tx.send( tx.send(Ok(hash
hash.verify_password(&[&Argon2::default(), &Pbkdf2, &Scrypt], &secret) .verify_password(&[&Argon2::default(), &Pbkdf2, &Scrypt], &secret)
.is_ok(), .is_ok()))
) .ok();
.ok();
} }
Err(_) => { Err(err) => {
tracing::warn!( tx.send(Err(trc::Cause::Invalid.reason(err).details(hashed_secret)))
context = "directory", .ok();
event = "error",
hash = hashed_secret,
"Invalid password hash"
);
tx.send(false).ok();
} }
}); });
match rx.await { match rx.await {
Ok(result) => result, Ok(result) => result,
Err(_) => { Err(err) => Err(trc::Cause::Thread.reason(err)),
tracing::warn!(context = "directory", event = "error", "Thread join error");
false
}
} }
} else if hashed_secret.starts_with("$2") { } else if hashed_secret.starts_with("$2") {
// Blowfish crypt // Blowfish crypt
bcrypt::verify(secret, hashed_secret) Ok(bcrypt::verify(secret, hashed_secret))
} else if hashed_secret.starts_with("$6$") { } else if hashed_secret.starts_with("$6$") {
// SHA-512 crypt // SHA-512 crypt
sha512_crypt::verify(secret, hashed_secret) Ok(sha512_crypt::verify(secret, hashed_secret))
} else if hashed_secret.starts_with("$5$") { } else if hashed_secret.starts_with("$5$") {
// SHA-256 crypt // SHA-256 crypt
sha256_crypt::verify(secret, hashed_secret) Ok(sha256_crypt::verify(secret, hashed_secret))
} else if hashed_secret.starts_with("$sha1") { } else if hashed_secret.starts_with("$sha1") {
// SHA-1 crypt // SHA-1 crypt
sha1_crypt::verify(secret, hashed_secret) Ok(sha1_crypt::verify(secret, hashed_secret))
} else if hashed_secret.starts_with("$1") { } else if hashed_secret.starts_with("$1") {
// MD5 based hash // MD5 based hash
md5_crypt::verify(secret, hashed_secret) Ok(md5_crypt::verify(secret, hashed_secret))
} else { } else {
// Unknown hash Err(trc::Cause::Invalid
tracing::warn!( .into_err()
context = "directory", .details(hashed_secret.to_string()))
event = "error",
hash = hashed_secret,
"Invalid password hash"
);
false
} }
} }
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('$') { if hashed_secret.starts_with('$') {
verify_hash_prefix(hashed_secret, secret).await verify_hash_prefix(hashed_secret, secret).await
} else if hashed_secret.starts_with('_') { } else if hashed_secret.starts_with('_') {
// Enhanced DES-based hash // 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('{') { } else if let Some(hashed_secret) = hashed_secret.strip_prefix('{') {
if let Some((algo, hashed_secret)) = hashed_secret.split_once('}') { if let Some((algo, hashed_secret)) = hashed_secret.split_once('}') {
match algo { match algo {
@ -186,9 +171,13 @@ pub async fn verify_secret_hash(hashed_secret: &str, secret: &str) -> bool {
// SHA-1 // SHA-1
let mut hasher = Sha1::new(); let mut hasher = Sha1::new();
hasher.update(secret.as_bytes()); 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() .unwrap()
== hashed_secret == hashed_secret,
)
} }
"SSHA" => { "SSHA" => {
// Salted SHA-1 // Salted SHA-1
@ -198,15 +187,19 @@ pub async fn verify_secret_hash(hashed_secret: &str, secret: &str) -> bool {
let mut hasher = Sha1::new(); let mut hasher = Sha1::new();
hasher.update(secret.as_bytes()); hasher.update(secret.as_bytes());
hasher.update(salt); hasher.update(salt);
&hasher.finalize()[..] == hash Ok(&hasher.finalize()[..] == hash)
} }
"SHA256" => { "SHA256" => {
// Verify hash // Verify hash
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
hasher.update(secret.as_bytes()); 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() .unwrap()
== hashed_secret == hashed_secret,
)
} }
"SSHA256" => { "SSHA256" => {
// Salted SHA-256 // Salted SHA-256
@ -216,15 +209,19 @@ pub async fn verify_secret_hash(hashed_secret: &str, secret: &str) -> bool {
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
hasher.update(secret.as_bytes()); hasher.update(secret.as_bytes());
hasher.update(salt); hasher.update(salt);
&hasher.finalize()[..] == hash Ok(&hasher.finalize()[..] == hash)
} }
"SHA512" => { "SHA512" => {
// SHA-512 // SHA-512
let mut hasher = Sha512::new(); let mut hasher = Sha512::new();
hasher.update(secret.as_bytes()); 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() .unwrap()
== hashed_secret == hashed_secret,
)
} }
"SSHA512" => { "SSHA512" => {
// Salted SHA-512 // Salted SHA-512
@ -234,43 +231,35 @@ pub async fn verify_secret_hash(hashed_secret: &str, secret: &str) -> bool {
let mut hasher = Sha512::new(); let mut hasher = Sha512::new();
hasher.update(secret.as_bytes()); hasher.update(secret.as_bytes());
hasher.update(salt); hasher.update(salt);
&hasher.finalize()[..] == hash Ok(&hasher.finalize()[..] == hash)
} }
"MD5" => { "MD5" => {
// MD5 // MD5
let digest = md5::compute(secret.as_bytes()); let digest = md5::compute(secret.as_bytes());
String::from_utf8(base64_encode(&digest[..]).unwrap_or_default()).unwrap() Ok(
== hashed_secret String::from_utf8(base64_encode(&digest[..]).unwrap_or_default()).unwrap()
== hashed_secret,
)
} }
"CRYPT" | "crypt" => { "CRYPT" | "crypt" => {
if hashed_secret.starts_with('$') { if hashed_secret.starts_with('$') {
verify_hash_prefix(hashed_secret, secret).await verify_hash_prefix(hashed_secret, secret).await
} else { } else {
// Unix crypt // Unix crypt
unix_crypt::verify(secret, hashed_secret) Ok(unix_crypt::verify(secret, hashed_secret))
} }
} }
"PLAIN" | "plain" | "CLEAR" | "clear" => hashed_secret == secret, "PLAIN" | "plain" | "CLEAR" | "clear" => Ok(hashed_secret == secret),
_ => { _ => Err(trc::Cause::Invalid
tracing::warn!( .ctx(trc::Key::Reason, "Unsupported algorithm")
context = "directory", .details(hashed_secret.to_string())),
event = "error",
algorithm = algo,
"Unsupported password hash algorithm"
);
false
}
} }
} else { } else {
tracing::warn!( Err(trc::Cause::Invalid
context = "directory", .into_err()
event = "error", .details(hashed_secret.to_string()))
hash = hashed_secret,
"Invalid password hash"
);
false
} }
} else { } else {
hashed_secret == secret Ok(hashed_secret == secret)
} }
} }

View file

@ -5,15 +5,11 @@
*/ */
use core::cache::CachedDirectory; use core::cache::CachedDirectory;
use std::{ use std::{fmt::Debug, sync::Arc};
fmt::{Debug, Display},
sync::Arc,
};
use ahash::AHashMap; use ahash::AHashMap;
use backend::{ use backend::{
imap::{ImapDirectory, ImapError}, imap::{ImapDirectory, ImapError},
internal::PrincipalField,
ldap::LdapDirectory, ldap::LdapDirectory,
memory::MemoryDirectory, memory::MemoryDirectory,
smtp::SmtpDirectory, smtp::SmtpDirectory,
@ -23,7 +19,6 @@ use deadpool::managed::PoolError;
use ldap3::LdapError; use ldap3::LdapError;
use mail_send::Credentials; use mail_send::Credentials;
use store::Store; use store::Store;
use totp_rs::TotpUrlError;
pub mod backend; pub mod backend;
pub mod core; pub mod core;
@ -72,30 +67,6 @@ pub enum Type {
Other = 6, 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 { pub enum DirectoryInner {
Internal(Store), Internal(Store),
Ldap(LdapDirectory), Ldap(LdapDirectory),
@ -158,38 +129,6 @@ pub struct Directories {
pub directories: AHashMap<String, Arc<Directory>>, 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> { impl Principal<u32> {
pub fn fallback_admin(fallback_pass: impl Into<String>) -> Self { pub fn fallback_admin(fallback_pass: impl Into<String>) -> Self {
Principal { Principal {
@ -211,109 +150,70 @@ impl<T: Ord> Principal<T> {
} }
} }
impl From<LdapError> for DirectoryError { trait IntoError {
fn from(error: LdapError) -> Self { fn into_error(self) -> trc::Error;
tracing::warn!(
context = "directory",
event = "error",
protocol = "ldap",
reason = %error,
"LDAP directory error"
);
DirectoryError::Ldap(error)
}
} }
impl From<store::Error> for DirectoryError { impl IntoError for PoolError<LdapError> {
fn from(error: store::Error) -> Self { fn into_error(self) -> trc::Error {
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 {
match self { match self {
Self::Ldap(error) => write!(f, "LDAP error: {}", error), PoolError::Backend(error) => error.into_error(),
Self::Store(error) => write!(f, "Store error: {}", error), PoolError::Timeout(_) => {
Self::Imap(error) => write!(f, "IMAP error: {}", error), trc::Cause::Timeout.ctx(trc::Key::Protocol, trc::Protocol::Ldap)
Self::Smtp(error) => write!(f, "SMTP error: {}", error), }
Self::Pool(error) => write!(f, "Pool error: {}", error), err => trc::Cause::Pool
Self::Management(error) => write!(f, "Management error: {:?}", error), .ctx(trc::Key::Protocol, trc::Protocol::Ldap)
Self::TimedOut => write!(f, "Directory timed out"), .reason(err),
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"), }
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)
} }
} }
} }

View file

@ -9,6 +9,7 @@ imap_proto = { path = "../imap-proto" }
jmap = { path = "../jmap" } jmap = { path = "../jmap" }
jmap_proto = { path = "../jmap-proto" } jmap_proto = { path = "../jmap-proto" }
directory = { path = "../directory" } directory = { path = "../directory" }
trc = { path = "../trc" }
store = { path = "../store" } store = { path = "../store" }
common = { path = "../common" } common = { path = "../common" }
nlp = { path = "../nlp" } nlp = { path = "../nlp" }

View file

@ -422,7 +422,7 @@ impl<T: SessionStream> SessionData<T> {
&self, &self,
account_id: u32, account_id: u32,
id: u32, id: u32,
) -> Result<Option<(TagManager<UidMailbox>, u32)>, MethodError> { ) -> trc::Result<Option<(TagManager<UidMailbox>, u32)>> {
// Obtain mailbox tags // Obtain mailbox tags
if let (Some(mailboxes), Some(thread_id)) = ( if let (Some(mailboxes), Some(thread_id)) = (
self.jmap self.jmap

View file

@ -7,6 +7,7 @@ resolver = "2"
[dependencies] [dependencies]
store = { path = "../store" } store = { path = "../store" }
utils = { path = "../utils" } utils = { path = "../utils" }
trc = { path = "../trc" }
mail-parser = { version = "0.9", features = ["full_encoding", "serde_support", "ludicrous_mode"] } mail-parser = { version = "0.9", features = ["full_encoding", "serde_support", "ludicrous_mode"] }
fast-float = "0.2.0" fast-float = "0.2.0"
serde = { version = "1.0", features = ["derive"]} serde = { version = "1.0", features = ["derive"]}

View file

@ -31,6 +31,123 @@ pub enum MethodError {
UnknownDataType, UnknownDataType,
} }
#[derive(Debug)]
pub struct MethodErrorWrapper(trc::Error);
impl From<MethodError> for trc::Error {
fn from(value: MethodError) -> Self {
let (typ, description): (&'static str, trc::Value) = match value {
MethodError::InvalidArguments(description) => ("invalidArguments", description.into()),
MethodError::RequestTooLarge => (
"requestTooLarge",
concat!(
"The number of ids requested by the client exceeds the maximum number ",
"the server is willing to process in a single method call."
)
.into(),
),
MethodError::StateMismatch => (
"stateMismatch",
concat!(
"An \"ifInState\" argument was supplied, but ",
"it does not match the current state."
)
.into(),
),
MethodError::AnchorNotFound => (
"anchorNotFound",
concat!(
"An anchor argument was supplied, but it ",
"cannot be found in the results of the query."
)
.into(),
),
MethodError::UnsupportedFilter(description) => {
("unsupportedFilter", description.into())
}
MethodError::UnsupportedSort(description) => ("unsupportedSort", description.into()),
MethodError::ServerFail(_) => ("serverFail", {
concat!(
"An unexpected error occurred while processing ",
"this call, please contact the system administrator."
)
.into()
}),
MethodError::NotFound => ("serverPartialFail", {
concat!(
"One or more items are no longer available on the ",
"server, please try again."
)
.into()
}),
MethodError::UnknownMethod(description) => ("unknownMethod", description.into()),
MethodError::ServerUnavailable => (
"serverUnavailable",
concat!(
"This server is temporarily unavailable. ",
"Attempting this same operation later may succeed."
)
.into(),
),
MethodError::ServerPartialFail => (
"serverPartialFail",
concat!(
"Some, but not all, expected changes described by the method ",
"occurred. Please resynchronize to determine server state."
)
.into(),
),
MethodError::InvalidResultReference(description) => {
("invalidResultReference", description.into())
}
MethodError::Forbidden(description) => ("forbidden", description.into()),
MethodError::AccountNotFound => (
"accountNotFound",
"The accountId does not correspond to a valid account".into(),
),
MethodError::AccountNotSupportedByMethod => (
"accountNotSupportedByMethod",
concat!(
"The accountId given corresponds to a valid account, ",
"but the account does not support this method or data type."
)
.into(),
),
MethodError::AccountReadOnly => (
"accountReadOnly",
"This method modifies state, but the account is read-only.".into(),
),
MethodError::UnknownDataType => (
"unknownDataType",
concat!(
"The server does not recognise this data type, ",
"or the capability to enable it is not present ",
"in the current Request Object."
)
.into(),
),
MethodError::CannotCalculateChanges => (
"cannotCalculateChanges",
concat!(
"The server cannot calculate the changes ",
"between the old and new states."
)
.into(),
),
};
trc::Cause::Jmap
.ctx(trc::Key::Type, typ)
.ctx(trc::Key::Details, description)
}
}
impl From<trc::Error> for MethodErrorWrapper {
fn from(value: trc::Error) -> Self {
MethodErrorWrapper(value)
}
}
impl Display for MethodError { impl Display for MethodError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self { match self {
@ -60,103 +177,32 @@ impl Display for MethodError {
} }
} }
impl Serialize for MethodError { impl Serialize for MethodErrorWrapper {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where where
S: serde::Serializer, S: serde::Serializer,
{ {
let mut map = serializer.serialize_map(2.into())?; let mut map = serializer.serialize_map(2.into())?;
let (error_type, description) = match self { let (error_type, description) = if self.0.matches(trc::Cause::Jmap) {
MethodError::InvalidArguments(description) => { (
("invalidArguments", description.as_str()) self.0
} .value(trc::Key::Type)
MethodError::RequestTooLarge => ( .and_then(|v| v.as_str())
"requestTooLarge", .unwrap(),
concat!( self.0
"The number of ids requested by the client exceeds the maximum number ", .value(trc::Key::Details)
"the server is willing to process in a single method call." .and_then(|v| v.as_str())
), .unwrap(),
), )
MethodError::StateMismatch => ( } else {
"stateMismatch", (
concat!(
"An \"ifInState\" argument was supplied, but ",
"it does not match the current state."
),
),
MethodError::AnchorNotFound => (
"anchorNotFound",
concat!(
"An anchor argument was supplied, but it ",
"cannot be found in the results of the query."
),
),
MethodError::UnsupportedFilter(description) => {
("unsupportedFilter", description.as_str())
}
MethodError::UnsupportedSort(description) => ("unsupportedSort", description.as_str()),
MethodError::ServerFail(_) => ("serverFail", {
concat!(
"An unexpected error occurred while processing ",
"this call, please contact the system administrator."
)
}),
MethodError::NotFound => ("serverPartialFail", {
concat!(
"One or more items are no longer available on the ",
"server, please try again."
)
}),
MethodError::UnknownMethod(description) => ("unknownMethod", description.as_str()),
MethodError::ServerUnavailable => (
"serverUnavailable", "serverUnavailable",
concat!( concat!(
"This server is temporarily unavailable. ", "This server is temporarily unavailable. ",
"Attempting this same operation later may succeed." "Attempting this same operation later may succeed."
), ),
), )
MethodError::ServerPartialFail => (
"serverPartialFail",
concat!(
"Some, but not all, expected changes described by the method ",
"occurred. Please resynchronize to determine server state."
),
),
MethodError::InvalidResultReference(description) => {
("invalidResultReference", description.as_str())
}
MethodError::Forbidden(description) => ("forbidden", description.as_str()),
MethodError::AccountNotFound => (
"accountNotFound",
"The accountId does not correspond to a valid account",
),
MethodError::AccountNotSupportedByMethod => (
"accountNotSupportedByMethod",
concat!(
"The accountId given corresponds to a valid account, ",
"but the account does not support this method or data type."
),
),
MethodError::AccountReadOnly => (
"accountReadOnly",
"This method modifies state, but the account is read-only.",
),
MethodError::UnknownDataType => (
"unknownDataType",
concat!(
"The server does not recognise this data type, ",
"or the capability to enable it is not present ",
"in the current Request Object."
),
),
MethodError::CannotCalculateChanges => (
"cannotCalculateChanges",
concat!(
"The server cannot calculate the changes ",
"between the old and new states."
),
),
}; };
map.serialize_entry("type", error_type)?; map.serialize_entry("type", error_type)?;

View file

@ -171,10 +171,7 @@ impl<T> GetRequest<T> {
} }
} }
pub fn unwrap_ids( pub fn unwrap_ids(&mut self, max_objects_in_get: usize) -> trc::Result<Option<Vec<Id>>> {
&mut self,
max_objects_in_get: usize,
) -> Result<Option<Vec<Id>>, MethodError> {
if let Some(ids) = self.ids.take() { if let Some(ids) = self.ids.take() {
let ids = ids.unwrap(); let ids = ids.unwrap();
if ids.len() <= max_objects_in_get { if ids.len() <= max_objects_in_get {
@ -184,7 +181,7 @@ impl<T> GetRequest<T> {
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
)) ))
} else { } else {
Err(MethodError::RequestTooLarge) Err(MethodError::RequestTooLarge.into())
} }
} else { } else {
Ok(None) Ok(None)
@ -194,7 +191,7 @@ impl<T> GetRequest<T> {
pub fn unwrap_blob_ids( pub fn unwrap_blob_ids(
&mut self, &mut self,
max_objects_in_get: usize, max_objects_in_get: usize,
) -> Result<Option<Vec<BlobId>>, MethodError> { ) -> trc::Result<Option<Vec<BlobId>>> {
if let Some(ids) = self.ids.take() { if let Some(ids) = self.ids.take() {
let ids = ids.unwrap(); let ids = ids.unwrap();
if ids.len() <= max_objects_in_get { if ids.len() <= max_objects_in_get {
@ -204,7 +201,7 @@ impl<T> GetRequest<T> {
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
)) ))
} else { } else {
Err(MethodError::RequestTooLarge) Err(MethodError::RequestTooLarge.into())
} }
} else { } else {
Ok(None) Ok(None)

View file

@ -400,7 +400,7 @@ impl RequestPropertyParser for RequestArguments {
} }
impl<T> SetRequest<T> { impl<T> SetRequest<T> {
pub fn validate(&self, max_objects_in_set: usize) -> Result<(), MethodError> { pub fn validate(&self, max_objects_in_set: usize) -> trc::Result<()> {
if self.create.as_ref().map_or(0, |objs| objs.len()) if self.create.as_ref().map_or(0, |objs| objs.len())
+ self.update.as_ref().map_or(0, |objs| objs.len()) + self.update.as_ref().map_or(0, |objs| objs.len())
+ self.destroy.as_ref().map_or(0, |objs| { + self.destroy.as_ref().map_or(0, |objs| {
@ -412,7 +412,7 @@ impl<T> SetRequest<T> {
}) })
> max_objects_in_set > max_objects_in_set
{ {
Err(MethodError::RequestTooLarge) Err(MethodError::RequestTooLarge.into())
} else { } else {
Ok(()) Ok(())
} }
@ -460,10 +460,7 @@ impl SetRequest<RequestArguments> {
} }
impl SetResponse { impl SetResponse {
pub fn from_request<T>( pub fn from_request<T>(request: &SetRequest<T>, max_objects: usize) -> trc::Result<Self> {
request: &SetRequest<T>,
max_objects: usize,
) -> Result<Self, MethodError> {
let n_create = request.create.as_ref().map_or(0, |objs| objs.len()); let n_create = request.create.as_ref().map_or(0, |objs| objs.len());
let n_update = request.update.as_ref().map_or(0, |objs| objs.len()); let n_update = request.update.as_ref().map_or(0, |objs| objs.len());
let n_destroy = request.destroy.as_ref().map_or(0, |objs| { let n_destroy = request.destroy.as_ref().map_or(0, |objs| {
@ -491,7 +488,7 @@ impl SetResponse {
state_change: None, state_change: None,
}) })
} else { } else {
Err(MethodError::RequestTooLarge) Err(MethodError::RequestTooLarge.into())
} }
} }

View file

@ -122,9 +122,12 @@ impl Serialize for Value {
} }
impl Deserialize for Value { impl Deserialize for Value {
fn deserialize(bytes: &[u8]) -> store::Result<Self> { fn deserialize(bytes: &[u8]) -> trc::Result<Self> {
Self::deserialize_from(&mut bytes.iter()) Self::deserialize_from(&mut bytes.iter()).ok_or_else(|| {
.ok_or_else(|| store::Error::InternalError("Failed to deserialize value.".to_string())) trc::Cause::DataCorruption
.caused_by(trc::location!())
.ctx(trc::Key::Value, bytes)
})
} }
} }
@ -143,9 +146,12 @@ impl Serialize for &Object<Value> {
} }
impl Deserialize for Object<Value> { impl Deserialize for Object<Value> {
fn deserialize(bytes: &[u8]) -> store::Result<Self> { fn deserialize(bytes: &[u8]) -> trc::Result<Self> {
Object::deserialize_from(&mut bytes.iter()) Object::deserialize_from(&mut bytes.iter()).ok_or_else(|| {
.ok_or_else(|| store::Error::InternalError("Failed to deserialize object.".to_string())) trc::Cause::DataCorruption
.caused_by(trc::location!())
.ctx(trc::Key::Value, bytes)
})
} }
} }

View file

@ -17,7 +17,6 @@ use std::{
}; };
use crate::{ use crate::{
error::method::MethodError,
method::{ method::{
changes::ChangesRequest, changes::ChangesRequest,
copy::{self, CopyBlobRequest, CopyRequest}, copy::{self, CopyBlobRequest, CopyRequest},
@ -74,7 +73,7 @@ pub enum RequestMethod {
LookupBlob(BlobLookupRequest), LookupBlob(BlobLookupRequest),
UploadBlob(BlobUploadRequest), UploadBlob(BlobUploadRequest),
Echo(Echo), Echo(Echo),
Error(MethodError), Error(trc::Error),
} }
impl JsonObjectParser for RequestProperty { impl JsonObjectParser for RequestProperty {

View file

@ -178,7 +178,7 @@ impl Request {
Ok(method) => method, Ok(method) => method,
Err(Error::Method(err)) => { Err(Error::Method(err)) => {
parser.skip_token(start_depth_array, start_depth_dict)?; parser.skip_token(start_depth_array, start_depth_dict)?;
RequestMethod::Error(err) RequestMethod::Error(err.into())
} }
Err(err) => { Err(err) => {
return Err(err.into()); return Err(err.into());

View file

@ -10,7 +10,7 @@ pub mod serialize;
use std::collections::HashMap; use std::collections::HashMap;
use crate::{ use crate::{
error::method::MethodError, error::method::MethodErrorWrapper,
method::{ method::{
changes::ChangesResponse, changes::ChangesResponse,
copy::{CopyBlobResponse, CopyResponse}, copy::{CopyBlobResponse, CopyResponse},
@ -48,7 +48,7 @@ pub enum ResponseMethod {
LookupBlob(BlobLookupResponse), LookupBlob(BlobLookupResponse),
UploadBlob(BlobUploadResponse), UploadBlob(BlobUploadResponse),
Echo(Echo), Echo(Echo),
Error(MethodError), Error(MethodErrorWrapper),
} }
#[derive(Debug, serde::Serialize)] #[derive(Debug, serde::Serialize)]
@ -87,10 +87,10 @@ impl Response {
}); });
} }
pub fn push_error(&mut self, id: String, err: MethodError) { pub fn push_error(&mut self, id: String, err: impl Into<MethodErrorWrapper>) {
self.method_responses.push(Call { self.method_responses.push(Call {
id, id,
method: ResponseMethod::Error(err), method: ResponseMethod::Error(err.into()),
name: MethodName::error(), name: MethodName::error(),
}); });
} }
@ -100,9 +100,9 @@ impl Response {
} }
} }
impl From<MethodError> for ResponseMethod { impl From<trc::Error> for ResponseMethod {
fn from(error: MethodError) -> Self { fn from(error: trc::Error) -> Self {
ResponseMethod::Error(error) ResponseMethod::Error(error.into())
} }
} }
@ -190,8 +190,8 @@ impl From<BlobLookupResponse> for ResponseMethod {
} }
} }
impl<T: Into<ResponseMethod>> From<Result<T, MethodError>> for ResponseMethod { impl<T: Into<ResponseMethod>> From<trc::Result<T>> for ResponseMethod {
fn from(result: Result<T, MethodError>) -> Self { fn from(result: trc::Result<T>) -> Self {
match result { match result {
Ok(value) => value.into(), Ok(value) => value.into(),
Err(error) => error.into(), Err(error) => error.into(),

View file

@ -33,7 +33,7 @@ enum EvalResult {
} }
impl Response { impl Response {
pub fn resolve_references(&self, request: &mut RequestMethod) -> Result<(), MethodError> { pub fn resolve_references(&self, request: &mut RequestMethod) -> trc::Result<()> {
match request { match request {
RequestMethod::Get(request) => { RequestMethod::Get(request) => {
// Resolve id references // Resolve id references
@ -52,7 +52,8 @@ impl Response {
} else { } else {
return Err(MethodError::InvalidResultReference(format!( return Err(MethodError::InvalidResultReference(format!(
"Id reference {reference:?} does not exist." "Id reference {reference:?} does not exist."
))); ))
.into());
} }
} }
} }
@ -151,7 +152,8 @@ impl Response {
Some(_) => { Some(_) => {
return Err(MethodError::InvalidResultReference(format!( return Err(MethodError::InvalidResultReference(format!(
"Id reference {parent_id:?} points to invalid type." "Id reference {parent_id:?} points to invalid type."
))); ))
.into());
} }
None => { None => {
graph graph
@ -246,13 +248,14 @@ impl Response {
EvalResult::Failed EvalResult::Failed
} }
fn eval_id_reference(&self, ir: &str) -> Result<Id, MethodError> { fn eval_id_reference(&self, ir: &str) -> trc::Result<Id> {
if let Some(AnyId::Id(id)) = self.created_ids.get(ir) { if let Some(AnyId::Id(id)) = self.created_ids.get(ir) {
Ok(*id) Ok(*id)
} else { } else {
Err(MethodError::InvalidResultReference(format!( Err(
"Id reference {ir:?} not found." MethodError::InvalidResultReference(format!("Id reference {ir:?} not found."))
))) .into(),
)
} }
} }
@ -260,7 +263,7 @@ impl Response {
&self, &self,
obj: &mut Object<SetValue>, obj: &mut Object<SetValue>,
mut graph: Option<(&str, &mut HashMap<String, Vec<String>>)>, mut graph: Option<(&str, &mut HashMap<String, Vec<String>>)>,
) -> Result<(), MethodError> { ) -> trc::Result<()> {
for set_value in obj.properties.values_mut() { for set_value in obj.properties.values_mut() {
match set_value { match set_value {
SetValue::IdReference(MaybeReference::Reference(parent_id)) => { SetValue::IdReference(MaybeReference::Reference(parent_id)) => {
@ -274,7 +277,8 @@ impl Response {
} else { } else {
return Err(MethodError::InvalidResultReference(format!( return Err(MethodError::InvalidResultReference(format!(
"Id reference {parent_id:?} not found." "Id reference {parent_id:?} not found."
))); ))
.into());
} }
} }
SetValue::IdReferences(id_refs) => { SetValue::IdReferences(id_refs) => {
@ -290,7 +294,8 @@ impl Response {
} else { } else {
return Err(MethodError::InvalidResultReference(format!( return Err(MethodError::InvalidResultReference(format!(
"Id reference {parent_id:?} not found." "Id reference {parent_id:?} not found."
))); ))
.into());
} }
} }
} }
@ -310,14 +315,15 @@ impl Response {
fn topological_sort<T>( fn topological_sort<T>(
create: &mut VecMap<String, T>, create: &mut VecMap<String, T>,
graph: HashMap<String, Vec<String>>, graph: HashMap<String, Vec<String>>,
) -> Result<VecMap<String, T>, MethodError> { ) -> trc::Result<VecMap<String, T>> {
// Make sure all references exist // Make sure all references exist
for (from_id, to_ids) in graph.iter() { for (from_id, to_ids) in graph.iter() {
for to_id in to_ids { for to_id in to_ids {
if !create.contains_key(to_id) { if !create.contains_key(to_id) {
return Err(MethodError::InvalidResultReference(format!( return Err(MethodError::InvalidResultReference(format!(
"Invalid reference to non-existing object {to_id:?} from {from_id:?}" "Invalid reference to non-existing object {to_id:?} from {from_id:?}"
))); ))
.into());
} }
} }
} }
@ -334,7 +340,8 @@ fn topological_sort<T>(
if it_stack.len() > 1000 { if it_stack.len() > 1000 {
return Err(MethodError::InvalidArguments( return Err(MethodError::InvalidArguments(
"Cyclical references are not allowed.".to_string(), "Cyclical references are not allowed.".to_string(),
)); )
.into());
} }
it = to_ids.iter(); it = to_ids.iter();
continue; continue;
@ -437,7 +444,7 @@ impl EvalObjectReferences for CopyResponse {
} }
impl EvalResult { impl EvalResult {
pub fn unwrap_ids(self, rr: &ResultReference) -> Result<Vec<Id>, MethodError> { pub fn unwrap_ids(self, rr: &ResultReference) -> trc::Result<Vec<Id>> {
if let EvalResult::Values(values) = self { if let EvalResult::Values(values) = self {
let mut ids = Vec::with_capacity(values.len()); let mut ids = Vec::with_capacity(values.len());
for value in values { for value in values {
@ -450,7 +457,8 @@ impl EvalResult {
_ => { _ => {
return Err(MethodError::InvalidResultReference(format!( return Err(MethodError::InvalidResultReference(format!(
"Failed to evaluate {rr} result reference." "Failed to evaluate {rr} result reference."
))); ))
.into());
} }
} }
} }
@ -458,7 +466,8 @@ impl EvalResult {
_ => { _ => {
return Err(MethodError::InvalidResultReference(format!( return Err(MethodError::InvalidResultReference(format!(
"Failed to evaluate {rr} result reference." "Failed to evaluate {rr} result reference."
))) ))
.into())
} }
} }
} }
@ -466,14 +475,15 @@ impl EvalResult {
} else { } else {
Err(MethodError::InvalidResultReference(format!( Err(MethodError::InvalidResultReference(format!(
"Failed to evaluate {rr} result reference." "Failed to evaluate {rr} result reference."
))) ))
.into())
} }
} }
pub fn unwrap_any_ids( pub fn unwrap_any_ids(
self, self,
rr: &ResultReference, rr: &ResultReference,
) -> Result<Vec<MaybeReference<AnyId, String>>, MethodError> { ) -> trc::Result<Vec<MaybeReference<AnyId, String>>> {
if let EvalResult::Values(values) = self { if let EvalResult::Values(values) = self {
let mut ids = Vec::with_capacity(values.len()); let mut ids = Vec::with_capacity(values.len());
for value in values { for value in values {
@ -490,7 +500,8 @@ impl EvalResult {
_ => { _ => {
return Err(MethodError::InvalidResultReference(format!( return Err(MethodError::InvalidResultReference(format!(
"Failed to evaluate {rr} result reference." "Failed to evaluate {rr} result reference."
))); ))
.into());
} }
} }
} }
@ -498,7 +509,8 @@ impl EvalResult {
_ => { _ => {
return Err(MethodError::InvalidResultReference(format!( return Err(MethodError::InvalidResultReference(format!(
"Failed to evaluate {rr} result reference." "Failed to evaluate {rr} result reference."
))) ))
.into())
} }
} }
} }
@ -506,17 +518,19 @@ impl EvalResult {
} else { } else {
Err(MethodError::InvalidResultReference(format!( Err(MethodError::InvalidResultReference(format!(
"Failed to evaluate {rr} result reference." "Failed to evaluate {rr} result reference."
))) ))
.into())
} }
} }
pub fn unwrap_properties(self, rr: &ResultReference) -> Result<Vec<Property>, MethodError> { pub fn unwrap_properties(self, rr: &ResultReference) -> trc::Result<Vec<Property>> {
if let EvalResult::Properties(properties) = self { if let EvalResult::Properties(properties) = self {
Ok(properties) Ok(properties)
} else { } else {
Err(MethodError::InvalidResultReference(format!( Err(MethodError::InvalidResultReference(format!(
"Failed to evaluate {rr} result reference." "Failed to evaluate {rr} result reference."
))) ))
.into())
} }
} }
} }
@ -526,7 +540,6 @@ mod tests {
use std::collections::HashMap; use std::collections::HashMap;
use crate::{ use crate::{
error::method::MethodError,
request::{Request, RequestMethod}, request::{Request, RequestMethod},
response::Response, response::Response,
types::{ types::{
@ -685,7 +698,10 @@ mod tests {
), ),
Err(err) => { Err(err) => {
assert_eq!(test_num, 3); assert_eq!(test_num, 3);
assert!(matches!(err, MethodError::InvalidArguments(_))); assert!(matches!(
err.value(trc::Key::Type).and_then(|v| v.as_str()).unwrap(),
"invalidArguments"
));
continue; continue;
} }
} }

View file

@ -12,6 +12,7 @@ smtp = { path = "../smtp" }
utils = { path = "../utils" } utils = { path = "../utils" }
common = { path = "../common" } common = { path = "../common" }
directory = { path = "../directory" } directory = { path = "../directory" }
trc = { path = "../trc" }
smtp-proto = { version = "0.1" } smtp-proto = { version = "0.1" }
mail-parser = { version = "0.9", features = ["full_encoding", "serde_support", "ludicrous_mode"] } mail-parser = { version = "0.9", features = ["full_encoding", "serde_support", "ludicrous_mode"] }
mail-builder = { version = "0.3", features = ["ludicrous_mode"] } mail-builder = { version = "0.3", features = ["ludicrous_mode"] }

View file

@ -529,7 +529,7 @@ impl<T: serde::Serialize> ToHttpResponse for JsonResponse<T> {
} }
} }
impl ToHttpResponse for store::Error { impl ToHttpResponse for trc::Error {
fn into_http_response(self) -> HttpResponse { fn into_http_response(self) -> HttpResponse {
tracing::error!(context = "store", error = %self, "Database error"); tracing::error!(context = "store", error = %self, "Database error");

View file

@ -66,6 +66,7 @@ impl JMAP {
return RequestError::not_found().into_http_response(); return RequestError::not_found().into_http_response();
} }
}; };
let todo = "bubble up error and log them";
let (pk, algo) = match ( let (pk, algo) = match (
self.core self.core
@ -163,7 +164,7 @@ impl JMAP {
id: impl AsRef<str>, id: impl AsRef<str>,
domain: impl Into<String>, domain: impl Into<String>,
selector: impl Into<String>, selector: impl Into<String>,
) -> store::Result<()> { ) -> trc::Result<()> {
let id = id.as_ref(); let id = id.as_ref();
let (algorithm, pk_type) = match algo { let (algorithm, pk_type) = match algo {
Algorithm::Rsa => ("rsa-sha256", "RSA PRIVATE KEY"), Algorithm::Rsa => ("rsa-sha256", "RSA PRIVATE KEY"),
@ -176,7 +177,7 @@ impl JMAP {
Algorithm::Rsa => DkimKeyPair::generate_rsa(2048), Algorithm::Rsa => DkimKeyPair::generate_rsa(2048),
Algorithm::Ed25519 => DkimKeyPair::generate_ed25519(), Algorithm::Ed25519 => DkimKeyPair::generate_ed25519(),
} }
.map_err(|err| store::Error::InternalError(err.to_string()))? .map_err(|err| trc::Cause::Crypto.reason(err).caused_by(trc::location!()))?
.private_key(), .private_key(),
) )
.unwrap_or_default() .unwrap_or_default()

View file

@ -123,7 +123,7 @@ impl JMAP {
} }
} }
async fn build_dns_records(&self, domain_name: &str) -> store::Result<Vec<DnsRecord>> { async fn build_dns_records(&self, domain_name: &str) -> trc::Result<Vec<DnsRecord>> {
// Obtain server name // Obtain server name
let server_name = self let server_name = self
.core .core

View file

@ -24,7 +24,7 @@ use crate::{
api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}, api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse},
email::ingest::{IngestEmail, IngestSource}, email::ingest::{IngestEmail, IngestSource},
mailbox::INBOX_ID, mailbox::INBOX_ID,
IngestError, JMAP, JMAP,
}; };
#[derive(serde::Deserialize)] #[derive(serde::Deserialize)]
@ -185,8 +185,10 @@ impl JMAP {
batch.clear(ValueClass::Blob(BlobOp::Reserve { hash: request.hash, until: cancel_deletion as u64 })); batch.clear(ValueClass::Blob(BlobOp::Reserve { hash: request.hash, until: cancel_deletion as u64 }));
} }
}, },
Err(IngestError::Permanent { reason, .. }) => { Err(mut err) if err.matches(trc::Cause::Ingest) => {
results.push(UndeleteResponse::Error { reason }); results.push(UndeleteResponse::Error { reason: err.take_value(trc::Key::Reason)
.and_then(|v| v.into_string())
.unwrap().into_owned() });
} }
Err(_) => { Err(_) => {
return RequestError::internal_server_error().into_http_response(); return RequestError::internal_server_error().into_http_response();

View file

@ -11,7 +11,7 @@ use directory::{
lookup::DirectoryStore, manage::ManageDirectory, PrincipalAction, PrincipalField, lookup::DirectoryStore, manage::ManageDirectory, PrincipalAction, PrincipalField,
PrincipalUpdate, PrincipalValue, SpecialSecrets, PrincipalUpdate, PrincipalValue, SpecialSecrets,
}, },
DirectoryError, DirectoryInner, ManagementError, Principal, QueryBy, Type, DirectoryInner, Principal, QueryBy, Type,
}; };
use hyper::{header, Method, StatusCode}; use hyper::{header, Method, StatusCode};
@ -116,7 +116,7 @@ impl JMAP {
"data": account_id, "data": account_id,
})) }))
.into_http_response(), .into_http_response(),
Err(err) => err.into_http_response(), Err(err) => into_directory_response(err),
} }
} }
Err(err) => err.into_http_response(), Err(err) => err.into_http_response(),
@ -150,7 +150,7 @@ impl JMAP {
})) }))
.into_http_response() .into_http_response()
} }
Err(err) => err.into_http_response(), Err(err) => into_directory_response(err),
} }
} }
(Some(name), method) => { (Some(name), method) => {
@ -167,7 +167,7 @@ impl JMAP {
.into_http_response(); .into_http_response();
} }
Err(err) => { Err(err) => {
return err.into_http_response(); return into_directory_response(err);
} }
}; };
@ -227,7 +227,7 @@ impl JMAP {
})) }))
.into_http_response() .into_http_response()
} }
Err(err) => err.into_http_response(), Err(err) => into_directory_response(err),
} }
} }
Method::DELETE => { Method::DELETE => {
@ -253,7 +253,7 @@ impl JMAP {
})) }))
.into_http_response() .into_http_response()
} }
Err(err) => err.into_http_response(), Err(err) => into_directory_response(err),
} }
} }
Method::PATCH => { Method::PATCH => {
@ -296,7 +296,7 @@ impl JMAP {
})) }))
.into_http_response() .into_http_response()
} }
Err(err) => err.into_http_response(), Err(err) => into_directory_response(err),
} }
} }
Err(err) => err.into_http_response(), Err(err) => err.into_http_response(),
@ -339,7 +339,7 @@ impl JMAP {
Ok(None) => { Ok(None) => {
return RequestError::not_found().into_http_response(); return RequestError::not_found().into_http_response();
} }
Err(err) => return err.into_http_response(), Err(err) => return into_directory_response(err),
} }
} }
@ -477,7 +477,7 @@ impl JMAP {
})) }))
.into_http_response() .into_http_response()
} }
Err(err) => err.into_http_response(), Err(err) => into_directory_response(err),
} }
} }
@ -515,40 +515,47 @@ impl From<Principal<String>> for PrincipalResponse {
} }
} }
impl ToHttpResponse for DirectoryError { fn into_directory_response(mut error: trc::Error) -> HttpResponse {
fn into_http_response(self) -> HttpResponse { let response = match error.as_ref() {
match self { trc::Cause::MissingParameter => ManagementApiError::FieldMissing {
DirectoryError::Management(err) => { field: error
let response = match err { .take_value(trc::Key::Key)
ManagementError::MissingField(field) => ManagementApiError::FieldMissing { .and_then(|v| v.into_string())
field: field.to_string().into(), .unwrap_or_default(),
}, },
ManagementError::AlreadyExists { field, value } => { trc::Cause::AlreadyExists => ManagementApiError::FieldAlreadyExists {
ManagementApiError::FieldAlreadyExists { field: error
field: field.to_string().into(), .take_value(trc::Key::Key)
value: value.into(), .and_then(|v| v.into_string())
} .unwrap_or_default(),
} value: error
ManagementError::NotFound(details) => ManagementApiError::NotFound { .take_value(trc::Key::Value)
item: details.into(), .and_then(|v| v.into_string())
}, .unwrap_or_default(),
}; },
JsonResponse::new(response).into_http_response() trc::Cause::NotFound => ManagementApiError::NotFound {
} item: error
DirectoryError::Unsupported => JsonResponse::new(ManagementApiError::Unsupported { .take_value(trc::Key::Key)
.and_then(|v| v.into_string())
.unwrap_or_default(),
},
trc::Cause::Unsupported => {
return JsonResponse::new(ManagementApiError::Unsupported {
details: "Requested action is unsupported".into(), details: "Requested action is unsupported".into(),
}) })
.into_http_response(), .into_http_response();
err => {
tracing::warn!(
context = "directory",
event = "error",
reason = ?err,
"Directory error"
);
RequestError::internal_server_error().into_http_response()
}
} }
} _ => {
tracing::warn!(
context = "directory",
event = "error",
reason = ?error,
"Directory error"
);
return RequestError::internal_server_error().into_http_response();
}
};
JsonResponse::new(response).into_http_response()
} }

View file

@ -113,7 +113,7 @@ impl JMAP {
access_token: &AccessToken, access_token: &AccessToken,
next_call: &mut Option<Call<RequestMethod>>, next_call: &mut Option<Call<RequestMethod>>,
instance: &Arc<ServerInstance>, instance: &Arc<ServerInstance>,
) -> Result<ResponseMethod, MethodError> { ) -> trc::Result<ResponseMethod> {
Ok(match method { Ok(match method {
RequestMethod::Get(mut req) => match req.take_arguments() { RequestMethod::Get(mut req) => match req.take_arguments() {
get::RequestArguments::Email(arguments) => { get::RequestArguments::Email(arguments) => {
@ -162,7 +162,8 @@ impl JMAP {
} else { } else {
return Err(MethodError::Forbidden( return Err(MethodError::Forbidden(
"Principal lookups are disabled".to_string(), "Principal lookups are disabled".to_string(),
)); )
.into());
} }
} }
get::RequestArguments::Quota => { get::RequestArguments::Quota => {
@ -209,7 +210,8 @@ impl JMAP {
} else { } else {
return Err(MethodError::Forbidden( return Err(MethodError::Forbidden(
"Principal lookups are disabled".to_string(), "Principal lookups are disabled".to_string(),
)); )
.into());
} }
} }
query::RequestArguments::Quota => { query::RequestArguments::Quota => {

View file

@ -6,7 +6,7 @@
use directory::QueryBy; use directory::QueryBy;
use jmap_proto::{ use jmap_proto::{
error::{method::MethodError, set::SetError}, error::set::SetError,
object::Object, object::Object,
types::{ types::{
acl::Acl, acl::Acl,
@ -21,6 +21,7 @@ use store::{
write::{assert::HashedValue, ValueClass}, write::{assert::HashedValue, ValueClass},
ValueKey, ValueKey,
}; };
use trc::AddContext;
use utils::map::bitmap::{Bitmap, BitmapItem}; use utils::map::bitmap::{Bitmap, BitmapItem};
use crate::JMAP; use crate::JMAP;
@ -96,7 +97,7 @@ impl JMAP {
to_account_id: u32, to_account_id: u32,
to_collection: Collection, to_collection: Collection,
check_acls: impl Into<Bitmap<Acl>>, check_acls: impl Into<Bitmap<Acl>>,
) -> Result<RoaringBitmap, MethodError> { ) -> trc::Result<RoaringBitmap> {
let check_acls = check_acls.into(); let check_acls = check_acls.into();
let mut document_ids = RoaringBitmap::new(); let mut document_ids = RoaringBitmap::new();
let to_collection = u8::from(to_collection); let to_collection = u8::from(to_collection);
@ -114,14 +115,7 @@ impl JMAP {
to_collection, to_collection,
}) })
.await .await
.map_err(|err| { .caused_by(trc::location!())?
tracing::error!(
event = "error",
context = "shared_documents",
error = ?err,
"Failed to iterate ACLs.");
MethodError::ServerPartialFail
})?
{ {
let mut acls = Bitmap::<Acl>::from(acl_item.permissions); let mut acls = Bitmap::<Acl>::from(acl_item.permissions);
@ -140,7 +134,7 @@ impl JMAP {
access_token: &AccessToken, access_token: &AccessToken,
to_account_id: u32, to_account_id: u32,
check_acls: impl Into<Bitmap<Acl>>, check_acls: impl Into<Bitmap<Acl>>,
) -> Result<RoaringBitmap, MethodError> { ) -> trc::Result<RoaringBitmap> {
let check_acls = check_acls.into(); let check_acls = check_acls.into();
let shared_mailboxes = self let shared_mailboxes = self
.shared_documents(access_token, to_account_id, Collection::Mailbox, check_acls) .shared_documents(access_token, to_account_id, Collection::Mailbox, check_acls)
@ -172,7 +166,7 @@ impl JMAP {
account_id: u32, account_id: u32,
collection: Collection, collection: Collection,
check_acls: impl Into<Bitmap<Acl>>, check_acls: impl Into<Bitmap<Acl>>,
) -> Result<RoaringBitmap, MethodError> { ) -> trc::Result<RoaringBitmap> {
let check_acls = check_acls.into(); let check_acls = check_acls.into();
let mut document_ids = self let mut document_ids = self
.get_document_ids(account_id, collection) .get_document_ids(account_id, collection)
@ -191,7 +185,7 @@ impl JMAP {
access_token: &AccessToken, access_token: &AccessToken,
account_id: u32, account_id: u32,
check_acls: impl Into<Bitmap<Acl>>, check_acls: impl Into<Bitmap<Acl>>,
) -> Result<RoaringBitmap, MethodError> { ) -> trc::Result<RoaringBitmap> {
let check_acls = check_acls.into(); let check_acls = check_acls.into();
let mut document_ids = self let mut document_ids = self
.get_document_ids(account_id, Collection::Email) .get_document_ids(account_id, Collection::Email)
@ -212,7 +206,7 @@ impl JMAP {
to_collection: impl Into<u8>, to_collection: impl Into<u8>,
to_document_id: u32, to_document_id: u32,
check_acls: impl Into<Bitmap<Acl>>, check_acls: impl Into<Bitmap<Acl>>,
) -> Result<bool, MethodError> { ) -> trc::Result<bool> {
let to_collection = to_collection.into(); let to_collection = to_collection.into();
let check_acls = check_acls.into(); let check_acls = check_acls.into();
for &grant_account_id in [access_token.primary_id] for &grant_account_id in [access_token.primary_id]
@ -241,12 +235,7 @@ impl JMAP {
} }
Ok(None) => (), Ok(None) => (),
Err(err) => { Err(err) => {
tracing::error!( return Err(err.caused_by(trc::location!()));
event = "error",
context = "has_access_to_document",
error = ?err,
"Failed to verify ACL.");
return Err(MethodError::ServerPartialFail);
} }
} }
} }

View file

@ -114,25 +114,26 @@ impl AccessToken {
&self, &self,
to_account_id: Id, to_account_id: Id,
to_collection: Collection, to_collection: Collection,
) -> Result<&Self, MethodError> { ) -> trc::Result<&Self> {
if self.has_access(to_account_id.document_id(), to_collection) { if self.has_access(to_account_id.document_id(), to_collection) {
Ok(self) Ok(self)
} else { } else {
Err(MethodError::Forbidden(format!( Err(MethodError::Forbidden(format!(
"You do not have access to account {}", "You do not have access to account {}",
to_account_id to_account_id
))) ))
.into())
} }
} }
pub fn assert_is_member(&self, account_id: Id) -> Result<&Self, MethodError> { pub fn assert_is_member(&self, account_id: Id) -> trc::Result<&Self> {
if self.is_member(account_id.document_id()) { if self.is_member(account_id.document_id()) {
Ok(self) Ok(self)
} else { } else {
Err(MethodError::Forbidden(format!( Err(
"You are not an owner of account {}", MethodError::Forbidden(format!("You are not an owner of account {}", account_id))
account_id .into(),
))) )
} }
} }
} }

View file

@ -5,10 +5,7 @@
*/ */
use jmap_proto::{ use jmap_proto::{
error::{ error::set::{SetError, SetErrorType},
method::MethodError,
set::{SetError, SetErrorType},
},
method::copy::{CopyBlobRequest, CopyBlobResponse}, method::copy::{CopyBlobRequest, CopyBlobResponse},
types::blob::BlobId, types::blob::BlobId,
}; };
@ -26,7 +23,7 @@ impl JMAP {
&self, &self,
request: CopyBlobRequest, request: CopyBlobRequest,
access_token: &AccessToken, access_token: &AccessToken,
) -> Result<CopyBlobResponse, MethodError> { ) -> trc::Result<CopyBlobResponse> {
let mut response = CopyBlobResponse { let mut response = CopyBlobResponse {
from_account_id: request.from_account_id, from_account_id: request.from_account_id,
account_id: request.account_id, account_id: request.account_id,

View file

@ -6,19 +6,17 @@
use std::ops::Range; use std::ops::Range;
use jmap_proto::{ use jmap_proto::types::{
error::method::MethodError, acl::Acl,
types::{ blob::{BlobId, BlobSection},
acl::Acl, collection::Collection,
blob::{BlobId, BlobSection},
collection::Collection,
},
}; };
use mail_parser::{ use mail_parser::{
decoders::{base64::base64_decode, quoted_printable::quoted_printable_decode}, decoders::{base64::base64_decode, quoted_printable::quoted_printable_decode},
Encoding, Encoding,
}; };
use store::BlobClass; use store::BlobClass;
use trc::AddContext;
use utils::BlobHash; use utils::BlobHash;
use crate::{auth::AccessToken, JMAP}; use crate::{auth::AccessToken, JMAP};
@ -29,20 +27,14 @@ impl JMAP {
&self, &self,
blob_id: &BlobId, blob_id: &BlobId,
access_token: &AccessToken, access_token: &AccessToken,
) -> Result<Option<Vec<u8>>, MethodError> { ) -> trc::Result<Option<Vec<u8>>> {
if !self if !self
.core .core
.storage .storage
.data .data
.blob_has_access(&blob_id.hash, &blob_id.class) .blob_has_access(&blob_id.hash, &blob_id.class)
.await .await
.map_err(|err| { .caused_by(trc::location!())?
tracing::error!(event = "error",
context = "blob_download",
error = ?err,
"Failed to validate blob access");
MethodError::ServerPartialFail
})?
{ {
return Ok(None); return Ok(None);
} }
@ -95,7 +87,7 @@ impl JMAP {
&self, &self,
hash: &BlobHash, hash: &BlobHash,
section: &BlobSection, section: &BlobSection,
) -> Result<Option<Vec<u8>>, MethodError> { ) -> trc::Result<Option<Vec<u8>>> {
Ok(self Ok(self
.get_blob( .get_blob(
hash, hash,
@ -113,38 +105,27 @@ impl JMAP {
&self, &self,
hash: &BlobHash, hash: &BlobHash,
range: Range<usize>, range: Range<usize>,
) -> Result<Option<Vec<u8>>, MethodError> { ) -> trc::Result<Option<Vec<u8>>> {
match self.core.storage.blob.get_blob(hash.as_ref(), range).await { self.core
Ok(blob) => Ok(blob), .storage
Err(err) => { .blob
tracing::error!(event = "error", .get_blob(hash.as_ref(), range)
context = "blob_store", .await
blob_id = ?hash, .caused_by(trc::location!())
error = ?err,
"Failed to retrieve blob");
Err(MethodError::ServerPartialFail)
}
}
} }
pub async fn has_access_blob( pub async fn has_access_blob(
&self, &self,
blob_id: &BlobId, blob_id: &BlobId,
access_token: &AccessToken, access_token: &AccessToken,
) -> Result<bool, MethodError> { ) -> trc::Result<bool> {
Ok(self Ok(self
.core .core
.storage .storage
.data .data
.blob_has_access(&blob_id.hash, &blob_id.class) .blob_has_access(&blob_id.hash, &blob_id.class)
.await .await
.map_err(|err| { .caused_by(trc::location!())?
tracing::error!(event = "error",
context = "has_access_blob",
error = ?err,
"Failed to validate blob access");
MethodError::ServerPartialFail
})?
&& match &blob_id.class { && match &blob_id.class {
BlobClass::Linked { BlobClass::Linked {
account_id, account_id,

View file

@ -33,7 +33,7 @@ impl JMAP {
&self, &self,
mut request: GetRequest<GetArguments>, mut request: GetRequest<GetArguments>,
access_token: &AccessToken, access_token: &AccessToken,
) -> Result<GetResponse, MethodError> { ) -> trc::Result<GetResponse> {
let ids = request let ids = request
.unwrap_blob_ids(self.core.jmap.get_max_objects)? .unwrap_blob_ids(self.core.jmap.get_max_objects)?
.unwrap_or_default(); .unwrap_or_default();
@ -151,7 +151,7 @@ impl JMAP {
pub async fn blob_lookup( pub async fn blob_lookup(
&self, &self,
request: BlobLookupRequest, request: BlobLookupRequest,
) -> Result<BlobLookupResponse, MethodError> { ) -> trc::Result<BlobLookupResponse> {
let mut include_email = false; let mut include_email = false;
let mut include_mailbox = false; let mut include_mailbox = false;
let mut include_thread = false; let mut include_thread = false;

View file

@ -18,6 +18,7 @@ use store::{
write::{now, BatchBuilder, BlobOp}, write::{now, BatchBuilder, BlobOp},
BlobClass, Serialize, BlobClass, Serialize,
}; };
use trc::AddContext;
use utils::BlobHash; use utils::BlobHash;
use crate::{auth::AccessToken, JMAP}; use crate::{auth::AccessToken, JMAP};
@ -33,7 +34,7 @@ impl JMAP {
&self, &self,
request: BlobUploadRequest, request: BlobUploadRequest,
access_token: &AccessToken, access_token: &AccessToken,
) -> Result<BlobUploadResponse, MethodError> { ) -> trc::Result<BlobUploadResponse> {
let mut response = BlobUploadResponse { let mut response = BlobUploadResponse {
account_id: request.account_id, account_id: request.account_id,
created: Default::default(), created: Default::default(),
@ -42,7 +43,7 @@ impl JMAP {
let account_id = request.account_id.document_id(); let account_id = request.account_id.document_id();
if request.create.len() > self.core.jmap.set_max_objects { if request.create.len() > self.core.jmap.set_max_objects {
return Err(MethodError::RequestTooLarge); return Err(MethodError::RequestTooLarge.into());
} }
'outer: for (create_id, upload_object) in request.create { 'outer: for (create_id, upload_object) in request.create {
@ -142,14 +143,7 @@ impl JMAP {
.data .data
.blob_quota(account_id) .blob_quota(account_id)
.await .await
.map_err(|err| { .caused_by(trc::location!())?;
tracing::error!(event = "error",
context = "blob_store",
account_id = account_id,
error = ?err,
"Failed to obtain blob quota");
MethodError::ServerPartialFail
})?;
if ((self.core.jmap.upload_tmp_quota_size > 0 if ((self.core.jmap.upload_tmp_quota_size > 0
&& used.bytes + data.len() > self.core.jmap.upload_tmp_quota_size) && used.bytes + data.len() > self.core.jmap.upload_tmp_quota_size)
@ -253,7 +247,7 @@ impl JMAP {
account_id: u32, account_id: u32,
data: &[u8], data: &[u8],
set_quota: bool, set_quota: bool,
) -> Result<BlobId, MethodError> { ) -> trc::Result<BlobId> {
// First reserve the hash // First reserve the hash
let hash = BlobHash::from(data); let hash = BlobHash::from(data);
let mut batch = BatchBuilder::new(); let mut batch = BatchBuilder::new();
@ -274,14 +268,7 @@ impl JMAP {
.data .data
.blob_exists(&hash) .blob_exists(&hash)
.await .await
.map_err(|err| { .caused_by(trc::location!())?
tracing::error!(
event = "error",
context = "put_blob",
error = ?err,
"Failed to verify blob hash existence.");
MethodError::ServerPartialFail
})?
{ {
// Upload blob to store // Upload blob to store
self.core self.core
@ -289,14 +276,7 @@ impl JMAP {
.blob .blob
.put_blob(hash.as_ref(), data) .put_blob(hash.as_ref(), data)
.await .await
.map_err(|err| { .caused_by(trc::location!())?;
tracing::error!(
event = "error",
context = "put_blob",
error = ?err,
"Failed to store blob.");
MethodError::ServerPartialFail
})?;
// Commit blob // Commit blob
let mut batch = BatchBuilder::new(); let mut batch = BatchBuilder::new();

View file

@ -10,6 +10,7 @@ use jmap_proto::{
types::{collection::Collection, property::Property, state::State}, types::{collection::Collection, property::Property, state::State},
}; };
use store::query::log::{Change, Changes, Query}; use store::query::log::{Change, Changes, Query};
use trc::AddContext;
use crate::{auth::AccessToken, JMAP}; use crate::{auth::AccessToken, JMAP};
@ -18,7 +19,7 @@ impl JMAP {
&self, &self,
request: ChangesRequest, request: ChangesRequest,
access_token: &AccessToken, access_token: &AccessToken,
) -> Result<ChangesResponse, MethodError> { ) -> trc::Result<ChangesResponse> {
// Map collection and validate ACLs // Map collection and validate ACLs
let collection = match request.arguments { let collection = match request.arguments {
RequestArguments::Email => { RequestArguments::Email => {
@ -48,7 +49,7 @@ impl JMAP {
RequestArguments::Quota => { RequestArguments::Quota => {
access_token.assert_is_member(request.account_id)?; access_token.assert_is_member(request.account_id)?;
return Err(MethodError::CannotCalculateChanges); return Err(MethodError::CannotCalculateChanges.into());
} }
}; };
@ -164,21 +165,12 @@ impl JMAP {
account_id: u32, account_id: u32,
collection: Collection, collection: Collection,
query: Query, query: Query,
) -> Result<Changes, MethodError> { ) -> trc::Result<Changes> {
self.core self.core
.storage .storage
.data .data
.changes(account_id, collection, query) .changes(account_id, collection, query)
.await .await
.map_err(|err| { .caused_by(trc::location!())
tracing::error!(
event = "error",
context = "changes",
account_id = account_id,
collection = ?collection,
error = ?err,
"Failed to query changes.");
MethodError::ServerPartialFail
})
} }
} }

View file

@ -20,7 +20,7 @@ impl JMAP {
&self, &self,
request: QueryChangesRequest, request: QueryChangesRequest,
access_token: &AccessToken, access_token: &AccessToken,
) -> Result<QueryChangesResponse, MethodError> { ) -> trc::Result<QueryChangesResponse> {
// Query changes // Query changes
let changes = self let changes = self
.changes( .changes(
@ -35,7 +35,11 @@ impl JMAP {
changes::RequestArguments::EmailSubmission changes::RequestArguments::EmailSubmission
} }
query::RequestArguments::Quota => changes::RequestArguments::Quota, query::RequestArguments::Quota => changes::RequestArguments::Quota,
_ => return Err(MethodError::UnknownMethod("Unknown method".to_string())), _ => {
return Err(
MethodError::UnknownMethod("Unknown method".to_string()).into()
)
}
}, },
}, },
access_token, access_token,

View file

@ -8,6 +8,7 @@ use jmap_proto::{
error::method::MethodError, error::method::MethodError,
types::{collection::Collection, state::State}, types::{collection::Collection, state::State},
}; };
use trc::AddContext;
use crate::JMAP; use crate::JMAP;
@ -16,26 +17,15 @@ impl JMAP {
&self, &self,
account_id: u32, account_id: u32,
collection: impl Into<u8>, collection: impl Into<u8>,
) -> Result<State, MethodError> { ) -> trc::Result<State> {
let collection = collection.into(); let collection = collection.into();
match self self.core
.core
.storage .storage
.data .data
.get_last_change_id(account_id, collection) .get_last_change_id(account_id, collection)
.await .await
{ .caused_by(trc::location!())
Ok(id) => Ok(id.into()), .map(State::from)
Err(err) => {
tracing::error!(event = "error",
context = "store",
account_id = account_id,
collection = ?Collection::from(collection),
error = ?err,
"Failed to obtain state");
Err(MethodError::ServerPartialFail)
}
}
} }
pub async fn assert_state( pub async fn assert_state(
@ -43,11 +33,11 @@ impl JMAP {
account_id: u32, account_id: u32,
collection: Collection, collection: Collection,
if_in_state: &Option<State>, if_in_state: &Option<State>,
) -> Result<State, MethodError> { ) -> trc::Result<State> {
let old_state: State = self.get_state(account_id, collection).await?; let old_state: State = self.get_state(account_id, collection).await?;
if let Some(if_in_state) = if_in_state { if let Some(if_in_state) = if_in_state {
if &old_state != if_in_state { if &old_state != if_in_state {
return Err(MethodError::StateMismatch); return Err(MethodError::StateMismatch.into());
} }
} }

View file

@ -6,33 +6,31 @@
use std::time::Duration; use std::time::Duration;
use jmap_proto::{error::method::MethodError, types::collection::Collection}; use jmap_proto::types::collection::Collection;
use store::{ use store::{
write::{log::ChangeLogBuilder, BatchBuilder}, write::{log::ChangeLogBuilder, BatchBuilder},
LogKey, LogKey,
}; };
use trc::AddContext;
use crate::JMAP; use crate::JMAP;
impl JMAP { impl JMAP {
pub async fn begin_changes(&self, account_id: u32) -> Result<ChangeLogBuilder, MethodError> { pub async fn begin_changes(&self, account_id: u32) -> trc::Result<ChangeLogBuilder> {
self.assign_change_id(account_id) self.assign_change_id(account_id)
.await .await
.map(ChangeLogBuilder::with_change_id) .map(ChangeLogBuilder::with_change_id)
} }
pub async fn assign_change_id(&self, _: u32) -> Result<u64, MethodError> { pub async fn assign_change_id(&self, _: u32) -> trc::Result<u64> {
self.generate_snowflake_id() self.generate_snowflake_id()
} }
pub fn generate_snowflake_id(&self) -> Result<u64, MethodError> { pub fn generate_snowflake_id(&self) -> trc::Result<u64> {
self.inner.snowflake_id.generate().ok_or_else(|| { self.inner.snowflake_id.generate().ok_or_else(|| {
tracing::error!( trc::Cause::Unexpected
event = "error", .caused_by(trc::location!())
context = "change_log", .ctx(trc::Key::Reason, "Failed to generate snowflake id.")
"Failed to generate snowflake id."
);
MethodError::ServerPartialFail
}) })
} }
@ -40,7 +38,7 @@ impl JMAP {
&self, &self,
account_id: u32, account_id: u32,
mut changes: ChangeLogBuilder, mut changes: ChangeLogBuilder,
) -> Result<u64, MethodError> { ) -> trc::Result<u64> {
if changes.change_id == u64::MAX || changes.change_id == 0 { if changes.change_id == u64::MAX || changes.change_id == 0 {
changes.change_id = self.assign_change_id(account_id).await?; changes.change_id = self.assign_change_id(account_id).await?;
} }
@ -53,21 +51,15 @@ impl JMAP {
.data .data
.write(builder.build()) .write(builder.build())
.await .await
.map_err(|err| { .caused_by(trc::location!())
tracing::error!( .map(|_| state)
event = "error",
context = "change_log",
error = ?err,
"Failed to write changes.");
MethodError::ServerPartialFail
})?;
Ok(state)
} }
pub async fn delete_changes(&self, account_id: u32, before: Duration) -> store::Result<()> { pub async fn delete_changes(&self, account_id: u32, before: Duration) -> trc::Result<()> {
let reference_cid = self.inner.snowflake_id.past_id(before).ok_or_else(|| { let reference_cid = self.inner.snowflake_id.past_id(before).ok_or_else(|| {
store::Error::InternalError("Failed to generate reference change id.".to_string()) trc::Cause::Unexpected
.caused_by(trc::location!())
.ctx(trc::Key::Reason, "Failed to generate reference change id.")
})?; })?;
for collection in [ for collection in [

View file

@ -6,11 +6,8 @@
use std::{collections::HashMap, sync::Arc}; use std::{collections::HashMap, sync::Arc};
use futures_util::TryFutureExt; use jmap_proto::types::{collection::Collection, property::Property};
use jmap_proto::{ use trc::AddContext;
error::method::MethodError,
types::{collection::Collection, property::Property},
};
use utils::lru_cache::LruCached; use utils::lru_cache::LruCached;
use crate::JMAP; use crate::JMAP;
@ -26,22 +23,15 @@ impl JMAP {
&self, &self,
account_id: u32, account_id: u32,
message_ids: impl Iterator<Item = u32>, message_ids: impl Iterator<Item = u32>,
) -> Result<Vec<(u32, u32)>, MethodError> { ) -> trc::Result<Vec<(u32, u32)>> {
// Obtain current state // Obtain current state
let modseq = self let modseq = self
.core .core
.storage .storage
.data .data
.get_last_change_id(account_id, Collection::Thread) .get_last_change_id(account_id, Collection::Thread)
.map_err(|err| { .await
tracing::error!(event = "error", .caused_by(trc::location!())?;
context = "store",
account_id = account_id,
error = ?err,
"Failed to retrieve threads last change id");
MethodError::ServerPartialFail
})
.await?;
// Lock the cache // Lock the cache
let thread_cache = if let Some(thread_cache) = let thread_cache = if let Some(thread_cache) =

View file

@ -38,6 +38,7 @@ use store::{
}, },
BlobClass, Serialize, BlobClass, Serialize,
}; };
use trc::AddContext;
use utils::map::vec_map::VecMap; use utils::map::vec_map::VecMap;
use crate::{auth::AccessToken, mailbox::UidMailbox, services::housekeeper::Event, JMAP}; use crate::{auth::AccessToken, mailbox::UidMailbox, services::housekeeper::Event, JMAP};
@ -54,14 +55,15 @@ impl JMAP {
request: CopyRequest<RequestArguments>, request: CopyRequest<RequestArguments>,
access_token: &AccessToken, access_token: &AccessToken,
next_call: &mut Option<Call<RequestMethod>>, next_call: &mut Option<Call<RequestMethod>>,
) -> Result<CopyResponse, MethodError> { ) -> trc::Result<CopyResponse> {
let account_id = request.account_id.document_id(); let account_id = request.account_id.document_id();
let from_account_id = request.from_account_id.document_id(); let from_account_id = request.from_account_id.document_id();
if account_id == from_account_id { if account_id == from_account_id {
return Err(MethodError::InvalidArguments( return Err(MethodError::InvalidArguments(
"From accountId is equal to fromAccountId".to_string(), "From accountId is equal to fromAccountId".to_string(),
)); )
.into());
} }
let old_state = self let old_state = self
.assert_state(account_id, Collection::Email, &request.if_in_state) .assert_state(account_id, Collection::Email, &request.if_in_state)
@ -277,7 +279,7 @@ impl JMAP {
mailboxes: Vec<u32>, mailboxes: Vec<u32>,
keywords: Vec<Keyword>, keywords: Vec<Keyword>,
received_at: Option<UTCDate>, received_at: Option<UTCDate>,
) -> Result<Result<IngestedEmail, SetError>, MethodError> { ) -> trc::Result<Result<IngestedEmail, SetError>> {
// Obtain metadata // Obtain metadata
let mut metadata = if let Some(metadata) = self let mut metadata = if let Some(metadata) = self
.get_property::<Bincode<MessageMetadata>>( .get_property::<Bincode<MessageMetadata>>(
@ -341,7 +343,7 @@ impl JMAP {
let thread_id = if !references.is_empty() { let thread_id = if !references.is_empty() {
self.find_or_merge_thread(account_id, subject, &references) self.find_or_merge_thread(account_id, subject, &references)
.await .await
.map_err(|_| MethodError::ServerPartialFail)? .caused_by(trc::location!())?
} else { } else {
None None
}; };
@ -360,14 +362,7 @@ impl JMAP {
let uid = self let uid = self
.assign_imap_uid(account_id, *mailbox_id) .assign_imap_uid(account_id, *mailbox_id)
.await .await
.map_err(|err| { .caused_by(trc::location!())?;
tracing::error!(
event = "error",
context = "email_copy",
error = ?err,
"Failed to assign IMAP UID.");
MethodError::ServerPartialFail
})?;
mailbox_ids.push(UidMailbox::new(*mailbox_id, uid)); mailbox_ids.push(UidMailbox::new(*mailbox_id, uid));
email.imap_uids.push(uid); email.imap_uids.push(uid);
} }
@ -416,23 +411,12 @@ impl JMAP {
.data .data
.write(batch.build()) .write(batch.build())
.await .await
.map_err(|err| { .caused_by(trc::location!())?;
tracing::error!(
event = "error",
context = "email_copy",
error = ?err,
"Failed to write message to database.");
MethodError::ServerPartialFail
})?;
let thread_id = match thread_id { let thread_id = match thread_id {
Some(thread_id) => thread_id, Some(thread_id) => thread_id,
None => ids None => ids.first_document_id().caused_by(trc::location!())?,
.first_document_id()
.map_err(|_| MethodError::ServerPartialFail)?,
}; };
let document_id = ids let document_id = ids.last_document_id().caused_by(trc::location!())?;
.last_document_id()
.map_err(|_| MethodError::ServerPartialFail)?;
// Request FTS index // Request FTS index
let _ = self.inner.housekeeper_tx.send(Event::IndexStart).await; let _ = self.inner.housekeeper_tx.send(Event::IndexStart).await;

View file

@ -605,24 +605,17 @@ impl Serialize for &EncryptionParams {
} }
impl Deserialize for EncryptionParams { impl Deserialize for EncryptionParams {
fn deserialize(bytes: &[u8]) -> store::Result<Self> { fn deserialize(bytes: &[u8]) -> trc::Result<Self> {
let version = *bytes.first().ok_or_else(|| { let version = *bytes
store::Error::InternalError( .first()
"Failed to read version while deserializing encryption params".to_string(), .ok_or_else(|| trc::Cause::DataCorruption.caused_by(trc::location!()))?;
)
})?;
match version { match version {
1 if bytes.len() > 1 => bincode::deserialize(&bytes[1..]).map_err(|err| { 1 if bytes.len() > 1 => bincode::deserialize(&bytes[1..])
store::Error::InternalError(format!( .map_err(|err| trc::Error::from(err).caused_by(trc::location!())),
"Failed to deserialize encryption params: {}",
err
))
}),
_ => Err(store::Error::InternalError(format!( _ => Err(trc::Cause::Deserialize
"Unknown encryption params version: {}", .caused_by(trc::location!())
version .ctx(trc::Key::Value, version as u64)),
))),
} }
} }
} }

View file

@ -6,12 +6,9 @@
use std::time::Duration; use std::time::Duration;
use jmap_proto::{ use jmap_proto::types::{
error::method::MethodError, collection::Collection, id::Id, keyword::Keyword, property::Property, state::StateChange,
types::{ type_state::DataType,
collection::Collection, id::Id, keyword::Keyword, property::Property, state::StateChange,
type_state::DataType,
},
}; };
use store::{ use store::{
ahash::AHashMap, ahash::AHashMap,
@ -22,6 +19,7 @@ use store::{
}, },
BitmapKey, IterateParams, ValueKey, U32_LEN, BitmapKey, IterateParams, ValueKey, U32_LEN,
}; };
use trc::AddContext;
use utils::codec::leb128::Leb128Reader; use utils::codec::leb128::Leb128Reader;
use crate::{ use crate::{
@ -37,7 +35,7 @@ impl JMAP {
&self, &self,
account_id: u32, account_id: u32,
mut document_ids: RoaringBitmap, mut document_ids: RoaringBitmap,
) -> Result<(ChangeLogBuilder, RoaringBitmap), MethodError> { ) -> trc::Result<(ChangeLogBuilder, RoaringBitmap)> {
// Create batch // Create batch
let mut changes = ChangeLogBuilder::with_change_id(0); let mut changes = ChangeLogBuilder::with_change_id(0);
let mut delete_properties = AHashMap::new(); let mut delete_properties = AHashMap::new();
@ -107,9 +105,7 @@ impl JMAP {
let (thread_id, _) = key let (thread_id, _) = key
.get(U32_LEN + 2..) .get(U32_LEN + 2..)
.and_then(|bytes| bytes.read_leb128::<u32>()) .and_then(|bytes| bytes.read_leb128::<u32>())
.ok_or_else(|| { .ok_or_else(|| trc::Error::corrupted_key(key, None, trc::location!()))?;
store::Error::InternalError("Failed to read threadId.".to_string())
})?;
if let Some(thread_count) = thread_ids.get_mut(&thread_id) { if let Some(thread_count) = thread_ids.get_mut(&thread_id) {
*thread_count -= 1; *thread_count -= 1;
} }
@ -118,15 +114,7 @@ impl JMAP {
}, },
) )
.await .await
.map_err(|err| { .caused_by(trc::location!())?;
tracing::error!(
event = "error",
context = "email_delete",
error = ?err,
"Failed to iterate threadIds."
);
MethodError::ServerPartialFail
})?;
// Tombstone message and untag it from the mailboxes // Tombstone message and untag it from the mailboxes
let mut batch = BatchBuilder::new(); let mut batch = BatchBuilder::new();
@ -189,14 +177,7 @@ impl JMAP {
.data .data
.write(batch.build()) .write(batch.build())
.await .await
.map_err(|err| { .caused_by(trc::location!())?;
tracing::error!(
event = "error",
context = "email_delete",
error = ?err,
"Failed to commit batch.");
MethodError::ServerPartialFail
})?;
batch = BatchBuilder::new(); batch = BatchBuilder::new();
batch batch
@ -221,14 +202,7 @@ impl JMAP {
.data .data
.write(batch.build()) .write(batch.build())
.await .await
.map_err(|err| { .caused_by(trc::location!())?;
tracing::error!(
event = "error",
context = "email_delete",
error = ?err,
"Failed to commit batch.");
MethodError::ServerPartialFail
})?;
} }
Ok((changes, document_ids)) Ok((changes, document_ids))
@ -339,11 +313,7 @@ impl JMAP {
} }
} }
pub async fn emails_auto_expunge( pub async fn emails_auto_expunge(&self, account_id: u32, period: Duration) -> trc::Result<()> {
&self,
account_id: u32,
period: Duration,
) -> Result<(), MethodError> {
let deletion_candidates = self let deletion_candidates = self
.get_tag( .get_tag(
account_id, account_id,
@ -367,13 +337,9 @@ impl JMAP {
return Ok(()); return Ok(());
} }
let reference_cid = self.inner.snowflake_id.past_id(period).ok_or_else(|| { let reference_cid = self.inner.snowflake_id.past_id(period).ok_or_else(|| {
tracing::error!( trc::Cause::Unexpected
event = "error", .caused_by(trc::location!())
context = "email_auto_expunge", .ctx(trc::Key::Reason, "Failed to generate reference cid.")
account_id = account_id,
"Failed to generate reference cid."
);
MethodError::ServerPartialFail
})?; })?;
// Find messages to destroy // Find messages to destroy
@ -422,7 +388,7 @@ impl JMAP {
Ok(()) Ok(())
} }
pub async fn emails_purge_tombstoned(&self, account_id: u32) -> store::Result<()> { pub async fn emails_purge_tombstoned(&self, account_id: u32) -> trc::Result<()> {
// Obtain tombstoned messages // Obtain tombstoned messages
let tombstoned_ids = self let tombstoned_ids = self
.core .core

View file

@ -21,6 +21,7 @@ use jmap_proto::{
}; };
use mail_parser::HeaderName; use mail_parser::HeaderName;
use store::{write::Bincode, BlobClass}; use store::{write::Bincode, BlobClass};
use trc::AddContext;
use crate::{auth::AccessToken, email::headers::HeaderToValue, mailbox::UidMailbox, JMAP}; use crate::{auth::AccessToken, email::headers::HeaderToValue, mailbox::UidMailbox, JMAP};
@ -35,7 +36,7 @@ impl JMAP {
&self, &self,
mut request: GetRequest<GetArguments>, mut request: GetRequest<GetArguments>,
access_token: &AccessToken, access_token: &AccessToken,
) -> Result<GetResponse, MethodError> { ) -> trc::Result<GetResponse> {
let ids = request.unwrap_ids(self.core.jmap.get_max_objects)?; let ids = request.unwrap_ids(self.core.jmap.get_max_objects)?;
let properties = request.unwrap_properties(&[ let properties = request.unwrap_properties(&[
Property::Id, Property::Id,
@ -95,14 +96,7 @@ impl JMAP {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
self.get_cached_thread_ids(account_id, document_ids.iter().copied()) self.get_cached_thread_ids(account_id, document_ids.iter().copied())
.await .await
.map_err(|err| { .caused_by(trc::location!())?
tracing::error!(event = "error",
context = "store",
account_id = account_id,
error = ?err,
"Failed to retrieve thread Ids");
MethodError::ServerPartialFail
})?
.into_iter() .into_iter()
.filter_map(|(document_id, thread_id)| { .filter_map(|(document_id, thread_id)| {
Id::from_parts(thread_id, document_id).into() Id::from_parts(thread_id, document_id).into()
@ -408,7 +402,8 @@ impl JMAP {
_ => { _ => {
return Err(MethodError::InvalidArguments(format!( return Err(MethodError::InvalidArguments(format!(
"Invalid property {property:?}" "Invalid property {property:?}"
))); ))
.into());
} }
} }
} }

View file

@ -5,10 +5,7 @@
*/ */
use jmap_proto::{ use jmap_proto::{
error::{ error::set::{SetError, SetErrorType},
method::MethodError,
set::{SetError, SetErrorType},
},
method::import::{ImportEmailRequest, ImportEmailResponse}, method::import::{ImportEmailRequest, ImportEmailResponse},
types::{ types::{
acl::Acl, acl::Acl,
@ -22,7 +19,7 @@ use jmap_proto::{
use mail_parser::MessageParser; use mail_parser::MessageParser;
use utils::map::vec_map::VecMap; use utils::map::vec_map::VecMap;
use crate::{auth::AccessToken, IngestError, JMAP}; use crate::{auth::AccessToken, JMAP};
use super::ingest::{IngestEmail, IngestSource}; use super::ingest::{IngestEmail, IngestSource};
@ -31,7 +28,7 @@ impl JMAP {
&self, &self,
request: ImportEmailRequest, request: ImportEmailRequest,
access_token: &AccessToken, access_token: &AccessToken,
) -> Result<ImportEmailResponse, MethodError> { ) -> trc::Result<ImportEmailResponse> {
// Validate state // Validate state
let account_id = request.account_id.document_id(); let account_id = request.account_id.document_id();
let old_state: State = self let old_state: State = self
@ -131,22 +128,28 @@ impl JMAP {
Ok(email) => { Ok(email) => {
response.created.append(id, email.into()); response.created.append(id, email.into());
} }
Err(IngestError::Permanent { reason, .. }) => { Err(mut err) => match err.as_ref() {
response.not_created.append( trc::Cause::OverQuota => {
id, response.not_created.append(
SetError::new(SetErrorType::InvalidEmail).with_description(reason), id,
); SetError::new(SetErrorType::OverQuota)
} .with_description("You have exceeded your disk quota."),
Err(IngestError::OverQuota) => { );
response.not_created.append( }
id, trc::Cause::Ingest => {
SetError::new(SetErrorType::OverQuota) response.not_created.append(
.with_description("You have exceeded your disk quota."), id,
); SetError::new(SetErrorType::InvalidEmail).with_description(
} err.take_value(trc::Key::Reason)
Err(IngestError::Temporary) => { .and_then(|v| v.into_string())
return Err(MethodError::ServerPartialFail); .unwrap(),
} ),
);
}
_ => {
return Err(err);
}
},
} }
} }

View file

@ -29,13 +29,14 @@ use store::{
}, },
BitmapKey, BlobClass, Serialize, BitmapKey, BlobClass, Serialize,
}; };
use trc::AddContext;
use utils::map::vec_map::VecMap; use utils::map::vec_map::VecMap;
use crate::{ use crate::{
email::index::{IndexMessage, VisitValues, MAX_ID_LENGTH}, email::index::{IndexMessage, VisitValues, MAX_ID_LENGTH},
mailbox::{UidMailbox, INBOX_ID, JUNK_ID}, mailbox::{UidMailbox, INBOX_ID, JUNK_ID},
services::housekeeper::Event, services::housekeeper::Event,
IngestError, JMAP, JMAP,
}; };
use super::{ use super::{
@ -75,25 +76,23 @@ const MAX_RETRIES: u32 = 10;
impl JMAP { impl JMAP {
#[allow(clippy::blocks_in_conditions)] #[allow(clippy::blocks_in_conditions)]
pub async fn email_ingest( pub async fn email_ingest(&self, mut params: IngestEmail<'_>) -> trc::Result<IngestedEmail> {
&self,
mut params: IngestEmail<'_>,
) -> Result<IngestedEmail, IngestError> {
// Check quota // Check quota
let mut raw_message_len = params.raw_message.len() as i64; let mut raw_message_len = params.raw_message.len() as i64;
if !self if !self
.has_available_quota(params.account_id, params.account_quota, raw_message_len) .has_available_quota(params.account_id, params.account_quota, raw_message_len)
.await .await
.map_err(|_| IngestError::Temporary)? .caused_by(trc::location!())?
{ {
return Err(IngestError::OverQuota); return Err(trc::Cause::OverQuota.into_err());
} }
// Parse message // Parse message
let mut raw_message = Cow::from(params.raw_message); let mut raw_message = Cow::from(params.raw_message);
let mut message = params.message.ok_or_else(|| IngestError::Permanent { let mut message = params.message.ok_or_else(|| {
code: [5, 5, 0], trc::Cause::Ingest
reason: "Failed to parse e-mail message.".to_string(), .ctx(trc::Key::Code, 550)
.ctx(trc::Key::Reason, "Failed to parse e-mail message.")
})?; })?;
// Check for Spam headers // Check for Spam headers
@ -162,25 +161,10 @@ impl JMAP {
vec![Filter::eq(Property::MessageId, message_id)], vec![Filter::eq(Property::MessageId, message_id)],
) )
.await .await
.map_err(|err| { .caused_by(trc::location!())?
tracing::error!(
event = "error",
context = "find_duplicates",
error = ?err,
"Duplicate message search failed.");
IngestError::Temporary
})?
.results .results
.is_empty() .is_empty()
{ {
tracing::debug!(
context = "email_ingest",
event = "skip",
account_id = ?params.account_id,
from = ?message.from(),
message_id = message_id,
"Duplicate message skipped.");
return Ok(IngestedEmail { return Ok(IngestedEmail {
id: Id::default(), id: Id::default(),
change_id: u64::MAX, change_id: u64::MAX,
@ -208,7 +192,7 @@ impl JMAP {
Property::Parameters, Property::Parameters,
) )
.await .await
.map_err(|_| IngestError::Temporary)? .caused_by(trc::location!())?
{ {
match message.encrypt(&encrypt_params).await { match message.encrypt(&encrypt_params).await {
Ok(new_raw_message) => { Ok(new_raw_message) => {
@ -216,9 +200,11 @@ impl JMAP {
raw_message_len = raw_message.len() as i64; raw_message_len = raw_message.len() as i64;
message = MessageParser::default() message = MessageParser::default()
.parse(raw_message.as_ref()) .parse(raw_message.as_ref())
.ok_or_else(|| IngestError::Permanent { .ok_or_else(|| {
code: [5, 5, 0], trc::Cause::Ingest.ctx(trc::Key::Code, 550).ctx(
reason: "Failed to parse encrypted e-mail message.".to_string(), trc::Key::Reason,
"Failed to parse encrypted e-mail message.",
)
})?; })?;
// Remove contents from parsed message // Remove contents from parsed message
@ -238,12 +224,7 @@ impl JMAP {
} }
} }
Err(EncryptMessageError::Error(err)) => { Err(EncryptMessageError::Error(err)) => {
tracing::error!( trc::bail!(trc::Cause::Crypto.caused_by(trc::location!()).reason(err));
event = "error",
context = "email_ingest",
error = ?err,
"Failed to encrypt message.");
return Err(IngestError::Temporary);
} }
_ => unreachable!(), _ => unreachable!(),
} }
@ -254,27 +235,13 @@ impl JMAP {
let change_id = self let change_id = self
.assign_change_id(params.account_id) .assign_change_id(params.account_id)
.await .await
.map_err(|_| { .caused_by(trc::location!())?;
tracing::error!(
event = "error",
context = "email_ingest",
"Failed to assign changeId."
);
IngestError::Temporary
})?;
// Store blob // Store blob
let blob_id = self let blob_id = self
.put_blob(params.account_id, raw_message.as_ref(), false) .put_blob(params.account_id, raw_message.as_ref(), false)
.await .await
.map_err(|err| { .caused_by(trc::location!())?;
tracing::error!(
event = "error",
context = "email_ingest",
error = ?err,
"Failed to write blob.");
IngestError::Temporary
})?;
// Assign IMAP UIDs // Assign IMAP UIDs
let mut mailbox_ids = Vec::with_capacity(params.mailbox_ids.len()); let mut mailbox_ids = Vec::with_capacity(params.mailbox_ids.len());
@ -283,14 +250,7 @@ impl JMAP {
let uid = self let uid = self
.assign_imap_uid(params.account_id, *mailbox_id) .assign_imap_uid(params.account_id, *mailbox_id)
.await .await
.map_err(|err| { .caused_by(trc::location!())?;
tracing::error!(
event = "error",
context = "email_ingest",
error = ?err,
"Failed to assign IMAP UID.");
IngestError::Temporary
})?;
mailbox_ids.push(UidMailbox::new(*mailbox_id, uid)); mailbox_ids.push(UidMailbox::new(*mailbox_id, uid));
imap_uids.push(uid); imap_uids.push(uid);
} }
@ -329,9 +289,7 @@ impl JMAP {
.tag(Property::ThreadId, TagValue::Id(maybe_thread_id), 0) .tag(Property::ThreadId, TagValue::Id(maybe_thread_id), 0)
.set( .set(
ValueClass::FtsQueue(FtsQueueClass { ValueClass::FtsQueue(FtsQueueClass {
seq: self seq: self.generate_snowflake_id().caused_by(trc::location!())?,
.generate_snowflake_id()
.map_err(|_| IngestError::Temporary)?,
hash: blob_id.hash.clone(), hash: blob_id.hash.clone(),
}), }),
0u64.serialize(), 0u64.serialize(),
@ -344,21 +302,12 @@ impl JMAP {
.data .data
.write(batch.build()) .write(batch.build())
.await .await
.map_err(|err| { .caused_by(trc::location!())?;
tracing::error!(
event = "error",
context = "email_ingest",
error = ?err,
"Failed to write message to database.");
IngestError::Temporary
})?;
let thread_id = match thread_id { let thread_id = match thread_id {
Some(thread_id) => thread_id, Some(thread_id) => thread_id,
None => ids None => ids.first_document_id().caused_by(trc::location!())?,
.first_document_id()
.map_err(|_| IngestError::Temporary)?,
}; };
let document_id = ids.last_document_id().map_err(|_| IngestError::Temporary)?; let document_id = ids.last_document_id().caused_by(trc::location!())?;
let id = Id::from_parts(thread_id, document_id); let id = Id::from_parts(thread_id, document_id);
// Request FTS index // Request FTS index
@ -422,7 +371,7 @@ impl JMAP {
account_id: u32, account_id: u32,
thread_name: &str, thread_name: &str,
references: &[&str], references: &[&str],
) -> Result<Option<u32>, IngestError> { ) -> trc::Result<Option<u32>> {
let mut try_count = 0; let mut try_count = 0;
loop { loop {
@ -447,14 +396,7 @@ impl JMAP {
.data .data
.filter(account_id, Collection::Email, filters) .filter(account_id, Collection::Email, filters)
.await .await
.map_err(|err| { .caused_by(trc::location!())?
tracing::error!(
event = "error",
context = "find_or_merge_thread",
error = ?err,
"Thread search failed.");
IngestError::Temporary
})?
.results; .results;
if results.is_empty() { if results.is_empty() {
@ -465,14 +407,7 @@ impl JMAP {
let thread_ids = self let thread_ids = self
.get_cached_thread_ids(account_id, results.iter()) .get_cached_thread_ids(account_id, results.iter())
.await .await
.map_err(|err| { .caused_by(trc::location!())?;
tracing::error!(
event = "error",
context = "find_or_merge_thread",
error = ?err,
"Failed to obtain threadIds.");
IngestError::Temporary
})?;
if thread_ids.len() == 1 { if thread_ids.len() == 1 {
return Ok(thread_ids return Ok(thread_ids
@ -502,14 +437,10 @@ impl JMAP {
// Delete all but the most common threadId // Delete all but the most common threadId
let mut batch = BatchBuilder::new(); let mut batch = BatchBuilder::new();
let change_id = self.assign_change_id(account_id).await.map_err(|_| { let change_id = self
tracing::error!( .assign_change_id(account_id)
event = "error", .await
context = "find_or_merge_thread", .caused_by(trc::location!())?;
"Failed to assign changeId for thread merge."
);
IngestError::Temporary
})?;
let mut changes = ChangeLogBuilder::with_change_id(change_id); let mut changes = ChangeLogBuilder::with_change_id(change_id);
batch batch
.with_account_id(account_id) .with_account_id(account_id)
@ -543,14 +474,7 @@ impl JMAP {
document_id: 0, document_id: 0,
}) })
.await .await
.map_err(|err| { .caused_by(trc::location!())?
tracing::error!(
event = "error",
context = "find_or_merge_thread",
error = ?err,
"Failed to obtain threadId bitmap.");
IngestError::Temporary
})?
.unwrap_or_default() .unwrap_or_default()
{ {
batch batch
@ -570,24 +494,19 @@ impl JMAP {
match self.core.storage.data.write(batch.build()).await { match self.core.storage.data.write(batch.build()).await {
Ok(_) => return Ok(Some(thread_id)), Ok(_) => return Ok(Some(thread_id)),
Err(store::Error::AssertValueFailed) if try_count < MAX_RETRIES => { Err(err) if err.matches(trc::Cause::AssertValue) && try_count < MAX_RETRIES => {
let backoff = rand::thread_rng().gen_range(50..=300); let backoff = rand::thread_rng().gen_range(50..=300);
tokio::time::sleep(Duration::from_millis(backoff)).await; tokio::time::sleep(Duration::from_millis(backoff)).await;
try_count += 1; try_count += 1;
} }
Err(err) => { Err(err) => {
tracing::error!( return Err(err.caused_by(trc::location!()));
event = "error",
context = "find_or_merge_thread",
error = ?err,
"Failed to write thread merge batch.");
return Err(IngestError::Temporary);
} }
} }
} }
} }
pub async fn assign_imap_uid(&self, account_id: u32, mailbox_id: u32) -> store::Result<u32> { pub async fn assign_imap_uid(&self, account_id: u32, mailbox_id: u32) -> trc::Result<u32> {
// Increment UID next // Increment UID next
let mut batch = BatchBuilder::new(); let mut batch = BatchBuilder::new();
batch batch
@ -613,7 +532,7 @@ impl LogEmailInsert {
} }
impl SerializeWithId for LogEmailInsert { impl SerializeWithId for LogEmailInsert {
fn serialize_with_id(&self, ids: &AssignedIds) -> store::Result<Vec<u8>> { fn serialize_with_id(&self, ids: &AssignedIds) -> trc::Result<Vec<u8>> {
let thread_id = match self.0 { let thread_id = match self.0 {
Some(thread_id) => thread_id, Some(thread_id) => thread_id,
None => ids.first_document_id()?, None => ids.first_document_id()?,

View file

@ -28,9 +28,9 @@ impl JMAP {
&self, &self,
request: ParseEmailRequest, request: ParseEmailRequest,
access_token: &AccessToken, access_token: &AccessToken,
) -> Result<ParseEmailResponse, MethodError> { ) -> trc::Result<ParseEmailResponse> {
if request.blob_ids.len() > self.core.jmap.mail_parse_max_items { if request.blob_ids.len() > self.core.jmap.mail_parse_max_items {
return Err(MethodError::RequestTooLarge); return Err(MethodError::RequestTooLarge.into());
} }
let properties = request.properties.unwrap_or_else(|| { let properties = request.properties.unwrap_or_else(|| {
vec![ vec![
@ -237,7 +237,8 @@ impl JMAP {
_ => { _ => {
return Err(MethodError::InvalidArguments(format!( return Err(MethodError::InvalidArguments(format!(
"Invalid property {property:?}" "Invalid property {property:?}"
))); ))
.into());
} }
} }
} }

View file

@ -27,7 +27,7 @@ impl JMAP {
&self, &self,
mut request: QueryRequest<QueryArguments>, mut request: QueryRequest<QueryArguments>,
access_token: &AccessToken, access_token: &AccessToken,
) -> Result<QueryResponse, MethodError> { ) -> trc::Result<QueryResponse> {
let account_id = request.account_id.document_id(); let account_id = request.account_id.document_id();
let mut filters = Vec::with_capacity(request.filter.len()); let mut filters = Vec::with_capacity(request.filter.len());
@ -118,7 +118,8 @@ impl JMAP {
Some(HeaderName::Other(header_name)) => { Some(HeaderName::Other(header_name)) => {
return Err(MethodError::InvalidArguments(format!( return Err(MethodError::InvalidArguments(format!(
"Querying header '{header_name}' is not supported.", "Querying header '{header_name}' is not supported.",
))); ))
.into());
} }
Some(header_name) => { Some(header_name) => {
if let Some(header_value) = header.next() { if let Some(header_value) = header.next() {
@ -153,7 +154,9 @@ impl JMAP {
Filter::And | Filter::Or | Filter::Not | Filter::Close => { Filter::And | Filter::Or | Filter::Not | Filter::Close => {
fts_filters.push(cond.into()); fts_filters.push(cond.into());
} }
other => return Err(MethodError::UnsupportedFilter(other.to_string())), other => {
return Err(MethodError::UnsupportedFilter(other.to_string()).into())
}
} }
} }
filters.push(query::Filter::is_in_set( filters.push(query::Filter::is_in_set(
@ -248,7 +251,9 @@ impl JMAP {
filters.push(cond.into()); filters.push(cond.into());
} }
other => return Err(MethodError::UnsupportedFilter(other.to_string())), other => {
return Err(MethodError::UnsupportedFilter(other.to_string()).into())
}
} }
} }
} }
@ -324,7 +329,7 @@ impl JMAP {
query::Comparator::field(Property::Cc, comparator.is_ascending) query::Comparator::field(Property::Cc, comparator.is_ascending)
} }
other => return Err(MethodError::UnsupportedSort(other.to_string())), other => return Err(MethodError::UnsupportedSort(other.to_string()).into()),
}); });
} }
@ -353,7 +358,7 @@ impl JMAP {
account_id: u32, account_id: u32,
keyword: Keyword, keyword: Keyword,
match_all: bool, match_all: bool,
) -> Result<RoaringBitmap, MethodError> { ) -> trc::Result<RoaringBitmap> {
let keyword_doc_ids = self let keyword_doc_ids = self
.get_tag(account_id, Collection::Email, Property::Keywords, keyword) .get_tag(account_id, Collection::Email, Property::Keywords, keyword)
.await? .await?

View file

@ -7,10 +7,7 @@
use std::{borrow::Cow, collections::HashMap, slice::IterMut}; use std::{borrow::Cow, collections::HashMap, slice::IterMut};
use jmap_proto::{ use jmap_proto::{
error::{ error::set::{SetError, SetErrorType},
method::MethodError,
set::{SetError, SetErrorType},
},
method::set::{RequestArguments, SetRequest, SetResponse}, method::set::{RequestArguments, SetRequest, SetResponse},
response::references::EvalObjectReferences, response::references::EvalObjectReferences,
types::{ types::{
@ -41,8 +38,9 @@ use store::{
}, },
Serialize, Serialize,
}; };
use trc::AddContext;
use crate::{auth::AccessToken, mailbox::UidMailbox, IngestError, JMAP}; use crate::{auth::AccessToken, mailbox::UidMailbox, JMAP};
use super::{ use super::{
headers::{BuildHeader, ValueToHeader}, headers::{BuildHeader, ValueToHeader},
@ -54,7 +52,7 @@ impl JMAP {
&self, &self,
mut request: SetRequest<RequestArguments>, mut request: SetRequest<RequestArguments>,
access_token: &AccessToken, access_token: &AccessToken,
) -> Result<SetResponse, MethodError> { ) -> trc::Result<SetResponse> {
// Prepare response // Prepare response
let account_id = request.account_id.document_id(); let account_id = request.account_id.document_id();
let mut response = self let mut response = self
@ -728,14 +726,14 @@ impl JMAP {
Ok(message) => { Ok(message) => {
response.created.insert(id, message.into()); response.created.insert(id, message.into());
} }
Err(IngestError::OverQuota) => { Err(err) if err.matches(trc::Cause::OverQuota) => {
response.not_created.append( response.not_created.append(
id, id,
SetError::new(SetErrorType::OverQuota) SetError::new(SetErrorType::OverQuota)
.with_description("You have exceeded your disk quota."), .with_description("You have exceeded your disk quota."),
); );
} }
Err(_) => return Err(MethodError::ServerPartialFail), Err(err) => return Err(err),
} }
} }
@ -944,14 +942,7 @@ impl JMAP {
uid_mailbox.uid = self uid_mailbox.uid = self
.assign_imap_uid(account_id, uid_mailbox.mailbox_id) .assign_imap_uid(account_id, uid_mailbox.mailbox_id)
.await .await
.map_err(|err| { .caused_by(trc::location!())?;
tracing::error!(
event = "error",
context = "email_copy",
error = ?err,
"Failed to assign IMAP UID.");
MethodError::ServerPartialFail
})?;
} }
} }
@ -971,7 +962,7 @@ impl JMAP {
// Add to updated list // Add to updated list
response.updated.append(id, None); response.updated.append(id, None);
} }
Err(store::Error::AssertValueFailed) => { Err(err) if err.matches(trc::Cause::AssertValue) => {
response.not_updated.append( response.not_updated.append(
id, id,
SetError::forbidden().with_description( SetError::forbidden().with_description(
@ -980,12 +971,7 @@ impl JMAP {
); );
} }
Err(err) => { Err(err) => {
tracing::error!( return Err(err.caused_by(trc::location!()));
event = "error",
context = "email_set",
error = ?err,
"Failed to write message changes to database.");
return Err(MethodError::ServerPartialFail);
} }
} }
} }

View file

@ -25,7 +25,7 @@ impl JMAP {
&self, &self,
request: GetSearchSnippetRequest, request: GetSearchSnippetRequest,
access_token: &AccessToken, access_token: &AccessToken,
) -> Result<GetSearchSnippetResponse, MethodError> { ) -> trc::Result<GetSearchSnippetResponse> {
let mut filter_stack = vec![]; let mut filter_stack = vec![];
let mut include_term = true; let mut include_term = true;
let mut terms = vec![]; let mut terms = vec![];
@ -83,7 +83,7 @@ impl JMAP {
}; };
if email_ids.len() > self.core.jmap.snippet_max_results { if email_ids.len() > self.core.jmap.snippet_max_results {
return Err(MethodError::RequestTooLarge); return Err(MethodError::RequestTooLarge.into());
} }
for email_id in email_ids { for email_id in email_ids {

View file

@ -6,7 +6,6 @@
use directory::QueryBy; use directory::QueryBy;
use jmap_proto::{ use jmap_proto::{
error::method::MethodError,
method::get::{GetRequest, GetResponse, RequestArguments}, method::get::{GetRequest, GetResponse, RequestArguments},
object::Object, object::Object,
types::{collection::Collection, property::Property, value::Value}, types::{collection::Collection, property::Property, value::Value},
@ -15,6 +14,7 @@ use store::{
roaring::RoaringBitmap, roaring::RoaringBitmap,
write::{BatchBuilder, F_VALUE}, write::{BatchBuilder, F_VALUE},
}; };
use trc::AddContext;
use crate::JMAP; use crate::JMAP;
@ -24,7 +24,7 @@ impl JMAP {
pub async fn identity_get( pub async fn identity_get(
&self, &self,
mut request: GetRequest<RequestArguments>, mut request: GetRequest<RequestArguments>,
) -> Result<GetResponse, MethodError> { ) -> trc::Result<GetResponse> {
let ids = request.unwrap_ids(self.core.jmap.get_max_objects)?; let ids = request.unwrap_ids(self.core.jmap.get_max_objects)?;
let properties = request.unwrap_properties(&[ let properties = request.unwrap_properties(&[
Property::Id, Property::Id,
@ -107,10 +107,7 @@ impl JMAP {
Ok(response) Ok(response)
} }
pub async fn identity_get_or_create( pub async fn identity_get_or_create(&self, account_id: u32) -> trc::Result<RoaringBitmap> {
&self,
account_id: u32,
) -> Result<RoaringBitmap, MethodError> {
let mut identity_ids = self let mut identity_ids = self
.get_document_ids(account_id, Collection::Identity) .get_document_ids(account_id, Collection::Identity)
.await? .await?
@ -126,14 +123,7 @@ impl JMAP {
.directory .directory
.query(QueryBy::Id(account_id), false) .query(QueryBy::Id(account_id), false)
.await .await
.map_err(|err| { .caused_by(trc::location!())?
tracing::error!(
event = "error",
context = "identity_get_or_create",
error = ?err,
"Failed to query directory.");
MethodError::ServerPartialFail
})?
.unwrap_or_default(); .unwrap_or_default();
if principal.emails.is_empty() { if principal.emails.is_empty() {
return Ok(identity_ids); return Ok(identity_ids);
@ -178,14 +168,7 @@ impl JMAP {
.data .data
.write(batch.build()) .write(batch.build())
.await .await
.map_err(|err| { .caused_by(trc::location!())?;
tracing::error!(
event = "error",
context = "identity_get_or_create",
error = ?err,
"Failed to create identities.");
MethodError::ServerPartialFail
})?;
Ok(identity_ids) Ok(identity_ids)
} }

View file

@ -6,7 +6,7 @@
use directory::QueryBy; use directory::QueryBy;
use jmap_proto::{ use jmap_proto::{
error::{method::MethodError, set::SetError}, error::set::SetError,
method::set::{RequestArguments, SetRequest, SetResponse}, method::set::{RequestArguments, SetRequest, SetResponse},
object::Object, object::Object,
response::references::EvalObjectReferences, response::references::EvalObjectReferences,
@ -24,7 +24,7 @@ impl JMAP {
pub async fn identity_set( pub async fn identity_set(
&self, &self,
mut request: SetRequest<RequestArguments>, mut request: SetRequest<RequestArguments>,
) -> Result<SetResponse, MethodError> { ) -> trc::Result<SetResponse> {
let account_id = request.account_id.document_id(); let account_id = request.account_id.document_id();
let mut identity_ids = self let mut identity_ids = self
.get_document_ids(account_id, Collection::Identity) .get_document_ids(account_id, Collection::Identity)

View file

@ -47,6 +47,7 @@ use store::{
BitmapKey, Deserialize, IterateParams, ValueKey, U32_LEN, BitmapKey, Deserialize, IterateParams, ValueKey, U32_LEN,
}; };
use tokio::sync::mpsc; use tokio::sync::mpsc;
use trc::AddContext;
use utils::{ use utils::{
config::Config, config::Config,
lru_cache::{LruCache, LruCached}, lru_cache::{LruCache, LruCached},
@ -103,13 +104,6 @@ pub struct Inner {
pub cache_threads: LruCache<u32, Arc<Threads>>, pub cache_threads: LruCache<u32, Arc<Threads>>,
} }
#[derive(Debug)]
pub enum IngestError {
Temporary,
OverQuota,
Permanent { code: [u8; 3], reason: String },
}
impl JMAP { impl JMAP {
pub async fn init( pub async fn init(
config: &mut Config, config: &mut Config,
@ -176,13 +170,13 @@ impl JMAP {
collection: Collection, collection: Collection,
document_id: u32, document_id: u32,
property: impl AsRef<Property>, property: impl AsRef<Property>,
) -> Result<Option<U>, MethodError> ) -> trc::Result<Option<U>>
where where
U: Deserialize + 'static, U: Deserialize + 'static,
{ {
let property = property.as_ref(); let property = property.as_ref();
match self
.core self.core
.storage .storage
.data .data
.get_value::<U>(ValueKey { .get_value::<U>(ValueKey {
@ -192,20 +186,13 @@ impl JMAP {
class: ValueClass::Property(property.into()), class: ValueClass::Property(property.into()),
}) })
.await .await
{ .add_context(|err| {
Ok(value) => Ok(value), err.caused_by(trc::location!())
Err(err) => { .account_id(account_id)
tracing::error!(event = "error", .collection(collection)
context = "store", .document_id(document_id)
account_id = account_id, .property(property)
collection = ?collection, })
document_id = document_id,
property = ?property,
error = ?err,
"Failed to retrieve property");
Err(MethodError::ServerPartialFail)
}
}
} }
pub async fn get_properties<U, I, P>( pub async fn get_properties<U, I, P>(
@ -214,7 +201,7 @@ impl JMAP {
collection: Collection, collection: Collection,
iterate: &I, iterate: &I,
property: P, property: P,
) -> Result<Vec<(u32, U)>, MethodError> ) -> trc::Result<Vec<(u32, U)>>
where where
I: DocumentSet + Send + Sync, I: DocumentSet + Send + Sync,
P: AsRef<Property>, P: AsRef<Property>,
@ -254,43 +241,30 @@ impl JMAP {
}, },
) )
.await .await
.map_err(|err| { .add_context(|err| {
tracing::error!(event = "error", err.caused_by(trc::location!())
context = "store", .account_id(account_id)
account_id = account_id, .collection(collection)
collection = ?collection, .property(property)
property = ?property, })
error = ?err, .map(|_| results)
"Failed to retrieve properties");
MethodError::ServerPartialFail
})?;
Ok(results)
} }
pub async fn get_document_ids( pub async fn get_document_ids(
&self, &self,
account_id: u32, account_id: u32,
collection: Collection, collection: Collection,
) -> Result<Option<RoaringBitmap>, MethodError> { ) -> trc::Result<Option<RoaringBitmap>> {
match self self.core
.core
.storage .storage
.data .data
.get_bitmap(BitmapKey::document_ids(account_id, collection)) .get_bitmap(BitmapKey::document_ids(account_id, collection))
.await .await
{ .add_context(|err| {
Ok(value) => Ok(value), err.caused_by(trc::location!())
Err(err) => { .account_id(account_id)
tracing::error!(event = "error", .collection(collection)
context = "store", })
account_id = account_id,
collection = ?collection,
error = ?err,
"Failed to retrieve document ids bitmap");
Err(MethodError::ServerPartialFail)
}
}
} }
pub async fn get_tag( pub async fn get_tag(
@ -299,10 +273,9 @@ impl JMAP {
collection: Collection, collection: Collection,
property: impl AsRef<Property>, property: impl AsRef<Property>,
value: impl Into<TagValue<u32>>, value: impl Into<TagValue<u32>>,
) -> Result<Option<RoaringBitmap>, MethodError> { ) -> trc::Result<Option<RoaringBitmap>> {
let property = property.as_ref(); let property = property.as_ref();
match self self.core
.core
.storage .storage
.data .data
.get_bitmap(BitmapKey { .get_bitmap(BitmapKey {
@ -315,26 +288,19 @@ impl JMAP {
document_id: 0, document_id: 0,
}) })
.await .await
{ .add_context(|err| {
Ok(value) => Ok(value), err.caused_by(trc::location!())
Err(err) => { .account_id(account_id)
tracing::error!(event = "error", .collection(collection)
context = "store", .property(property)
account_id = account_id, })
collection = ?collection,
property = ?property,
error = ?err,
"Failed to retrieve tag bitmap");
Err(MethodError::ServerPartialFail)
}
}
} }
pub async fn prepare_set_response<T>( pub async fn prepare_set_response<T>(
&self, &self,
request: &SetRequest<T>, request: &SetRequest<T>,
collection: Collection, collection: Collection,
) -> Result<SetResponse, MethodError> { ) -> trc::Result<SetResponse> {
Ok( Ok(
SetResponse::from_request(request, self.core.jmap.set_max_objects)?.with_state( SetResponse::from_request(request, self.core.jmap.set_max_objects)?.with_state(
self.assert_state( self.assert_state(
@ -347,11 +313,7 @@ impl JMAP {
) )
} }
pub async fn get_quota( pub async fn get_quota(&self, access_token: &AccessToken, account_id: u32) -> trc::Result<i64> {
&self,
access_token: &AccessToken,
account_id: u32,
) -> Result<i64, MethodError> {
Ok(if access_token.primary_id == account_id { Ok(if access_token.primary_id == account_id {
access_token.quota as i64 access_token.quota as i64
} else { } else {
@ -360,35 +322,19 @@ impl JMAP {
.directory .directory
.query(QueryBy::Id(account_id), false) .query(QueryBy::Id(account_id), false)
.await .await
.map_err(|err| { .add_context(|err| err.caused_by(trc::location!()).account_id(account_id))?
tracing::error!(
event = "error",
context = "get_quota",
account_id = account_id,
error = ?err,
"Failed to obtain disk quota for account.");
MethodError::ServerPartialFail
})?
.map(|p| p.quota as i64) .map(|p| p.quota as i64)
.unwrap_or_default() .unwrap_or_default()
}) })
} }
pub async fn get_used_quota(&self, account_id: u32) -> Result<i64, MethodError> { pub async fn get_used_quota(&self, account_id: u32) -> trc::Result<i64> {
self.core self.core
.storage .storage
.data .data
.get_counter(DirectoryClass::UsedQuota(account_id)) .get_counter(DirectoryClass::UsedQuota(account_id))
.await .await
.map_err(|err| { .add_context(|err| err.caused_by(trc::location!()).account_id(account_id))
tracing::error!(
event = "error",
context = "get_used_quota",
account_id = account_id,
error = ?err,
"Failed to obtain used disk quota for account.");
MethodError::ServerPartialFail
})
} }
pub async fn has_available_quota( pub async fn has_available_quota(
@ -396,7 +342,7 @@ impl JMAP {
account_id: u32, account_id: u32,
account_quota: i64, account_quota: i64,
item_size: i64, item_size: i64,
) -> Result<bool, MethodError> { ) -> trc::Result<bool> {
if account_quota == 0 { if account_quota == 0 {
return Ok(true); return Ok(true);
} }
@ -433,21 +379,16 @@ impl JMAP {
account_id: u32, account_id: u32,
collection: Collection, collection: Collection,
filters: Vec<Filter>, filters: Vec<Filter>,
) -> Result<ResultSet, MethodError> { ) -> trc::Result<ResultSet> {
self.core self.core
.storage .storage
.data .data
.filter(account_id, collection, filters) .filter(account_id, collection, filters)
.await .await
.map_err(|err| { .add_context(|err| {
tracing::error!(event = "error", err.caused_by(trc::location!())
context = "filter", .account_id(account_id)
account_id = account_id, .collection(collection)
collection = ?collection,
error = ?err,
"Failed to execute filter.");
MethodError::ServerPartialFail
}) })
} }
@ -456,21 +397,16 @@ impl JMAP {
account_id: u32, account_id: u32,
collection: Collection, collection: Collection,
filters: Vec<FtsFilter<T>>, filters: Vec<FtsFilter<T>>,
) -> Result<RoaringBitmap, MethodError> { ) -> trc::Result<RoaringBitmap> {
self.core self.core
.storage .storage
.fts .fts
.query(account_id, collection, filters) .query(account_id, collection, filters)
.await .await
.map_err(|err| { .add_context(|err| {
tracing::error!(event = "error", err.caused_by(trc::location!())
context = "fts-filter", .account_id(account_id)
account_id = account_id, .collection(collection)
collection = ?collection,
error = ?err,
"Failed to execute filter.");
MethodError::ServerPartialFail
}) })
} }
@ -478,7 +414,7 @@ impl JMAP {
&self, &self,
result_set: &ResultSet, result_set: &ResultSet,
request: &QueryRequest<T>, request: &QueryRequest<T>,
) -> Result<(QueryResponse, Option<Pagination>), MethodError> { ) -> trc::Result<(QueryResponse, Option<Pagination>)> {
let total = result_set.results.len() as usize; let total = result_set.results.len() as usize;
let (limit_total, limit) = if let Some(limit) = request.limit { let (limit_total, limit) = if let Some(limit) = request.limit {
if limit > 0 { if limit > 0 {
@ -529,75 +465,39 @@ impl JMAP {
comparators: Vec<Comparator>, comparators: Vec<Comparator>,
paginate: Pagination, paginate: Pagination,
mut response: QueryResponse, mut response: QueryResponse,
) -> Result<QueryResponse, MethodError> { ) -> trc::Result<QueryResponse> {
// Sort results // Sort results
let collection = result_set.collection; let collection = result_set.collection;
let account_id = result_set.account_id; let account_id = result_set.account_id;
response.update_results( response.update_results(
match self self.core
.core
.storage .storage
.data .data
.sort(result_set, comparators, paginate) .sort(result_set, comparators, paginate)
.await .await
{ .add_context(|err| {
Ok(result) => result, err.caused_by(trc::location!())
Err(err) => { .account_id(account_id)
tracing::error!(event = "error", .collection(collection)
context = "store", })?,
account_id = account_id,
collection = ?collection,
error = ?err,
"Sort failed");
return Err(MethodError::ServerPartialFail);
}
},
)?; )?;
Ok(response) Ok(response)
} }
pub async fn write_batch(&self, batch: BatchBuilder) -> Result<AssignedIds, MethodError> { pub async fn write_batch(&self, batch: BatchBuilder) -> trc::Result<AssignedIds> {
self.core self.core
.storage .storage
.data .data
.write(batch.build()) .write(batch.build())
.await .await
.map_err(|err| { .caused_by(trc::location!())
match err {
store::Error::InternalError(err) => {
tracing::error!(
event = "error",
context = "write_batch",
error = ?err,
"Failed to write batch.");
MethodError::ServerPartialFail
}
store::Error::AssertValueFailed => {
// This should not occur, as we are not using assertions.
tracing::debug!(
event = "assert_failed",
context = "write_batch",
"Failed to assert value."
);
MethodError::ServerUnavailable
}
}
})
} }
pub async fn write_batch_expect_id(&self, batch: BatchBuilder) -> Result<u32, MethodError> { pub async fn write_batch_expect_id(&self, batch: BatchBuilder) -> trc::Result<u32> {
self.write_batch(batch).await.and_then(|ids| { self.write_batch(batch)
ids.last_document_id().map_err(|err| { .await
tracing::error!( .and_then(|ids| ids.last_document_id().caused_by(trc::location!()))
event = "error",
context = "write_batch_expect_id",
error = ?err,
"Failed to obtain last document id."
);
MethodError::ServerPartialFail
})
})
} }
} }
@ -625,11 +525,11 @@ impl From<JmapInstance> for JMAP {
} }
trait UpdateResults: Sized { trait UpdateResults: Sized {
fn update_results(&mut self, sorted_results: SortedResultSet) -> Result<(), MethodError>; fn update_results(&mut self, sorted_results: SortedResultSet) -> trc::Result<()>;
} }
impl UpdateResults for QueryResponse { impl UpdateResults for QueryResponse {
fn update_results(&mut self, sorted_results: SortedResultSet) -> Result<(), MethodError> { fn update_results(&mut self, sorted_results: SortedResultSet) -> trc::Result<()> {
// Prepare response // Prepare response
if sorted_results.found_anchor { if sorted_results.found_anchor {
self.position = sorted_results.position; self.position = sorted_results.position;
@ -640,7 +540,7 @@ impl UpdateResults for QueryResponse {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
Ok(()) Ok(())
} else { } else {
Err(MethodError::AnchorNotFound) Err(MethodError::AnchorNotFound.into())
} }
} }
} }

View file

@ -5,12 +5,12 @@
*/ */
use jmap_proto::{ use jmap_proto::{
error::method::MethodError,
method::get::{GetRequest, GetResponse, RequestArguments}, method::get::{GetRequest, GetResponse, RequestArguments},
object::Object, object::Object,
types::{acl::Acl, collection::Collection, keyword::Keyword, property::Property, value::Value}, types::{acl::Acl, collection::Collection, keyword::Keyword, property::Property, value::Value},
}; };
use store::{ahash::AHashSet, query::Filter, roaring::RoaringBitmap}; use store::{ahash::AHashSet, query::Filter, roaring::RoaringBitmap};
use trc::AddContext;
use crate::{ use crate::{
auth::{acl::EffectiveAcl, AccessToken}, auth::{acl::EffectiveAcl, AccessToken},
@ -22,7 +22,7 @@ impl JMAP {
&self, &self,
mut request: GetRequest<RequestArguments>, mut request: GetRequest<RequestArguments>,
access_token: &AccessToken, access_token: &AccessToken,
) -> Result<GetResponse, MethodError> { ) -> trc::Result<GetResponse> {
let ids = request.unwrap_ids(self.core.jmap.get_max_objects)?; let ids = request.unwrap_ids(self.core.jmap.get_max_objects)?;
let properties = request.unwrap_properties(&[ let properties = request.unwrap_properties(&[
Property::Id, Property::Id,
@ -241,19 +241,12 @@ impl JMAP {
&self, &self,
account_id: u32, account_id: u32,
document_ids: Option<RoaringBitmap>, document_ids: Option<RoaringBitmap>,
) -> Result<usize, MethodError> { ) -> trc::Result<usize> {
if let Some(document_ids) = document_ids { if let Some(document_ids) = document_ids {
let mut thread_ids = AHashSet::default(); let mut thread_ids = AHashSet::default();
self.get_cached_thread_ids(account_id, document_ids.into_iter()) self.get_cached_thread_ids(account_id, document_ids.into_iter())
.await .await
.map_err(|err| { .caused_by(trc::location!())?
tracing::error!(event = "error",
context = "store",
account_id = account_id,
error = ?err,
"Failed to retrieve thread Ids");
MethodError::ServerPartialFail
})?
.into_iter() .into_iter()
.for_each(|(_, thread_id)| { .for_each(|(_, thread_id)| {
thread_ids.insert(thread_id); thread_ids.insert(thread_id);
@ -269,7 +262,7 @@ impl JMAP {
account_id: u32, account_id: u32,
document_id: u32, document_id: u32,
message_ids: &Option<RoaringBitmap>, message_ids: &Option<RoaringBitmap>,
) -> Result<Option<RoaringBitmap>, MethodError> { ) -> trc::Result<Option<RoaringBitmap>> {
if let (Some(message_ids), Some(mailbox_message_ids)) = ( if let (Some(message_ids), Some(mailbox_message_ids)) = (
message_ids, message_ids,
self.get_tag( self.get_tag(
@ -309,7 +302,7 @@ impl JMAP {
account_id: u32, account_id: u32,
path: &'x str, path: &'x str,
exact_match: bool, exact_match: bool,
) -> Result<Option<ExpandPath<'x>>, MethodError> { ) -> trc::Result<Option<ExpandPath<'x>>> {
let path = path let path = path
.split('/') .split('/')
.filter_map(|p| { .filter_map(|p| {
@ -376,7 +369,7 @@ impl JMAP {
&self, &self,
account_id: u32, account_id: u32,
path: &str, path: &str,
) -> Result<Option<u32>, MethodError> { ) -> trc::Result<Option<u32>> {
Ok(self Ok(self
.mailbox_expand_path(account_id, path, true) .mailbox_expand_path(account_id, path, true)
.await? .await?
@ -399,7 +392,7 @@ impl JMAP {
&self, &self,
account_id: u32, account_id: u32,
role: &str, role: &str,
) -> Result<Option<u32>, MethodError> { ) -> trc::Result<Option<u32>> {
self.filter( self.filter(
account_id, account_id,
Collection::Mailbox, Collection::Mailbox,

View file

@ -23,7 +23,7 @@ impl JMAP {
&self, &self,
mut request: QueryRequest<QueryArguments>, mut request: QueryRequest<QueryArguments>,
access_token: &AccessToken, access_token: &AccessToken,
) -> Result<QueryResponse, MethodError> { ) -> trc::Result<QueryResponse> {
let account_id = request.account_id.document_id(); let account_id = request.account_id.document_id();
let sort_as_tree = request.arguments.sort_as_tree.unwrap_or(false); let sort_as_tree = request.arguments.sort_as_tree.unwrap_or(false);
let filter_as_tree = request.arguments.filter_as_tree.unwrap_or(false); let filter_as_tree = request.arguments.filter_as_tree.unwrap_or(false);
@ -80,7 +80,7 @@ impl JMAP {
filters.push(cond.into()); filters.push(cond.into());
} }
other => return Err(MethodError::UnsupportedFilter(other.to_string())), other => return Err(MethodError::UnsupportedFilter(other.to_string()).into()),
} }
} }
@ -182,7 +182,7 @@ impl JMAP {
query::Comparator::field(Property::ParentId, comparator.is_ascending) query::Comparator::field(Property::ParentId, comparator.is_ascending)
} }
other => return Err(MethodError::UnsupportedSort(other.to_string())), other => return Err(MethodError::UnsupportedSort(other.to_string()).into()),
}); });
} }

View file

@ -6,10 +6,7 @@
use common::config::jmap::settings::SpecialUse; use common::config::jmap::settings::SpecialUse;
use jmap_proto::{ use jmap_proto::{
error::{ error::set::{SetError, SetErrorType},
method::MethodError,
set::{SetError, SetErrorType},
},
method::set::{SetRequest, SetResponse}, method::set::{SetRequest, SetResponse},
object::{ object::{
index::{IndexAs, IndexProperty, ObjectIndexBuilder}, index::{IndexAs, IndexProperty, ObjectIndexBuilder},
@ -36,6 +33,7 @@ use store::{
BatchBuilder, F_BITMAP, F_CLEAR, F_VALUE, BatchBuilder, F_BITMAP, F_CLEAR, F_VALUE,
}, },
}; };
use trc::AddContext;
use crate::{ use crate::{
auth::{acl::EffectiveAcl, AccessToken}, auth::{acl::EffectiveAcl, AccessToken},
@ -79,7 +77,7 @@ impl JMAP {
&self, &self,
mut request: SetRequest<SetArguments>, mut request: SetRequest<SetArguments>,
access_token: &AccessToken, access_token: &AccessToken,
) -> Result<SetResponse, MethodError> { ) -> trc::Result<SetResponse> {
// Prepare response // Prepare response
let account_id = request.account_id.document_id(); let account_id = request.account_id.document_id();
let on_destroy_remove_emails = request.arguments.on_destroy_remove_emails.unwrap_or(false); let on_destroy_remove_emails = request.arguments.on_destroy_remove_emails.unwrap_or(false);
@ -130,7 +128,7 @@ impl JMAP {
ctx.mailbox_ids.insert(document_id); ctx.mailbox_ids.insert(document_id);
ctx.response.created(id, document_id); ctx.response.created(id, document_id);
} }
Err(store::Error::AssertValueFailed) => { Err(err) if err.matches(trc::Cause::AssertValue) => {
ctx.response.not_created.append( ctx.response.not_created.append(
id, id,
SetError::forbidden().with_description( SetError::forbidden().with_description(
@ -140,13 +138,7 @@ impl JMAP {
continue 'create; continue 'create;
} }
Err(err) => { Err(err) => {
tracing::error!( return Err(err.caused_by(trc::location!()));
event = "error",
context = "mailbox_set",
account_id = account_id,
error = ?err,
"Failed to update mailbox(es).");
return Err(MethodError::ServerPartialFail);
} }
} }
} }
@ -229,20 +221,14 @@ impl JMAP {
Ok(_) => { Ok(_) => {
changes.log_update(Collection::Mailbox, document_id); changes.log_update(Collection::Mailbox, document_id);
} }
Err(store::Error::AssertValueFailed) => { Err(err) if err.matches(trc::Cause::AssertValue) => {
ctx.response.not_updated.append(id, SetError::forbidden().with_description( ctx.response.not_updated.append(id, SetError::forbidden().with_description(
"Another process modified this mailbox, please try again.", "Another process modified this mailbox, please try again.",
)); ));
continue 'update; continue 'update;
} }
Err(err) => { Err(err) => {
tracing::error!( return Err(err.caused_by(trc::location!()));
event = "error",
context = "mailbox_set",
account_id = account_id,
error = ?err,
"Failed to update mailbox(es).");
return Err(MethodError::ServerPartialFail);
} }
} }
} }
@ -306,7 +292,7 @@ impl JMAP {
changes: &mut ChangeLogBuilder, changes: &mut ChangeLogBuilder,
access_token: &AccessToken, access_token: &AccessToken,
remove_emails: bool, remove_emails: bool,
) -> Result<Result<bool, SetError>, MethodError> { ) -> trc::Result<Result<bool, SetError>> {
// Internal folders cannot be deleted // Internal folders cannot be deleted
#[cfg(feature = "test_mode")] #[cfg(feature = "test_mode")]
if [INBOX_ID, TRASH_ID].contains(&document_id) && !access_token.is_super_user() { if [INBOX_ID, TRASH_ID].contains(&document_id) && !access_token.is_super_user() {
@ -405,7 +391,7 @@ impl JMAP {
Collection::Email, Collection::Email,
Id::from_parts(thread_id, message_id), Id::from_parts(thread_id, message_id),
), ),
Err(store::Error::AssertValueFailed) => { Err(err) if err.matches(trc::Cause::AssertValue) => {
return Ok(Err(SetError::forbidden().with_description( return Ok(Err(SetError::forbidden().with_description(
concat!( concat!(
"Another process modified a message in this mailbox ", "Another process modified a message in this mailbox ",
@ -414,15 +400,7 @@ impl JMAP {
))); )));
} }
Err(err) => { Err(err) => {
tracing::error!( return Err(err.caused_by(trc::location!()));
event = "error",
context = "mailbox_set",
account_id = account_id,
mailbox_id = document_id,
message_id = message_id,
error = ?err,
"Failed to update message while deleting mailbox.");
return Err(MethodError::ServerPartialFail);
} }
} }
} else { } else {
@ -491,21 +469,12 @@ impl JMAP {
changes.log_delete(Collection::Mailbox, document_id); changes.log_delete(Collection::Mailbox, document_id);
Ok(Ok(did_remove_emails)) Ok(Ok(did_remove_emails))
} }
Err(store::Error::AssertValueFailed) => Ok(Err(SetError::forbidden() Err(err) if err.matches(trc::Cause::AssertValue) => Ok(Err(SetError::forbidden()
.with_description(concat!( .with_description(concat!(
"Another process modified this mailbox ", "Another process modified this mailbox ",
"while deleting it, please try again." "while deleting it, please try again."
)))), )))),
Err(err) => { Err(err) => Err(err.caused_by(trc::location!())),
tracing::error!(
event = "error",
context = "mailbox_set",
account_id = account_id,
document_id = document_id,
error = ?err,
"Failed to delete mailbox.");
Err(MethodError::ServerPartialFail)
}
} }
} else { } else {
Ok(Err(SetError::not_found())) Ok(Err(SetError::not_found()))
@ -518,7 +487,7 @@ impl JMAP {
changes_: Object<SetValue>, changes_: Object<SetValue>,
update: Option<(u32, HashedValue<Object<Value>>)>, update: Option<(u32, HashedValue<Object<Value>>)>,
ctx: &SetContext<'_>, ctx: &SetContext<'_>,
) -> Result<Result<ObjectIndexBuilder, SetError>, MethodError> { ) -> trc::Result<Result<ObjectIndexBuilder, SetError>> {
// Parse properties // Parse properties
let mut changes = Object::with_capacity(changes_.properties.len()); let mut changes = Object::with_capacity(changes_.properties.len());
for (property, value) in changes_.properties { for (property, value) in changes_.properties {
@ -799,10 +768,7 @@ impl JMAP {
.validate()) .validate())
} }
pub async fn mailbox_get_or_create( pub async fn mailbox_get_or_create(&self, account_id: u32) -> trc::Result<RoaringBitmap> {
&self,
account_id: u32,
) -> Result<RoaringBitmap, MethodError> {
let mut mailbox_ids = self let mut mailbox_ids = self
.get_document_ids(account_id, Collection::Mailbox) .get_document_ids(account_id, Collection::Mailbox)
.await? .await?
@ -865,23 +831,15 @@ impl JMAP {
.data .data
.write(batch.build()) .write(batch.build())
.await .await
.map_err(|err| { .caused_by(trc::location!())
tracing::error!( .map(|_| mailbox_ids)
event = "error",
context = "mailbox_get_or_create",
error = ?err,
"Failed to create mailboxes.");
MethodError::ServerPartialFail
})?;
Ok(mailbox_ids)
} }
pub async fn mailbox_create_path( pub async fn mailbox_create_path(
&self, &self,
account_id: u32, account_id: u32,
path: &str, path: &str,
) -> Result<Option<(u32, Option<u64>)>, MethodError> { ) -> trc::Result<Option<(u32, Option<u64>)>> {
let expanded_path = let expanded_path =
if let Some(expand_path) = self.mailbox_expand_path(account_id, path, false).await? { if let Some(expand_path) = self.mailbox_expand_path(account_id, path, false).await? {
expand_path expand_path

View file

@ -6,7 +6,6 @@
use directory::QueryBy; use directory::QueryBy;
use jmap_proto::{ use jmap_proto::{
error::method::MethodError,
method::get::{GetRequest, GetResponse, RequestArguments}, method::get::{GetRequest, GetResponse, RequestArguments},
object::Object, object::Object,
types::{collection::Collection, property::Property, state::State, value::Value}, types::{collection::Collection, property::Property, state::State, value::Value},
@ -18,7 +17,7 @@ impl JMAP {
pub async fn principal_get( pub async fn principal_get(
&self, &self,
mut request: GetRequest<RequestArguments>, mut request: GetRequest<RequestArguments>,
) -> Result<GetResponse, MethodError> { ) -> trc::Result<GetResponse> {
let ids = request.unwrap_ids(self.core.jmap.get_max_objects)?; let ids = request.unwrap_ids(self.core.jmap.get_max_objects)?;
let properties = request.unwrap_properties(&[ let properties = request.unwrap_properties(&[
Property::Id, Property::Id,
@ -56,8 +55,7 @@ impl JMAP {
.storage .storage
.directory .directory
.query(QueryBy::Id(id.document_id()), false) .query(QueryBy::Id(id.document_id()), false)
.await .await?
.map_err(|_| MethodError::ServerPartialFail)?
{ {
principal principal
} else { } else {

View file

@ -18,7 +18,7 @@ impl JMAP {
pub async fn principal_query( pub async fn principal_query(
&self, &self,
mut request: QueryRequest<RequestArguments>, mut request: QueryRequest<RequestArguments>,
) -> Result<QueryResponse, MethodError> { ) -> trc::Result<QueryResponse> {
let account_id = request.account_id.document_id(); let account_id = request.account_id.document_id();
let mut result_set = ResultSet { let mut result_set = ResultSet {
account_id, account_id,
@ -35,8 +35,7 @@ impl JMAP {
.storage .storage
.directory .directory
.query(QueryBy::Name(name.as_str()), false) .query(QueryBy::Name(name.as_str()), false)
.await .await?
.map_err(|_| MethodError::ServerPartialFail)?
{ {
if is_set || result_set.results.contains(principal.id) { if is_set || result_set.results.contains(principal.id) {
result_set.results = result_set.results =
@ -54,8 +53,7 @@ impl JMAP {
for id in self for id in self
.core .core
.email_to_ids(&self.core.storage.directory, &email) .email_to_ids(&self.core.storage.directory, &email)
.await .await?
.map_err(|_| MethodError::ServerPartialFail)?
{ {
ids.insert(id); ids.insert(id);
} }
@ -67,7 +65,7 @@ impl JMAP {
} }
} }
Filter::Type(_) => {} Filter::Type(_) => {}
other => return Err(MethodError::UnsupportedFilter(other.to_string())), other => return Err(MethodError::UnsupportedFilter(other.to_string()).into()),
} }
} }

View file

@ -26,7 +26,7 @@ impl JMAP {
&self, &self,
mut request: GetRequest<RequestArguments>, mut request: GetRequest<RequestArguments>,
access_token: &AccessToken, access_token: &AccessToken,
) -> Result<GetResponse, MethodError> { ) -> trc::Result<GetResponse> {
let ids = request.unwrap_ids(self.core.jmap.get_max_objects)?; let ids = request.unwrap_ids(self.core.jmap.get_max_objects)?;
let properties = request.unwrap_properties(&[ let properties = request.unwrap_properties(&[
Property::Id, Property::Id,
@ -86,7 +86,8 @@ impl JMAP {
Property::Url | Property::Keys | Property::Value => { Property::Url | Property::Keys | Property::Value => {
return Err(MethodError::Forbidden( return Err(MethodError::Forbidden(
"The 'url' and 'keys' properties are not readable".to_string(), "The 'url' and 'keys' properties are not readable".to_string(),
)); )
.into());
} }
property => { property => {
result.append(property.clone(), push.remove(property)); result.append(property.clone(), push.remove(property));
@ -99,7 +100,7 @@ impl JMAP {
Ok(response) Ok(response)
} }
pub async fn fetch_push_subscriptions(&self, account_id: u32) -> store::Result<state::Event> { pub async fn fetch_push_subscriptions(&self, account_id: u32) -> trc::Result<state::Event> {
let mut subscriptions = Vec::new(); let mut subscriptions = Vec::new();
let document_ids = self let document_ids = self
.core .core
@ -127,10 +128,9 @@ impl JMAP {
}) })
.await? .await?
.ok_or_else(|| { .ok_or_else(|| {
store::Error::InternalError(format!( trc::Cause::NotFound
"Could not find push subscription {}", .caused_by(trc::location!())
document_id .document_id(document_id)
))
})?; })?;
let expires = subscription let expires = subscription
@ -138,10 +138,9 @@ impl JMAP {
.get(&Property::Expires) .get(&Property::Expires)
.and_then(|p| p.as_date()) .and_then(|p| p.as_date())
.ok_or_else(|| { .ok_or_else(|| {
store::Error::InternalError(format!( trc::Cause::Unexpected
"Missing expires property for push subscription {}", .caused_by(trc::location!())
document_id .document_id(document_id)
))
})? })?
.timestamp() as u64; .timestamp() as u64;
if expires > current_time { if expires > current_time {
@ -175,20 +174,18 @@ impl JMAP {
.remove(&Property::Value) .remove(&Property::Value)
.and_then(|p| p.try_unwrap_string()) .and_then(|p| p.try_unwrap_string())
.ok_or_else(|| { .ok_or_else(|| {
store::Error::InternalError(format!( trc::Cause::Unexpected
"Missing verificationCode property for push subscription {}", .caused_by(trc::location!())
document_id .document_id(document_id)
))
})?; })?;
let url = subscription let url = subscription
.properties .properties
.remove(&Property::Url) .remove(&Property::Url)
.and_then(|p| p.try_unwrap_string()) .and_then(|p| p.try_unwrap_string())
.ok_or_else(|| { .ok_or_else(|| {
store::Error::InternalError(format!( trc::Cause::Unexpected
"Missing Url property for push subscription {}", .caused_by(trc::location!())
document_id .document_id(document_id)
))
})?; })?;
if subscription if subscription

View file

@ -6,7 +6,7 @@
use base64::{engine::general_purpose, Engine}; use base64::{engine::general_purpose, Engine};
use jmap_proto::{ use jmap_proto::{
error::{method::MethodError, set::SetError}, error::set::SetError,
method::set::{RequestArguments, SetRequest, SetResponse}, method::set::{RequestArguments, SetRequest, SetResponse},
object::Object, object::Object,
response::references::EvalObjectReferences, response::references::EvalObjectReferences,
@ -33,7 +33,7 @@ impl JMAP {
&self, &self,
mut request: SetRequest<RequestArguments>, mut request: SetRequest<RequestArguments>,
access_token: &AccessToken, access_token: &AccessToken,
) -> Result<SetResponse, MethodError> { ) -> trc::Result<SetResponse> {
let account_id = access_token.primary_id(); let account_id = access_token.primary_id();
let mut push_ids = self let mut push_ids = self
.get_document_ids(account_id, Collection::PushSubscription) .get_document_ids(account_id, Collection::PushSubscription)

View file

@ -5,7 +5,6 @@
*/ */
use jmap_proto::{ use jmap_proto::{
error::method::MethodError,
method::get::{GetRequest, GetResponse, RequestArguments}, method::get::{GetRequest, GetResponse, RequestArguments},
object::Object, object::Object,
types::{id::Id, property::Property, state::State, type_state::DataType, value::Value}, types::{id::Id, property::Property, state::State, type_state::DataType, value::Value},
@ -18,7 +17,7 @@ impl JMAP {
&self, &self,
mut request: GetRequest<RequestArguments>, mut request: GetRequest<RequestArguments>,
access_token: &AccessToken, access_token: &AccessToken,
) -> Result<GetResponse, MethodError> { ) -> trc::Result<GetResponse> {
let ids = request.unwrap_ids(self.core.jmap.get_max_objects)?; let ids = request.unwrap_ids(self.core.jmap.get_max_objects)?;
let properties = request.unwrap_properties(&[ let properties = request.unwrap_properties(&[
Property::Id, Property::Id,

View file

@ -5,7 +5,6 @@
*/ */
use jmap_proto::{ use jmap_proto::{
error::method::MethodError,
method::query::{QueryRequest, QueryResponse, RequestArguments}, method::query::{QueryRequest, QueryResponse, RequestArguments},
types::{id::Id, state::State}, types::{id::Id, state::State},
}; };
@ -17,7 +16,7 @@ impl JMAP {
&self, &self,
request: QueryRequest<RequestArguments>, request: QueryRequest<RequestArguments>,
access_token: &AccessToken, access_token: &AccessToken,
) -> Result<QueryResponse, MethodError> { ) -> trc::Result<QueryResponse> {
Ok(QueryResponse { Ok(QueryResponse {
account_id: request.account_id, account_id: request.account_id,
query_state: State::Initial, query_state: State::Initial,

View file

@ -16,6 +16,6 @@ impl JMAP {
&self, &self,
account_id: u32, account_id: u32,
quota: &AccessToken, quota: &AccessToken,
) -> Result<SetResponse, MethodError> { ) -> trc::Result<SetResponse> {
} }
} }

View file

@ -222,7 +222,7 @@ impl JMAP {
.set(event.value_class(), (now() + INDEX_LOCK_EXPIRY).serialize()); .set(event.value_class(), (now() + INDEX_LOCK_EXPIRY).serialize());
match self.core.storage.data.write(batch.build()).await { match self.core.storage.data.write(batch.build()).await {
Ok(_) => true, Ok(_) => true,
Err(store::Error::AssertValueFailed) => { Err(err) if err.matches(trc::Cause::AssertValue) => {
tracing::trace!( tracing::trace!(
context = "queue", context = "queue",
event = "locked", event = "locked",
@ -253,7 +253,7 @@ impl IndexEmail {
}) })
} }
fn deserialize(key: &[u8], value: &[u8]) -> store::Result<Self> { fn deserialize(key: &[u8], value: &[u8]) -> trc::Result<Self> {
Ok(IndexEmail { Ok(IndexEmail {
seq: key.deserialize_be_u64(0)?, seq: key.deserialize_be_u64(0)?,
account_id: key.deserialize_be_u32(U64_LEN)?, account_id: key.deserialize_be_u32(U64_LEN)?,
@ -265,7 +265,7 @@ impl IndexEmail {
..U64_LEN + U32_LEN + U32_LEN + BLOB_HASH_LEN + 1, ..U64_LEN + U32_LEN + U32_LEN + BLOB_HASH_LEN + 1,
) )
.and_then(|bytes| BlobHash::try_from_hash_slice(bytes).ok()) .and_then(|bytes| BlobHash::try_from_hash_slice(bytes).ok())
.ok_or_else(|| store::Error::InternalError("Invalid blob hash".to_string()))?, .ok_or_else(|| trc::Error::corrupted_key(key, value.into(), trc::location!()))?,
}) })
} }
} }

View file

@ -13,11 +13,13 @@ use store::ahash::AHashMap;
use crate::{ use crate::{
email::ingest::{IngestEmail, IngestSource}, email::ingest::{IngestEmail, IngestSource},
mailbox::INBOX_ID, mailbox::INBOX_ID,
IngestError, JMAP, JMAP,
}; };
impl JMAP { impl JMAP {
pub async fn deliver_message(&self, message: IngestMessage) -> Vec<DeliveryResult> { pub async fn deliver_message(&self, message: IngestMessage) -> Vec<DeliveryResult> {
let todo = "trace all errors";
// Read message // Read message
let raw_message = match self let raw_message = match self
.core .core
@ -137,21 +139,28 @@ impl JMAP {
.await; .await;
} }
} }
Err(err) => match err { Err(mut err) => match err.as_ref() {
IngestError::OverQuota => { trc::Cause::OverQuota => {
*status = DeliveryResult::TemporaryFailure { *status = DeliveryResult::TemporaryFailure {
reason: "Mailbox over quota.".into(), reason: "Mailbox over quota.".into(),
} }
} }
IngestError::Temporary => { trc::Cause::Ingest => {
*status = DeliveryResult::TemporaryFailure { *status = DeliveryResult::PermanentFailure {
reason: "Transient server failure.".into(), code: err
.value(trc::Key::Reason)
.and_then(|v| v.to_uint())
.map(|n| [(n / 100) as u8, ((n % 100) / 10) as u8, (n % 10) as u8])
.unwrap(),
reason: err
.take_value(trc::Key::Reason)
.and_then(|v| v.into_string())
.unwrap(),
} }
} }
IngestError::Permanent { code, reason } => { _ => {
*status = DeliveryResult::PermanentFailure { *status = DeliveryResult::TemporaryFailure {
code, reason: "Transient server failure.".into(),
reason: reason.into(),
} }
} }
}, },

View file

@ -7,7 +7,6 @@
use std::sync::Arc; use std::sync::Arc;
use jmap_proto::{ use jmap_proto::{
error::method::MethodError,
method::get::{GetRequest, GetResponse, RequestArguments}, method::get::{GetRequest, GetResponse, RequestArguments},
object::Object, object::Object,
types::{collection::Collection, property::Property, value::Value}, types::{collection::Collection, property::Property, value::Value},
@ -27,7 +26,7 @@ impl JMAP {
pub async fn sieve_script_get( pub async fn sieve_script_get(
&self, &self,
mut request: GetRequest<RequestArguments>, mut request: GetRequest<RequestArguments>,
) -> Result<GetResponse, MethodError> { ) -> trc::Result<GetResponse> {
let ids = request.unwrap_ids(self.core.jmap.get_max_objects)?; let ids = request.unwrap_ids(self.core.jmap.get_max_objects)?;
let properties = let properties =
request.unwrap_properties(&[Property::Id, Property::Name, Property::BlobId]); request.unwrap_properties(&[Property::Id, Property::Name, Property::BlobId]);
@ -115,7 +114,7 @@ impl JMAP {
pub async fn sieve_script_get_active( pub async fn sieve_script_get_active(
&self, &self,
account_id: u32, account_id: u32,
) -> Result<Option<ActiveScript>, MethodError> { ) -> trc::Result<Option<ActiveScript>> {
// Find the currently active script // Find the currently active script
if let Some(document_id) = self if let Some(document_id) = self
.filter( .filter(
@ -157,7 +156,7 @@ impl JMAP {
&self, &self,
account_id: u32, account_id: u32,
name: &str, name: &str,
) -> Result<Option<Sieve>, MethodError> { ) -> trc::Result<Option<Sieve>> {
// Find the script by name // Find the script by name
if let Some(document_id) = self if let Some(document_id) = self
.filter( .filter(
@ -182,7 +181,7 @@ impl JMAP {
&self, &self,
account_id: u32, account_id: u32,
document_id: u32, document_id: u32,
) -> Result<(Sieve, Object<Value>), MethodError> { ) -> trc::Result<(Sieve, Object<Value>)> {
// Obtain script object // Obtain script object
let script_object = self let script_object = self
.get_property::<HashedValue<Object<Value>>>( .get_property::<HashedValue<Object<Value>>>(
@ -193,15 +192,9 @@ impl JMAP {
) )
.await? .await?
.ok_or_else(|| { .ok_or_else(|| {
tracing::warn!( trc::Cause::NotFound
context = "sieve_script_compile", .caused_by(trc::location!())
event = "error", .document_id(document_id)
account_id = account_id,
document_id = document_id,
"Failed to obtain sieve script object"
);
MethodError::ServerPartialFail
})?; })?;
// Obtain the sieve script length // Obtain the sieve script length
@ -212,22 +205,20 @@ impl JMAP {
.and_then(|v| v.as_blob_id()) .and_then(|v| v.as_blob_id())
.and_then(|v| (v.section.as_ref()?.size, v).into()) .and_then(|v| (v.section.as_ref()?.size, v).into())
.ok_or_else(|| { .ok_or_else(|| {
tracing::warn!( trc::Cause::NotFound
context = "sieve_script_compile", .caused_by(trc::location!())
event = "error", .document_id(document_id)
account_id = account_id,
document_id = document_id,
"Failed to obtain sieve script blobId"
);
MethodError::ServerPartialFail
})?; })?;
// Obtain the sieve script blob // Obtain the sieve script blob
let script_bytes = self let script_bytes = self
.get_blob(&blob_id.hash, 0..usize::MAX) .get_blob(&blob_id.hash, 0..usize::MAX)
.await? .await?
.ok_or(MethodError::ServerPartialFail)?; .ok_or_else(|| {
trc::Cause::NotFound
.caused_by(trc::location!())
.document_id(document_id)
})?;
// Obtain the precompiled script // Obtain the precompiled script
if let Some(sieve) = script_bytes if let Some(sieve) = script_bytes
@ -239,15 +230,9 @@ impl JMAP {
// Deserialization failed, probably because the script compiler version changed // Deserialization failed, probably because the script compiler version changed
match self.core.sieve.untrusted_compiler.compile( match self.core.sieve.untrusted_compiler.compile(
script_bytes.get(0..script_offset).ok_or_else(|| { script_bytes.get(0..script_offset).ok_or_else(|| {
tracing::warn!( trc::Cause::NotFound
context = "sieve_script_compile", .caused_by(trc::location!())
event = "error", .document_id(document_id)
account_id = account_id,
document_id = document_id,
"Invalid sieve script offset"
);
MethodError::ServerPartialFail
})?, })?,
) { ) {
Ok(sieve) => { Ok(sieve) => {
@ -289,16 +274,10 @@ impl JMAP {
Ok((sieve.inner, new_script_object)) Ok((sieve.inner, new_script_object))
} }
Err(error) => { Err(error) => Err(trc::Cause::Unexpected
tracing::warn!( .caused_by(trc::location!())
context = "sieve_script_compile", .reason(error)
event = "error", .details("Failed to compile Sieve script")),
account_id = account_id,
document_id = document_id,
reason = %error,
"Failed to compile sieve script");
Err(MethodError::ServerPartialFail)
}
} }
} }
} }

View file

@ -16,12 +16,13 @@ use store::{
ahash::AHashSet, ahash::AHashSet,
write::{now, BatchBuilder, Bincode, F_VALUE}, write::{now, BatchBuilder, Bincode, F_VALUE},
}; };
use trc::AddContext;
use crate::{ use crate::{
email::ingest::{IngestEmail, IngestSource, IngestedEmail}, email::ingest::{IngestEmail, IngestSource, IngestedEmail},
mailbox::{INBOX_ID, TRASH_ID}, mailbox::{INBOX_ID, TRASH_ID},
sieve::SeenIdHash, sieve::SeenIdHash,
IngestError, JMAP, JMAP,
}; };
use super::ActiveScript; use super::ActiveScript;
@ -41,22 +42,21 @@ impl JMAP {
envelope_to: &str, envelope_to: &str,
account_id: u32, account_id: u32,
mut active_script: ActiveScript, mut active_script: ActiveScript,
) -> Result<IngestedEmail, IngestError> { ) -> trc::Result<IngestedEmail> {
// Parse message // Parse message
let message = if let Some(message) = MessageParser::new().parse(raw_message) { let message = if let Some(message) = MessageParser::new().parse(raw_message) {
message message
} else { } else {
return Err(IngestError::Permanent { return Err(trc::Cause::Ingest
code: [5, 5, 0], .ctx(trc::Key::Code, 550)
reason: "Failed to parse message.".to_string(), .ctx(trc::Key::Reason, "Failed to parse e-mail message."));
});
}; };
// Obtain mailboxIds // Obtain mailboxIds
let mailbox_ids = self let mailbox_ids = self
.mailbox_get_or_create(account_id) .mailbox_get_or_create(account_id)
.await .await
.map_err(|_| IngestError::Temporary)?; .caused_by(trc::location!())?;
// Create Sieve instance // Create Sieve instance
let mut instance = self.core.sieve.untrusted_runtime.filter_parsed(message); let mut instance = self.core.sieve.untrusted_runtime.filter_parsed(message);
@ -74,8 +74,8 @@ impl JMAP {
(p.quota as i64, p.emails.into_iter().next()) (p.quota as i64, p.emails.into_iter().next())
} }
Ok(None) => (0, None), Ok(None) => (0, None),
Err(_) => { Err(err) => {
return Err(IngestError::Temporary); return Err(err.caused_by(trc::location!()));
} }
}; };
@ -462,10 +462,9 @@ impl JMAP {
} }
if let Some(reject_reason) = reject_reason { if let Some(reject_reason) = reject_reason {
Err(IngestError::Permanent { Err(trc::Cause::Ingest
code: [5, 7, 1], .ctx(trc::Key::Code, 571)
reason: reject_reason, .ctx(trc::Key::Reason, reject_reason))
})
} else if has_delivered || last_temp_error.is_none() { } else if has_delivered || last_temp_error.is_none() {
Ok(ingested_message) Ok(ingested_message)
} else { } else {

View file

@ -19,7 +19,7 @@ impl JMAP {
pub async fn sieve_script_query( pub async fn sieve_script_query(
&self, &self,
mut request: QueryRequest<RequestArguments>, mut request: QueryRequest<RequestArguments>,
) -> Result<QueryResponse, MethodError> { ) -> trc::Result<QueryResponse> {
let account_id = request.account_id.document_id(); let account_id = request.account_id.document_id();
let mut filters = Vec::with_capacity(request.filter.len()); let mut filters = Vec::with_capacity(request.filter.len());
@ -32,7 +32,7 @@ impl JMAP {
Filter::And | Filter::Or | Filter::Not | Filter::Close => { Filter::And | Filter::Or | Filter::Not | Filter::Close => {
filters.push(cond.into()); filters.push(cond.into());
} }
other => return Err(MethodError::UnsupportedFilter(other.to_string())), other => return Err(MethodError::UnsupportedFilter(other.to_string()).into()),
} }
} }
@ -57,7 +57,7 @@ impl JMAP {
SortProperty::IsActive => { SortProperty::IsActive => {
query::Comparator::field(Property::IsActive, comparator.is_ascending) query::Comparator::field(Property::IsActive, comparator.is_ascending)
} }
other => return Err(MethodError::UnsupportedSort(other.to_string())), other => return Err(MethodError::UnsupportedSort(other.to_string()).into()),
}); });
} }

View file

@ -5,10 +5,7 @@
*/ */
use jmap_proto::{ use jmap_proto::{
error::{ error::set::{SetError, SetErrorType},
method::MethodError,
set::{SetError, SetErrorType},
},
method::set::{SetRequest, SetResponse}, method::set::{SetRequest, SetResponse},
object::{ object::{
index::{IndexAs, IndexProperty, ObjectIndexBuilder}, index::{IndexAs, IndexProperty, ObjectIndexBuilder},
@ -61,7 +58,7 @@ impl JMAP {
&self, &self,
mut request: SetRequest<SetArguments>, mut request: SetRequest<SetArguments>,
access_token: &AccessToken, access_token: &AccessToken,
) -> Result<SetResponse, MethodError> { ) -> trc::Result<SetResponse> {
let account_id = request.account_id.document_id(); let account_id = request.account_id.document_id();
let mut sieve_ids = self let mut sieve_ids = self
.get_document_ids(account_id, Collection::SieveScript) .get_document_ids(account_id, Collection::SieveScript)
@ -162,14 +159,9 @@ impl JMAP {
.inner .inner
.blob_id() .blob_id()
.ok_or_else(|| { .ok_or_else(|| {
tracing::warn!( trc::Cause::NotFound
event = "error", .caused_by(trc::location!())
context = "sieve_set", .document_id(document_id)
account_id = account_id,
document_id = document_id,
"Sieve does not contain a blobId."
);
MethodError::ServerPartialFail
})? })?
.clone(); .clone();
@ -233,20 +225,14 @@ impl JMAP {
changes.log_update(Collection::SieveScript, document_id); changes.log_update(Collection::SieveScript, document_id);
match self.core.storage.data.write(batch.build()).await { match self.core.storage.data.write(batch.build()).await {
Ok(_) => (), Ok(_) => (),
Err(store::Error::AssertValueFailed) => { Err(err) if err.matches(trc::Cause::AssertValue) => {
ctx.response.not_updated.append(id, SetError::forbidden().with_description( ctx.response.not_updated.append(id, SetError::forbidden().with_description(
"Another process modified this sieve, please try again.", "Another process modified this sieve, please try again.",
)); ));
continue 'update; continue 'update;
} }
Err(err) => { Err(err) => {
tracing::error!( return Err(err.caused_by(trc::location!()));
event = "error",
context = "sieve_set",
account_id = account_id,
error = ?err,
"Failed to update sieve script(s).");
return Err(MethodError::ServerPartialFail);
} }
} }
} }
@ -339,7 +325,7 @@ impl JMAP {
account_id: u32, account_id: u32,
document_id: u32, document_id: u32,
fail_if_active: bool, fail_if_active: bool,
) -> Result<bool, MethodError> { ) -> trc::Result<bool> {
// Fetch record // Fetch record
let obj = self let obj = self
.get_property::<HashedValue<Object<Value>>>( .get_property::<HashedValue<Object<Value>>>(
@ -350,14 +336,9 @@ impl JMAP {
) )
.await? .await?
.ok_or_else(|| { .ok_or_else(|| {
tracing::warn!( trc::Cause::NotFound
event = "error", .caused_by(trc::location!())
context = "sieve_script_delete", .document_id(document_id)
account_id = account_id,
document_id = document_id,
"Sieve script not found."
);
MethodError::ServerPartialFail
})?; })?;
// Make sure the script is not active // Make sure the script is not active
@ -373,14 +354,9 @@ impl JMAP {
// Delete record // Delete record
let mut batch = BatchBuilder::new(); let mut batch = BatchBuilder::new();
let blob_id = obj.inner.blob_id().ok_or_else(|| { let blob_id = obj.inner.blob_id().ok_or_else(|| {
tracing::warn!( trc::Cause::NotFound
event = "error", .caused_by(trc::location!())
context = "sieve_script_delete", .document_id(document_id)
account_id = account_id,
document_id = document_id,
"Sieve does not contain a blobId."
);
MethodError::ServerPartialFail
})?; })?;
batch batch
.with_account_id(account_id) .with_account_id(account_id)
@ -405,7 +381,7 @@ impl JMAP {
changes_: Object<SetValue>, changes_: Object<SetValue>,
update: Option<(u32, HashedValue<Object<Value>>)>, update: Option<(u32, HashedValue<Object<Value>>)>,
ctx: &SetContext<'_>, ctx: &SetContext<'_>,
) -> Result<Result<(ObjectIndexBuilder, Option<Vec<u8>>), SetError>, MethodError> { ) -> trc::Result<Result<(ObjectIndexBuilder, Option<Vec<u8>>), SetError>> {
// Vacation script cannot be modified // Vacation script cannot be modified
if matches!(update.as_ref().and_then(|(_, obj)| obj.inner.properties.get(&Property::Name)), Some(Value::Text ( value )) if value.eq_ignore_ascii_case("vacation")) if matches!(update.as_ref().and_then(|(_, obj)| obj.inner.properties.get(&Property::Name)), Some(Value::Text ( value )) if value.eq_ignore_ascii_case("vacation"))
{ {
@ -562,7 +538,7 @@ impl JMAP {
&self, &self,
account_id: u32, account_id: u32,
mut activate_id: Option<u32>, mut activate_id: Option<u32>,
) -> Result<Vec<(u32, bool)>, MethodError> { ) -> trc::Result<Vec<(u32, bool)>> {
let mut changed_ids = Vec::new(); let mut changed_ids = Vec::new();
// Find the currently active script // Find the currently active script
let mut active_ids = self let mut active_ids = self
@ -640,17 +616,11 @@ impl JMAP {
if !changed_ids.is_empty() { if !changed_ids.is_empty() {
match self.core.storage.data.write(batch.build()).await { match self.core.storage.data.write(batch.build()).await {
Ok(_) => (), Ok(_) => (),
Err(store::Error::AssertValueFailed) => { Err(err) if err.matches(trc::Cause::AssertValue) => {
return Ok(vec![]); return Ok(vec![]);
} }
Err(err) => { Err(err) => {
tracing::error!( return Err(err.caused_by(trc::location!()));
event = "error",
context = "sieve_activate_script",
account_id = account_id,
error = ?err,
"Failed to activate sieve script(s).");
return Err(MethodError::ServerPartialFail);
} }
} }
} }

View file

@ -5,10 +5,7 @@
*/ */
use jmap_proto::{ use jmap_proto::{
error::{ error::set::{SetError, SetErrorType},
method::MethodError,
set::{SetError, SetErrorType},
},
method::validate::{ValidateSieveScriptRequest, ValidateSieveScriptResponse}, method::validate::{ValidateSieveScriptRequest, ValidateSieveScriptResponse},
}; };
@ -19,7 +16,7 @@ impl JMAP {
&self, &self,
request: ValidateSieveScriptRequest, request: ValidateSieveScriptRequest,
access_token: &AccessToken, access_token: &AccessToken,
) -> Result<ValidateSieveScriptResponse, MethodError> { ) -> trc::Result<ValidateSieveScriptResponse> {
Ok(ValidateSieveScriptResponse { Ok(ValidateSieveScriptResponse {
account_id: request.account_id, account_id: request.account_id,
error: match self error: match self

View file

@ -5,7 +5,6 @@
*/ */
use jmap_proto::{ use jmap_proto::{
error::method::MethodError,
method::get::{GetRequest, GetResponse, RequestArguments}, method::get::{GetRequest, GetResponse, RequestArguments},
object::Object, object::Object,
types::{collection::Collection, property::Property, value::Value}, types::{collection::Collection, property::Property, value::Value},
@ -18,7 +17,7 @@ impl JMAP {
pub async fn email_submission_get( pub async fn email_submission_get(
&self, &self,
mut request: GetRequest<RequestArguments>, mut request: GetRequest<RequestArguments>,
) -> Result<GetResponse, MethodError> { ) -> trc::Result<GetResponse> {
let ids = request.unwrap_ids(self.core.jmap.get_max_objects)?; let ids = request.unwrap_ids(self.core.jmap.get_max_objects)?;
let properties = request.unwrap_properties(&[ let properties = request.unwrap_properties(&[
Property::Id, Property::Id,

Some files were not shown because too many files have changed in this diff Show more