diff options
author | mdecimus <mauro@stalw.art> | 2024-07-17 18:33:22 +0200 |
---|---|---|
committer | mdecimus <mauro@stalw.art> | 2024-07-17 18:33:22 +0200 |
commit | d2ad44cf9f085db5b067338d560291db667aeaa9 (patch) | |
tree | c5fd2b3859a70f0cda86668379d954b4c1a92d25 | |
parent | e74b29189a45c4afb6b204fced1b07032b16af61 (diff) |
Improved error handling (part 3)
166 files changed, 2813 insertions, 2699 deletions
@@ -3623,6 +3623,7 @@ dependencies = [ "tokio", "tokio-rustls 0.26.0", "tracing", + "trc", "utils", ] @@ -5572,9 +5573,9 @@ checksum = "ece8e78b2f38ec51c51f5d475df0a7187ba5111b2a28bdc761ee05b075d40a71" [[package]] name = "scc" -version = "2.1.1" +version = "2.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76ad2bbb0ae5100a07b7a6f2ed7ab5fd0045551a4c507989b7a620046ea3efdc" +checksum = "a4465c22496331e20eb047ff46e7366455bc01c0c02015c4a376de0b2cd3a1af" dependencies = [ "sdd", ] @@ -5627,9 +5628,9 @@ dependencies = [ [[package]] name = "sdd" -version = "0.2.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b84345e4c9bd703274a082fb80caaa99b7612be48dfaa1dd9266577ec412309d" +checksum = "1e806d6633ef141556fef75e345275e35652e9c045bbbc21e6ecfce3e9aa2638" [[package]] name = "seahash" @@ -6416,6 +6417,7 @@ dependencies = [ "tokio-rustls 0.26.0", "tracing", "tracing-subscriber", + "trc", "utils", ] diff --git a/crates/common/src/config/smtp/auth.rs b/crates/common/src/config/smtp/auth.rs index 9bf809d5..99a6f1f8 100644 --- a/crates/common/src/config/smtp/auth.rs +++ b/crates/common/src/config/smtp/auth.rs @@ -142,7 +142,7 @@ impl Default for MailAuthConfig { }, iprev: IpRevAuthConfig { verify: IfBlock::new::<VerifyStrategy>( - "auth.ipref.verify", + "auth.iprev.verify", [("local_port == 25", "relaxed")], #[cfg(not(feature = "test_mode"))] "disable", diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 9e7812c3..26e901dc 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -255,7 +255,7 @@ impl Core { } Ok(None) => Ok(()), Err(err) => { - if err.matches(trc::Cause::MissingTotp) { + if err.matches(trc::Cause::Auth(trc::AuthCause::MissingTotp)) { return Err(err); } else { Err(err) @@ -331,7 +331,7 @@ impl Core { .await; } - Err(trc::Cause::Authentication.into()) + Err(trc::AuthCause::Failed.into()) }; } } @@ -377,7 +377,7 @@ impl Core { .await; } - Err(trc::Cause::Banned.into()) + Err(trc::AuthCause::Banned.into()) } else { // Send webhook event if self.has_webhook_subscribers(WebhookType::AuthFailure) { @@ -394,7 +394,7 @@ impl Core { .await; } - Err(trc::Cause::Authentication.into()) + Err(trc::AuthCause::Failed.into()) } } else { // Send webhook event @@ -411,7 +411,7 @@ impl Core { ) .await; } - Err(trc::Cause::Authentication.into()) + Err(trc::AuthCause::Failed.into()) } } } diff --git a/crates/common/src/listener/acme/cache.rs b/crates/common/src/listener/acme/cache.rs index 95b9d046..99421d43 100644 --- a/crates/common/src/listener/acme/cache.rs +++ b/crates/common/src/listener/acme/cache.rs @@ -75,7 +75,12 @@ impl Core { { URL_SAFE_NO_PAD .decode(content.as_bytes()) - .map_err(Into::into) + .map_err(|err| { + trc::Cause::Acme + .caused_by(trc::location!()) + .reason(err) + .details("failed to decode certificate") + }) .map(Some) } else { Ok(None) diff --git a/crates/common/src/listener/acme/directory.rs b/crates/common/src/listener/acme/directory.rs index ed5b9357..2c1e5ece 100644 --- a/crates/common/src/listener/acme/directory.rs +++ b/crates/common/src/listener/acme/directory.rs @@ -60,7 +60,7 @@ impl Account { I: IntoIterator<Item = &'a S>, { let key_pair = EcdsaKeyPair::from_pkcs8(ALG, key_pair, &SystemRandom::new()) - .map_err(|err| trc::Cause::Crypto.reason(err).caused_by(trc::location!()))?; + .map_err(|err| trc::Cause::Acme.reason(err).caused_by(trc::location!()))?; let contact: Vec<&'a str> = contact.into_iter().map(AsRef::<str>::as_ref).collect(); let payload = json!({ "termsOfServiceAgreed": true, @@ -97,13 +97,19 @@ impl Account { )?; let response = https(url.as_ref(), Method::POST, Some(body)).await?; let location = get_header(&response, "Location").ok(); - let body = response.text().await?; + let body = response + .text() + .await + .map_err(|err| trc::Cause::Acme.from_http_error(err))?; Ok((location, body)) } 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 payload = format!("{{\"identifiers\":{}}}", serde_json::to_string(&domains)?); + let payload = format!( + "{{\"identifiers\":{}}}", + serde_json::to_string(&domains).map_err(|err| trc::Cause::Acme.from_json_error(err))? + ); let response = self.request(&self.directory.new_order, &payload).await?; let url = response.0.ok_or( trc::Cause::Acme @@ -111,13 +117,14 @@ impl Account { .details("Missing header") .ctx(trc::Key::Id, "Location"), )?; - let order = serde_json::from_str(&response.1)?; + let order = serde_json::from_str(&response.1) + .map_err(|err| trc::Cause::Acme.from_json_error(err))?; Ok((url, order)) } pub async fn auth(&self, url: impl AsRef<str>) -> trc::Result<Auth> { let response = self.request(url, "").await?; - serde_json::from_str(&response.1).map_err(Into::into) + serde_json::from_str(&response.1).map_err(|err| trc::Cause::Acme.from_json_error(err)) } pub async fn challenge(&self, url: impl AsRef<str>) -> trc::Result<()> { @@ -126,13 +133,13 @@ impl Account { pub async fn order(&self, url: impl AsRef<str>) -> trc::Result<Order> { let response = self.request(&url, "").await?; - serde_json::from_str(&response.1).map_err(Into::into) + serde_json::from_str(&response.1).map_err(|err| trc::Cause::Acme.from_json_error(err)) } pub async fn finalize(&self, url: impl AsRef<str>, csr: Vec<u8>) -> trc::Result<Order> { let payload = format!("{{\"csr\":\"{}\"}}", URL_SAFE_NO_PAD.encode(csr)); 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(|err| trc::Cause::Acme.from_json_error(err)) } pub async fn certificate(&self, url: impl AsRef<str>) -> trc::Result<String> { @@ -155,12 +162,12 @@ impl Account { params.alg = &PKCS_ECDSA_P256_SHA256; params.custom_extensions = vec![CustomExtension::new_acme_identifier(key_auth.as_ref())]; let cert = Certificate::from_params(params) - .map_err(|err| trc::Cause::Crypto.caused_by(trc::location!()).reason(err))?; + .map_err(|err| trc::Cause::Acme.caused_by(trc::location!()).reason(err))?; Ok(Bincode::new(SerializedCert { certificate: cert .serialize_der() - .map_err(|err| trc::Cause::Crypto.caused_by(trc::location!()).reason(err))?, + .map_err(|err| trc::Cause::Acme.caused_by(trc::location!()).reason(err))?, private_key: cert.serialize_private_key_der(), }) .serialize()) @@ -183,9 +190,14 @@ pub struct Directory { impl Directory { pub async fn discover(url: impl AsRef<str>) -> trc::Result<Self> { - Ok(serde_json::from_str( - &https(url, Method::GET, None).await?.text().await?, - )?) + serde_json::from_str( + &https(url, Method::GET, None) + .await? + .text() + .await + .map_err(|err| trc::Cause::Acme.from_http_error(err))?, + ) + .map_err(|err| trc::Cause::Acme.from_json_error(err)) } pub async fn nonce(&self) -> trc::Result<String> { get_header( @@ -286,7 +298,10 @@ async fn https( ); } - let mut request = builder.build()?.request(method, url); + let mut request = builder + .build() + .map_err(|err| trc::Cause::Acme.from_http_error(err))? + .request(method, url); if let Some(body) = body { request = request @@ -294,12 +309,20 @@ async fn https( .body(body); } - request.send().await?.assert_success().await + request + .send() + .await + .map_err(|err| trc::Cause::Acme.from_http_error(err))? + .assert_success(trc::Cause::Acme) + .await } fn get_header(response: &Response, header: &'static str) -> trc::Result<String> { match response.headers().get_all(header).iter().last() { - Some(value) => Ok(value.to_str()?.to_string()), + Some(value) => Ok(value + .to_str() + .map_err(|err| trc::Cause::Acme.from_http_str_error(err))? + .to_string()), None => Err(trc::Cause::Acme .caused_by(trc::location!()) .details("Missing header") diff --git a/crates/common/src/listener/acme/jose.rs b/crates/common/src/listener/acme/jose.rs index 805218d7..612a37a5 100644 --- a/crates/common/src/listener/acme/jose.rs +++ b/crates/common/src/listener/acme/jose.rs @@ -23,14 +23,15 @@ pub(crate) fn sign( let combined = format!("{}.{}", &protected, &payload); let signature = key .sign(&SystemRandom::new(), combined.as_bytes()) - .map_err(|err| trc::Cause::Crypto.caused_by(trc::location!()).reason(err))?; + .map_err(|err| trc::Cause::Acme.caused_by(trc::location!()).reason(err))?; let signature = URL_SAFE_NO_PAD.encode(signature.as_ref()); let body = Body { protected, payload, signature, }; - Ok(serde_json::to_string(&body)?) + + serde_json::to_string(&body).map_err(|err| trc::Cause::Acme.from_json_error(err)) } pub(crate) fn key_authorization(key: &EcdsaKeyPair, token: &str) -> trc::Result<String> { @@ -84,7 +85,8 @@ impl<'a> Protected<'a> { nonce, url, }; - let protected = serde_json::to_vec(&protected)?; + let protected = + serde_json::to_vec(&protected).map_err(|err| trc::Cause::Acme.from_json_error(err))?; Ok(URL_SAFE_NO_PAD.encode(protected)) } } @@ -119,7 +121,8 @@ impl Jwk { x: &self.x, y: &self.y, }; - let json = serde_json::to_vec(&jwk_thumb)?; + let json = + serde_json::to_vec(&jwk_thumb).map_err(|err| trc::Cause::Acme.from_json_error(err))?; let hash = digest(&SHA256, &json); Ok(URL_SAFE_NO_PAD.encode(hash)) } diff --git a/crates/common/src/listener/acme/order.rs b/crates/common/src/listener/acme/order.rs index 152e71a0..12a92bb5 100644 --- a/crates/common/src/listener/acme/order.rs +++ b/crates/common/src/listener/acme/order.rs @@ -87,7 +87,7 @@ impl Core { params.distinguished_name = DistinguishedName::new(); params.alg = &PKCS_ECDSA_P256_SHA256; let cert = rcgen::Certificate::from_params(params) - .map_err(|err| trc::Cause::Crypto.caused_by(trc::location!()).reason(err))?; + .map_err(|err| trc::Cause::Acme.caused_by(trc::location!()).reason(err))?; let (order_url, mut order) = account.new_order(provider.domains.clone()).await?; loop { @@ -122,7 +122,7 @@ impl Core { } } if order.status == OrderStatus::Processing { - return Err(trc::Cause::Timeout + return Err(trc::Cause::Acme .caused_by(trc::location!()) .details("Order processing timed out")); } @@ -135,9 +135,9 @@ impl Core { "Sending CSR" ); - let csr = cert.serialize_request_der().map_err(|err| { - trc::Cause::Crypto.caused_by(trc::location!()).reason(err) - })?; + let csr = cert + .serialize_request_der() + .map_err(|err| trc::Cause::Acme.caused_by(trc::location!()).reason(err))?; order = account.finalize(order.finalize, csr).await? } OrderStatus::Valid { certificate } => { @@ -165,7 +165,7 @@ impl Core { "Invalid order" ); - return Err(trc::Cause::Invalid.into_err().details("Invalid ACME order")); + return Err(trc::Cause::Acme.into_err().details("Invalid ACME order")); } } } @@ -194,8 +194,9 @@ impl Core { .iter() .find(|c| c.typ == challenge_type) .ok_or( - trc::Cause::MissingParameter + trc::Cause::Acme .into_err() + .details("Missing Parameter") .ctx(trc::Key::Id, challenge_type.as_str()), )?; @@ -342,8 +343,9 @@ impl Core { } AuthStatus::Valid => return Ok(()), _ => { - return Err(trc::Cause::Authentication + return Err(trc::Cause::Acme .into_err() + .details("Authentication error") .ctx(trc::Key::Status, auth.status.as_str())) } }; @@ -373,24 +375,25 @@ impl Core { return Ok(()); } _ => { - return Err(trc::Cause::Authentication + return Err(trc::Cause::Acme .into_err() + .details("Authentication error") .ctx(trc::Key::Status, auth.status.as_str())) } } } - Err(trc::Cause::Authentication + Err(trc::Cause::Acme .into_err() - .details("Too many attempts") + .details("Too many authentication attempts") .ctx(trc::Key::Id, domain)) } } fn parse_cert(pem: &[u8]) -> trc::Result<(CertifiedKey, [DateTime<Utc>; 2])> { let mut pems = pem::parse_many(pem) - .map_err(|err| trc::Cause::Crypto.reason(err).caused_by(trc::location!()))?; + .map_err(|err| trc::Cause::Acme.reason(err).caused_by(trc::location!()))?; if pems.len() < 2 { - return Err(trc::Cause::Crypto + return Err(trc::Cause::Acme .caused_by(trc::location!()) .ctx(trc::Key::Size, pems.len()) .details("Too few PEMs")); @@ -399,7 +402,7 @@ fn parse_cert(pem: &[u8]) -> trc::Result<(CertifiedKey, [DateTime<Utc>; 2])> { pems.remove(0).contents(), ))) { Ok(pk) => pk, - Err(err) => return Err(trc::Cause::Crypto.reason(err).caused_by(trc::location!())), + Err(err) => return Err(trc::Cause::Acme.reason(err).caused_by(trc::location!())), }; let cert_chain: Vec<CertificateDer> = pems .into_iter() @@ -414,7 +417,7 @@ fn parse_cert(pem: &[u8]) -> trc::Result<(CertifiedKey, [DateTime<Utc>; 2])> { .unwrap_or_default() }) } - Err(err) => return Err(trc::Cause::Crypto.reason(err).caused_by(trc::location!())), + Err(err) => return Err(trc::Cause::Acme.reason(err).caused_by(trc::location!())), }; let cert = CertifiedKey::new(cert_chain, pk); Ok((cert, validity)) diff --git a/crates/common/src/listener/mod.rs b/crates/common/src/listener/mod.rs index 45c74ba3..2d051346 100644 --- a/crates/common/src/listener/mod.rs +++ b/crates/common/src/listener/mod.rs @@ -77,6 +77,13 @@ pub trait SessionStream: AsyncRead + AsyncWrite + Unpin + 'static + Sync + Send fn tls_version_and_cipher(&self) -> (Cow<'static, str>, Cow<'static, str>); } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SessionResult { + Continue, + Close, + UpgradeTls, +} + pub trait SessionManager: Sync + Send + 'static + Clone { fn spawn<T: SessionStream>( &self, diff --git a/crates/common/src/manager/backup.rs b/crates/common/src/manager/backup.rs index bcfc61aa..4838c3b7 100644 --- a/crates/common/src/manager/backup.rs +++ b/crates/common/src/manager/backup.rs @@ -1117,19 +1117,19 @@ pub(super) trait DeserializeBytes { impl DeserializeBytes for &[u8] { fn range(&self, range: Range<usize>) -> trc::Result<&[u8]> { self.get(range.start..std::cmp::min(range.end, self.len())) - .ok_or_else(|| trc::Cause::DataCorruption.caused_by(trc::location!())) + .ok_or_else(|| trc::StoreCause::DataCorruption.caused_by(trc::location!())) } fn deserialize_u8(&self, offset: usize) -> trc::Result<u8> { self.get(offset) .copied() - .ok_or_else(|| trc::Cause::DataCorruption.caused_by(trc::location!())) + .ok_or_else(|| trc::StoreCause::DataCorruption.caused_by(trc::location!())) } fn deserialize_leb128<U: Leb128_>(&self) -> trc::Result<U> { self.read_leb128::<U>() .map(|(v, _)| v) - .ok_or_else(|| trc::Cause::DataCorruption.caused_by(trc::location!())) + .ok_or_else(|| trc::StoreCause::DataCorruption.caused_by(trc::location!())) } } diff --git a/crates/common/src/manager/config.rs b/crates/common/src/manager/config.rs index 7a9460e5..dcd9e4cb 100644 --- a/crates/common/src/manager/config.rs +++ b/crates/common/src/manager/config.rs @@ -316,7 +316,8 @@ impl ConfigManager { .await .map_err(|err| { trc::Cause::Configuration - .caused_by(trc::Error::from(err)) + .reason(err) + .details("Failed to write local configuration") .ctx(trc::Key::Path, self.cfg_local_path.display().to_string()) }) } diff --git a/crates/common/src/manager/webadmin.rs b/crates/common/src/manager/webadmin.rs index 89324b52..6571e4fc 100644 --- a/crates/common/src/manager/webadmin.rs +++ b/crates/common/src/manager/webadmin.rs @@ -36,7 +36,7 @@ impl WebAdminManager { } } - pub async fn get(&self, path: &str) -> io::Result<Resource<Vec<u8>>> { + pub async fn get(&self, path: &str) -> trc::Result<Resource<Vec<u8>>> { let routes = self.routes.load(); if let Some(resource) = routes.get(path).or_else(|| routes.get("index.html")) { tokio::fs::read(&resource.contents) @@ -45,6 +45,12 @@ impl WebAdminManager { content_type: resource.content_type, contents, }) + .map_err(|err| { + trc::ResourceCause::Error + .reason(err) + .ctx(trc::Key::Path, path.to_string()) + .caused_by(trc::location!()) + }) } else { Ok(Resource::default()) } @@ -52,42 +58,46 @@ impl WebAdminManager { pub async fn unpack(&self, blob_store: &BlobStore) -> trc::Result<()> { // Delete any existing bundles - self.bundle_path.clean().await?; + self.bundle_path.clean().await.map_err(unpack_error)?; // Obtain webadmin bundle let bundle = blob_store .get_blob(WEBADMIN_KEY, 0..usize::MAX) .await? .ok_or_else(|| { - trc::Cause::NotFound + trc::ResourceCause::NotFound .caused_by(trc::location!()) .details("Webadmin bundle not found") })?; // Uncompress let mut bundle = zip::ZipArchive::new(Cursor::new(bundle)).map_err(|err| { - trc::Cause::Decompress + trc::ResourceCause::Error .caused_by(trc::location!()) .reason(err) + .details("Failed to decompress webadmin bundle") })?; let mut routes = AHashMap::new(); for i in 0..bundle.len() { let (file_name, contents) = { let mut file = bundle.by_index(i).map_err(|err| { - trc::Cause::Decompress + trc::ResourceCause::Error .caused_by(trc::location!()) .reason(err) + .details("Failed to read file from webadmin bundle") })?; if file.is_dir() { continue; } let mut contents = Vec::new(); - file.read_to_end(&mut contents)?; + file.read_to_end(&mut contents).map_err(unpack_error)?; (file.name().to_string(), contents) }; let path = self.bundle_path.path.join(format!("{i:02}")); - tokio::fs::write(&path, contents).await?; + tokio::fs::write(&path, contents) + .await + .map_err(unpack_error)?; let resource = Resource { content_type: match file_name @@ -164,6 +174,12 @@ impl TempDir { } } +fn unpack_error(err: std::io::Error) -> trc::Error { + trc::ResourceCause::Error + .reason(err) + .details("Failed to unpack webadmin bundle") +} + impl Default for WebAdminManager { fn default() -> Self { Self::new() diff --git a/crates/directory/src/backend/imap/lookup.rs b/crates/directory/src/backend/imap/lookup.rs index e5a427b4..02613054 100644 --- a/crates/directory/src/backend/imap/lookup.rs +++ b/crates/directory/src/backend/imap/lookup.rs @@ -38,7 +38,7 @@ impl ImapDirectory { AUTH_XOAUTH2 } _ => { - trc::bail!(trc::Cause::Unsupported + trc::bail!(trc::StoreCause::NotSupported .ctx( trc::Key::Reason, "IMAP server does not offer any supported auth mechanisms." @@ -58,32 +58,32 @@ impl ImapDirectory { }, } } else { - Err(trc::Cause::Unsupported + Err(trc::StoreCause::NotSupported .caused_by(trc::location!()) .protocol(trc::Protocol::Imap)) } } pub async fn email_to_ids(&self, _address: &str) -> trc::Result<Vec<u32>> { - Err(trc::Cause::Unsupported + Err(trc::StoreCause::NotSupported .caused_by(trc::location!()) .protocol(trc::Protocol::Imap)) } pub async fn rcpt(&self, _address: &str) -> trc::Result<bool> { - Err(trc::Cause::Unsupported + Err(trc::StoreCause::NotSupported .caused_by(trc::location!()) .protocol(trc::Protocol::Imap)) } pub async fn vrfy(&self, _address: &str) -> trc::Result<Vec<String>> { - Err(trc::Cause::Unsupported + Err(trc::StoreCause::NotSupported .caused_by(trc::location!()) .protocol(trc::Protocol::Imap)) } pub async fn expn(&self, _address: &str) -> trc::Result<Vec<String>> { - Err(trc::Cause::Unsupported + Err(trc::StoreCause::NotSupported .caused_by(trc::location!()) .protocol(trc::Protocol::Imap)) } diff --git a/crates/directory/src/backend/internal/manage.rs b/crates/directory/src/backend/internal/manage.rs index 7518b296..de88d7fa 100644 --- a/crates/directory/src/backend/internal/manage.rs +++ b/crates/directory/src/backend/internal/manage.rs @@ -122,7 +122,7 @@ impl ManageDirectory for Store { return Ok(account_id); } Err(err) => { - if err.matches(trc::Cause::AssertValue) && try_count < 3 { + if err.is_assertion_failure() && try_count < 3 { try_count += 1; continue; } else { @@ -422,7 +422,7 @@ impl ManageDirectory for Store { continue; } } - return Err(trc::Cause::Unsupported.caused_by(trc::location!())); + return Err(trc::ManageCause::NotSupported.caused_by(trc::location!())); } ( PrincipalAction::Set, @@ -762,7 +762,7 @@ impl ManageDirectory for Store { } _ => { - return Err(trc::Cause::Unsupported.caused_by(trc::location!())); + return Err(trc::StoreCause::NotSupported.caused_by(trc::location!())); } } } @@ -1055,18 +1055,28 @@ 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) +pub fn err_missing(field: impl Into<trc::Value>) -> trc::Error { + trc::ManageCause::MissingParameter.ctx(trc::Key::Key, field) } -fn err_exists(field: impl Into<trc::Value>, value: impl Into<trc::Value>) -> trc::Error { - trc::Cause::AlreadyExists +pub fn err_exists(field: impl Into<trc::Value>, value: impl Into<trc::Value>) -> trc::Error { + trc::ManageCause::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) +pub fn not_found(value: impl Into<trc::Value>) -> trc::Error { + trc::ManageCause::NotFound.ctx(trc::Key::Key, value) +} + +pub fn unsupported(details: impl Into<trc::Value>) -> trc::Error { + trc::ManageCause::NotSupported.ctx(trc::Key::Details, details) +} + +pub fn error(details: impl Into<trc::Value>, reason: Option<impl Into<trc::Value>>) -> trc::Error { + trc::ManageCause::Error + .ctx(trc::Key::Details, details) + .ctx_opt(trc::Key::Reason, reason) } impl From<PrincipalField> for trc::Value { diff --git a/crates/directory/src/backend/internal/mod.rs b/crates/directory/src/backend/internal/mod.rs index 4dba13a5..4aa0013f 100644 --- a/crates/directory/src/backend/internal/mod.rs +++ b/crates/directory/src/backend/internal/mod.rs @@ -58,7 +58,7 @@ impl Serialize for &Principal<u32> { impl Deserialize for Principal<u32> { fn deserialize(bytes: &[u8]) -> trc::Result<Self> { deserialize(bytes).ok_or_else(|| { - trc::Cause::DataCorruption + trc::StoreCause::DataCorruption .caused_by(trc::location!()) .ctx(trc::Key::Value, bytes) }) @@ -79,12 +79,12 @@ impl Deserialize for PrincipalIdType { let mut bytes = bytes_.iter(); Ok(PrincipalIdType { account_id: bytes.next_leb128().ok_or_else(|| { - trc::Cause::DataCorruption + trc::StoreCause::DataCorruption .caused_by(trc::location!()) .ctx(trc::Key::Value, bytes_) })?, typ: Type::from_u8(*bytes.next().ok_or_else(|| { - trc::Cause::DataCorruption + trc::StoreCause::DataCorruption .caused_by(trc::location!()) .ctx(trc::Key::Value, bytes_) })?), diff --git a/crates/directory/src/backend/ldap/lookup.rs b/crates/directory/src/backend/ldap/lookup.rs index d837cfda..7191735f 100644 --- a/crates/directory/src/backend/ldap/lookup.rs +++ b/crates/directory/src/backend/ldap/lookup.rs @@ -79,7 +79,7 @@ impl LdapDirectory { { Ok(Some(principal)) => principal, Err(err) - if err.matches(trc::Cause::Ldap) + if err.matches(trc::Cause::Store(trc::StoreCause::Ldap)) && err .value(trc::Key::Code) .and_then(|v| v.to_uint()) diff --git a/crates/directory/src/backend/smtp/lookup.rs b/crates/directory/src/backend/smtp/lookup.rs index 298c4db2..f879b8ed 100644 --- a/crates/directory/src/backend/smtp/lookup.rs +++ b/crates/directory/src/backend/smtp/lookup.rs @@ -21,14 +21,14 @@ impl SmtpDirectory { .authenticate(credentials) .await } else { - Err(trc::Cause::Unsupported + Err(trc::StoreCause::NotSupported .caused_by(trc::location!()) .protocol(trc::Protocol::Smtp)) } } pub async fn email_to_ids(&self, _address: &str) -> trc::Result<Vec<u32>> { - Err(trc::Cause::Unsupported + Err(trc::StoreCause::NotSupported .caused_by(trc::location!()) .protocol(trc::Protocol::Smtp)) } @@ -64,7 +64,7 @@ impl SmtpDirectory { Ok(true) } Severity::PermanentNegativeCompletion => Ok(false), - _ => Err(trc::Cause::Unexpected + _ => Err(trc::StoreCause::Unexpected .ctx(trc::Key::Protocol, trc::Protocol::Smtp) .ctx(trc::Key::Code, reply.code()) .ctx(trc::Key::Details, reply.message)), @@ -127,10 +127,10 @@ impl SmtpClient { .split('\n') .map(|p| p.to_string()) .collect::<Vec<String>>()), - code @ (550 | 551 | 553 | 500 | 502) => Err(trc::Cause::Unsupported + code @ (550 | 551 | 553 | 500 | 502) => Err(trc::StoreCause::NotSupported .ctx(trc::Key::Protocol, trc::Protocol::Smtp) .ctx(trc::Key::Code, code)), - code => Err(trc::Cause::Unexpected + code => Err(trc::StoreCause::Unexpected .ctx(trc::Key::Protocol, trc::Protocol::Smtp) .ctx(trc::Key::Code, code) .ctx(trc::Key::Details, reply.message)), diff --git a/crates/directory/src/core/secret.rs b/crates/directory/src/core/secret.rs index fb27ca97..136f1657 100644 --- a/crates/directory/src/core/secret.rs +++ b/crates/directory/src/core/secret.rs @@ -58,7 +58,11 @@ impl<T: serde::Serialize + serde::de::DeserializeOwned> Principal<T> { // Token needs to validate with at least one of the TOTP secrets is_totp_verified = TOTP::from_url(secret) - .map_err(|err| trc::Cause::Invalid.reason(err).details(secret.to_string()))? + .map_err(|err| { + trc::AuthCause::Invalid + .reason(err) + .details(secret.to_string()) + })? .check_current(totp_token) .unwrap_or(false); } @@ -82,7 +86,7 @@ impl<T: serde::Serialize + serde::de::DeserializeOwned> Principal<T> { // Only let the client know if the TOTP code is missing // if the password is correct - Err(trc::Cause::MissingTotp.into_err()) + Err(trc::AuthCause::MissingTotp.into_err()) } else { // Return the TOTP verification status @@ -124,7 +128,9 @@ async fn verify_hash_prefix(hashed_secret: &str, secret: &str) -> trc::Result<bo .ok(); } Err(err) => { - tx.send(Err(trc::Cause::Invalid.reason(err).details(hashed_secret))) + tx.send(Err(trc::AuthCause::Invalid + .reason(err) + .details(hashed_secret))) .ok(); } }); @@ -149,7 +155,7 @@ async fn verify_hash_prefix(hashed_secret: &str, secret: &str) -> trc::Result<bo // MD5 based hash Ok(md5_crypt::verify(secret, hashed_secret)) } else { - Err(trc::Cause::Invalid + Err(trc::AuthCause::Invalid .into_err() .details(hashed_secret.to_string())) } @@ -250,12 +256,12 @@ pub async fn verify_secret_hash(hashed_secret: &str, secret: &str) -> trc::Resul } } "PLAIN" | "plain" | "CLEAR" | "clear" => Ok(hashed_secret == secret), - _ => Err(trc::Cause::Invalid + _ => Err(trc::AuthCause::Invalid .ctx(trc::Key::Reason, "Unsupported algorithm") .details(hashed_secret.to_string())), } } else { - Err(trc::Cause::Invalid + Err(trc::AuthCause::Invalid .into_err() .details(hashed_secret.to_string())) } diff --git a/crates/directory/src/lib.rs b/crates/directory/src/lib.rs index 9697cd63..8de4aee1 100644 --- a/crates/directory/src/lib.rs +++ b/crates/directory/src/lib.rs @@ -158,10 +158,10 @@ impl IntoError for PoolError<LdapError> { 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::Ldap) - } - err => trc::Cause::Pool + PoolError::Timeout(_) => trc::StoreCause::Pool + .ctx(trc::Key::Protocol, trc::Protocol::Ldap) + .details("Connection timed out"), + err => trc::StoreCause::Pool .ctx(trc::Key::Protocol, trc::Protocol::Ldap) .reason(err), } @@ -172,10 +172,10 @@ 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 + PoolError::Timeout(_) => trc::StoreCause::Pool + .ctx(trc::Key::Protocol, trc::Protocol::Imap) + .details("Connection timed out"), + err => trc::StoreCause::Pool .ctx(trc::Key::Protocol, trc::Protocol::Imap) .reason(err), } @@ -186,10 +186,10 @@ 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 + PoolError::Timeout(_) => trc::StoreCause::Pool + .ctx(trc::Key::Protocol, trc::Protocol::Smtp) + .details("Connection timed out"), + err => trc::StoreCause::Pool .ctx(trc::Key::Protocol, trc::Protocol::Smtp) .reason(err), } @@ -211,9 +211,11 @@ impl IntoError for mail_send::Error { 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) + trc::StoreCause::Ldap + .ctx(trc::Key::Code, result.rc) + .reason(self) } else { - trc::Cause::Ldap.reason(self) + trc::StoreCause::Ldap.reason(self) } } } diff --git a/crates/imap/src/op/acl.rs b/crates/imap/src/op/acl.rs index f088921c..32cd9ef0 100644 --- a/crates/imap/src/op/acl.rs +++ b/crates/imap/src/op/acl.rs @@ -250,7 +250,7 @@ impl<T: SessionStream> Session<T> { }) { acl } else { - return Err(trc::Cause::DataCorruption + return Err(trc::StoreCause::DataCorruption .into_err() .id(arguments.tag) .ctx(trc::Key::Reason, "Invalid mailbox ACL") diff --git a/crates/imap/src/op/append.rs b/crates/imap/src/op/append.rs index 01aac285..b923e83c 100644 --- a/crates/imap/src/op/append.rs +++ b/crates/imap/src/op/append.rs @@ -116,7 +116,7 @@ impl<T: SessionStream> SessionData<T> { last_change_id = Some(email.change_id); } Err(err) => { - return Err(if err.matches(trc::Cause::OverQuota) { + return Err(if err.matches(trc::Cause::Limit(trc::LimitCause::Quota)) { err.details("Disk quota exceeded.") .code(ResponseCode::OverQuota) } else { diff --git a/crates/imap/src/op/authenticate.rs b/crates/imap/src/op/authenticate.rs index 337179c5..9096146b 100644 --- a/crates/imap/src/op/authenticate.rs +++ b/crates/imap/src/op/authenticate.rs @@ -25,7 +25,7 @@ impl<T: SessionStream> Session<T> { if !args.params.is_empty() { let challenge = base64_decode(args.params.pop().unwrap().as_bytes()) .ok_or_else(|| { - trc::Cause::Authentication + trc::AuthCause::Error .into_err() .details("Failed to decode challenge.") .id(args.tag.clone()) @@ -38,7 +38,7 @@ impl<T: SessionStream> Session<T> { decode_challenge_oauth(&challenge) } .map_err(|err| { - trc::Cause::Authentication + trc::AuthCause::Error .into_err() .details(err) .id(args.tag.clone()) @@ -55,7 +55,7 @@ impl<T: SessionStream> Session<T> { self.write_bytes(b"+ \"\"\r\n".to_vec()).await } } - _ => Err(trc::Cause::Authentication + _ => Err(trc::AuthCause::Error .into_err() .details("Authentication mechanism not supported.") .id(args.tag) @@ -96,7 +96,7 @@ impl<T: SessionStream> Session<T> { Some(Some(limiter)) => Some(limiter), None => None, Some(None) => { - return Err(trc::Cause::TooManyConcurrentRequests.into()); + return Err(trc::LimitCause::ConcurrentRequest.into_err()); } }; diff --git a/crates/imap/src/op/expunge.rs b/crates/imap/src/op/expunge.rs index aeacac9e..093868ba 100644 --- a/crates/imap/src/op/expunge.rs +++ b/crates/imap/src/op/expunge.rs @@ -239,7 +239,7 @@ impl<T: SessionStream> SessionData<T> { changelog.log_child_update(Collection::Mailbox, mailbox_id.mailbox_id); } Err(err) => { - if !err.matches(trc::Cause::AssertValue) { + if !err.is_assertion_failure() { return Err(err.caused_by(trc::location!())); } } diff --git a/crates/imap/src/op/fetch.rs b/crates/imap/src/op/fetch.rs index f1f38716..634b45fe 100644 --- a/crates/imap/src/op/fetch.rs +++ b/crates/imap/src/op/fetch.rs @@ -516,7 +516,7 @@ impl<T: SessionStream> SessionData<T> { changelog.log_update(Collection::Email, id); } Err(err) => { - if !err.matches(trc::Cause::AssertValue) { + if !err.is_assertion_failure() { return Err(err.id(arguments.tag)); } } diff --git a/crates/imap/src/op/status.rs b/crates/imap/src/op/status.rs index 933e7ad1..d4fa855e 100644 --- a/crates/imap/src/op/status.rs +++ b/crates/imap/src/op/status.rs @@ -239,7 +239,7 @@ impl<T: SessionStream> SessionData<T> { .await? .and_then(|obj| obj.get(&Property::Cid).as_uint()) .ok_or_else(|| { - trc::Cause::Unexpected + trc::StoreCause::Unexpected .into_err() .details("Mailbox unavailable") .ctx(trc::Key::Reason, "Failed to obtain uid validity") diff --git a/crates/imap/src/op/store.rs b/crates/imap/src/op/store.rs index 044dbf19..608c2c5d 100644 --- a/crates/imap/src/op/store.rs +++ b/crates/imap/src/op/store.rs @@ -288,7 +288,7 @@ impl<T: SessionStream> SessionData<T> { }); } } - Err(err) if err.matches(trc::Cause::AssertValue) => { + Err(err) if err.is_assertion_failure() => { if try_count < MAX_RETRIES { try_count += 1; continue; diff --git a/crates/jmap-proto/src/error/method.rs b/crates/jmap-proto/src/error/method.rs index 93879b30..b94c4789 100644 --- a/crates/jmap-proto/src/error/method.rs +++ b/crates/jmap-proto/src/error/method.rs @@ -136,7 +136,9 @@ impl From<MethodError> for trc::Error { ), }; - trc::Cause::Jmap + let todo = "fix"; + + trc::JmapCause::RequestTooLarge .ctx(trc::Key::Type, typ) .ctx(trc::Key::Details, description) } @@ -184,7 +186,11 @@ impl Serialize for MethodErrorWrapper { { let mut map = serializer.serialize_map(2.into())?; - let (error_type, description) = if self.0.matches(trc::Cause::Jmap) { + let todo = "fix"; + let (error_type, description) = if self + .0 + .matches(trc::Cause::Jmap(trc::JmapCause::RequestTooLarge)) + { ( self.0 .value(trc::Key::Type) diff --git a/crates/jmap-proto/src/method/changes.rs b/crates/jmap-proto/src/method/changes.rs index 9993779e..1450b2c1 100644 --- a/crates/jmap-proto/src/method/changes.rs +++ b/crates/jmap-proto/src/method/changes.rs @@ -5,8 +5,7 @@ */ use crate::{ - error::method::MethodError, - parser::{json::Parser, Error, Ignore, JsonObjectParser, Token}, + parser::{json::Parser, Ignore, JsonObjectParser, Token}, request::{method::MethodObject, RequestProperty}, types::{id::Id, property::Property, state::State}, }; @@ -55,7 +54,7 @@ pub enum RequestArguments { } impl JsonObjectParser for ChangesRequest { - fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + fn parse(parser: &mut Parser<'_>) -> trc::Result<Self> where Self: Sized, { @@ -68,10 +67,9 @@ impl JsonObjectParser for ChangesRequest { MethodObject::EmailSubmission => RequestArguments::EmailSubmission, MethodObject::Quota => RequestArguments::Quota, _ => { - return Err(Error::Method(MethodError::UnknownMethod(format!( - "{}/changes", - parser.ctx - )))) + return Err(trc::JmapCause::UnknownMethod + .into_err() + .details(format!("{}/changes", parser.ctx))) } }, account_id: Id::default(), diff --git a/crates/jmap-proto/src/method/copy.rs b/crates/jmap-proto/src/method/copy.rs index bd0cea63..7e69bfa2 100644 --- a/crates/jmap-proto/src/method/copy.rs +++ b/crates/jmap-proto/src/method/copy.rs @@ -8,9 +8,9 @@ use serde::Serialize; use utils::map::vec_map::VecMap; use crate::{ - error::{method::MethodError, set::SetError}, + error::set::SetError, object::Object, - parser::{json::Parser, Error, JsonObjectParser, Token}, + parser::{json::Parser, JsonObjectParser, Token}, request::{method::MethodObject, reference::MaybeReference, RequestProperty}, types::{ blob::BlobId, @@ -88,7 +88,7 @@ pub enum RequestArguments { } impl JsonObjectParser for CopyRequest<RequestArguments> { - fn parse(parser: &mut Parser) -> crate::parser::Result<Self> + fn parse(parser: &mut Parser) -> trc::Result<Self> where Self: Sized, { @@ -96,10 +96,9 @@ impl JsonObjectParser for CopyRequest<RequestArguments> { arguments: match &parser.ctx { MethodObject::Email => RequestArguments::Email, _ => { - return Err(Error::Method(MethodError::UnknownMethod(format!( - "{}/copy", - parser.ctx - )))) + return Err(trc::JmapCause::UnknownMethod + .into_err() + .details(format!("{}/copy", parser.ctx))) } }, account_id: Id::default(), @@ -159,7 +158,7 @@ impl JsonObjectParser for CopyRequest<RequestArguments> { } impl JsonObjectParser for CopyBlobRequest { - fn parse(parser: &mut Parser) -> crate::parser::Result<Self> + fn parse(parser: &mut Parser) -> trc::Result<Self> where Self: Sized, { diff --git a/crates/jmap-proto/src/method/get.rs b/crates/jmap-proto/src/method/get.rs index 62e49b90..5a9c9905 100644 --- a/crates/jmap-proto/src/method/get.rs +++ b/crates/jmap-proto/src/method/get.rs @@ -7,7 +7,7 @@ use crate::{ error::method::MethodError, object::{blob, email, Object}, - parser::{json::Parser, Error, JsonObjectParser, Token}, + parser::{json::Parser, JsonObjectParser, Token}, request::{ method::MethodObject, reference::{MaybeReference, ResultReference}, @@ -55,7 +55,7 @@ pub struct GetResponse { } impl JsonObjectParser for GetRequest<RequestArguments> { - fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + fn parse(parser: &mut Parser<'_>) -> trc::Result<Self> where Self: Sized, { @@ -73,10 +73,9 @@ impl JsonObjectParser for GetRequest<RequestArguments> { MethodObject::Blob => RequestArguments::Blob(Default::default()), MethodObject::Quota => RequestArguments::Quota, _ => { - return Err(Error::Method(MethodError::UnknownMethod(format!( - "{}/get", - parser.ctx - )))) + return Err(trc::JmapCause::UnknownMethod + .into_err() + .details(format!("{}/get", parser.ctx))) } }, account_id: Id::default(), @@ -130,11 +129,7 @@ impl JsonObjectParser for GetRequest<RequestArguments> { } impl RequestPropertyParser for RequestArguments { - fn parse( - &mut self, - parser: &mut Parser, - property: RequestProperty, - ) -> crate::parser::Result<bool> { + fn parse(&mut self, parser: &mut Parser, property: RequestProperty) -> trc::Result<bool> { match self { RequestArguments::Email(arguments) => arguments.parse(parser, property), RequestArguments::Blob(arguments) => arguments.parse(parser, property), diff --git a/crates/jmap-proto/src/method/import.rs b/crates/jmap-proto/src/method/import.rs index c13daad5..447892f9 100644 --- a/crates/jmap-proto/src/method/import.rs +++ b/crates/jmap-proto/src/method/import.rs @@ -66,7 +66,7 @@ pub struct ImportEmailResponse { } impl JsonObjectParser for ImportEmailRequest { - fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + fn parse(parser: &mut Parser<'_>) -> trc::Result<Self> where Self: Sized, { @@ -104,7 +104,7 @@ impl JsonObjectParser for ImportEmailRequest { } impl JsonObjectParser for ImportEmail { - fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + fn parse(parser: &mut Parser<'_>) -> trc::Result<Self> where Self: Sized, { diff --git a/crates/jmap-proto/src/method/lookup.rs b/crates/jmap-proto/src/method/lookup.rs index 7e823ee6..3148d984 100644 --- a/crates/jmap-proto/src/method/lookup.rs +++ b/crates/jmap-proto/src/method/lookup.rs @@ -39,7 +39,7 @@ pub struct BlobInfo { } impl JsonObjectParser for BlobLookupRequest { - fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + fn parse(parser: &mut Parser<'_>) -> trc::Result<Self> where Self: Sized, { diff --git a/crates/jmap-proto/src/method/parse.rs b/crates/jmap-proto/src/method/parse.rs index ca1715fc..c33d84fd 100644 --- a/crates/jmap-proto/src/method/parse.rs +++ b/crates/jmap-proto/src/method/parse.rs @@ -44,7 +44,7 @@ pub struct ParseEmailResponse { } impl JsonObjectParser for ParseEmailRequest { - fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + fn parse(parser: &mut Parser<'_>) -> trc::Result<Self> where Self: Sized, { diff --git a/crates/jmap-proto/src/method/query.rs b/crates/jmap-proto/src/method/query.rs index 8f531d45..515a3cbf 100644 --- a/crates/jmap-proto/src/method/query.rs +++ b/crates/jmap-proto/src/method/query.rs @@ -9,9 +9,8 @@ use std::fmt::Display; use store::fts::{FilterItem, FilterType, FtsFilter}; use crate::{ - error::method::MethodError, object::{email, mailbox}, - parser::{json::Parser, Error, Ignore, JsonObjectParser, Token}, + parser::{json::Parser, Ignore, JsonObjectParser, Token}, request::{method::MethodObject, RequestProperty, RequestPropertyParser}, types::{date::UTCDate, id::Id, keyword::Keyword, state::State}, }; @@ -151,7 +150,7 @@ pub enum RequestArguments { } impl JsonObjectParser for QueryRequest<RequestArguments> { - fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + fn parse(parser: &mut Parser<'_>) -> trc::Result<Self> where Self: Sized, { @@ -164,10 +163,9 @@ impl JsonObjectParser for QueryRequest<RequestArguments> { MethodObject::Principal => RequestArguments::Principal, MethodObject::Quota => RequestArguments::Quota, _ => { - return Err(Error::Method(MethodError::UnknownMethod(format!( - "{}/query", - parser.ctx - )))) + return Err(trc::JmapCause::UnknownMethod + .into_err() + .details(format!("{}/query", parser.ctx))) } }, filter: vec![], @@ -243,7 +241,7 @@ impl JsonObjectParser for QueryRequest<RequestArguments> { } } -pub fn parse_filter(parser: &mut Parser) -> crate::parser::Result<Vec<Filter>> { +pub fn parse_filter(parser: &mut Parser) -> trc::Result<Vec<Filter>> { let mut filter = vec![Filter::Close]; let mut pos_stack = vec![0]; @@ -453,9 +451,9 @@ pub fn parse_filter(parser: &mut Parser) -> crate::parser::Result<Vec<Filter>> { break; } } else { - return Err(Error::Method(MethodError::InvalidArguments( - "Malformed filter".to_string(), - ))); + return Err(trc::JmapCause::InvalidArguments + .into_err() + .details("Malformed filter")); } } Token::ArrayEnd => { @@ -471,7 +469,7 @@ pub fn parse_filter(parser: &mut Parser) -> crate::parser::Result<Vec<Filter>> { Ok(filter) } -pub fn parse_sort(parser: &mut Parser) -> crate::parser::Result<Vec<Comparator>> { +pub fn parse_sort(parser: &mut Parser) -> trc::Result<Vec<Comparator>> { let mut sort = vec![]; loop { @@ -527,7 +525,7 @@ pub fn parse_sort(parser: &mut Parser) -> crate::parser::Result<Vec<Comparator>> } impl JsonObjectParser for SortProperty { - fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + fn parse(parser: &mut Parser<'_>) -> trc::Result<Self> where Self: Sized, { @@ -667,11 +665,7 @@ impl Display for SortProperty { } impl RequestPropertyParser for RequestArguments { - fn parse( - &mut self, - parser: &mut Parser, - property: RequestProperty, - ) -> crate::parser::Result<bool> { + fn parse(&mut self, parser: &mut Parser, property: RequestProperty) -> trc::Result<bool> { match self { RequestArguments::Email(args) => args.parse(parser, property), RequestArguments::Mailbox(args) => args.parse(parser, property), diff --git a/crates/jmap-proto/src/method/query_changes.rs b/crates/jmap-proto/src/method/query_changes.rs index 290321bc..f4181840 100644 --- a/crates/jmap-proto/src/method/query_changes.rs +++ b/crates/jmap-proto/src/method/query_changes.rs @@ -5,8 +5,7 @@ */ use crate::{ - error::method::MethodError, - parser::{json::Parser, Error, Ignore, JsonObjectParser, Token}, + parser::{json::Parser, Ignore, JsonObjectParser, Token}, request::{method::MethodObject, RequestProperty, RequestPropertyParser}, types::{id::Id, state::State}, }; @@ -60,7 +59,7 @@ impl AddedItem { } impl JsonObjectParser for QueryChangesRequest { - fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + fn parse(parser: &mut Parser<'_>) -> trc::Result<Self> where Self: Sized, { @@ -71,10 +70,9 @@ impl JsonObjectParser for QueryChangesRequest { MethodObject::EmailSubmission => RequestArguments::EmailSubmission, MethodObject::Quota => RequestArguments::Quota, _ => { - return Err(Error::Method(MethodError::UnknownMethod(format!( - "{}/queryChanges", - parser.ctx - )))) + return Err(trc::JmapCause::UnknownMethod + .into_err() + .details(format!("{}/queryChanges", parser.ctx))) } }, filter: vec![], diff --git a/crates/jmap-proto/src/method/search_snippet.rs b/crates/jmap-proto/src/method/search_snippet.rs index e73cd514..203ad226 100644 --- a/crates/jmap-proto/src/method/search_snippet.rs +++ b/crates/jmap-proto/src/method/search_snippet.rs @@ -48,7 +48,7 @@ pub struct SearchSnippet { } impl JsonObjectParser for GetSearchSnippetRequest { - fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + fn parse(parser: &mut Parser<'_>) -> trc::Result<Self> where Self: Sized, { diff --git a/crates/jmap-proto/src/method/set.rs b/crates/jmap-proto/src/method/set.rs index 28421cad..4efa7bbd 100644 --- a/crates/jmap-proto/src/method/set.rs +++ b/crates/jmap-proto/src/method/set.rs @@ -13,7 +13,7 @@ use crate::{ set::{InvalidProperty, SetError}, }, object::{email_submission, mailbox, sieve, Object}, - parser::{json::Parser, Error, JsonObjectParser, Token}, + parser::{json::Parser, JsonObjectParser, Token}, request::{ method::MethodObject, reference::{MaybeReference, ResultReference}, @@ -99,7 +99,7 @@ pub struct SetResponse { } impl JsonObjectParser for SetRequest<RequestArguments> { - fn parse(parser: &mut Parser) -> crate::parser::Result<Self> + fn parse(parser: &mut Parser) -> trc::Result<Self> where Self: Sized, { @@ -115,10 +115,9 @@ impl JsonObjectParser for SetRequest<RequestArguments> { MethodObject::VacationResponse => RequestArguments::VacationResponse, MethodObject::SieveScript => RequestArguments::SieveScript(Default::default()), _ => { - return Err(Error::Method(MethodError::UnknownMethod(format!( - "{}/set", - parser.ctx - )))) + return Err(trc::JmapCause::UnknownMethod + .into_err() + .details(format!("{}/set", parser.ctx))) } }, account_id: Id::default(), @@ -168,7 +167,7 @@ impl JsonObjectParser for SetRequest<RequestArguments> { } impl JsonObjectParser for Object<SetValue> { - fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + fn parse(parser: &mut Parser<'_>) -> trc::Result<Self> where Self: Sized, { @@ -385,11 +384,7 @@ impl<T: Into<AnyId>> From<Vec<MaybeReference<T, String>>> for SetValue { } impl RequestPropertyParser for RequestArguments { - fn parse( - &mut self, - parser: &mut Parser, - property: RequestProperty, - ) -> crate::parser::Result<bool> { + fn parse(&mut self, parser: &mut Parser, property: RequestProperty) -> trc::Result<bool> { match self { RequestArguments::Mailbox(args) => args.parse(parser, property), RequestArguments::EmailSubmission(args) => args.parse(parser, property), diff --git a/crates/jmap-proto/src/method/upload.rs b/crates/jmap-proto/src/method/upload.rs index 25d037f9..e8be489d 100644 --- a/crates/jmap-proto/src/method/upload.rs +++ b/crates/jmap-proto/src/method/upload.rs @@ -64,7 +64,7 @@ pub struct BlobUploadResponseObject { } impl JsonObjectParser for BlobUploadRequest { - fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + fn parse(parser: &mut Parser<'_>) -> trc::Result<Self> where Self: Sized, { @@ -96,7 +96,7 @@ impl JsonObjectParser for BlobUploadRequest { } impl JsonObjectParser for UploadObject { - fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + fn parse(parser: &mut Parser<'_>) -> trc::Result<Self> where Self: Sized, { @@ -140,7 +140,7 @@ impl JsonObjectParser for UploadObject { } impl JsonObjectParser for DataSourceObject { - fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + fn parse(parser: &mut Parser<'_>) -> trc::Result<Self> where Self: Sized, { diff --git a/crates/jmap-proto/src/method/validate.rs b/crates/jmap-proto/src/method/validate.rs index 3dcbd405..19d05ea8 100644 --- a/crates/jmap-proto/src/method/validate.rs +++ b/crates/jmap-proto/src/method/validate.rs @@ -27,7 +27,7 @@ pub struct ValidateSieveScriptResponse { } impl JsonObjectParser for ValidateSieveScriptRequest { - fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + fn parse(parser: &mut Parser<'_>) -> trc::Result<Self> where Self: Sized, { diff --git a/crates/jmap-proto/src/object/blob.rs b/crates/jmap-proto/src/object/blob.rs index 0a44e4bf..d38b0664 100644 --- a/crates/jmap-proto/src/object/blob.rs +++ b/crates/jmap-proto/src/object/blob.rs @@ -20,7 +20,7 @@ impl RequestPropertyParser for GetArguments { &mut self, parser: &mut Parser, property: RequestProperty, - ) -> crate::parser::Result<bool> { + ) -> trc::Result<bool> { match &property.hash[0] { 0x7465_7366_666f => { self.offset = parser diff --git a/crates/jmap-proto/src/object/email.rs b/crates/jmap-proto/src/object/email.rs index 64661b17..28876fcd 100644 --- a/crates/jmap-proto/src/object/email.rs +++ b/crates/jmap-proto/src/object/email.rs @@ -29,7 +29,7 @@ impl RequestPropertyParser for GetArguments { &mut self, parser: &mut Parser, property: RequestProperty, - ) -> crate::parser::Result<bool> { + ) -> trc::Result<bool> { match (&property.hash[0], &property.hash[1]) { (0x7365_6974_7265_706f_7250_7964_6f62, _) => { self.body_properties = <Option<Vec<Property>>>::parse(parser)?; @@ -66,7 +66,7 @@ impl RequestPropertyParser for QueryArguments { &mut self, parser: &mut Parser, property: RequestProperty, - ) -> crate::parser::Result<bool> { + ) -> trc::Result<bool> { if property.hash[0] == 0x0073_6461_6572_6854_6573_7061_6c6c_6f63 { self.collapse_threads = parser .next_token::<Ignore>()? diff --git a/crates/jmap-proto/src/object/email_submission.rs b/crates/jmap-proto/src/object/email_submission.rs index 7191a5e5..2f429748 100644 --- a/crates/jmap-proto/src/object/email_submission.rs +++ b/crates/jmap-proto/src/object/email_submission.rs @@ -25,7 +25,7 @@ impl RequestPropertyParser for SetArguments { &mut self, parser: &mut Parser, property: RequestProperty, - ) -> crate::parser::Result<bool> { + ) -> trc::Result<bool> { if property.hash[0] == 0x4565_7461_6470_5573_7365_6363_7553_6e6f && property.hash[1] == 0x6c69_616d { diff --git a/crates/jmap-proto/src/object/mailbox.rs b/crates/jmap-proto/src/object/mailbox.rs index ad3e8a4b..fd34573a 100644 --- a/crates/jmap-proto/src/object/mailbox.rs +++ b/crates/jmap-proto/src/object/mailbox.rs @@ -25,7 +25,7 @@ impl RequestPropertyParser for SetArguments { &mut self, parser: &mut Parser, property: RequestProperty, - ) -> crate::parser::Result<bool> { + ) -> trc::Result<bool> { if property.hash[0] == 0x4565_766f_6d65_5279_6f72_7473_6544_6e6f && property.hash[1] == 0x0073_6c69_616d { @@ -44,7 +44,7 @@ impl RequestPropertyParser for QueryArguments { &mut self, parser: &mut Parser, property: RequestProperty, - ) -> crate::parser::Result<bool> { + ) -> trc::Result<bool> { match &property.hash[0] { 0x6565_7254_7341_7472_6f73 => { self.sort_as_tree = parser diff --git a/crates/jmap-proto/src/object/mod.rs b/crates/jmap-proto/src/object/mod.rs index 13c70b67..78157198 100644 --- a/crates/jmap-proto/src/object/mod.rs +++ b/crates/jmap-proto/src/object/mod.rs @@ -124,7 +124,7 @@ impl Serialize for Value { impl Deserialize for Value { fn deserialize(bytes: &[u8]) -> trc::Result<Self> { Self::deserialize_from(&mut bytes.iter()).ok_or_else(|| { - trc::Cause::DataCorruption + trc::StoreCause::DataCorruption .caused_by(trc::location!()) .ctx(trc::Key::Value, bytes) }) @@ -148,7 +148,7 @@ impl Serialize for &Object<Value> { impl Deserialize for Object<Value> { fn deserialize(bytes: &[u8]) -> trc::Result<Self> { Object::deserialize_from(&mut bytes.iter()).ok_or_else(|| { - trc::Cause::DataCorruption + trc::StoreCause::DataCorruption .caused_by(trc::location!()) .ctx(trc::Key::Value, bytes) }) diff --git a/crates/jmap-proto/src/object/sieve.rs b/crates/jmap-proto/src/object/sieve.rs index 1f05a7f9..bca31205 100644 --- a/crates/jmap-proto/src/object/sieve.rs +++ b/crates/jmap-proto/src/object/sieve.rs @@ -21,7 +21,7 @@ impl RequestPropertyParser for SetArguments { &mut self, parser: &mut Parser, property: RequestProperty, - ) -> crate::parser::Result<bool> { + ) -> trc::Result<bool> { if property.hash[0] == 0x7461_7669_7463_4173_7365_6363_7553_6e6f && property.hash[1] == 0x0074_7069_7263_5365 { diff --git a/crates/jmap-proto/src/parser/base32.rs b/crates/jmap-proto/src/parser/base32.rs index 597d6448..999b6980 100644 --- a/crates/jmap-proto/src/parser/base32.rs +++ b/crates/jmap-proto/src/parser/base32.rs @@ -6,7 +6,7 @@ use utils::codec::{base32_custom::BASE32_INVERSE, leb128::Leb128Iterator}; -use super::{json::Parser, Error}; +use super::json::Parser; #[derive(Debug)] pub struct JsonBase32Reader<'x, 'y> { @@ -38,7 +38,7 @@ impl<'x, 'y> JsonBase32Reader<'x, 'y> { } } - pub fn error(&mut self) -> Error { + pub fn error(&mut self) -> trc::Error { self.bytes.error_value() } } diff --git a/crates/jmap-proto/src/parser/impls.rs b/crates/jmap-proto/src/parser/impls.rs index 677aaf34..6f984d09 100644 --- a/crates/jmap-proto/src/parser/impls.rs +++ b/crates/jmap-proto/src/parser/impls.rs @@ -14,7 +14,7 @@ use utils::map::{ use super::{json::Parser, Ignore, JsonObjectParser, Token}; impl JsonObjectParser for u64 { - fn parse(parser: &mut Parser<'_>) -> super::Result<Self> + fn parse(parser: &mut Parser<'_>) -> trc::Result<Self> where Self: Sized, { @@ -36,7 +36,7 @@ impl JsonObjectParser for u64 { } impl JsonObjectParser for u128 { - fn parse(parser: &mut Parser<'_>) -> super::Result<Self> + fn parse(parser: &mut Parser<'_>) -> trc::Result<Self> where Self: Sized, { @@ -58,7 +58,7 @@ impl JsonObjectParser for u128 { } impl JsonObjectParser for String { - fn parse(parser: &mut Parser<'_>) -> super::Result<Self> + fn parse(parser: &mut Parser<'_>) -> trc::Result<Self> where Self: Sized, { @@ -177,7 +177,7 @@ impl JsonObjectParser for String { } impl<T: JsonObjectParser + Eq> JsonObjectParser for Vec<T> { - fn parse(parser: &mut Parser<'_>) -> super::Result<Self> + fn parse(parser: &mut Parser<'_>) -> trc::Result<Self> where Self: Sized, { @@ -197,7 +197,7 @@ impl<T: JsonObjectParser + Eq> JsonObjectParser for Vec<T> { } impl<T: JsonObjectParser + Eq> JsonObjectParser for Option<Vec<T>> { - fn parse(parser: &mut Parser<'_>) -> super::Result<Self> + fn parse(parser: &mut Parser<'_>) -> trc::Result<Self> where Self: Sized, { @@ -221,7 +221,7 @@ impl<T: JsonObjectParser + Eq> JsonObjectParser for Option<Vec<T>> { } impl<T: JsonObjectParser + Eq + BitmapItem> JsonObjectParser for Bitmap<T> { - fn parse(parser: &mut Parser<'_>) -> super::Result<Self> + fn parse(parser: &mut Parser<'_>) -> trc::Result<Self> where Self: Sized, { @@ -245,7 +245,7 @@ impl<T: JsonObjectParser + Eq + BitmapItem> JsonObjectParser for Bitmap<T> { } impl<K: JsonObjectParser + Eq + Display, V: JsonObjectParser> JsonObjectParser for VecMap<K, V> { - fn parse(parser: &mut Parser<'_>) -> super::Result<Self> + fn parse(parser: &mut Parser<'_>) -> trc::Result<Self> where Self: Sized, { @@ -263,7 +263,7 @@ impl<K: JsonObjectParser + Eq + Display, V: JsonObjectParser> JsonObjectParser f impl<K: JsonObjectParser + Eq + Display, V: JsonObjectParser> JsonObjectParser for Option<VecMap<K, V>> { - fn parse(parser: &mut Parser<'_>) -> super::Result<Self> + fn parse(parser: &mut Parser<'_>) -> trc::Result<Self> where Self: Sized, { @@ -284,7 +284,7 @@ impl<K: JsonObjectParser + Eq + Display, V: JsonObjectParser> JsonObjectParser } impl JsonObjectParser for bool { - fn parse(parser: &mut Parser<'_>) -> super::Result<Self> + fn parse(parser: &mut Parser<'_>) -> trc::Result<Self> where Self: Sized, { @@ -297,7 +297,7 @@ impl JsonObjectParser for bool { } impl JsonObjectParser for Ignore { - fn parse(parser: &mut Parser<'_>) -> super::Result<Self> + fn parse(parser: &mut Parser<'_>) -> trc::Result<Self> where Self: Sized, { diff --git a/crates/jmap-proto/src/parser/json.rs b/crates/jmap-proto/src/parser/json.rs index ce790e6f..5d60aee8 100644 --- a/crates/jmap-proto/src/parser/json.rs +++ b/crates/jmap-proto/src/parser/json.rs @@ -6,9 +6,9 @@ use std::{fmt::Display, iter::Peekable, slice::Iter}; -use crate::{error::method::MethodError, request::method::MethodObject}; +use crate::request::method::MethodObject; -use super::{Error, Ignore, JsonObjectParser, Token}; +use super::{Ignore, JsonObjectParser, Token}; const MAX_NESTED_LEVELS: u32 = 16; @@ -40,25 +40,33 @@ impl<'x> Parser<'x> { } } - pub fn error(&self, message: &str) -> Error { - format!("{message} at position {}.", self.pos).into() + pub fn error(&self, message: &str) -> trc::Error { + trc::JmapCause::NotJSON + .into_err() + .details(format!("{message} at position {}.", self.pos)) } - pub fn error_unterminated(&self) -> Error { - format!("Unterminated string at position {pos}.", pos = self.pos).into() + pub fn error_unterminated(&self) -> trc::Error { + trc::JmapCause::NotJSON.into_err().details(format!( + "Unterminated string at position {pos}.", + pos = self.pos + )) } - pub fn error_utf8(&self) -> Error { - format!("Invalid UTF-8 sequence at position {pos}.", pos = self.pos).into() + pub fn error_utf8(&self) -> trc::Error { + trc::JmapCause::NotJSON.into_err().details(format!( + "Invalid UTF-8 sequence at position {pos}.", + pos = self.pos + )) } - pub fn error_value(&mut self) -> Error { + pub fn error_value(&mut self) -> trc::Error { if self.is_eof || self.skip_string() { - Error::Method(MethodError::InvalidArguments(format!( + trc::JmapCause::InvalidArguments.into_err().details(format!( "Invalid value {:?} at position {}.", String::from_utf8_lossy(self.bytes[self.pos_marker..self.pos - 1].as_ref()), self.pos - ))) + )) } else { self.error_unterminated() } @@ -76,7 +84,7 @@ impl<'x> Parser<'x> { } #[inline(always)] - pub fn next_unescaped(&mut self) -> super::Result<Option<u8>> { + pub fn next_unescaped(&mut self) -> trc::Result<Option<u8>> { match self.next_char() { Some(b'"') => { self.is_eof = true; @@ -112,7 +120,7 @@ impl<'x> Parser<'x> { false } - pub fn next_token<T: JsonObjectParser>(&mut self) -> super::Result<Token<T>> { + pub fn next_token<T: JsonObjectParser>(&mut self) -> trc::Result<Token<T>> { let mut next_ch = self.next_ch.take().or_else(|| self.next_char()); while let Some(mut ch) = next_ch { @@ -263,9 +271,7 @@ impl<'x> Parser<'x> { Err(self.error("Unexpected EOF")) } - pub fn next_dict_key<T: JsonObjectParser + Display + Eq>( - &mut self, - ) -> super::Result<Option<T>> { + pub fn next_dict_key<T: JsonObjectParser + Display + Eq>(&mut self) -> trc::Result<Option<T>> { loop { match self.next_token::<T>()? { Token::String(k) => { @@ -281,11 +287,7 @@ impl<'x> Parser<'x> { } } - pub fn skip_token( - &mut self, - start_depth_array: u32, - start_depth_dict: u32, - ) -> super::Result<()> { + pub fn skip_token(&mut self, start_depth_array: u32, start_depth_dict: u32) -> trc::Result<()> { while { self.next_token::<Ignore>()?; start_depth_array != self.depth_array || start_depth_dict != self.depth_dict diff --git a/crates/jmap-proto/src/parser/mod.rs b/crates/jmap-proto/src/parser/mod.rs index a21133a2..56fca203 100644 --- a/crates/jmap-proto/src/parser/mod.rs +++ b/crates/jmap-proto/src/parser/mod.rs @@ -6,8 +6,6 @@ use std::fmt::Display; -use crate::error::{method::MethodError, request::RequestError}; - use self::json::Parser; pub mod base32; @@ -32,31 +30,23 @@ pub enum Token<T> { impl<T: PartialEq> Eq for Token<T> {} pub trait JsonObjectParser { - fn parse(parser: &mut Parser<'_>) -> Result<Self> + fn parse(parser: &mut Parser<'_>) -> trc::Result<Self> where Self: Sized; } -pub type Result<T> = std::result::Result<T, Error>; - -#[derive(Debug)] -pub enum Error { - Request(RequestError), - Method(MethodError), -} - #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct Ignore {} impl<T: Eq> Token<T> { - pub fn unwrap_string(self, property: &str) -> Result<T> { + pub fn unwrap_string(self, property: &str) -> trc::Result<T> { match self { Token::String(s) => Ok(s), token => Err(token.error(property, "string")), } } - pub fn unwrap_string_or_null(self, property: &str) -> Result<Option<T>> { + pub fn unwrap_string_or_null(self, property: &str) -> trc::Result<Option<T>> { match self { Token::String(s) => Ok(Some(s)), Token::Null => Ok(None), @@ -64,14 +54,14 @@ impl<T: Eq> Token<T> { } } - pub fn unwrap_bool(self, property: &str) -> Result<bool> { + pub fn unwrap_bool(self, property: &str) -> trc::Result<bool> { match self { Token::Boolean(v) => Ok(v), token => Err(token.error(property, "boolean")), } } - pub fn unwrap_bool_or_null(self, property: &str) -> Result<Option<bool>> { + pub fn unwrap_bool_or_null(self, property: &str) -> trc::Result<Option<bool>> { match self { Token::Boolean(v) => Ok(Some(v)), Token::Null => Ok(None), @@ -79,7 +69,7 @@ impl<T: Eq> Token<T> { } } - pub fn unwrap_usize_or_null(self, property: &str) -> Result<Option<usize>> { + pub fn unwrap_usize_or_null(self, property: &str) -> trc::Result<Option<usize>> { match self { Token::Integer(v) if v >= 0 => Ok(Some(v as usize)), Token::Float(v) if v >= 0.0 => Ok(Some(v as usize)), @@ -88,7 +78,7 @@ impl<T: Eq> Token<T> { } } - pub fn unwrap_uint_or_null(self, property: &str) -> Result<Option<u64>> { + pub fn unwrap_uint_or_null(self, property: &str) -> trc::Result<Option<u64>> { match self { Token::Integer(v) if v >= 0 => Ok(Some(v as u64)), Token::Float(v) if v >= 0.0 => Ok(Some(v as u64)), @@ -97,7 +87,7 @@ impl<T: Eq> Token<T> { } } - pub fn unwrap_int_or_null(self, property: &str) -> Result<Option<i64>> { + pub fn unwrap_int_or_null(self, property: &str) -> trc::Result<Option<i64>> { match self { Token::Integer(v) => Ok(Some(v)), Token::Float(v) => Ok(Some(v as i64)), @@ -106,7 +96,7 @@ impl<T: Eq> Token<T> { } } - pub fn unwrap_ints_or_null(self, property: &str) -> Result<Option<i32>> { + pub fn unwrap_ints_or_null(self, property: &str) -> trc::Result<Option<i32>> { match self { Token::Integer(v) => Ok(Some(v as i32)), Token::Float(v) => Ok(Some(v as i32)), @@ -115,7 +105,7 @@ impl<T: Eq> Token<T> { } } - pub fn assert(self, token: Token<T>) -> Result<()> { + pub fn assert(self, token: Token<T>) -> trc::Result<()> { if self == token { Ok(()) } else { @@ -123,22 +113,22 @@ impl<T: Eq> Token<T> { } } - pub fn assert_jmap(self, token: Token<T>) -> Result<()> { + pub fn assert_jmap(self, token: Token<T>) -> trc::Result<()> { if self == token { Ok(()) } else { - Err(Error::Request(RequestError::not_request(format!( + Err(trc::JmapCause::NotRequest.into_err().details(format!( "Invalid JMAP request: expected '{token}', got '{self}'." - )))) + ))) } } - pub fn error(&self, property: &str, expected: &str) -> Error { - Error::Method(MethodError::InvalidArguments(if !property.is_empty() { + pub fn error(&self, property: &str, expected: &str) -> trc::Error { + trc::JmapCause::InvalidArguments.into_err().details(if !property.is_empty() { format!("Invalid argument for '{property:?}': expected '{expected}', got '{self}'.",) } else { format!("Invalid argument: expected '{expected}', got '{self}'.") - })) + }) } } @@ -148,18 +138,6 @@ impl Display for Ignore { } } -impl From<String> for Error { - fn from(s: String) -> Self { - Error::Request(RequestError::not_json(&s)) - } -} - -impl From<&str> for Error { - fn from(s: &str) -> Self { - Error::Request(RequestError::not_json(s)) - } -} - impl<T> Display for Token<T> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { diff --git a/crates/jmap-proto/src/request/capability.rs b/crates/jmap-proto/src/request/capability.rs index 7ec457ef..79d8e58e 100644 --- a/crates/jmap-proto/src/request/capability.rs +++ b/crates/jmap-proto/src/request/capability.rs @@ -7,8 +7,7 @@ use utils::map::vec_map::VecMap; use crate::{ - error::request::RequestError, - parser::{json::Parser, Error, JsonObjectParser}, + parser::{json::Parser, JsonObjectParser}, response::serialize::serialize_hex, types::{id::Id, type_state::DataType}, }; @@ -315,7 +314,7 @@ impl WebSocketCapabilities { } impl JsonObjectParser for Capability { - fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + fn parse(parser: &mut Parser<'_>) -> trc::Result<Self> where Self: Sized, { @@ -343,18 +342,19 @@ impl JsonObjectParser for Capability { 0x0061_746f_7571 => Ok(Capability::Quota), _ => Err(parser.error_capability()), }, - Err(Error::Method(_)) => Err(parser.error_capability()), - Err(err @ Error::Request(_)) => Err(err), + Err(err) if err.is_jmap_method_error() => Err(parser.error_capability()), + Err(err) => Err(err), } } } impl<'x> Parser<'x> { - fn error_capability(&mut self) -> Error { + fn error_capability(&mut self) -> trc::Error { if self.is_eof || self.skip_string() { - Error::Request(RequestError::unknown_capability(&String::from_utf8_lossy( - self.bytes[self.pos_marker..self.pos - 1].as_ref(), - ))) + trc::JmapCause::UnknownCapability.into_err().details( + String::from_utf8_lossy(self.bytes[self.pos_marker..self.pos - 1].as_ref()) + .into_owned(), + ) } else { self.error_unterminated() } diff --git a/crates/jmap-proto/src/request/echo.rs b/crates/jmap-proto/src/request/echo.rs index b82a4d86..561bf088 100644 --- a/crates/jmap-proto/src/request/echo.rs +++ b/crates/jmap-proto/src/request/echo.rs @@ -15,7 +15,7 @@ pub struct Echo { } impl JsonObjectParser for Echo { - fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + fn parse(parser: &mut Parser<'_>) -> trc::Result<Self> where Self: Sized, { diff --git a/crates/jmap-proto/src/request/method.rs b/crates/jmap-proto/src/request/method.rs index b9b51594..be2f4f84 100644 --- a/crates/jmap-proto/src/request/method.rs +++ b/crates/jmap-proto/src/request/method.rs @@ -48,7 +48,7 @@ pub enum MethodFunction { } impl JsonObjectParser for MethodName { - fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + fn parse(parser: &mut Parser<'_>) -> trc::Result<Self> where Self: Sized, { diff --git a/crates/jmap-proto/src/request/mod.rs b/crates/jmap-proto/src/request/mod.rs index 79468a9d..a223b528 100644 --- a/crates/jmap-proto/src/request/mod.rs +++ b/crates/jmap-proto/src/request/mod.rs @@ -77,7 +77,7 @@ pub enum RequestMethod { } impl JsonObjectParser for RequestProperty { - fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + fn parse(parser: &mut Parser<'_>) -> trc::Result<Self> where Self: Sized, { @@ -116,5 +116,5 @@ pub trait RequestPropertyParser { &mut self, parser: &mut Parser, property: RequestProperty, - ) -> crate::parser::Result<bool>; + ) -> trc::Result<bool>; } diff --git a/crates/jmap-proto/src/request/parser.rs b/crates/jmap-proto/src/request/parser.rs index 74306337..b25327f5 100644 --- a/crates/jmap-proto/src/request/parser.rs +++ b/crates/jmap-proto/src/request/parser.rs @@ -7,10 +7,6 @@ use std::collections::HashMap; use crate::{ - error::{ - method::MethodError, - request::{RequestError, RequestLimitError}, - }, method::{ changes::ChangesRequest, copy::{CopyBlobRequest, CopyRequest}, @@ -25,7 +21,7 @@ use crate::{ upload::BlobUploadRequest, validate::ValidateSieveScriptRequest, }, - parser::{json::Parser, Error, Ignore, JsonObjectParser, Token}, + parser::{json::Parser, Ignore, JsonObjectParser, Token}, types::any_id::AnyId, }; @@ -37,7 +33,7 @@ use super::{ }; impl Request { - pub fn parse(json: &[u8], max_calls: usize, max_size: usize) -> Result<Self, RequestError> { + pub fn parse(json: &[u8], max_calls: usize, max_size: usize) -> trc::Result<Self> { if json.len() <= max_size { let mut request = Request { using: 0, @@ -54,10 +50,12 @@ impl Request { if found_valid_keys { Ok(request) } else { - Err(RequestError::not_request("Invalid JMAP request")) + Err(trc::JmapCause::NotRequest + .into_err() + .details("Invalid JMAP request")) } } else { - Err(RequestError::limit(RequestLimitError::SizeRequest)) + Err(trc::LimitCause::SizeRequest.into_err()) } } @@ -66,7 +64,7 @@ impl Request { parser: &mut Parser, max_calls: usize, key: u128, - ) -> Result<bool, RequestError> { + ) -> trc::Result<bool> { match key { 0x0067_6e69_7375 => { parser.next_token::<Ignore>()?.assert(Token::ArrayStart)?; @@ -77,7 +75,7 @@ impl Request { } Token::Comma => (), Token::ArrayEnd => break, - token => return Err(token.error("capability", &token.to_string()).into()), + token => return Err(token.error("capability", &token.to_string())), } } Ok(true) @@ -92,20 +90,28 @@ impl Request { Token::Comma => continue, Token::ArrayEnd => break, _ => { - return Err(RequestError::not_request("Invalid JMAP request")); + return Err(trc::JmapCause::NotRequest + .into_err() + .details("Invalid JMAP request")); } }; if self.method_calls.len() < max_calls { let method_name = match parser.next_token::<MethodName>() { Ok(Token::String(method)) => method, Ok(_) => { - return Err(RequestError::not_request("Invalid JMAP request")); + return Err(trc::JmapCause::NotRequest + .into_err() + .details("Invalid JMAP request")); } - Err(Error::Method(MethodError::InvalidArguments(_))) => { + Err(err) + if err.matches(trc::Cause::Jmap( + trc::JmapCause::InvalidArguments, + )) => + { MethodName::error() } Err(err) => { - return Err(err.into()); + return Err(err); } }; parser.next_token::<Ignore>()?.assert_jmap(Token::Comma)?; @@ -169,19 +175,19 @@ impl Request { (MethodFunction::Echo, MethodObject::Core) => { Echo::parse(parser).map(RequestMethod::Echo) } - _ => Err(Error::Method(MethodError::UnknownMethod( - method_name.to_string(), - ))), + _ => Err(trc::JmapCause::UnknownMethod + .into_err() + .details(method_name.to_string())), }; let method = match method { Ok(method) => method, - Err(Error::Method(err)) => { + Err(err) if !err.is_jmap_method_error() => { parser.skip_token(start_depth_array, start_depth_dict)?; - RequestMethod::Error(err.into()) + RequestMethod::Error(err) } Err(err) => { - return Err(err.into()); + return Err(err); } }; @@ -196,7 +202,7 @@ impl Request { name: method_name, }); } else { - return Err(RequestError::limit(RequestLimitError::CallsIn)); + return Err(trc::LimitCause::CallsIn.into_err()); } } Ok(true) @@ -221,15 +227,6 @@ impl Request { } } -impl From<Error> for RequestError { - fn from(value: Error) -> Self { - match value { - Error::Request(err) => err, - Error::Method(err) => RequestError::not_request(err.to_string()), - } - } -} - #[cfg(test)] mod tests { use crate::request::Request; diff --git a/crates/jmap-proto/src/request/reference.rs b/crates/jmap-proto/src/request/reference.rs index 62ca66ba..cd61b941 100644 --- a/crates/jmap-proto/src/request/reference.rs +++ b/crates/jmap-proto/src/request/reference.rs @@ -7,8 +7,7 @@ use std::fmt::Display; use crate::{ - error::method::MethodError, - parser::{json::Parser, Error, JsonObjectParser, Token}, + parser::{json::Parser, JsonObjectParser, Token}, types::{id::Id, pointer::JSONPointer}, }; @@ -45,7 +44,7 @@ impl<V, R> MaybeReference<V, R> { } impl JsonObjectParser for ResultReference { - fn parse(parser: &mut Parser) -> crate::parser::Result<Self> + fn parse(parser: &mut Parser) -> trc::Result<Self> where Self: Sized, { @@ -81,15 +80,15 @@ impl JsonObjectParser for ResultReference { path, }) } else { - Err(Error::Method(MethodError::InvalidResultReference( - "Missing required fields".into(), - ))) + Err(trc::JmapCause::InvalidResultReference + .into_err() + .details("Missing required fields")) } } } impl<T: JsonObjectParser> JsonObjectParser for MaybeReference<T, String> { - fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + fn parse(parser: &mut Parser<'_>) -> trc::Result<Self> where Self: Sized, { diff --git a/crates/jmap-proto/src/request/websocket.rs b/crates/jmap-proto/src/request/websocket.rs index c2a42851..b1fcbe4b 100644 --- a/crates/jmap-proto/src/request/websocket.rs +++ b/crates/jmap-proto/src/request/websocket.rs @@ -8,7 +8,7 @@ use std::{borrow::Cow, collections::HashMap}; use crate::{ error::request::{RequestError, RequestErrorType, RequestLimitError}, - parser::{json::Parser, Error, JsonObjectParser, Token}, + parser::{json::Parser, JsonObjectParser, Token}, request::Call, response::{serialize::serialize_hex, Response, ResponseMethod}, types::{any_id::AnyId, id::Id, state::State, type_state::DataType}, @@ -108,11 +108,7 @@ enum MessageType { } impl WebSocketMessage { - pub fn parse( - json: &[u8], - max_calls: usize, - max_size: usize, - ) -> Result<Self, WebSocketRequestError> { + pub fn parse(json: &[u8], max_calls: usize, max_size: usize) -> trc::Result<Self> { if json.len() <= max_size { let mut message_type = MessageType::None; let mut request = WebSocketRequest { @@ -174,10 +170,12 @@ impl WebSocketMessage { MessageType::PushDisable if !found_request_keys && !found_push_keys => { Ok(WebSocketMessage::PushDisable) } - _ => Err(RequestError::not_request("Invalid WebSocket JMAP request").into()), + _ => Err(trc::JmapCause::NotRequest + .into_err() + .details("Invalid WebSocket JMAP request")), } } else { - Err(RequestError::limit(RequestLimitError::SizeRequest).into()) + Err(trc::LimitCause::SizeRequest.into_err()) } } } @@ -205,12 +203,6 @@ impl From<RequestError> for WebSocketRequestError { } } -impl From<Error> for WebSocketRequestError { - fn from(value: Error) -> Self { - RequestError::from(value).into() - } -} - impl WebSocketResponse { pub fn from_response(response: Response, request_id: Option<String>) -> Self { Self { diff --git a/crates/jmap-proto/src/types/acl.rs b/crates/jmap-proto/src/types/acl.rs index 8b898a4b..f6a7efcd 100644 --- a/crates/jmap-proto/src/types/acl.rs +++ b/crates/jmap-proto/src/types/acl.rs @@ -27,7 +27,7 @@ pub enum Acl { } impl JsonObjectParser for Acl { - fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + fn parse(parser: &mut Parser<'_>) -> trc::Result<Self> where Self: Sized, { diff --git a/crates/jmap-proto/src/types/any_id.rs b/crates/jmap-proto/src/types/any_id.rs index 62ec2397..3adb36f0 100644 --- a/crates/jmap-proto/src/types/any_id.rs +++ b/crates/jmap-proto/src/types/any_id.rs @@ -96,7 +96,7 @@ impl From<&AnyId> for Value { } impl JsonObjectParser for AnyId { - fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + fn parse(parser: &mut Parser<'_>) -> trc::Result<Self> where Self: Sized, { diff --git a/crates/jmap-proto/src/types/blob.rs b/crates/jmap-proto/src/types/blob.rs index 7c920823..86b577da 100644 --- a/crates/jmap-proto/src/types/blob.rs +++ b/crates/jmap-proto/src/types/blob.rs @@ -38,7 +38,7 @@ pub struct BlobSection { } impl JsonObjectParser for BlobId { - fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + fn parse(parser: &mut Parser<'_>) -> trc::Result<Self> where Self: Sized, { diff --git a/crates/jmap-proto/src/types/date.rs b/crates/jmap-proto/src/types/date.rs index 1c72a8d3..923bc3f7 100644 --- a/crates/jmap-proto/src/types/date.rs +++ b/crates/jmap-proto/src/types/date.rs @@ -24,7 +24,7 @@ pub struct UTCDate { } impl JsonObjectParser for UTCDate { - fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + fn parse(parser: &mut Parser<'_>) -> trc::Result<Self> where Self: Sized, { diff --git a/crates/jmap-proto/src/types/id.rs b/crates/jmap-proto/src/types/id.rs index e820ca95..da3498d1 100644 --- a/crates/jmap-proto/src/types/id.rs +++ b/crates/jmap-proto/src/types/id.rs @@ -24,7 +24,7 @@ impl Default for Id { } impl JsonObjectParser for Id { - fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + fn parse(parser: &mut Parser<'_>) -> trc::Result<Self> where Self: Sized, { diff --git a/crates/jmap-proto/src/types/keyword.rs b/crates/jmap-proto/src/types/keyword.rs index 737ea3e0..701baf81 100644 --- a/crates/jmap-proto/src/types/keyword.rs +++ b/crates/jmap-proto/src/types/keyword.rs @@ -61,7 +61,7 @@ pub enum Keyword { } impl JsonObjectParser for Keyword { - fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + fn parse(parser: &mut Parser<'_>) -> trc::Result<Self> where Self: Sized, { diff --git a/crates/jmap-proto/src/types/mod.rs b/crates/jmap-proto/src/types/mod.rs index 4315d0ad..199bc8ac 100644 --- a/crates/jmap-proto/src/types/mod.rs +++ b/crates/jmap-proto/src/types/mod.rs @@ -29,7 +29,7 @@ pub enum MaybeUnparsable<V> { } impl<V: JsonObjectParser> JsonObjectParser for MaybeUnparsable<V> { - fn parse(parser: &mut Parser) -> crate::parser::Result<Self> { + fn parse(parser: &mut Parser) -> trc::Result<Self> { match V::parse(parser) { Ok(value) => Ok(MaybeUnparsable::Value(value)), Err(_) if parser.is_eof || parser.skip_string() => Ok(MaybeUnparsable::ParseError( diff --git a/crates/jmap-proto/src/types/pointer.rs b/crates/jmap-proto/src/types/pointer.rs index 2d6731b3..2062e6f5 100644 --- a/crates/jmap-proto/src/types/pointer.rs +++ b/crates/jmap-proto/src/types/pointer.rs @@ -26,7 +26,7 @@ enum TokenType { } impl JsonObjectParser for JSONPointer { - fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + fn parse(parser: &mut Parser<'_>) -> trc::Result<Self> where Self: Sized, { diff --git a/crates/jmap-proto/src/types/property.rs b/crates/jmap-proto/src/types/property.rs index 40214d59..bc76da48 100644 --- a/crates/jmap-proto/src/types/property.rs +++ b/crates/jmap-proto/src/types/property.rs @@ -10,7 +10,7 @@ use mail_parser::HeaderName; use serde::Serialize; use store::write::{DeserializeFrom, SerializeInto}; -use crate::parser::{json::Parser, Error, JsonObjectParser}; +use crate::parser::{json::Parser, JsonObjectParser}; use super::{acl::Acl, id::Id, keyword::Keyword, value::Value}; @@ -153,7 +153,7 @@ pub trait IntoProperty: Eq + Display { } impl JsonObjectParser for Property { - fn parse(parser: &mut Parser) -> crate::parser::Result<Self> { + fn parse(parser: &mut Parser) -> trc::Result<Self> { let mut first_char = 0; let mut hash = 0; let mut shift = 0; @@ -190,7 +190,7 @@ impl JsonObjectParser for Property { } impl JsonObjectParser for SetProperty { - fn parse(parser: &mut Parser) -> crate::parser::Result<Self> { + fn parse(parser: &mut Parser) -> trc::Result<Self> { let mut first_char = 0; let mut hash = 0; let mut shift = 0; @@ -251,7 +251,7 @@ impl JsonObjectParser for SetProperty { Ok(id) => { patch.push(Value::Id(id)); } - Err(Error::Method(_)) => { + Err(err) if err.is_jmap_method_error() => { property = parser.invalid_property()?; } Err(err) => { @@ -262,7 +262,7 @@ impl JsonObjectParser for SetProperty { Ok(keyword) => { patch.push(Value::Keyword(keyword)); } - Err(Error::Method(_)) => { + Err(err) if err.is_jmap_method_error() => { property = parser.invalid_property()?; } Err(err) => { @@ -290,7 +290,7 @@ impl JsonObjectParser for SetProperty { Ok(acl) => { patch.push(Value::UnsignedInt(acl as u64)); } - Err(Error::Method(_)) => { + Err(err) if err.is_jmap_method_error() => { property = parser.invalid_property()?; } Err(err) => { @@ -469,7 +469,7 @@ fn parse_property(first_char: u8, hash: u128) -> Option<Property> { }) } -fn parse_header_property(parser: &mut Parser) -> crate::parser::Result<Property> { +fn parse_header_property(parser: &mut Parser) -> trc::Result<Property> { let hdr_start_pos = parser.pos; let mut has_next = false; @@ -553,7 +553,7 @@ fn parse_sub_property( parser: &mut Parser, first_char: u8, parent_hash: u128, -) -> crate::parser::Result<Property> { +) -> trc::Result<Property> { let mut hash = 0; let mut shift = 0; @@ -585,7 +585,7 @@ fn parse_sub_property( } impl JsonObjectParser for ObjectProperty { - fn parse(parser: &mut Parser) -> crate::parser::Result<Self> { + fn parse(parser: &mut Parser) -> trc::Result<Self> { let mut first_char = 0; let mut hash = 0; let mut shift = 0; @@ -707,7 +707,7 @@ impl JsonObjectParser for ObjectProperty { } impl<'x> Parser<'x> { - fn invalid_property(&mut self) -> crate::parser::Result<Property> { + fn invalid_property(&mut self) -> trc::Result<Property> { if self.is_eof || self.skip_string() { Ok(Property::_T( String::from_utf8_lossy(self.bytes[self.pos_marker..self.pos - 1].as_ref()) diff --git a/crates/jmap-proto/src/types/state.rs b/crates/jmap-proto/src/types/state.rs index 20da85dc..422c0e97 100644 --- a/crates/jmap-proto/src/types/state.rs +++ b/crates/jmap-proto/src/types/state.rs @@ -72,7 +72,7 @@ impl From<Option<ChangeId>> for State { } impl JsonObjectParser for State { - fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + fn parse(parser: &mut Parser<'_>) -> trc::Result<Self> where Self: Sized, { diff --git a/crates/jmap-proto/src/types/type_state.rs b/crates/jmap-proto/src/types/type_state.rs index 44bd7aaf..563dc14d 100644 --- a/crates/jmap-proto/src/types/type_state.rs +++ b/crates/jmap-proto/src/types/type_state.rs @@ -85,7 +85,7 @@ impl From<DataType> for u64 { } impl JsonObjectParser for DataType { - fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + fn parse(parser: &mut Parser<'_>) -> trc::Result<Self> where Self: Sized, { diff --git a/crates/jmap-proto/src/types/value.rs b/crates/jmap-proto/src/types/value.rs index 90a2952d..65c83d88 100644 --- a/crates/jmap-proto/src/types/value.rs +++ b/crates/jmap-proto/src/types/value.rs @@ -78,7 +78,7 @@ impl Value { pub fn parse<K: JsonObjectParser + IntoProperty, V: JsonObjectParser + IntoValue>( token: Token<V>, parser: &mut Parser<'_>, - ) -> crate::parser::Result<Self> { + ) -> trc::Result<Self> { Ok(match token { Token::String(v) => v.into_value(), Token::DictStart => { @@ -114,7 +114,7 @@ impl Value { pub fn from_property( parser: &mut Parser<'_>, property: &Property, - ) -> crate::parser::Result<Self> { + ) -> trc::Result<Self> { match &property { Property::BlobId => Ok(parser .next_token::<BlobId>()? @@ -327,7 +327,7 @@ impl Value { } impl<T: JsonObjectParser + Display + Eq> JsonObjectParser for SetValueMap<T> { - fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self> + fn parse(parser: &mut Parser<'_>) -> trc::Result<Self> where Self: Sized, { diff --git a/crates/jmap/src/api/autoconfig.rs b/crates/jmap/src/api/autoconfig.rs index c280e3f4..d18470d5 100644 --- a/crates/jmap/src/api/autoconfig.rs +++ b/crates/jmap/src/api/autoconfig.rs @@ -8,7 +8,6 @@ use std::fmt::Write; use common::manager::webadmin::Resource; use directory::QueryBy; -use jmap_proto::error::request::RequestError; use quick_xml::events::Event; use quick_xml::Reader; use utils::url_params::UrlParams; @@ -18,22 +17,15 @@ use crate::{api::http::ToHttpResponse, JMAP}; use super::{HttpRequest, HttpResponse}; impl JMAP { - pub async fn handle_autoconfig_request(&self, req: &HttpRequest) -> HttpResponse { + pub async fn handle_autoconfig_request(&self, req: &HttpRequest) -> trc::Result<HttpResponse> { // Obtain parameters let params = UrlParams::new(req.uri().query()); let emailaddress = params .get("emailaddress") .unwrap_or_default() .to_lowercase(); - let (account_name, server_name, domain) = - match self.autoconfig_parameters(&emailaddress).await { - Ok(result) => result, - Err(err) => return err.into_http_response(), - }; - let services = match self.core.storage.config.get_services().await { - Ok(services) => services, - Err(err) => return err.into_http_response(), - }; + let (account_name, server_name, domain) = self.autoconfig_parameters(&emailaddress).await?; + let services = self.core.storage.config.get_services().await?; // Build XML response let mut config = String::with_capacity(1024); @@ -75,30 +67,27 @@ impl JMAP { ); config.push_str("</clientConfig>\n"); - Resource { + Ok(Resource { content_type: "application/xml; charset=utf-8", contents: config.into_bytes(), } - .into_http_response() + .into_http_response()) } - pub async fn handle_autodiscover_request(&self, body: Option<Vec<u8>>) -> HttpResponse { + pub async fn handle_autodiscover_request( + &self, + body: Option<Vec<u8>>, + ) -> trc::Result<HttpResponse> { // Obtain parameters - let emailaddress = match parse_autodiscover_request(body.as_deref().unwrap_or_default()) { - Ok(emailaddress) => emailaddress, - Err(err) => { - return RequestError::blank(400, "Failed to parse autodiscover request", err) - .into_http_response() - } - }; - let (account_name, server_name, _) = match self.autoconfig_parameters(&emailaddress).await { - Ok(result) => result, - Err(err) => return err.into_http_response(), - }; - let services = match self.core.storage.config.get_services().await { - Ok(services) => services, - Err(err) => return err.into_http_response(), - }; + let emailaddress = parse_autodiscover_request(body.as_deref().unwrap_or_default()) + .map_err(|err| { + trc::ResourceCause::BadParameters + .into_err() + .details("Failed to parse autodiscover request") + .ctx(trc::Key::Reason, err) + })?; + let (account_name, server_name, _) = self.autoconfig_parameters(&emailaddress).await?; + let services = self.core.storage.config.get_services().await?; // Build XML response let mut config = String::with_capacity(1024); @@ -158,36 +147,35 @@ impl JMAP { let _ = writeln!(&mut config, "\t</Response>"); let _ = writeln!(&mut config, "</Autodiscover>"); - Resource { + Ok(Resource { content_type: "application/xml; charset=utf-8", contents: config.into_bytes(), } - .into_http_response() + .into_http_response()) } async fn autoconfig_parameters<'x>( &self, emailaddress: &'x str, - ) -> Result<(String, String, &'x str), RequestError> { - let domain = if let Some((_, domain)) = emailaddress.rsplit_once('@') { - domain - } else { - return Err(RequestError::invalid_parameters()); - }; + ) -> trc::Result<(String, String, &'x str)> { + let (_, domain) = emailaddress.rsplit_once('@').ok_or_else(|| { + trc::ResourceCause::BadParameters + .into_err() + .details("Missing domain in email address") + })?; // Obtain server name - let server_name = if let Ok(Some(server_name)) = self + let server_name = self .core .storage .config .get("lookup.default.hostname") - .await - { - server_name - } else { - tracing::error!("Autoconfig request failed: Server name not configured"); - return Err(RequestError::internal_server_error()); - }; + .await? + .ok_or_else(|| { + trc::Cause::Configuration + .into_err() + .details("Server name not configured") + })?; // Find the account name by e-mail address let mut account_name = emailaddress.to_string(); diff --git a/crates/jmap/src/api/event_source.rs b/crates/jmap/src/api/event_source.rs index e1076a33..66d1d5cb 100644 --- a/crates/jmap/src/api/event_source.rs +++ b/crates/jmap/src/api/event_source.rs @@ -14,12 +14,12 @@ use hyper::{ body::{Bytes, Frame}, header, StatusCode, }; -use jmap_proto::{error::request::RequestError, types::type_state::DataType}; +use jmap_proto::types::type_state::DataType; use utils::map::bitmap::Bitmap; use crate::{auth::AccessToken, JMAP, LONG_SLUMBER}; -use super::{http::ToHttpResponse, HttpRequest, HttpResponse, StateChangeResponse}; +use super::{HttpRequest, HttpResponse, StateChangeResponse}; struct Ping { interval: Duration, @@ -32,7 +32,7 @@ impl JMAP { &self, req: HttpRequest, access_token: Arc<AccessToken>, - ) -> HttpResponse { + ) -> trc::Result<HttpResponse> { // Parse query let mut ping = 0; let mut types = Bitmap::default(); @@ -49,7 +49,7 @@ impl JMAP { } else if let Ok(type_state) = DataType::try_from(type_state) { types.insert(type_state); } else { - return RequestError::invalid_parameters().into_http_response(); + return Err(trc::ResourceCause::BadParameters.into_err()); } } } @@ -58,13 +58,13 @@ impl JMAP { close_after_state = true; } "no" => {} - _ => return RequestError::invalid_parameters().into_http_response(), + _ => return Err(trc::ResourceCause::BadParameters.into_err()), }, "ping" => match value.parse::<u32>() { Ok(value) => { ping = value; } - Err(_) => return RequestError::invalid_parameters().into_http_response(), + Err(_) => return Err(trc::ResourceCause::BadParameters.into_err()), }, _ => {} } @@ -92,17 +92,11 @@ impl JMAP { let throttle = self.core.jmap.event_source_throttle; // Register with state manager - let mut change_rx = if let Ok(change_rx) = self + let mut change_rx = self .subscribe_state_manager(access_token.primary_id(), types) - .await - { - let todo = "return error"; - change_rx - } else { - return RequestError::internal_server_error().into_http_response(); - }; + .await?; - hyper::Response::builder() + Ok(hyper::Response::builder() .status(StatusCode::OK) .header(header::CONTENT_TYPE, "text/event-stream") .header(header::CACHE_CONTROL, "no-store") @@ -160,6 +154,6 @@ impl JMAP { }; } }))) - .unwrap() + .unwrap()) } } diff --git a/crates/jmap/src/api/http.rs b/crates/jmap/src/api/http.rs index 0e279dbc..de1c94dd 100644 --- a/crates/jmap/src/api/http.rs +++ b/crates/jmap/src/api/http.rs @@ -22,7 +22,7 @@ use hyper::{ }; use hyper_util::rt::TokioIo; use jmap_proto::{ - error::request::{RequestError, RequestLimitError}, + error::request::RequestError, request::{capability::Session, Request}, response::Response, types::{blob::BlobId, id::Id}, @@ -51,28 +51,19 @@ impl JMAP { &self, mut req: HttpRequest, session: HttpSessionData, - ) -> HttpResponse { + ) -> trc::Result<HttpResponse> { let mut path = req.uri().path().split('/'); path.next(); match path.next().unwrap_or_default() { "jmap" => { - // Authenticate request - let (_in_flight, access_token) = - match self.authenticate_headers(&req, session.remote_ip).await { - Ok(session) => session, - Err(err) => { - return if req.method() != Method::OPTIONS { - err.into_http_response() - } else { - StatusCode::NO_CONTENT.into_http_response() - } - } - }; - match (path.next().unwrap_or_default(), req.method()) { ("", &Method::POST) => { - return match fetch_body( + // Authenticate request + let (_in_flight, access_token) = + self.authenticate_headers(&req, session.remote_ip).await?; + + let request = fetch_body( &mut req, if !access_token.is_super_user() { self.core.jmap.upload_max_size @@ -81,7 +72,7 @@ impl JMAP { }, ) .await - .ok_or_else(|| RequestError::limit(RequestLimitError::SizeRequest)) + .ok_or_else(|| trc::LimitCause::SizeRequest.into_err()) .and_then(|bytes| { //let c = println!("<- {}", String::from_utf8_lossy(&bytes)); @@ -90,34 +81,25 @@ impl JMAP { self.core.jmap.request_max_calls, self.core.jmap.request_max_size, ) - }) { - Ok(request) => { - match self - .handle_request(request, access_token, &session.instance) - .await - { - Ok(response) => { - /*let c = println!( - "-> {}", - serde_json::to_string_pretty(&response).unwrap() - );*/ - - response.into_http_response() - } - Err(err) => err.into_http_response(), - } - } - Err(err) => err.into_http_response(), - }; + })?; + + return Ok(self + .handle_request(request, access_token, &session.instance) + .await + .into_http_response()); } ("download", &Method::GET) => { + // Authenticate request + let (_in_flight, access_token) = + self.authenticate_headers(&req, session.remote_ip).await?; + if let (Some(_), Some(blob_id), Some(name)) = ( path.next().and_then(|p| Id::from_bytes(p.as_bytes())), path.next().and_then(BlobId::from_base32), path.next(), ) { - return match self.blob_download(&blob_id, &access_token).await { - Ok(Some(blob)) => DownloadResponse { + return match self.blob_download(&blob_id, &access_token).await? { + Some(blob) => Ok(DownloadResponse { filename: name.to_string(), content_type: req .uri() @@ -130,15 +112,16 @@ impl JMAP { .unwrap_or("application/octet-stream".to_string()), blob, } - .into_http_response(), - Ok(None) => RequestError::not_found().into_http_response(), - Err(_) => { - RequestError::internal_server_error().into_http_response() - } + .into_http_response()), + None => Err(trc::ResourceCause::NotFound.into_err()), }; } } ("upload", &Method::POST) => { + // Authenticate request + let (_in_flight, access_token) = + self.authenticate_headers(&req, session.remote_ip).await?; + if let Some(account_id) = path.next().and_then(|p| Id::from_bytes(p.as_bytes())) { @@ -152,32 +135,34 @@ impl JMAP { ) .await { - Some(bytes) => { - match self - .blob_upload( - account_id, - req.headers() - .get(CONTENT_TYPE) - .and_then(|h| h.to_str().ok()) - .unwrap_or("application/octet-stream"), - &bytes, - access_token, - ) - .await - { - Ok(response) => response.into_http_response(), - Err(err) => err.into_http_response(), - } - } - None => RequestError::limit(RequestLimitError::SizeUpload) - .into_http_response(), + Some(bytes) => Ok(self + .blob_upload( + account_id, + req.headers() + .get(CONTENT_TYPE) + .and_then(|h| h.to_str().ok()) + .unwrap_or("application/octet-stream"), + &bytes, + access_token, + ) + .await? + .into_http_response()), + None => Err(trc::LimitCause::SizeUpload.into_err()), }; } } ("eventsource", &Method::GET) => { - return self.handle_event_source(req, access_token).await + // Authenticate request + let (_in_flight, access_token) = + self.authenticate_headers(&req, session.remote_ip).await?; + + return self.handle_event_source(req, access_token).await; } ("ws", &Method::GET) => { + // Authenticate request + let (_in_flight, access_token) = + self.authenticate_headers(&req, session.remote_ip).await?; + return self .upgrade_websocket_connection( req, @@ -187,7 +172,7 @@ impl JMAP { .await; } (_, &Method::OPTIONS) => { - return StatusCode::NO_CONTENT.into_http_response(); + return Ok(StatusCode::NO_CONTENT.into_http_response()); } _ => (), } @@ -196,31 +181,24 @@ impl JMAP { ("jmap", &Method::GET) => { // Authenticate request let (_in_flight, access_token) = - match self.authenticate_headers(&req, session.remote_ip).await { - Ok(session) => session, - Err(err) => return err.into_http_response(), - }; + self.authenticate_headers(&req, session.remote_ip).await?; - return match self + return Ok(self .handle_session_resource( session.resolve_url(&self.core).await, access_token, ) - .await - { - Ok(session) => session.into_http_response(), - Err(err) => err.into_http_response(), - }; + .await? + .into_http_response()); } ("oauth-authorization-server", &Method::GET) => { // Limit anonymous requests - return match self.is_anonymous_allowed(&session.remote_ip).await { - Ok(_) => JsonResponse::new(OAuthMetadata::new( - session.resolve_url(&self.core).await, - )) - .into_http_response(), - Err(err) => err.into_http_response(), - }; + self.is_anonymous_allowed(&session.remote_ip).await?; + + return Ok(JsonResponse::new(OAuthMetadata::new( + session.resolve_url(&self.core).await, + )) + .into_http_response()); } ("acme-challenge", &Method::GET) if self.core.has_acme_http_providers() => { if let Some(token) = path.next() { @@ -229,27 +207,26 @@ impl JMAP { .storage .lookup .key_get::<String>(format!("acme:{token}").into_bytes()) - .await + .await? { - Ok(Some(proof)) => Resource { + Some(proof) => Ok(Resource { content_type: "text/plain", contents: proof.into_bytes(), } - .into_http_response(), - Ok(None) => RequestError::not_found().into_http_response(), - Err(err) => err.into_http_response(), + .into_http_response()), + None => Err(trc::ResourceCause::NotFound.into_err()), }; } } ("mta-sts.txt", &Method::GET) => { if let Some(policy) = self.core.build_mta_sts_policy() { - return Resource { + return Ok(Resource { content_type: "text/plain", contents: policy.to_string().into_bytes(), } - .into_http_response(); + .into_http_response()); } else { - return RequestError::not_found().into_http_response(); + return Err(trc::ResourceCause::NotFound.into_err()); } } ("mail-v1.xml", &Method::GET) => { @@ -263,46 +240,40 @@ impl JMAP { } } (_, &Method::OPTIONS) => { - return StatusCode::NO_CONTENT.into_http_response(); + return Ok(StatusCode::NO_CONTENT.into_http_response()); } _ => (), }, "auth" => match (path.next().unwrap_or_default(), req.method()) { ("device", &Method::POST) => { - return match self.is_anonymous_allowed(&session.remote_ip).await { - Ok(_) => { - self.handle_device_auth(&mut req, session.resolve_url(&self.core).await) - .await - } - Err(err) => err.into_http_response(), - } + self.is_anonymous_allowed(&session.remote_ip).await?; + + return self + .handle_device_auth(&mut req, session.resolve_url(&self.core).await) + .await; } ("token", &Method::POST) => { - return match self.is_anonymous_allowed(&session.remote_ip).await { - Ok(_) => self.handle_token_request(&mut req).await, - Err(err) => err.into_http_response(), - } + self.is_anonymous_allowed(&session.remote_ip).await?; + + return self.handle_token_request(&mut req).await; } (_, &Method::OPTIONS) => { - return StatusCode::NO_CONTENT.into_http_response(); + return Ok(StatusCode::NO_CONTENT.into_http_response()); } _ => (), }, "api" => { // Allow CORS preflight requests if req.method() == Method::OPTIONS { - return StatusCode::NO_CONTENT.into_http_response(); + return Ok(StatusCode::NO_CONTENT.into_http_response()); } // Authenticate user - return match self.authenticate_headers(&req, session.remote_ip).await { - Ok((_, access_token)) => { - let body = fetch_body(&mut req, 1024 * 1024).await; - self.handle_api_manage_request(&req, body, access_token) - .await - } - Err(err) => err.into_http_response(), - }; + let (_, access_token) = self.authenticate_headers(&req, session.remote_ip).await?; + let body = fetch_body(&mut req, 1024 * 1024).await; + return self + .handle_api_manage_request(&req, body, access_token) + .await; } "mail" => { if req.method() == Method::GET @@ -321,43 +292,45 @@ impl JMAP { } } "robots.txt" => { - return Resource { + return Ok(Resource { content_type: "text/plain", contents: b"User-agent: *\nDisallow: /\n".to_vec(), } - .into_http_response(); + .into_http_response()); } "healthz" => match path.next().unwrap_or_default() { "live" => { - return StatusCode::OK.into_http_response(); + return Ok(StatusCode::OK.into_http_response()); } "ready" => { - return { + return Ok({ if !self.core.storage.data.is_none() { StatusCode::OK } else { StatusCode::SERVICE_UNAVAILABLE } } - .into_http_response(); + .into_http_response()); } _ => (), }, _ => { let path = req.uri().path(); - return match self + let resource = self .inner .webadmin .get(path.strip_prefix('/').unwrap_or(path)) - .await - { - Ok(resource) if !resource.is_empty() => resource.into_http_response(), - Err(err) => err.into_http_response(), - _ => RequestError::not_found().into_http_response(), + .await?; + + return if !resource.is_empty() { + Ok(resource.into_http_response()) + } else { + Err(trc::ResourceCause::NotFound.into_err()) }; } } - RequestError::not_found().into_http_response() + + Err(trc::ResourceCause::NotFound.into_err()) } } @@ -403,7 +376,7 @@ impl JmapInstance { }; // Parse HTTP request - let mut response = jmap + let mut response = match jmap .parse_http_request( req, HttpSessionData { @@ -415,7 +388,19 @@ impl JmapInstance { is_tls, }, ) - .await; + .await + { + Ok(response) => response, + Err(err) => { + tracing::error!( + parent: &span, + event = "error", + context = "http", + reason = %err, + ); + err.into_http_response() + } + }; // Add custom headers if !jmap.core.jmap.http_headers.is_empty() { @@ -534,7 +519,7 @@ impl ToHttpResponse for trc::Error { } } -impl ToHttpResponse for std::io::Error { +/*impl ToHttpResponse for std::io::Error { fn into_http_response(self) -> HttpResponse { tracing::error!(context = "i/o", error = %self, "I/O error"); @@ -551,7 +536,7 @@ impl ToHttpResponse for serde_json::Error { ) .into_http_response() } -} +}*/ impl<T: serde::Serialize> JsonResponse<T> { pub fn new(inner: T) -> Self { diff --git a/crates/jmap/src/api/management/dkim.rs b/crates/jmap/src/api/management/dkim.rs index cda3af68..cba78295 100644 --- a/crates/jmap/src/api/management/dkim.rs +++ b/crates/jmap/src/api/management/dkim.rs @@ -7,8 +7,8 @@ use std::str::FromStr; use common::config::smtp::auth::simple_pem_parse; +use directory::backend::internal::manage; use hyper::Method; -use jmap_proto::error::request::RequestError; use mail_auth::{ common::crypto::{Ed25519Key, RsaKey, Sha256}, dkim::generate::DkimKeyPair, @@ -22,10 +22,7 @@ use serde_json::json; use store::write::now; use crate::{ - api::{ - http::ToHttpResponse, management::ManagementApiError, HttpRequest, HttpResponse, - JsonResponse, - }, + api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}, JMAP, }; @@ -51,22 +48,21 @@ impl JMAP { req: &HttpRequest, path: Vec<&str>, body: Option<Vec<u8>>, - ) -> HttpResponse { + ) -> trc::Result<HttpResponse> { match *req.method() { Method::GET => self.handle_get_public_key(path).await, Method::POST => self.handle_create_signature(body).await, - _ => RequestError::not_found().into_http_response(), + _ => Err(trc::ResourceCause::NotFound.into_err()), } } - async fn handle_get_public_key(&self, path: Vec<&str>) -> HttpResponse { + async fn handle_get_public_key(&self, path: Vec<&str>) -> trc::Result<HttpResponse> { let signature_id = match path.get(1) { Some(signature_id) => decode_path_element(signature_id), None => { - return RequestError::not_found().into_http_response(); + return Err(trc::ResourceCause::NotFound.into_err()); } }; - let todo = "bubble up error and log them"; let (pk, algo) = match ( self.core @@ -82,27 +78,26 @@ impl JMAP { .map(|algo| algo.and_then(|algo| algo.parse::<Algorithm>().ok())), ) { (Ok(Some(pk)), Ok(Some(algorithm))) => (pk, algorithm), - (Err(err), _) | (_, Err(err)) => return err.into_http_response(), - _ => return RequestError::not_found().into_http_response(), + (Err(err), _) | (_, Err(err)) => return Err(err.caused_by(trc::location!())), + _ => return Err(trc::ResourceCause::NotFound.into_err()), }; match obtain_dkim_public_key(algo, &pk) { - Ok(data) => JsonResponse::new(json!({ + Ok(data) => Ok(JsonResponse::new(json!({ "data": data, })) - .into_http_response(), - Err(err) => ManagementApiError::Other { - details: err.into(), - } - .into_http_response(), + .into_http_response()), + Err(details) => Err(manage::error(details, None::<u32>)), } } - async fn handle_create_signature(&self, body: Option<Vec<u8>>) -> HttpResponse { + async fn handle_create_signature(&self, body: Option<Vec<u8>>) -> trc::Result<HttpResponse> { let request = match serde_json::from_slice::<DkimSignature>(body.as_deref().unwrap_or_default()) { Ok(request) => request, - Err(err) => return err.into_http_response(), + Err(err) => { + return Err(trc::Cause::Resource(trc::ResourceCause::BadParameters).reason(err)) + } }; let algo_str = match request.algorithm { @@ -127,35 +122,27 @@ impl JMAP { }); // Make sure the signature does not exist already - match self + if let Some(value) = self .core .storage .config .get(&format!("signature.{id}.private-key")) - .await + .await? { - Ok(None) => (), - Ok(Some(value)) => { - return ManagementApiError::FieldAlreadyExists { - field: format!("signature.{id}.private-key").into(), - value: value.into(), - } - .into_http_response(); - } - Err(err) => return err.into_http_response(), + return Err(manage::err_exists( + format!("signature.{id}.private-key"), + value, + )); } // Create signature - match self - .create_dkim_key(request.algorithm, id, request.domain, selector) - .await - { - Ok(_) => JsonResponse::new(json!({ - "data": (), - })) - .into_http_response(), - Err(err) => err.into_http_response(), - } + self.create_dkim_key(request.algorithm, id, request.domain, selector) + .await?; + + Ok(JsonResponse::new(json!({ + "data": (), + })) + .into_http_response()) } async fn create_dkim_key( @@ -177,7 +164,10 @@ impl JMAP { Algorithm::Rsa => DkimKeyPair::generate_rsa(2048), Algorithm::Ed25519 => DkimKeyPair::generate_ed25519(), } - .map_err(|err| trc::Cause::Crypto.reason(err).caused_by(trc::location!()))? + .map_err(|err| { + manage::error("Failed to generate key", err.to_string().into()) + .caused_by(trc::location!()) + })? .private_key(), ) .unwrap_or_default() diff --git a/crates/jmap/src/api/management/domain.rs b/crates/jmap/src/api/management/domain.rs index 4ff4d247..e7a007c5 100644 --- a/crates/jmap/src/api/management/domain.rs +++ b/crates/jmap/src/api/management/domain.rs @@ -7,7 +7,6 @@ use directory::backend::internal::manage::ManageDirectory; use hyper::Method; -use jmap_proto::error::request::RequestError; use serde::{Deserialize, Serialize}; use serde_json::json; use sha1::Digest; @@ -35,7 +34,11 @@ struct DnsRecord { } impl JMAP { - pub async fn handle_manage_domain(&self, req: &HttpRequest, path: Vec<&str>) -> HttpResponse { + pub async fn handle_manage_domain( + &self, + req: &HttpRequest, + path: Vec<&str>, + ) -> trc::Result<HttpResponse> { match (path.get(1), req.method()) { (None, &Method::GET) => { // List domains @@ -44,82 +47,78 @@ impl JMAP { let page: usize = params.parse("page").unwrap_or(0); let limit: usize = params.parse("limit").unwrap_or(0); - match self.core.storage.data.list_domains(filter).await { - Ok(domains) => { - let (total, domains) = if limit > 0 { - let offset = page.saturating_sub(1) * limit; - ( - domains.len(), - domains.into_iter().skip(offset).take(limit).collect(), - ) - } else { - (domains.len(), domains) - }; + let domains = self.core.storage.data.list_domains(filter).await?; + let (total, domains) = if limit > 0 { + let offset = page.saturating_sub(1) * limit; + ( + domains.len(), + domains.into_iter().skip(offset).take(limit).collect(), + ) + } else { + (domains.len(), domains) + }; - JsonResponse::new(json!({ - "data": { - "items": domains, - "total": total, - }, - })) - .into_http_response() - } - Err(err) => err.into_http_response(), - } + Ok(JsonResponse::new(json!({ + "data": { + "items": domains, + "total": total, + }, + })) + .into_http_response()) } (Some(domain), &Method::GET) => { // Obtain DNS records let domain = decode_path_element(domain); - match self.build_dns_records(domain.as_ref()).await { - Ok(records) => JsonResponse::new(json!({ - "data": records, - })) - .into_http_response(), - Err(err) => err.into_http_response(), - } + Ok(JsonResponse::new(json!({ + "data": self.build_dns_records(domain.as_ref()).await?, + })) + .into_http_response()) } (Some(domain), &Method::POST) => { // Create domain let domain = decode_path_element(domain); - match self.core.storage.data.create_domain(domain.as_ref()).await { - Ok(_) => { - // Set default domain name if missing - if matches!( - self.core.storage.config.get("lookup.default.domain").await, - Ok(None) - ) { - if let Err(err) = self - .core - .storage - .config - .set([("lookup.default.domain", domain.as_ref())]) - .await - { - tracing::error!("Failed to set default domain name: {}", err); - } - } - - JsonResponse::new(json!({ - "data": (), - })) - .into_http_response() - } - Err(err) => err.into_http_response(), + self.core + .storage + .data + .create_domain(domain.as_ref()) + .await?; + // Set default domain name if missing + if self + .core + .storage + .config + .get("lookup.default.domain") + .await? + .is_none() + { + self.core + .storage + .config + .set([("lookup.default.domain", domain.as_ref())]) + .await?; } + + Ok(JsonResponse::new(json!({ + "data": (), + })) + .into_http_response()) } (Some(domain), &Method::DELETE) => { // Delete domain let domain = decode_path_element(domain); - match self.core.storage.data.delete_domain(domain.as_ref()).await { - Ok(_) => JsonResponse::new(json!({ - "data": (), - })) - .into_http_response(), - Err(err) => err.into_http_response(), - } + self.core + .storage + .data + .delete_domain(domain.as_ref()) + .await?; + + Ok(JsonResponse::new(json!({ + "data": (), + })) + .into_http_response()) } - _ => RequestError::not_found().into_http_response(), + _ => Err(trc::ResourceCause::NotFound.into_err()), } } diff --git a/crates/jmap/src/api/management/enterprise.rs b/crates/jmap/src/api/management/enterprise.rs index 1931c70b..7ba9c2fb 100644 --- a/crates/jmap/src/api/management/enterprise.rs +++ b/crates/jmap/src/api/management/enterprise.rs @@ -14,10 +14,11 @@ use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; use common::enterprise::undelete::DeletedBlob; use directory::backend::internal::manage::ManageDirectory; use hyper::Method; -use jmap_proto::{error::request::RequestError, types::collection::Collection}; +use jmap_proto::types::collection::Collection; use mail_parser::{DateTime, MessageParser}; use serde_json::json; use store::write::{BatchBuilder, BlobOp, ValueClass}; +use trc::AddContext; use utils::{url_params::UrlParams, BlobHash}; use crate::{ @@ -53,10 +54,10 @@ impl JMAP { req: &HttpRequest, path: Vec<&str>, body: Option<Vec<u8>>, - ) -> HttpResponse { + ) -> trc::Result<HttpResponse> { match path.get(1).copied().unwrap_or_default() { "undelete" => self.handle_undelete_api_request(req, path, body).await, - _ => RequestError::not_found().into_http_response(), + _ => Err(trc::ResourceCause::NotFound.into_err()), } } @@ -65,72 +66,76 @@ impl JMAP { req: &HttpRequest, path: Vec<&str>, body: Option<Vec<u8>>, - ) -> HttpResponse { + ) -> trc::Result<HttpResponse> { match (path.get(2).copied(), req.method()) { (Some(account_name), &Method::GET) => { - match self.core.storage.data.get_account_id(account_name).await { - Ok(Some(account_id)) => match self.core.list_deleted(account_id).await { - Ok(mut deleted) => { - let params = UrlParams::new(req.uri().query()); - let limit = params.parse::<usize>("limit").unwrap_or_default(); - let mut offset = params - .parse::<usize>("page") - .unwrap_or_default() - .saturating_sub(1) - * limit; + let account_id = self + .core + .storage + .data + .get_account_id(account_name) + .await? + .ok_or_else(|| trc::ResourceCause::NotFound.into_err())?; + let mut deleted = self.core.list_deleted(account_id).await?; - // Sort ascending by deleted_at - let total = deleted.len(); - deleted.sort_by(|a, b| a.deleted_at.cmp(&b.deleted_at)); - let mut results = - Vec::with_capacity(if limit > 0 { limit } else { total }); + let params = UrlParams::new(req.uri().query()); + let limit = params.parse::<usize>("limit").unwrap_or_default(); + let mut offset = params + .parse::<usize>("page") + .unwrap_or_default() + .saturating_sub(1) + * limit; - for blob in deleted { - if offset == 0 { - results.push(DeletedBlob { - hash: URL_SAFE_NO_PAD.encode(blob.hash.as_slice()), - size: blob.size, - deleted_at: DateTime::from_timestamp( - blob.deleted_at as i64, - ) - .to_rfc3339(), - expires_at: DateTime::from_timestamp( - blob.expires_at as i64, - ) - .to_rfc3339(), - collection: Collection::from(blob.collection).to_string(), - }); - if results.len() == limit { - break; - } - } else { - offset -= 1; - } - } + // Sort ascending by deleted_at + let total = deleted.len(); + deleted.sort_by(|a, b| a.deleted_at.cmp(&b.deleted_at)); + let mut results = Vec::with_capacity(if limit > 0 { limit } else { total }); - JsonResponse::new(json!({ - "data":{ - "items": results, - "total": total, - }, - })) - .into_http_response() + for blob in deleted { + if offset == 0 { + results.push(DeletedBlob { + hash: URL_SAFE_NO_PAD.encode(blob.hash.as_slice()), + size: blob.size, + deleted_at: DateTime::from_timestamp(blob.deleted_at as i64) + .to_rfc3339(), + expires_at: DateTime::from_timestamp(blob.expires_at as i64) + .to_rfc3339(), + collection: Collection::from(blob.collection).to_string(), + }); + if results.len() == limit { + break; } - Err(err) => err.into_http_response(), - }, - Ok(None) => RequestError::not_found().into_http_response(), - Err(err) => err.into_http_response(), + } else { + offset -= 1; + } } + + Ok(JsonResponse::new(json!({ + "data":{ + "items": results, + "total": total, + }, + })) + .into_http_response()) } (Some(account_name), &Method::POST) => { - match self.core.storage.data.get_account_id(account_name).await { - Ok(Some(account_id)) => { - match serde_json::from_slice::<Vec<UndeleteRequest<String, String, String>>>( - body.as_deref().unwrap_or_default(), - ) - .ok() - .and_then(|request| { - request.into_iter().map(|request| { + let account_id = self + .core + .storage + .data + .get_account_id(account_name) + .await? + .ok_or_else(|| trc::ResourceCause::NotFound.into_err())?; + + let requests = + serde_json::from_slice::<Vec<UndeleteRequest<String, String, String>>>( + body.as_deref().unwrap_or_default(), + ) + .ok() + .and_then(|request| { + request + .into_iter() + .map(|request| { UndeleteRequest { hash: BlobHash::try_from_hash_slice( URL_SAFE_NO_PAD @@ -139,99 +144,100 @@ impl JMAP { .as_slice(), ) .ok()?, - collection: Collection::from_str( - request.collection.as_str(), - ) - .ok()?, + collection: Collection::from_str(request.collection.as_str()) + .ok()?, time: DateTime::parse_rfc3339(request.time.as_str())? .to_timestamp(), - cancel_deletion: if let Some(cancel_deletion) = request.cancel_deletion { + cancel_deletion: if let Some(cancel_deletion) = + request.cancel_deletion + { DateTime::parse_rfc3339(cancel_deletion.as_str())? - .to_timestamp().into() + .to_timestamp() + .into() } else { None - } + }, } .into() - }).collect::<Option<Vec<_>>>() - }) { - Some(requests) => { - let mut results = Vec::with_capacity(requests.len()); - let mut batch = BatchBuilder::new(); - batch.with_account_id(account_id); - for request in requests { - match request.collection { - Collection::Email => { - match self.get_blob(&request.hash, 0..usize::MAX).await - { - Ok(Some(bytes)) => { - match self - .email_ingest(IngestEmail { - raw_message: &bytes, - message: MessageParser::new().parse(&bytes), - account_id, - account_quota: 0, - mailbox_ids: vec![INBOX_ID], - keywords: vec![], - received_at: (request.time as u64).into(), - source: IngestSource::Smtp, - encrypt: false, - }) - .await - { - Ok(_) => { - results.push(UndeleteResponse::Success); - if let Some(cancel_deletion) = request.cancel_deletion { - batch.clear(ValueClass::Blob(BlobOp::Reserve { hash: request.hash, until: cancel_deletion as u64 })); - } - }, - Err(mut err) if err.matches(trc::Cause::Ingest) => { - results.push(UndeleteResponse::Error { reason: err.take_value(trc::Key::Reason) - .and_then(|v| v.into_string()) - .unwrap().into_owned() }); - } - Err(_) => { - return RequestError::internal_server_error().into_http_response(); - }, - } - } - Ok(None) => { - results.push(UndeleteResponse::NotFound); - }, - Err(_) => { - return RequestError::internal_server_error().into_http_response(); - }, + }) + .collect::<Option<Vec<_>>>() + }) + .ok_or_else(|| trc::ResourceCause::BadParameters.into_err())?; + + let mut results = Vec::with_capacity(requests.len()); + let mut batch = BatchBuilder::new(); + batch.with_account_id(account_id); + for request in requests { + match request.collection { + Collection::Email => { + match self.get_blob(&request.hash, 0..usize::MAX).await? { + Some(bytes) => { + match self + .email_ingest(IngestEmail { + raw_message: &bytes, + message: MessageParser::new().parse(&bytes), + account_id, + account_quota: 0, + mailbox_ids: vec![INBOX_ID], + keywords: vec![], + received_at: (request.time as u64).into(), + source: IngestSource::Smtp, + encrypt: false, + }) + .await + { + Ok(_) => { + results.push(UndeleteResponse::Success); + if let Some(cancel_deletion) = request.cancel_deletion { + batch.clear(ValueClass::Blob(BlobOp::Reserve { + hash: request.hash, + until: cancel_deletion as u64, + })); } } - _ => { + Err(mut err) if err.matches(trc::Cause::Ingest) => { results.push(UndeleteResponse::Error { - reason: "Unsupported collection".to_string(), + reason: err + .take_value(trc::Key::Reason) + .and_then(|v| v.into_string()) + .unwrap() + .into_owned(), }); } + Err(err) => { + return Err(err.caused_by(trc::location!())); + } } } - - // Commit batch - if !batch.is_empty() { - match self.core.storage.data.write(batch.build()).await { - Ok(_) => (), - Err(err) => return err.into_http_response(), - } + None => { + results.push(UndeleteResponse::NotFound); } - - JsonResponse::new(json!({ - "data": results, - })) - .into_http_response() - }, - None => RequestError::invalid_parameters().into_http_response(), + } + } + _ => { + results.push(UndeleteResponse::Error { + reason: "Unsupported collection".to_string(), + }); } } - Ok(None) => RequestError::not_found().into_http_response(), - Err(err) => err.into_http_response(), } + + // Commit batch + if !batch.is_empty() { + self.core + .storage + .data + .write(batch.build()) + .await + .caused_by(trc::location!())?; + } + + Ok(JsonResponse::new(json!({ + "data": results, + })) + .into_http_response()) } - _ => RequestError::not_found().into_http_response(), + _ => Err(trc::ResourceCause::NotFound.into_err()), } } } diff --git a/crates/jmap/src/api/management/log.rs b/crates/jmap/src/api/management/log.rs index 0e029bc0..557b8cd8 100644 --- a/crates/jmap/src/api/management/log.rs +++ b/crates/jmap/src/api/management/log.rs @@ -5,6 +5,7 @@ use std::{ }; use chrono::DateTime; +use directory::backend::internal::manage; use rev_lines::RevLines; use serde::Serialize; use serde_json::json; @@ -16,8 +17,6 @@ use crate::{ JMAP, }; -use super::ManagementApiError; - #[derive(Serialize)] struct LogEntry { timestamp: String, @@ -26,18 +25,15 @@ struct LogEntry { } impl JMAP { - pub async fn handle_view_logs(&self, req: &HttpRequest) -> HttpResponse { + pub async fn handle_view_logs(&self, req: &HttpRequest) -> trc::Result<HttpResponse> { // Obtain log file path - let path = match self.core.storage.config.get("tracer.log.path").await { - Ok(Some(path)) => path, - Ok(None) => { - return ManagementApiError::Unsupported { - details: "Tracer log path not configured".into(), - } - .into_http_response() - } - Err(err) => return err.into_http_response(), - }; + let path = self + .core + .storage + .config + .get("tracer.log.path") + .await? + .ok_or_else(|| manage::unsupported("Tracer log path not configured"))?; let params = UrlParams::new(req.uri().query()); let filter = params.get("filter").unwrap_or_default().to_string(); @@ -51,25 +47,23 @@ impl JMAP { let _ = tx.send(read_log_files(path, &filter, offset, limit)); }); - match rx.await { - Ok(result) => match result { - Ok((total, items)) => JsonResponse::new(json!({ - "data": { - "items": items, - "total": total, - }, - })) - .into_http_response(), - Err(err) => err.into_http_response(), + let (total, items) = rx + .await + .map_err(|err| trc::Cause::Thread.reason(err).caused_by(trc::location!()))? + .map_err(|err| { + trc::ManageCause::Error + .reason(err) + .details("Failed to read log files") + .caused_by(trc::location!()) + })?; + + Ok(JsonResponse::new(json!({ + "data": { + "items": items, + "total": total, }, - Err(_) => { - tracing::warn!(context = "view_logs", event = "error", "Thread join error"); - ManagementApiError::Other { - details: "Thread join error".into(), - } - .into_http_response() - } - } + })) + .into_http_response()) } } diff --git a/crates/jmap/src/api/management/mod.rs b/crates/jmap/src/api/management/mod.rs index 9297b2b4..a3e8d9fe 100644 --- a/crates/jmap/src/api/management/mod.rs +++ b/crates/jmap/src/api/management/mod.rs @@ -19,11 +19,11 @@ pub mod stores; use std::{borrow::Cow, sync::Arc}; +use directory::backend::internal::manage; use hyper::Method; -use jmap_proto::error::request::RequestError; use serde::Serialize; -use super::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}; +use super::{HttpRequest, HttpResponse}; use crate::{auth::AccessToken, JMAP}; #[derive(Serialize)] @@ -57,7 +57,7 @@ impl JMAP { req: &HttpRequest, body: Option<Vec<u8>>, access_token: Arc<AccessToken>, - ) -> HttpResponse { + ) -> trc::Result<HttpResponse> { let path = req.uri().path().split('/').skip(2).collect::<Vec<_>>(); let is_superuser = access_token.is_super_user(); @@ -76,10 +76,7 @@ impl JMAP { } "sieve" if is_superuser => self.handle_run_sieve(req, path, body).await, "restart" if is_superuser && req.method() == Method::GET => { - ManagementApiError::Unsupported { - details: "Restart is not yet supported".into(), - } - .into_http_response() + Err(manage::unsupported("Restart is not yet supported")) } "oauth" => self.handle_oauth_api_request(access_token, body).await, "account" => match (path.get(1).copied().unwrap_or_default(), req.method()) { @@ -89,7 +86,7 @@ impl JMAP { ("auth", &Method::POST) => { self.handle_account_auth_post(req, access_token, body).await } - _ => RequestError::not_found().into_http_response(), + _ => Err(trc::ResourceCause::NotFound.into_err()), }, // SPDX-SnippetBegin @@ -109,34 +106,13 @@ impl JMAP { if self.core.is_enterprise_edition() { self.handle_enterprise_api_request(req, path, body).await } else { - ManagementApiError::Unsupported { - details: "This feature is only available in the Enterprise version".into(), - } - .into_http_response() + Err(manage::unsupported( + "This feature is only available in the Enterprise version", + )) } } // SPDX-SnippetEnd - _ => RequestError::not_found().into_http_response(), - } - } -} - -impl ToHttpResponse for ManagementApiError { - fn into_http_response(self) -> super::HttpResponse { - JsonResponse::new(self).into_http_response() - } -} - -impl From<Cow<'static, str>> for ManagementApiError { - fn from(details: Cow<'static, str>) -> Self { - ManagementApiError::Other { details } - } -} - -impl From<String> for ManagementApiError { - fn from(details: String) -> Self { - ManagementApiError::Other { - details: details.into(), + _ => Err(trc::ResourceCause::NotFound.into_err()), } } } diff --git a/crates/jmap/src/api/management/principal.rs b/crates/jmap/src/api/management/principal.rs index 6da26b8e..e197c017 100644 --- a/crates/jmap/src/api/management/principal.rs +++ b/crates/jmap/src/api/management/principal.rs @@ -8,14 +8,14 @@ use std::sync::Arc; use directory::{ backend::internal::{ - lookup::DirectoryStore, manage::ManageDirectory, PrincipalAction, PrincipalField, - PrincipalUpdate, PrincipalValue, SpecialSecrets, + lookup::DirectoryStore, + manage::{self, ManageDirectory}, + PrincipalAction, PrincipalField, PrincipalUpdate, PrincipalValue, SpecialSecrets, }, DirectoryInner, Principal, QueryBy, Type, }; -use hyper::{header, Method, StatusCode}; -use jmap_proto::error::request::RequestError; +use hyper::{header, Method}; use serde_json::json; use utils::url_params::UrlParams; @@ -25,7 +25,7 @@ use crate::{ JMAP, }; -use super::{decode_path_element, ManagementApiError}; +use super::decode_path_element; #[derive(Debug, serde::Serialize, serde::Deserialize)] pub struct PrincipalResponse { @@ -80,47 +80,41 @@ impl JMAP { req: &HttpRequest, path: Vec<&str>, body: Option<Vec<u8>>, - ) -> HttpResponse { + ) -> trc::Result<HttpResponse> { match (path.get(1), req.method()) { (None, &Method::POST) => { // Make sure the current directory supports updates - if let Some(response) = self.assert_supported_directory() { - return response; - } + self.assert_supported_directory()?; // Create principal - match serde_json::from_slice::<PrincipalResponse>( + let principal = serde_json::from_slice::<PrincipalResponse>( body.as_deref().unwrap_or_default(), - ) { - Ok(principal) => { - match self - .core - .storage - .data - .create_account( - Principal { - id: principal.id, - typ: principal.typ, - quota: principal.quota, - name: principal.name, - secrets: principal.secrets, - emails: principal.emails, - member_of: principal.member_of, - description: principal.description, - }, - principal.members, - ) - .await - { - Ok(account_id) => JsonResponse::new(json!({ - "data": account_id, - })) - .into_http_response(), - Err(err) => into_directory_response(err), - } - } - Err(err) => err.into_http_response(), - } + ) + .map_err(|err| { + trc::Cause::Resource(trc::ResourceCause::BadParameters).from_json_error(err) + })?; + + Ok(JsonResponse::new(json!({ + "data": self + .core + .storage + .data + .create_account( + Principal { + id: principal.id, + typ: principal.typ, + quota: principal.quota, + name: principal.name, + secrets: principal.secrets, + emails: principal.emails, + member_of: principal.member_of, + description: principal.description, + }, + principal.members, + ) + .await?, + })) + .into_http_response()) } (None, &Method::GET) => { // List principal ids @@ -130,187 +124,137 @@ impl JMAP { let page: usize = params.parse("page").unwrap_or(0); let limit: usize = params.parse("limit").unwrap_or(0); - match self.core.storage.data.list_accounts(filter, typ).await { - Ok(accounts) => { - let (total, accounts) = if limit > 0 { - let offset = page.saturating_sub(1) * limit; - ( - accounts.len(), - accounts.into_iter().skip(offset).take(limit).collect(), - ) - } else { - (accounts.len(), accounts) - }; - - JsonResponse::new(json!({ - "data": { - "items": accounts, - "total": total, - }, - })) - .into_http_response() - } - Err(err) => into_directory_response(err), - } + let accounts = self.core.storage.data.list_accounts(filter, typ).await?; + let (total, accounts) = if limit > 0 { + let offset = page.saturating_sub(1) * limit; + ( + accounts.len(), + accounts.into_iter().skip(offset).take(limit).collect(), + ) + } else { + (accounts.len(), accounts) + }; + + Ok(JsonResponse::new(json!({ + "data": { + "items": accounts, + "total": total, + }, + })) + .into_http_response()) } (Some(name), method) => { // Fetch, update or delete principal let name = decode_path_element(name); - let account_id = match self.core.storage.data.get_account_id(name.as_ref()).await { - Ok(Some(account_id)) => account_id, - Ok(None) => { - return RequestError::blank( - StatusCode::NOT_FOUND.as_u16(), - "Not found", - "Account not found.", - ) - .into_http_response(); - } - Err(err) => { - return into_directory_response(err); - } - }; + let account_id = self + .core + .storage + .data + .get_account_id(name.as_ref()) + .await? + .ok_or_else(|| trc::ManageCause::NotFound.into_err())?; match *method { Method::GET => { - let result = match self + let principal = self .core .storage .data .query(QueryBy::Id(account_id), true) - .await - { - Ok(Some(principal)) => { - self.core.storage.data.map_group_ids(principal).await - } - Ok(None) => { - return RequestError::blank( - StatusCode::NOT_FOUND.as_u16(), - "Not found", - "Account not found.", - ) - .into_http_response() - } - Err(err) => Err(err), - }; - - match result { - Ok(principal) => { - // Obtain quota usage - let mut principal = PrincipalResponse::from(principal); - principal.used_quota = - self.get_used_quota(account_id).await.unwrap_or_default() - as u64; - - // Obtain member names - for member_id in self - .core - .storage - .data - .get_members(account_id) - .await - .unwrap_or_default() - { - if let Ok(Some(member_principal)) = self - .core - .storage - .data - .query(QueryBy::Id(member_id), false) - .await - { - principal.members.push(member_principal.name); - } - } - - JsonResponse::new(json!({ - "data": principal, - })) - .into_http_response() + .await? + .ok_or_else(|| trc::ManageCause::NotFound.into_err())?; + let principal = self.core.storage.data.map_group_ids(principal).await?; + + // Obtain quota usage + let mut principal = PrincipalResponse::from(principal); + principal.used_quota = self.get_used_quota(account_id).await? as u64; + + // Obtain member names + for member_id in self.core.storage.data.get_members(account_id).await? { + if let Some(member_principal) = self + .core + .storage + .data + .query(QueryBy::Id(member_id), false) + .await? + { + principal.members.push(member_principal.name); } - Err(err) => into_directory_response(err), } + + Ok(JsonResponse::new(json!({ + "data": principal, + })) + .into_http_response()) } Method::DELETE => { // Remove FTS index - if let Err(err) = self.core.storage.fts.remove_all(account_id).await { - return err.into_http_response(); - } + self.core.storage.fts.remove_all(account_id).await; // Delete account - match self - .core + self.core .storage .data .delete_account(QueryBy::Id(account_id)) - .await - { - Ok(_) => { - // Remove entries from cache - self.inner.sessions.retain(|_, id| id.item != account_id); - - JsonResponse::new(json!({ - "data": (), - })) - .into_http_response() - } - Err(err) => into_directory_response(err), - } + .await?; + // Remove entries from cache + self.inner.sessions.retain(|_, id| id.item != account_id); + + Ok(JsonResponse::new(json!({ + "data": (), + })) + .into_http_response()) } Method::PATCH => { - match serde_json::from_slice::<Vec<PrincipalUpdate>>( + let changes = serde_json::from_slice::<Vec<PrincipalUpdate>>( body.as_deref().unwrap_or_default(), - ) { - Ok(changes) => { - // Make sure the current directory supports updates - if let Some(response) = self.assert_supported_directory() { - if changes.iter().any(|change| { - !matches!( - change.field, - PrincipalField::Quota | PrincipalField::Description - ) - }) { - return response; - } - } - let is_password_change = changes - .iter() - .any(|change| matches!(change.field, PrincipalField::Secrets)); - - match self - .core - .storage - .data - .update_account(QueryBy::Id(account_id), changes) - .await - { - Ok(_) => { - if is_password_change { - // Remove entries from cache - self.inner - .sessions - .retain(|_, id| id.item != account_id); - } - - JsonResponse::new(json!({ - "data": (), - })) - .into_http_response() - } - Err(err) => into_directory_response(err), - } - } - Err(err) => err.into_http_response(), + ) + .map_err(|err| { + trc::Cause::Resource(trc::ResourceCause::BadParameters) + .from_json_error(err) + })?; + + // Make sure the current directory supports updates + if changes.iter().any(|change| { + !matches!( + change.field, + PrincipalField::Quota | PrincipalField::Description + ) + }) { + self.assert_supported_directory()?; + } + + let is_password_change = changes + .iter() + .any(|change| matches!(change.field, PrincipalField::Secrets)); + + self.core + .storage + .data + .update_account(QueryBy::Id(account_id), changes) + .await?; + if is_password_change { + // Remove entries from cache + self.inner.sessions.retain(|_, id| id.item != account_id); } + + Ok(JsonResponse::new(json!({ + "data": (), + })) + .into_http_response()) } - _ => RequestError::not_found().into_http_response(), + _ => Err(trc::ResourceCause::NotFound.into_err()), } } - _ => RequestError::not_found().into_http_response(), + _ => Err(trc::ResourceCause::NotFound.into_err()), } } - pub async fn handle_account_auth_get(&self, access_token: Arc<AccessToken>) -> HttpResponse { + pub async fn handle_account_auth_get( + &self, + access_token: Arc<AccessToken>, + ) -> trc::Result<HttpResponse> { let mut response = AccountAuthResponse { otp_auth: false, is_admin: access_token.is_super_user(), @@ -318,35 +262,29 @@ impl JMAP { }; if access_token.primary_id() != u32::MAX { - match self + let principal = self .core .storage .directory .query(QueryBy::Id(access_token.primary_id()), false) - .await - { - Ok(Some(principal)) => { - for secret in principal.secrets { - if secret.is_otp_auth() { - response.otp_auth = true; - } else if let Some((app_name, _)) = - secret.strip_prefix("$app$").and_then(|s| s.split_once('$')) - { - response.app_passwords.push(app_name.to_string()); - } - } + .await? + .ok_or_else(|| trc::ManageCause::NotFound.into_err())?; + + for secret in principal.secrets { + if secret.is_otp_auth() { + response.otp_auth = true; + } else if let Some((app_name, _)) = + secret.strip_prefix("$app$").and_then(|s| s.split_once('$')) + { + response.app_passwords.push(app_name.to_string()); } - Ok(None) => { - return RequestError::not_found().into_http_response(); - } - Err(err) => return into_directory_response(err), } } - JsonResponse::new(json!({ + Ok(JsonResponse::new(json!({ "data": response, })) - .into_http_response() + .into_http_response()) } pub async fn handle_account_auth_post( @@ -354,16 +292,18 @@ impl JMAP { req: &HttpRequest, access_token: Arc<AccessToken>, body: Option<Vec<u8>>, - ) -> HttpResponse { + ) -> trc::Result<HttpResponse> { // Parse request - let requests = match serde_json::from_slice::<Vec<AccountAuthRequest>>( - body.as_deref().unwrap_or_default(), - ) { - Ok(request) => request, - Err(err) => return err.into_http_response(), - }; + let requests = + serde_json::from_slice::<Vec<AccountAuthRequest>>(body.as_deref().unwrap_or_default()) + .map_err(|err| { + trc::Cause::Resource(trc::ResourceCause::BadParameters).from_json_error(err) + })?; + if requests.is_empty() { - return RequestError::invalid_parameters().into_http_response(); + return Err(trc::Cause::Resource(trc::ResourceCause::BadParameters) + .into_err() + .details("Empty request")); } // Make sure the user authenticated using Basic auth @@ -380,50 +320,41 @@ impl JMAP { .and_then(|h| h.to_str().ok()) .map_or(true, |header| !header.to_lowercase().starts_with("basic ")) { - return ManagementApiError::Other { - details: "Password changes only allowed using Basic auth".into(), - } - .into_http_response(); + return Err(manage::error( + "Password changes only allowed using Basic auth", + None::<u32>, + )); } // Handle Fallback admin password changes if access_token.is_super_user() && access_token.primary_id() == u32::MAX { match requests.into_iter().next().unwrap() { AccountAuthRequest::SetPassword { password } => { - return match self - .core + self.core .storage .config .set([("authentication.fallback-admin.secret", password)]) - .await - { - Ok(_) => { - // Remove entries from cache - self.inner.sessions.retain(|_, id| id.item != u32::MAX); + .await?; - JsonResponse::new(json!({ - "data": (), - })) - .into_http_response() - } - Err(err) => err.into_http_response(), - }; + // Remove entries from cache + self.inner.sessions.retain(|_, id| id.item != u32::MAX); + + return Ok(JsonResponse::new(json!({ + "data": (), + })) + .into_http_response()); } _ => { - return ManagementApiError::Other { - details: - "Fallback administrator accounts do not support 2FA or AppPasswords" - .into(), - } - .into_http_response() + return Err(manage::error( + "Fallback administrator accounts do not support 2FA or AppPasswords", + None::<u32>, + )); } } } // Make sure the current directory supports updates - if let Some(response) = self.assert_supported_directory() { - return response; - } + self.assert_supported_directory()?; // Build actions let mut actions = Vec::with_capacity(requests.len()); @@ -459,42 +390,38 @@ impl JMAP { } // Update password - match self - .core + self.core .storage .data .update_account(QueryBy::Id(access_token.primary_id()), actions) - .await - { - Ok(_) => { - // Remove entries from cache - self.inner - .sessions - .retain(|_, id| id.item != access_token.primary_id()); - - JsonResponse::new(json!({ - "data": (), - })) - .into_http_response() - } - Err(err) => into_directory_response(err), - } + .await?; + + // Remove entries from cache + self.inner + .sessions + .retain(|_, id| id.item != access_token.primary_id()); + + Ok(JsonResponse::new(json!({ + "data": (), + })) + .into_http_response()) } - pub fn assert_supported_directory(&self) -> Option<HttpResponse> { - ManagementApiError::UnsupportedDirectoryOperation { - class: match &self.core.storage.directory.store { - DirectoryInner::Internal(_) => return None, - DirectoryInner::Ldap(_) => "LDAP", - DirectoryInner::Sql(_) => "SQL", - DirectoryInner::Imap(_) => "IMAP", - DirectoryInner::Smtp(_) => "SMTP", - DirectoryInner::Memory(_) => "In-Memory", - } - .into(), - } - .into_http_response() - .into() + pub fn assert_supported_directory(&self) -> trc::Result<()> { + let todo = "update webadmin"; + + let class = match &self.core.storage.directory.store { + DirectoryInner::Internal(_) => return Ok(()), + DirectoryInner::Ldap(_) => "LDAP", + DirectoryInner::Sql(_) => "SQL", + DirectoryInner::Imap(_) => "IMAP", + DirectoryInner::Smtp(_) => "SMTP", + DirectoryInner::Memory(_) => "In-Memory", + }; + + Err(manage::unsupported(format!( + "Requested action is unsupported for {class} directories.", + ))) } } @@ -515,7 +442,8 @@ impl From<Principal<String>> for PrincipalResponse { } } -fn into_directory_response(mut error: trc::Error) -> HttpResponse { +/* +fn into_directory_response(mut error: trc::Error) -> trc::Result<HttpResponse> { let response = match error.as_ref() { trc::Cause::MissingParameter => ManagementApiError::FieldMissing { field: error @@ -533,7 +461,7 @@ fn into_directory_response(mut error: trc::Error) -> HttpResponse { .and_then(|v| v.into_string()) .unwrap_or_default(), }, - trc::Cause::NotFound => ManagementApiError::NotFound { + trc::StoreCause::NotFound.into_err() => ManagementApiError::NotFound { item: error .take_value(trc::Key::Key) .and_then(|v| v.into_string()) @@ -559,3 +487,4 @@ fn into_directory_response(mut error: trc::Error) -> HttpResponse { JsonResponse::new(response).into_http_response() } +*/ diff --git a/crates/jmap/src/api/management/queue.rs b/crates/jmap/src/api/management/queue.rs index d5db8b76..923a20e2 100644 --- a/crates/jmap/src/api/management/queue.rs +++ b/crates/jmap/src/api/management/queue.rs @@ -8,7 +8,6 @@ use std::str::FromStr; use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; use hyper::Method; -use jmap_proto::error::request::RequestError; use mail_auth::{ dmarc::URI, mta_sts::ReportUri, @@ -104,7 +103,11 @@ pub enum Report { } impl JMAP { - pub async fn handle_manage_queue(&self, req: &HttpRequest, path: Vec<&str>) -> HttpResponse { + pub async fn handle_manage_queue( + &self, + req: &HttpRequest, + path: Vec<&str>, + ) -> trc::Result<HttpResponse> { let params = UrlParams::new(req.uri().query()); match ( @@ -138,8 +141,7 @@ impl JMAP { let mut offset = page.saturating_sub(1) * limit; let mut total = 0; let mut total_returned = 0; - let _ = self - .core + self.core .storage .data .iterate( @@ -193,9 +195,9 @@ impl JMAP { Ok(max_total == 0 || total < max_total) }, ) - .await; + .await?; - if values { + Ok(if values { JsonResponse::new(json!({ "data":{ "items": result_values, @@ -210,7 +212,7 @@ impl JMAP { }, })) } - .into_http_response() + .into_http_response()) } ("messages", Some(queue_id), &Method::GET) => { if let Some(message) = self @@ -218,12 +220,12 @@ impl JMAP { .read_message(queue_id.parse().unwrap_or_default()) .await { - JsonResponse::new(json!({ + Ok(JsonResponse::new(json!({ "data": Message::from(&message), })) - .into_http_response() + .into_http_response()) } else { - RequestError::not_found().into_http_response() + Err(trc::ResourceCause::NotFound.into_err()) } } ("messages", Some(queue_id), &Method::PATCH) => { @@ -265,12 +267,12 @@ impl JMAP { let _ = self.smtp.inner.queue_tx.send(queue::Event::Reload).await; } - JsonResponse::new(json!({ + Ok(JsonResponse::new(json!({ "data": found, })) - .into_http_response() + .into_http_response()) } else { - RequestError::not_found().into_http_response() + Err(trc::ResourceCause::NotFound.into_err()) } } ("messages", Some(queue_id), &Method::DELETE) => { @@ -345,12 +347,12 @@ impl JMAP { found = true; } - JsonResponse::new(json!({ + Ok(JsonResponse::new(json!({ "data": found, })) - .into_http_response() + .into_http_response()) } else { - RequestError::not_found().into_http_response() + Err(trc::ResourceCause::NotFound.into_err()) } } ("reports", None, &Method::GET) => { @@ -387,8 +389,7 @@ impl JMAP { let mut offset = page.saturating_sub(1) * limit; let mut total = 0; let mut total_returned = 0; - let _ = self - .core + self.core .storage .data .iterate( @@ -422,15 +423,15 @@ impl JMAP { Ok(max_total == 0 || total < max_total) }, ) - .await; + .await?; - JsonResponse::new(json!({ + Ok(JsonResponse::new(json!({ "data": { "items": result, "total": total, }, })) - .into_http_response() + .into_http_response()) } ("reports", Some(report_id), &Method::GET) => { let mut result = None; @@ -438,20 +439,20 @@ impl JMAP { match report_id { QueueClass::DmarcReportHeader(event) => { let mut rua = Vec::new(); - if let Ok(Some(report)) = self + if let Some(report) = self .smtp .generate_dmarc_aggregate_report(&event, &mut rua, None) - .await + .await? { result = Report::dmarc(event, report, rua).into(); } } QueueClass::TlsReportHeader(event) => { let mut rua = Vec::new(); - if let Ok(Some(report)) = self + if let Some(report) = self .smtp .generate_tls_aggregate_report(&[event.clone()], &mut rua, None) - .await + .await? { result = Report::tls(event, report, rua).into(); } @@ -461,12 +462,12 @@ impl JMAP { } if let Some(result) = result { - JsonResponse::new(json!({ + Ok(JsonResponse::new(json!({ "data": result, })) - .into_http_response() + .into_http_response()) } else { - RequestError::not_found().into_http_response() + Err(trc::ResourceCause::NotFound.into_err()) } } ("reports", Some(report_id), &Method::DELETE) => { @@ -481,15 +482,15 @@ impl JMAP { _ => (), } - JsonResponse::new(json!({ + Ok(JsonResponse::new(json!({ "data": true, })) - .into_http_response() + .into_http_response()) } else { - RequestError::not_found().into_http_response() + Err(trc::ResourceCause::NotFound.into_err()) } } - _ => RequestError::not_found().into_http_response(), + _ => Err(trc::ResourceCause::NotFound.into_err()), } } } diff --git a/crates/jmap/src/api/management/reload.rs b/crates/jmap/src/api/management/reload.rs index 52721e10..bca1d9ec 100644 --- a/crates/jmap/src/api/management/reload.rs +++ b/crates/jmap/src/api/management/reload.rs @@ -5,7 +5,6 @@ */ use hyper::Method; -use jmap_proto::error::request::RequestError; use serde_json::json; use utils::url_params::UrlParams; @@ -16,107 +15,88 @@ use crate::{ }; impl JMAP { - pub async fn handle_manage_reload(&self, req: &HttpRequest, path: Vec<&str>) -> HttpResponse { + pub async fn handle_manage_reload( + &self, + req: &HttpRequest, + path: Vec<&str>, + ) -> trc::Result<HttpResponse> { match (path.get(1).copied(), req.method()) { (Some("lookup"), &Method::GET) => { - match self.core.reload_lookups().await { - Ok(result) => { - // Update core - if let Some(core) = result.new_core { - self.shared_core.store(core.into()); - } - - JsonResponse::new(json!({ - "data": result.config, - })) - .into_http_response() - } - Err(err) => err.into_http_response(), + let result = self.core.reload_lookups().await?; + // Update core + if let Some(core) = result.new_core { + self.shared_core.store(core.into()); } - } - (Some("certificate"), &Method::GET) => match self.core.reload_certificates().await { - Ok(result) => JsonResponse::new(json!({ + + Ok(JsonResponse::new(json!({ "data": result.config, })) - .into_http_response(), - Err(err) => err.into_http_response(), - }, + .into_http_response()) + } + (Some("certificate"), &Method::GET) => Ok(JsonResponse::new(json!({ + "data": self.core.reload_certificates().await?.config, + })) + .into_http_response()), (Some("server.blocked-ip"), &Method::GET) => { - match self.core.reload_blocked_ips().await { - Ok(result) => { - // Increment version counter - self.core.network.blocked_ips.increment_version(); + let result = self.core.reload_blocked_ips().await?; + // Increment version counter + self.core.network.blocked_ips.increment_version(); - JsonResponse::new(json!({ - "data": result.config, - })) - .into_http_response() - } - Err(err) => err.into_http_response(), - } + Ok(JsonResponse::new(json!({ + "data": result.config, + })) + .into_http_response()) } (_, &Method::GET) => { - match self.core.reload().await { - Ok(result) => { - if !UrlParams::new(req.uri().query()).has_key("dry-run") { - if let Some(core) = result.new_core { - // Update core - self.shared_core.store(core.into()); - - // Increment version counter - self.inner.increment_config_version(); - } + let result = self.core.reload().await?; + if !UrlParams::new(req.uri().query()).has_key("dry-run") { + if let Some(core) = result.new_core { + // Update core + self.shared_core.store(core.into()); - // Reload ACME - if let Err(err) = - self.inner.housekeeper_tx.send(Event::AcmeReload).await - { - tracing::warn!( - "Failed to send ACME reload event to housekeeper: {}", - err - ); - } - } + // Increment version counter + self.inner.increment_config_version(); + } - JsonResponse::new(json!({ - "data": result.config, - })) - .into_http_response() + // Reload ACME + if let Err(err) = self.inner.housekeeper_tx.send(Event::AcmeReload).await { + tracing::warn!("Failed to send ACME reload event to housekeeper: {}", err); } - Err(err) => err.into_http_response(), } + + Ok(JsonResponse::new(json!({ + "data": result.config, + })) + .into_http_response()) } - _ => RequestError::not_found().into_http_response(), + _ => Err(trc::ResourceCause::NotFound.into_err()), } } - pub async fn handle_manage_update(&self, req: &HttpRequest, path: Vec<&str>) -> HttpResponse { + pub async fn handle_manage_update( + &self, + req: &HttpRequest, + path: Vec<&str>, + ) -> trc::Result<HttpResponse> { match (path.get(1).copied(), req.method()) { - (Some("spam-filter"), &Method::GET) => { - match self - .core - .storage - .config - .update_config_resource("spam-filter") - .await - { - Ok(result) => JsonResponse::new(json!({ - "data": result, - })) - .into_http_response(), - Err(err) => err.into_http_response(), - } - } + (Some("spam-filter"), &Method::GET) => Ok(JsonResponse::new(json!({ + "data": self + .core + .storage + .config + .update_config_resource("spam-filter") + .await?, + })) + .into_http_response()), (Some("webadmin"), &Method::GET) => { - match self.inner.webadmin.update_and_unpack(&self.core).await { - Ok(_) => JsonResponse::new(json!({ - "data": (), - })) - .into_http_response(), - Err(err) => err.into_http_response(), - } + self.inner.webadmin.update_and_unpack(&self.core).await?; + + Ok(JsonResponse::new(json!({ + "data": (), + })) + .into_http_response()) } - _ => RequestError::not_found().into_http_response(), + _ => Err(trc::ResourceCause::NotFound.into_err()), } } } diff --git a/crates/jmap/src/api/management/report.rs b/crates/jmap/src/api/management/report.rs index 1bed1e1b..a07f62eb 100644 --- a/crates/jmap/src/api/management/report.rs +++ b/crates/jmap/src/api/management/report.rs @@ -5,7 +5,6 @@ */ use hyper::Method; -use jmap_proto::error::request::RequestError; use mail_auth::report::{ tlsrpt::{FailureDetails, Policy, TlsReport}, Feedback, @@ -32,7 +31,11 @@ enum ReportType { } impl JMAP { - pub async fn handle_manage_reports(&self, req: &HttpRequest, path: Vec<&str>) -> HttpResponse { + pub async fn handle_manage_reports( + &self, + req: &HttpRequest, + path: Vec<&str>, + ) -> trc::Result<HttpResponse> { match ( path.get(1).copied().unwrap_or_default(), path.get(2).copied().map(decode_path_element), @@ -89,8 +92,7 @@ impl JMAP { let mut offset = page.saturating_sub(1) * limit; let mut total = 0; let mut last_id = 0; - let result = self - .core + self.core .storage .data .iterate( @@ -141,17 +143,15 @@ impl JMAP { Ok(max_total == 0 || total < max_total) }, ) - .await; - match result { - Ok(_) => JsonResponse::new(json!({ - "data": { - "items": results, - "total": total, - }, - })) - .into_http_response(), - Err(err) => err.into_http_response(), - } + .await?; + + Ok(JsonResponse::new(json!({ + "data": { + "items": results, + "total": total, + }, + })) + .into_http_response()) } (class @ ("dmarc" | "tls" | "arf"), Some(report_id), &Method::GET) => { if let Some(report_id) = parse_incoming_report_id(class, report_id.as_ref()) { @@ -163,14 +163,13 @@ impl JMAP { .get_value::<Bincode<IncomingReport<TlsReport>>>(ValueKey::from( ValueClass::Report(report_id), )) - .await + .await? { - Ok(Some(report)) => JsonResponse::new(json!({ + Some(report) => Ok(JsonResponse::new(json!({ "data": report.inner, })) - .into_http_response(), - Ok(None) => RequestError::not_found().into_http_response(), - Err(err) => err.into_http_response(), + .into_http_response()), + None => Err(trc::ResourceCause::NotFound.into_err()), }, ReportClass::Dmarc { .. } => match self .core @@ -179,14 +178,13 @@ impl JMAP { .get_value::<Bincode<IncomingReport<mail_auth::report::Report>>>( ValueKey::from(ValueClass::Report(report_id)), ) - .await + .await? { - Ok(Some(report)) => JsonResponse::new(json!({ + Some(report) => Ok(JsonResponse::new(json!({ "data": report.inner, })) - .into_http_response(), - Ok(None) => RequestError::not_found().into_http_response(), - Err(err) => err.into_http_response(), + .into_http_response()), + None => Err(trc::ResourceCause::NotFound.into_err()), }, ReportClass::Arf { .. } => match self .core @@ -195,35 +193,34 @@ impl JMAP { .get_value::<Bincode<IncomingReport<Feedback>>>(ValueKey::from( ValueClass::Report(report_id), )) - .await + .await? { - Ok(Some(report)) => JsonResponse::new(json!({ + Some(report) => Ok(JsonResponse::new(json!({ "data": report.inner, })) - .into_http_response(), - Ok(None) => RequestError::not_found().into_http_response(), - Err(err) => err.into_http_response(), + .into_http_response()), + None => Err(trc::ResourceCause::NotFound.into_err()), }, } } else { - RequestError::not_found().into_http_response() + Err(trc::ResourceCause::NotFound.into_err()) } } (class @ ("dmarc" | "tls" | "arf"), Some(report_id), &Method::DELETE) => { if let Some(report_id) = parse_incoming_report_id(class, report_id.as_ref()) { let mut batch = BatchBuilder::new(); batch.clear(ValueClass::Report(report_id)); - let result = self.core.storage.data.write(batch.build()).await.is_ok(); + self.core.storage.data.write(batch.build()).await?; - JsonResponse::new(json!({ - "data": result, + Ok(JsonResponse::new(json!({ + "data": true, })) - .into_http_response() + .into_http_response()) } else { - RequestError::not_found().into_http_response() + Err(trc::ResourceCause::NotFound.into_err()) } } - _ => RequestError::not_found().into_http_response(), + _ => Err(trc::ResourceCause::NotFound.into_err()), } } } diff --git a/crates/jmap/src/api/management/settings.rs b/crates/jmap/src/api/management/settings.rs index 931ff52e..86fd0f0c 100644 --- a/crates/jmap/src/api/management/settings.rs +++ b/crates/jmap/src/api/management/settings.rs @@ -5,7 +5,6 @@ */ use hyper::Method; -use jmap_proto::error::request::RequestError; use serde_json::json; use store::ahash::AHashMap; use utils::{config::ConfigKey, url_params::UrlParams}; @@ -15,7 +14,7 @@ use crate::{ JMAP, }; -use super::{decode_path_element, ManagementApiError}; +use super::decode_path_element; #[derive(Debug, serde::Serialize, serde::Deserialize)] #[serde(tag = "type")] @@ -39,7 +38,7 @@ impl JMAP { req: &HttpRequest, path: Vec<&str>, body: Option<Vec<u8>>, - ) -> HttpResponse { + ) -> trc::Result<HttpResponse> { match (path.get(1).copied(), req.method()) { (Some("group"), &Method::GET) => { // List settings @@ -71,103 +70,101 @@ impl JMAP { params.parse::<usize>("page").unwrap_or(0).saturating_sub(1) * limit; let has_filter = !filter.is_empty(); - match self.core.storage.config.list(&prefix, true).await { - Ok(settings) => if !suffix.is_empty() && !settings.is_empty() { - // Obtain record ids - let mut total = 0; - let mut ids = Vec::new(); - for (key, _) in &settings { - if let Some(id) = key.strip_suffix(&suffix) { - if !id.is_empty() { - if !has_filter { - if offset == 0 { - if limit == 0 || ids.len() < limit { - ids.push(id); - } - } else { - offset -= 1; + let settings = self.core.storage.config.list(&prefix, true).await?; + if !suffix.is_empty() && !settings.is_empty() { + // Obtain record ids + let mut total = 0; + let mut ids = Vec::new(); + for (key, _) in &settings { + if let Some(id) = key.strip_suffix(&suffix) { + if !id.is_empty() { + if !has_filter { + if offset == 0 { + if limit == 0 || ids.len() < limit { + ids.push(id); } - total += 1; } else { - ids.push(id); + offset -= 1; } + total += 1; + } else { + ids.push(id); } } } + } - // Group settings by record id - let mut records = Vec::new(); - for id in ids { - let mut record = AHashMap::new(); - let prefix = format!("{id}."); - record.insert("_id".to_string(), id.to_string()); - for (k, v) in &settings { - if let Some(k) = k.strip_prefix(&prefix) { - if field.map_or(true, |field| field == k) { - record.insert(k.to_string(), v.to_string()); - } - } else if record.len() > 1 { - break; + // Group settings by record id + let mut records = Vec::new(); + for id in ids { + let mut record = AHashMap::new(); + let prefix = format!("{id}."); + record.insert("_id".to_string(), id.to_string()); + for (k, v) in &settings { + if let Some(k) = k.strip_prefix(&prefix) { + if field.map_or(true, |field| field == k) { + record.insert(k.to_string(), v.to_string()); } + } else if record.len() > 1 { + break; } + } - if has_filter { - if record - .iter() - .any(|(_, v)| v.to_lowercase().contains(&filter)) - { - if offset == 0 { - if limit == 0 || records.len() < limit { - records.push(record); - } - } else { - offset -= 1; + if has_filter { + if record + .iter() + .any(|(_, v)| v.to_lowercase().contains(&filter)) + { + if offset == 0 { + if limit == 0 || records.len() < limit { + records.push(record); } - total += 1; + } else { + offset -= 1; } - } else { - records.push(record); + total += 1; } + } else { + records.push(record); } + } - JsonResponse::new(json!({ - "data": { - "total": total, - "items": records, - }, - })) - } else { - let total = settings.len(); - let items = settings - .into_iter() - .filter_map(|(k, v)| { - if filter.is_empty() - || k.to_lowercase().contains(&filter) - || v.to_lowercase().contains(&filter) - { - let k = - k.strip_prefix(&prefix).map(|k| k.to_string()).unwrap_or(k); - Some(json!({ - "_id": k, - "_value": v, - })) - } else { - None - } - }) - .skip(offset) - .take(if limit == 0 { total } else { limit }) - .collect::<Vec<_>>(); + Ok(JsonResponse::new(json!({ + "data": { + "total": total, + "items": records, + }, + })) + .into_http_response()) + } else { + let total = settings.len(); + let items = settings + .into_iter() + .filter_map(|(k, v)| { + if filter.is_empty() + || k.to_lowercase().contains(&filter) + || v.to_lowercase().contains(&filter) + { + let k = k.strip_prefix(&prefix).map(|k| k.to_string()).unwrap_or(k); + Some(json!({ + "_id": k, + "_value": v, + })) + } else { + None + } + }) + .skip(offset) + .take(if limit == 0 { total } else { limit }) + .collect::<Vec<_>>(); - JsonResponse::new(json!({ - "data": { - "total": total, - "items": items, - }, - })) - } - .into_http_response(), - Err(err) => err.into_http_response(), + Ok(JsonResponse::new(json!({ + "data": { + "total": total, + "items": items, + }, + })) + .into_http_response()) } } (Some("list"), &Method::GET) => { @@ -186,25 +183,21 @@ impl JMAP { let limit: usize = params.parse("limit").unwrap_or(0); let offset = params.parse::<usize>("page").unwrap_or(0).saturating_sub(1) * limit; - match self.core.storage.config.list(&prefix, true).await { - Ok(settings) => { - let total = settings.len(); - let items = settings - .into_iter() - .skip(offset) - .take(if limit == 0 { total } else { limit }) - .collect::<AHashMap<_, _>>(); + let settings = self.core.storage.config.list(&prefix, true).await?; + let total = settings.len(); + let items = settings + .into_iter() + .skip(offset) + .take(if limit == 0 { total } else { limit }) + .collect::<AHashMap<_, _>>(); - JsonResponse::new(json!({ - "data": { - "total": total, - "items": items, - }, - })) - .into_http_response() - } - Err(err) => err.into_http_response(), - } + Ok(JsonResponse::new(json!({ + "data": { + "total": total, + "items": items, + }, + })) + .into_http_response()) } (Some("keys"), &Method::GET) => { // Obtain keys @@ -217,19 +210,11 @@ impl JMAP { .get("prefixes") .map(|s| s.split(',').collect::<Vec<_>>()) .unwrap_or_default(); - let mut err = None; let mut results = AHashMap::with_capacity(keys.len()); for key in keys { - match self.core.storage.config.get(key).await { - Ok(Some(value)) => { - results.insert(key.to_string(), value); - } - Ok(None) => {} - Err(err_) => { - err = err_.into(); - break; - } + if let Some(value) = self.core.storage.config.get(key).await? { + results.insert(key.to_string(), value); } } for prefix in prefixes { @@ -238,134 +223,88 @@ impl JMAP { } else { prefix.to_string() }; - match self.core.storage.config.list(&prefix, false).await { - Ok(values) => { - results.extend(values); - } - Err(err_) => { - err = err_.into(); - break; - } - } + results.extend(self.core.storage.config.list(&prefix, false).await?); } - match err { - None => JsonResponse::new(json!({ - "data": results, - })) - .into_http_response(), - Some(err) => err.into_http_response(), - } + Ok(JsonResponse::new(json!({ + "data": results, + })) + .into_http_response()) } (Some(prefix), &Method::DELETE) if !prefix.is_empty() => { let prefix = decode_path_element(prefix); - match self.core.storage.config.clear(prefix.as_ref()).await { - Ok(_) => JsonResponse::new(json!({ - "data": (), - })) - .into_http_response(), - Err(err) => err.into_http_response(), - } + self.core.storage.config.clear(prefix.as_ref()).await?; + + Ok(JsonResponse::new(json!({ + "data": (), + })) + .into_http_response()) } (None, &Method::POST) => { - match serde_json::from_slice::<Vec<UpdateSettings>>( + let changes = serde_json::from_slice::<Vec<UpdateSettings>>( body.as_deref().unwrap_or_default(), - ) { - Ok(changes) => { - let mut result = Ok(true); + ) + .map_err(|err| { + trc::Cause::Resource(trc::ResourceCause::BadParameters).from_json_error(err) + })?; - 'next: for change in changes { - match change { - UpdateSettings::Delete { keys } => { - for key in keys { - result = - self.core.storage.config.clear(key).await.map(|_| true); - if result.is_err() { - break 'next; - } - } - } - UpdateSettings::Clear { prefix } => { - result = self + for change in changes { + match change { + UpdateSettings::Delete { keys } => { + for key in keys { + self.core.storage.config.clear(key).await?; + } + } + UpdateSettings::Clear { prefix } => { + self.core.storage.config.clear_prefix(&prefix).await?; + } + UpdateSettings::Insert { + prefix, + values, + assert_empty, + } => { + if assert_empty { + if let Some(prefix) = &prefix { + if !self .core .storage .config - .clear_prefix(&prefix) - .await - .map(|_| true); - if result.is_err() { - break; + .list(&format!("{prefix}."), true) + .await? + .is_empty() + { + return Err(trc::ManageCause::AssertFailed.into_err()); } - } - UpdateSettings::Insert { - prefix, - values, - assert_empty, - } => { - if assert_empty { - if let Some(prefix) = &prefix { - result = self - .core - .storage - .config - .list(&format!("{prefix}."), true) - .await - .map(|items| items.is_empty()); - - if matches!(result, Ok(false) | Err(_)) { - break; - } - } else if let Some((key, _)) = values.first() { - result = self - .core - .storage - .config - .get(key) - .await - .map(|items| items.is_none()); - - if matches!(result, Ok(false) | Err(_)) { - break; - } - } - } - - result = self - .core - .storage - .config - .set(values.into_iter().map(|(key, value)| ConfigKey { - key: if let Some(prefix) = &prefix { - format!("{prefix}.{key}") - } else { - key - }, - value, - })) - .await - .map(|_| true); - if result.is_err() { - break; + } else if let Some((key, _)) = values.first() { + if self.core.storage.config.get(key).await?.is_some() { + return Err(trc::ManageCause::AssertFailed.into_err()); } } } - } - match result { - Ok(true) => JsonResponse::new(json!({ - "data": (), - })) - .into_http_response(), - Ok(false) => JsonResponse::new(ManagementApiError::AssertFailed) - .into_http_response(), - Err(err) => err.into_http_response(), + self.core + .storage + .config + .set(values.into_iter().map(|(key, value)| ConfigKey { + key: if let Some(prefix) = &prefix { + format!("{prefix}.{key}") + } else { + key + }, + value, + })) + .await?; } } - Err(err) => err.into_http_response(), } + + Ok(JsonResponse::new(json!({ + "data": (), + })) + .into_http_response()) } - _ => RequestError::not_found().into_http_response(), + _ => Err(trc::ResourceCause::NotFound.into_err()), } } } diff --git a/crates/jmap/src/api/management/sieve.rs b/crates/jmap/src/api/management/sieve.rs index fbcc25d2..33bb3f03 100644 --- a/crates/jmap/src/api/management/sieve.rs +++ b/crates/jmap/src/api/management/sieve.rs @@ -8,7 +8,6 @@ use std::time::SystemTime; use common::{scripts::ScriptModification, IntoString}; use hyper::Method; -use jmap_proto::error::request::RequestError; use serde_json::json; use sieve::{runtime::Variable, Envelope}; use smtp::scripts::{ScriptParameters, ScriptResult}; @@ -42,7 +41,7 @@ impl JMAP { req: &HttpRequest, path: Vec<&str>, body: Option<Vec<u8>>, - ) -> HttpResponse { + ) -> trc::Result<HttpResponse> { let script = match ( path.get(1) .and_then(|name| self.core.sieve.scripts.get(*name)) @@ -51,7 +50,7 @@ impl JMAP { ) { (Some(script), &Method::POST) => script, _ => { - return RequestError::not_found().into_http_response(); + return Err(trc::ResourceCause::NotFound.into_err()); } }; @@ -112,9 +111,9 @@ impl JMAP { ScriptResult::Discard => Response::Discard, }; - JsonResponse::new(json!({ + Ok(JsonResponse::new(json!({ "data": result, })) - .into_http_response() + .into_http_response()) } } diff --git a/crates/jmap/src/api/management/stores.rs b/crates/jmap/src/api/management/stores.rs index c31ac884..3c82c127 100644 --- a/crates/jmap/src/api/management/stores.rs +++ b/crates/jmap/src/api/management/stores.rs @@ -8,7 +8,6 @@ use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; use common::manager::webadmin::Resource; use directory::backend::internal::manage::ManageDirectory; use hyper::Method; -use jmap_proto::error::request::RequestError; use serde_json::json; use utils::url_params::UrlParams; @@ -21,7 +20,11 @@ use crate::{ use super::decode_path_element; impl JMAP { - pub async fn handle_manage_store(&self, req: &HttpRequest, path: Vec<&str>) -> HttpResponse { + pub async fn handle_manage_store( + &self, + req: &HttpRequest, + path: Vec<&str>, + ) -> trc::Result<HttpResponse> { match ( path.get(1).copied(), path.get(2).copied(), @@ -29,41 +32,36 @@ impl JMAP { req.method(), ) { (Some("blobs"), Some(blob_hash), _, &Method::GET) => { - match URL_SAFE_NO_PAD.decode(decode_path_element(blob_hash).as_bytes()) { - Ok(blob_hash) => { - match self - .core - .storage - .blob - .get_blob(&blob_hash, 0..usize::MAX) - .await - { - Ok(Some(contents)) => { - let params = UrlParams::new(req.uri().query()); - let offset = params.parse("offset").unwrap_or(0); - let limit = params.parse("limit").unwrap_or(usize::MAX); - - let contents = if offset == 0 && limit == usize::MAX { - contents - } else { - contents - .get(offset..std::cmp::min(offset + limit, contents.len())) - .unwrap_or_default() - .to_vec() - }; + let blob_hash = URL_SAFE_NO_PAD + .decode(decode_path_element(blob_hash).as_bytes()) + .map_err(|err| { + trc::Cause::Resource(trc::ResourceCause::BadParameters) + .from_base64_error(err) + })?; + let contents = self + .core + .storage + .blob + .get_blob(&blob_hash, 0..usize::MAX) + .await? + .ok_or_else(|| trc::ManageCause::NotFound.into_err())?; + let params = UrlParams::new(req.uri().query()); + let offset = params.parse("offset").unwrap_or(0); + let limit = params.parse("limit").unwrap_or(usize::MAX); + let contents = if offset == 0 && limit == usize::MAX { + contents + } else { + contents + .get(offset..std::cmp::min(offset + limit, contents.len())) + .unwrap_or_default() + .to_vec() + }; - Resource { - content_type: "application/octet-stream", - contents, - } - .into_http_response() - } - Ok(None) => RequestError::not_found().into_http_response(), - Err(err) => err.into_http_response(), - } - } - Err(_) => RequestError::invalid_parameters().into_http_response(), + Ok(Resource { + content_type: "application/octet-stream", + contents, } + .into_http_response()) } (Some("purge"), Some("blob"), _, &Method::GET) => { self.housekeeper_request(Event::Purge(PurgeType::Blobs { @@ -77,7 +75,7 @@ impl JMAP { if let Some(store) = self.core.storage.stores.get(id) { store.clone() } else { - return RequestError::not_found().into_http_response(); + return Err(trc::ResourceCause::NotFound.into_err()); } } else { self.core.storage.data.clone() @@ -91,7 +89,7 @@ impl JMAP { if let Some(store) = self.core.storage.lookups.get(id) { store.clone() } else { - return RequestError::not_found().into_http_response(); + return Err(trc::ResourceCause::NotFound.into_err()); } } else { self.core.storage.lookup.clone() @@ -102,17 +100,13 @@ impl JMAP { } (Some("purge"), Some("account"), id, &Method::GET) => { let account_id = if let Some(id) = id { - match self - .core + self.core .storage .data .get_account_id(decode_path_element(id).as_ref()) - .await - { - Ok(Some(id)) => id.into(), - Ok(None) => return RequestError::not_found().into_http_response(), - Err(err) => return err.into_http_response(), - } + .await? + .ok_or_else(|| trc::ManageCause::NotFound.into_err())? + .into() } else { None }; @@ -120,20 +114,20 @@ impl JMAP { self.housekeeper_request(Event::Purge(PurgeType::Account(account_id))) .await } - _ => RequestError::not_found().into_http_response(), + _ => Err(trc::ResourceCause::NotFound.into_err()), } } - async fn housekeeper_request(&self, event: Event) -> HttpResponse { - match self.inner.housekeeper_tx.send(event).await { - Ok(_) => JsonResponse::new(json!({ - "data": (), - })) - .into_http_response(), - Err(_) => { - tracing::error!("Failed to send housekeeper event"); - RequestError::internal_server_error().into_http_response() - } - } + async fn housekeeper_request(&self, event: Event) -> trc::Result<HttpResponse> { + self.inner.housekeeper_tx.send(event).await.map_err(|err| { + trc::Cause::Thread + .reason(err) + .details("Failed to send housekeeper event") + })?; + + Ok(JsonResponse::new(json!({ + "data": (), + })) + .into_http_response()) } } diff --git a/crates/jmap/src/api/request.rs b/crates/jmap/src/api/request.rs index 32a79704..58a9afc0 100644 --- a/crates/jmap/src/api/request.rs +++ b/crates/jmap/src/api/request.rs @@ -8,7 +8,7 @@ use std::sync::Arc; use common::listener::ServerInstance; use jmap_proto::{ - error::{method::MethodError, request::RequestError}, + error::method::MethodError, method::{ get, query, set::{self}, @@ -26,7 +26,7 @@ impl JMAP { request: Request, access_token: Arc<AccessToken>, instance: &Arc<ServerInstance>, - ) -> Result<Response, RequestError> { + ) -> Response { let mut response = Response::new( access_token.state(), request.created_ids.unwrap_or_default(), @@ -104,7 +104,7 @@ impl JMAP { response.created_ids.clear(); } - Ok(response) + response } async fn handle_method_call( diff --git a/crates/jmap/src/api/session.rs b/crates/jmap/src/api/session.rs index 34687cbf..15c342d2 100644 --- a/crates/jmap/src/api/session.rs +++ b/crates/jmap/src/api/session.rs @@ -8,10 +8,10 @@ use std::sync::Arc; use directory::QueryBy; use jmap_proto::{ - error::request::RequestError, request::capability::{Capability, Session}, types::{acl::Acl, collection::Collection, id::Id}, }; +use trc::AddContext; use crate::{auth::AccessToken, JMAP}; @@ -20,7 +20,7 @@ impl JMAP { &self, base_url: String, access_token: Arc<AccessToken>, - ) -> Result<Session, RequestError> { + ) -> trc::Result<Session> { let mut session = Session::new(base_url, &self.core.jmap.capabilities); session.set_state(access_token.state()); session.set_primary_account( @@ -41,7 +41,8 @@ impl JMAP { && self .shared_documents(&access_token, *id, Collection::Mailbox, Acl::AddItems) .await - .map_or(true, |ids| ids.is_empty()); + .caused_by(trc::location!())? + .is_empty(); session.add_account( (*id).into(), @@ -50,7 +51,7 @@ impl JMAP { .directory .query(QueryBy::Id(*id), false) .await - .unwrap_or_default() + .caused_by(trc::location!())? .map(|p| p.name) .unwrap_or_else(|| Id::from(*id).to_string()), is_personal, diff --git a/crates/jmap/src/auth/acl.rs b/crates/jmap/src/auth/acl.rs index 39104a5e..676e77ef 100644 --- a/crates/jmap/src/auth/acl.rs +++ b/crates/jmap/src/auth/acl.rs @@ -49,7 +49,7 @@ impl JMAP { let acl = Bitmap::<Acl>::from(acl_item.permissions); let collection = Collection::from(acl_item.to_collection); if !collection.is_valid() { - return Err(trc::Cause::DataCorruption + return Err(trc::StoreCause::DataCorruption .ctx(trc::Key::Reason, "Corrupted collection found in ACL key.") .details(format!("{acl_item:?}")) .account_id(grant_account_id) diff --git a/crates/jmap/src/auth/authenticate.rs b/crates/jmap/src/auth/authenticate.rs index e50ff679..86883455 100644 --- a/crates/jmap/src/auth/authenticate.rs +++ b/crates/jmap/src/auth/authenticate.rs @@ -48,7 +48,7 @@ impl JMAP { self.authenticate_plain(&account, &secret, remote_ip, ServerProtocol::Http) .await? } else { - return Err(trc::Cause::Authentication + return Err(trc::AuthCause::Error .into_err() .details("Failed to decode Basic auth request.") .id(token) @@ -65,7 +65,7 @@ impl JMAP { } else { // Enforce anonymous rate limit self.is_anonymous_allowed(&remote_ip).await?; - return Err(trc::Cause::Authentication + return Err(trc::AuthCause::Error .into_err() .reason("Unsupported authentication mechanism.") .details(token) @@ -87,7 +87,7 @@ impl JMAP { // Enforce anonymous rate limit self.is_anonymous_allowed(&remote_ip).await?; - Err(trc::Cause::Authentication + Err(trc::AuthCause::Error .into_err() .details("Missing Authorization header.") .caused_by(trc::location!())) @@ -147,7 +147,7 @@ impl JMAP { { Ok(principal) => Ok(AccessToken::new(principal)), Err(err) => { - if !err.matches(trc::Cause::MissingTotp) { + if !err.matches(trc::Cause::Auth(trc::AuthCause::MissingTotp)) { let _ = self.is_auth_allowed_hard(&remote_ip).await; } Err(err) @@ -164,7 +164,7 @@ impl JMAP { .await { Ok(Some(principal)) => self.update_access_token(AccessToken::new(principal)).await, - Ok(None) => Err(trc::Cause::Authentication + Ok(None) => Err(trc::AuthCause::Error .into_err() .details("Account not found.") .caused_by(trc::location!())), diff --git a/crates/jmap/src/auth/oauth/auth.rs b/crates/jmap/src/auth/oauth/auth.rs index ebdc49f5..22cc020b 100644 --- a/crates/jmap/src/auth/oauth/auth.rs +++ b/crates/jmap/src/auth/oauth/auth.rs @@ -6,7 +6,6 @@ use std::sync::Arc; -use hyper::StatusCode; use rand::distributions::Standard; use serde_json::json; use store::{ @@ -16,10 +15,7 @@ use store::{ }; use crate::{ - api::{ - http::ToHttpResponse, management::ManagementApiError, HtmlResponse, HttpRequest, - HttpResponse, JsonResponse, - }, + api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}, auth::{oauth::OAuthStatus, AccessToken}, JMAP, }; @@ -34,155 +30,133 @@ impl JMAP { &self, access_token: Arc<AccessToken>, body: Option<Vec<u8>>, - ) -> HttpResponse { - match serde_json::from_slice::<OAuthCodeRequest>(body.as_deref().unwrap_or_default()) { - Ok(request) => { - let response = match request { - OAuthCodeRequest::Code { - client_id, - redirect_uri, - } => { - // Validate clientId - if client_id.len() > CLIENT_ID_MAX_LEN { - return ManagementApiError::Other { - details: "Client ID is invalid.".into(), - } - .into_http_response(); - } else if redirect_uri - .as_ref() - .map_or(false, |uri| !uri.starts_with("https://")) - { - return ManagementApiError::Other { - details: "Redirect URI must be HTTPS.".into(), - } - .into_http_response(); - } - - // Generate client code - let client_code = thread_rng() - .sample_iter(Alphanumeric) - .take(DEVICE_CODE_LEN) - .map(char::from) - .collect::<String>(); - - // Serialize OAuth code - let value = Bincode::new(OAuthCode { - status: OAuthStatus::Authorized, - account_id: access_token.primary_id(), - client_id, - params: redirect_uri.unwrap_or_default(), - }) - .serialize(); + ) -> trc::Result<HttpResponse> { + let request = + serde_json::from_slice::<OAuthCodeRequest>(body.as_deref().unwrap_or_default()) + .map_err(|err| { + trc::Cause::Resource(trc::ResourceCause::BadParameters).from_json_error(err) + })?; + + let response = match request { + OAuthCodeRequest::Code { + client_id, + redirect_uri, + } => { + // Validate clientId + if client_id.len() > CLIENT_ID_MAX_LEN { + return Err(trc::ManageCause::Error + .into_err() + .details("Client ID is invalid.")); + } else if redirect_uri + .as_ref() + .map_or(false, |uri| !uri.starts_with("https://")) + { + return Err(trc::ManageCause::Error + .into_err() + .details("Redirect URI must be HTTPS.")); + } + + // Generate client code + let client_code = thread_rng() + .sample_iter(Alphanumeric) + .take(DEVICE_CODE_LEN) + .map(char::from) + .collect::<String>(); + + // Serialize OAuth code + let value = Bincode::new(OAuthCode { + status: OAuthStatus::Authorized, + account_id: access_token.primary_id(), + client_id, + params: redirect_uri.unwrap_or_default(), + }) + .serialize(); + + // Insert client code + self.core + .storage + .lookup + .key_set( + format!("oauth:{client_code}").into_bytes(), + value, + self.core.jmap.oauth_expiry_auth_code.into(), + ) + .await?; + + #[cfg(not(feature = "enterprise"))] + let is_enterprise = false; + #[cfg(feature = "enterprise")] + let is_enterprise = self.core.is_enterprise_edition(); + + json!({ + "data": { + "code": client_code, + "is_admin": access_token.is_super_user(), + "is_enterprise": is_enterprise, + }, + }) + } + OAuthCodeRequest::Device { code } => { + let mut success = false; + + // Obtain code + if let Some(mut auth_code) = self + .core + .storage + .lookup + .key_get::<Bincode<OAuthCode>>(format!("oauth:{code}").into_bytes()) + .await? + { + if auth_code.inner.status == OAuthStatus::Pending { + auth_code.inner.status = OAuthStatus::Authorized; + auth_code.inner.account_id = access_token.primary_id(); + let device_code = std::mem::take(&mut auth_code.inner.params); + success = true; + + // Delete issued user code + self.core + .storage + .lookup + .key_delete(format!("oauth:{code}").into_bytes()) + .await?; - // Insert client code - if let Err(err) = self - .core + // Update device code status + self.core .storage .lookup .key_set( - format!("oauth:{client_code}").into_bytes(), - value, + format!("oauth:{device_code}").into_bytes(), + auth_code.serialize(), self.core.jmap.oauth_expiry_auth_code.into(), ) - .await - { - return err.into_http_response(); - } - - #[cfg(not(feature = "enterprise"))] - let is_enterprise = false; - #[cfg(feature = "enterprise")] - let is_enterprise = self.core.is_enterprise_edition(); - - json!({ - "data": { - "code": client_code, - "is_admin": access_token.is_super_user(), - "is_enterprise": is_enterprise, - }, - }) - } - OAuthCodeRequest::Device { code } => { - let mut success = false; - - // Obtain code - match self - .core - .storage - .lookup - .key_get::<Bincode<OAuthCode>>(format!("oauth:{code}").into_bytes()) - .await - { - Ok(Some(mut auth_code)) - if auth_code.inner.status == OAuthStatus::Pending => - { - auth_code.inner.status = OAuthStatus::Authorized; - auth_code.inner.account_id = access_token.primary_id(); - let device_code = std::mem::take(&mut auth_code.inner.params); - success = true; - - // Delete issued user code - if let Err(err) = self - .core - .storage - .lookup - .key_delete(format!("oauth:{code}").into_bytes()) - .await - { - return err.into_http_response(); - } - - // Update device code status - if let Err(err) = self - .core - .storage - .lookup - .key_set( - format!("oauth:{device_code}").into_bytes(), - auth_code.serialize(), - self.core.jmap.oauth_expiry_auth_code.into(), - ) - .await - { - return err.into_http_response(); - } - } - Err(err) => return err.into_http_response(), - _ => (), - } - - json!({ - "data": success, - }) + .await?; } - }; + } - JsonResponse::new(response).into_http_response() + json!({ + "data": success, + }) } - Err(err) => err.into_http_response(), - } + }; + + Ok(JsonResponse::new(response).into_http_response()) } pub async fn handle_device_auth( &self, req: &mut HttpRequest, base_url: impl AsRef<str>, - ) -> HttpResponse { + ) -> trc::Result<HttpResponse> { // Parse form - let client_id = match FormData::from_request(req, MAX_POST_LEN) - .await - .map(|mut p| p.remove("client_id")) - { - Ok(Some(client_id)) if client_id.len() < CLIENT_ID_MAX_LEN => client_id, - Err(err) => return err, - _ => { - return HtmlResponse::with_status( - StatusCode::BAD_REQUEST, - "Client ID is invalid.".to_string(), - ) - .into_http_response(); - } - }; + let client_id = FormData::from_request(req, MAX_POST_LEN) + .await? + .remove("client_id") + .filter(|client_id| client_id.len() < CLIENT_ID_MAX_LEN) + .ok_or_else(|| { + trc::ResourceCause::BadParameters + .into_err() + .details("Client ID is missing.") + })?; // Generate device code let device_code = thread_rng() @@ -215,8 +189,7 @@ impl JMAP { .serialize(); // Insert device code - if let Err(err) = self - .core + self.core .storage .lookup .key_set( @@ -224,14 +197,10 @@ impl JMAP { oauth_code.clone(), self.core.jmap.oauth_expiry_user_code.into(), ) - .await - { - return err.into_http_response(); - } + .await?; // Insert user code - if let Err(err) = self - .core + self.core .storage .lookup .key_set( @@ -239,14 +208,11 @@ impl JMAP { oauth_code, self.core.jmap.oauth_expiry_user_code.into(), ) - .await - { - return err.into_http_response(); - } + .await?; // Build response let base_url = base_url.as_ref(); - JsonResponse::new(DeviceAuthResponse { + Ok(JsonResponse::new(DeviceAuthResponse { verification_uri: format!("{}/authorize", base_url), verification_uri_complete: format!("{}/authorize/?code={}", base_url, user_code), device_code, @@ -254,6 +220,6 @@ impl JMAP { expires_in: self.core.jmap.oauth_expiry_user_code, interval: 5, }) - .into_http_response() + .into_http_response()) } } diff --git a/crates/jmap/src/auth/oauth/mod.rs b/crates/jmap/src/auth/oauth/mod.rs index 6ff31a53..65c74e6f 100644 --- a/crates/jmap/src/auth/oauth/mod.rs +++ b/crates/jmap/src/auth/oauth/mod.rs @@ -6,13 +6,10 @@ use std::collections::HashMap; -use hyper::{header::CONTENT_TYPE, StatusCode}; +use hyper::header::CONTENT_TYPE; use serde::{Deserialize, Serialize}; -use crate::api::{ - http::{fetch_body, ToHttpResponse}, - HtmlResponse, HttpRequest, HttpResponse, -}; +use crate::api::{http::fetch_body, HttpRequest}; pub mod auth; pub mod token; @@ -205,7 +202,7 @@ pub struct FormData { } impl FormData { - pub async fn from_request(req: &mut HttpRequest, max_len: usize) -> Result<Self, HttpResponse> { + pub async fn from_request(req: &mut HttpRequest, max_len: usize) -> trc::Result<Self> { match ( req.headers() .get(CONTENT_TYPE) @@ -229,11 +226,9 @@ impl FormData { } Ok(FormData { fields }) } - _ => Err(HtmlResponse::with_status( - StatusCode::BAD_REQUEST, - "Invalid post request".to_string(), - ) - .into_http_response()), + _ => Err(trc::ResourceCause::BadParameters + .into_err() + .details("Invalid post request")), } } diff --git a/crates/jmap/src/auth/oauth/token.rs b/crates/jmap/src/auth/oauth/token.rs index eda4230a..de44353e 100644 --- a/crates/jmap/src/auth/oauth/token.rs +++ b/crates/jmap/src/auth/oauth/token.rs @@ -30,12 +30,9 @@ use super::{ impl JMAP { // Token endpoint - pub async fn handle_token_request(&self, req: &mut HttpRequest) -> HttpResponse { + pub async fn handle_token_request(&self, req: &mut HttpRequest) -> trc::Result<HttpResponse> { // Parse form - let params = match FormData::from_request(req, MAX_POST_LEN).await { - Ok(params) => params, - Err(err) => return err, - }; + let params = FormData::from_request(req, MAX_POST_LEN).await?; let grant_type = params.get("grant_type").unwrap_or_default(); let mut response = TokenResponse::error(ErrorType::InvalidGrant); @@ -52,38 +49,35 @@ impl JMAP { .storage .lookup .key_get::<Bincode<OAuthCode>>(format!("oauth:{code}").into_bytes()) - .await + .await? { - Ok(Some(auth_code)) => { + Some(auth_code) => { let oauth = auth_code.inner; if client_id != oauth.client_id || redirect_uri != oauth.params { TokenResponse::error(ErrorType::InvalidClient) } else if oauth.status == OAuthStatus::Authorized { // Mark this token as issued - if let Err(err) = self - .core + self.core .storage .lookup .key_delete(format!("oauth:{code}").into_bytes()) - .await - { - return err.into_http_response(); - } + .await?; // Issue token self.issue_token(oauth.account_id, &oauth.client_id, true) .await .map(TokenResponse::Granted) - .unwrap_or_else(|err| { - tracing::error!("Failed to generate OAuth token: {}", err); - TokenResponse::error(ErrorType::InvalidRequest) - }) + .map_err(|err| { + trc::AuthCause::Error + .into_err() + .details(err) + .caused_by(trc::location!()) + })? } else { TokenResponse::error(ErrorType::InvalidGrant) } } - Ok(None) => TokenResponse::error(ErrorType::AccessDenied), - Err(err) => return err.into_http_response(), + None => TokenResponse::error(ErrorType::AccessDenied), } } else { TokenResponse::error(ErrorType::InvalidClient) @@ -100,9 +94,9 @@ impl JMAP { .storage .lookup .key_get::<Bincode<OAuthCode>>(format!("oauth:{device_code}").into_bytes()) - .await + .await? { - Ok(Some(auth_code)) => { + Some(auth_code) => { let oauth = auth_code.inner; response = if oauth.client_id != client_id { TokenResponse::error(ErrorType::InvalidClient) @@ -110,27 +104,22 @@ impl JMAP { match oauth.status { OAuthStatus::Authorized => { // Mark this token as issued - if let Err(err) = self - .core + self.core .storage .lookup .key_delete(format!("oauth:{device_code}").into_bytes()) - .await - { - return err.into_http_response(); - } + .await?; // Issue token self.issue_token(oauth.account_id, &oauth.client_id, true) .await .map(TokenResponse::Granted) - .unwrap_or_else(|err| { - tracing::error!( - "Failed to generate OAuth token: {}", - err - ); - TokenResponse::error(ErrorType::InvalidRequest) - }) + .map_err(|err| { + trc::AuthCause::Error + .into_err() + .details(err) + .caused_by(trc::location!()) + })? } OAuthStatus::Pending => { TokenResponse::error(ErrorType::AuthorizationPending) @@ -141,37 +130,36 @@ impl JMAP { } }; } - Ok(None) => (), - Err(err) => return err.into_http_response(), + None => (), } } } else if grant_type.eq_ignore_ascii_case("refresh_token") { - let todo = "return error"; if let Some(refresh_token) = params.get("refresh_token") { - if let Ok((account_id, client_id, time_left)) = self + let (account_id, client_id, time_left) = self .validate_access_token("refresh_token", refresh_token) + .await?; + + // TODO: implement revoking client ids + response = self + .issue_token( + account_id, + &client_id, + time_left <= self.core.jmap.oauth_expiry_refresh_token_renew, + ) .await - { - // TODO: implement revoking client ids - response = self - .issue_token( - account_id, - &client_id, - time_left <= self.core.jmap.oauth_expiry_refresh_token_renew, - ) - .await - .map(TokenResponse::Granted) - .unwrap_or_else(|err| { - tracing::debug!("Failed to refresh OAuth token: {}", err); - TokenResponse::error(ErrorType::InvalidGrant) - }); - } + .map(TokenResponse::Granted) + .map_err(|err| { + trc::AuthCause::Error + .into_err() + .details(err) + .caused_by(trc::location!()) + })?; } else { response = TokenResponse::error(ErrorType::InvalidRequest); } } - JsonResponse::with_status( + Ok(JsonResponse::with_status( if response.is_error() { StatusCode::BAD_REQUEST } else { @@ -179,7 +167,7 @@ impl JMAP { }, response, ) - .into_http_response() + .into_http_response()) } async fn password_hash(&self, account_id: u32) -> Result<String, &'static str> { @@ -293,7 +281,8 @@ impl JMAP { ) -> trc::Result<(u32, String, u64)> { // Base64 decode token let token = base64_decode(token_.as_bytes()).ok_or_else(|| { - trc::Cause::Authentication + trc::AuthCause::Error + .into_err() .ctx(trc::Key::Reason, "Failed to decode token") .details(token_.to_string()) })?; @@ -309,7 +298,8 @@ impl JMAP { .into() }) .ok_or_else(|| { - trc::Cause::Authentication + trc::AuthCause::Error + .into_err() .ctx(trc::Key::Reason, "Failed to decode token") .details(token_.to_string()) })?; @@ -321,14 +311,16 @@ impl JMAP { .unwrap_or(0) .saturating_sub(946684800); // Jan 1, 2000 if expiry <= now { - return Err(trc::Cause::Authentication.ctx(trc::Key::Reason, "Token expired")); + return Err(trc::AuthCause::Error + .into_err() + .ctx(trc::Key::Reason, "Token expired")); } // Obtain password hash let password_hash = self .password_hash(account_id) .await - .map_err(|err| trc::Cause::Authentication.ctx(trc::Key::Details, err))?; + .map_err(|err| trc::AuthCause::Error.into_err().ctx(trc::Key::Details, err))?; // Build context let key = self.core.jmap.oauth_key.clone(); @@ -357,7 +349,8 @@ impl JMAP { &nonce, ) .map_err(|err| { - trc::Cause::Authentication + trc::AuthCause::Error + .into_err() .ctx(trc::Key::Details, "Failed to decode token") .reason(err) })?; diff --git a/crates/jmap/src/auth/rate_limit.rs b/crates/jmap/src/auth/rate_limit.rs index 78877688..3462b5cf 100644 --- a/crates/jmap/src/auth/rate_limit.rs +++ b/crates/jmap/src/auth/rate_limit.rs @@ -64,12 +64,12 @@ impl JMAP { } else if access_token.is_super_user() { Ok(InFlight::default()) } else { - Err(trc::Cause::TooManyConcurrentRequests.into()) + Err(trc::LimitCause::ConcurrentRequest.into_err()) } } else if access_token.is_super_user() { Ok(InFlight::default()) } else { - Err(trc::Cause::TooManyRequests.into()) + Err(trc::LimitCause::TooManyRequests.into_err()) } } @@ -84,7 +84,7 @@ impl JMAP { .caused_by(trc::location!())? .is_some() { - return Err(trc::Cause::TooManyRequests.into()); + return Err(trc::LimitCause::TooManyRequests.into_err()); } } Ok(()) @@ -100,7 +100,7 @@ impl JMAP { } else if access_token.is_super_user() { Ok(InFlight::default()) } else { - Err(trc::Cause::TooManyConcurrentUploads.into()) + Err(trc::LimitCause::ConcurrentUpload.into_err()) } } @@ -117,7 +117,7 @@ impl JMAP { .caused_by(trc::location!())? .is_some() { - return Err(trc::Cause::TooManyAuthAttempts.into()); + return Err(trc::AuthCause::TooManyAttempts.into_err()); } } Ok(()) @@ -134,7 +134,7 @@ impl JMAP { .caused_by(trc::location!())? .is_some() { - return Err(trc::Cause::TooManyAuthAttempts.into()); + return Err(trc::AuthCause::TooManyAttempts.into_err()); } } Ok(()) diff --git a/crates/jmap/src/blob/upload.rs b/crates/jmap/src/blob/upload.rs index ab841d91..13d1cf2b 100644 --- a/crates/jmap/src/blob/upload.rs +++ b/crates/jmap/src/blob/upload.rs @@ -211,7 +211,8 @@ impl JMAP { && used.count + 1 > self.core.jmap.upload_tmp_quota_amount)) && !access_token.is_super_user() { - let err = Err(trc::Cause::OverBlobQuota + let err = Err(trc::LimitCause::BlobQuota + .into_err() .ctx(trc::Key::Size, self.core.jmap.upload_tmp_quota_size) .ctx(trc::Key::Total, self.core.jmap.upload_tmp_quota_amount)); diff --git a/crates/jmap/src/changes/write.rs b/crates/jmap/src/changes/write.rs index 36ebd9ef..2047a300 100644 --- a/crates/jmap/src/changes/write.rs +++ b/crates/jmap/src/changes/write.rs @@ -28,7 +28,8 @@ impl JMAP { pub fn generate_snowflake_id(&self) -> trc::Result<u64> { self.inner.snowflake_id.generate().ok_or_else(|| { - trc::Cause::Unexpected + trc::StoreCause::Unexpected + .into_err() .caused_by(trc::location!()) .ctx(trc::Key::Reason, "Failed to generate snowflake id.") }) @@ -57,7 +58,7 @@ impl JMAP { 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(|| { - trc::Cause::Unexpected + trc::StoreCause::Unexpected .caused_by(trc::location!()) .ctx(trc::Key::Reason, "Failed to generate reference change id.") })?; diff --git a/crates/jmap/src/email/crypto.rs b/crates/jmap/src/email/crypto.rs index 0119ad80..b3c779b4 100644 --- a/crates/jmap/src/email/crypto.rs +++ b/crates/jmap/src/email/crypto.rs @@ -7,15 +7,13 @@ use std::{borrow::Cow, collections::BTreeSet, fmt::Display, io::Cursor, sync::Arc}; use crate::{ - api::{http::ToHttpResponse, management::ManagementApiError, HttpResponse, JsonResponse}, + api::{http::ToHttpResponse, HttpResponse, JsonResponse}, auth::AccessToken, JMAP, }; use aes::cipher::{block_padding::Pkcs7, BlockEncryptMut, KeyIvInit}; -use jmap_proto::{ - error::request::RequestError, - types::{collection::Collection, property::Property}, -}; +use directory::backend::internal::manage; +use jmap_proto::types::{collection::Collection, property::Property}; use mail_builder::{encoders::base64::base64_encode_mime, mime::make_boundary}; use mail_parser::{decoders::base64::base64_decode, Message, MessageParser, MimeHeaders}; use openpgp::{ @@ -608,12 +606,16 @@ impl Deserialize for EncryptionParams { fn deserialize(bytes: &[u8]) -> trc::Result<Self> { let version = *bytes .first() - .ok_or_else(|| trc::Cause::DataCorruption.caused_by(trc::location!()))?; + .ok_or_else(|| trc::StoreCause::DataCorruption.caused_by(trc::location!()))?; match version { - 1 if bytes.len() > 1 => bincode::deserialize(&bytes[1..]) - .map_err(|err| trc::Error::from(err).caused_by(trc::location!())), - - _ => Err(trc::Cause::Deserialize + 1 if bytes.len() > 1 => bincode::deserialize(&bytes[1..]).map_err(|err| { + trc::Cause::Store(trc::StoreCause::Deserialize) + .from_bincode_error(err) + .caused_by(trc::location!()) + }), + + _ => Err(trc::StoreCause::Deserialize + .into_err() .caused_by(trc::location!()) .ctx(trc::Key::Value, version as u64)), } @@ -627,66 +629,48 @@ impl ToBitmaps for &EncryptionParams { } impl JMAP { - pub async fn handle_crypto_get(&self, access_token: Arc<AccessToken>) -> HttpResponse { - match self + pub async fn handle_crypto_get( + &self, + access_token: Arc<AccessToken>, + ) -> trc::Result<HttpResponse> { + let params = self .get_property::<EncryptionParams>( access_token.primary_id(), Collection::Principal, 0, Property::Parameters, ) - .await - { - Ok(params) => { - let ec = params - .map(|params| { - let method = params.method; - let algo = params.algo; - let mut certs = Vec::new(); - certs.extend_from_slice(b"-----STALWART CERTIFICATE-----\r\n"); - let _ = base64_encode_mime( - &Bincode::new(params).serialize(), - &mut certs, - false, - ); - certs.extend_from_slice(b"\r\n"); - let certs = String::from_utf8(certs).unwrap_or_default(); - - match method { - EncryptionMethod::PGP => EncryptionType::PGP { algo, certs }, - EncryptionMethod::SMIME => EncryptionType::SMIME { algo, certs }, - } - }) - .unwrap_or(EncryptionType::Disabled); - - JsonResponse::new(json!({ - "data": ec, - })) - .into_http_response() - } - Err(err) => { - tracing::warn!( - context = "store", - event = "error", - reason = ?err, - "Database error while fetching encryption parameters" - ); + .await?; + let ec = params + .map(|params| { + let method = params.method; + let algo = params.algo; + let mut certs = Vec::new(); + certs.extend_from_slice(b"-----STALWART CERTIFICATE-----\r\n"); + let _ = base64_encode_mime(&Bincode::new(params).serialize(), &mut certs, false); + certs.extend_from_slice(b"\r\n"); + let certs = String::from_utf8(certs).unwrap_or_default(); + + match method { + EncryptionMethod::PGP => EncryptionType::PGP { algo, certs }, + EncryptionMethod::SMIME => EncryptionType::SMIME { algo, certs }, + } + }) + .unwrap_or(EncryptionType::Disabled); - RequestError::internal_server_error().into_http_response() - } - } + Ok(JsonResponse::new(json!({ + "data": ec, + })) + .into_http_response()) } pub async fn handle_crypto_post( &self, access_token: Arc<AccessToken>, body: Option<Vec<u8>>, - ) -> HttpResponse { - let request = - match serde_json::from_slice::<EncryptionType>(body.as_deref().unwrap_or_default()) { - Ok(request) => request, - Err(err) => return err.into_http_response(), - }; + ) -> trc::Result<HttpResponse> { + let request = serde_json::from_slice::<EncryptionType>(body.as_deref().unwrap_or_default()) + .map_err(|err| trc::ResourceCause::BadParameters.into_err().reason(err))?; let (method, algo, certs) = match request { EncryptionType::PGP { algo, certs } => (EncryptionMethod::PGP, algo, certs), @@ -699,32 +683,27 @@ impl JMAP { .with_collection(Collection::Principal) .update_document(0) .value(Property::Parameters, (), F_VALUE | F_CLEAR); - return match self.core.storage.data.write(batch.build()).await { - Ok(_) => JsonResponse::new(json!({ - "data": (), - })) - .into_http_response(), - Err(err) => err.into_http_response(), - }; + self.core.storage.data.write(batch.build()).await?; + return Ok(JsonResponse::new(json!({ + "data": (), + })) + .into_http_response()); } }; // Make sure Encryption is enabled if !self.core.jmap.encrypt { - return ManagementApiError::Unsupported { - details: "Encryption-at-rest has been disabled by the system administrator".into(), - } - .into_http_response(); + return Err(manage::unsupported( + "Encryption-at-rest has been disabled by the system administrator", + )); } // Parse certificates - let params = match try_parse_certs(method, certs.into_bytes()) { - Ok(certs) => EncryptionParams { - method, - algo, - certs, - }, - Err(err) => return ManagementApiError::from(err).into_http_response(), + let params = EncryptionParams { + method, + algo, + certs: try_parse_certs(method, certs.into_bytes()) + .map_err(|err| manage::error(err, None::<u32>))?, }; // Try a test encryption @@ -734,7 +713,7 @@ impl JMAP { .encrypt(¶ms) .await { - return ManagementApiError::from(message).into_http_response(); + return Err(manage::error(message, None::<u32>)); } // Save encryption params @@ -745,13 +724,12 @@ impl JMAP { .with_collection(Collection::Principal) .update_document(0) .value(Property::Parameters, ¶ms, F_VALUE); - match self.core.storage.data.write(batch.build()).await { - Ok(_) => JsonResponse::new(json!({ - "data": num_certs, - })) - .into_http_response(), - Err(err) => err.into_http_response(), - } + self.core.storage.data.write(batch.build()).await?; + + Ok(JsonResponse::new(json!({ + "data": num_certs, + })) + .into_http_response()) } } diff --git a/crates/jmap/src/email/delete.rs b/crates/jmap/src/email/delete.rs index 7edebd9d..87c1ebd0 100644 --- a/crates/jmap/src/email/delete.rs +++ b/crates/jmap/src/email/delete.rs @@ -337,7 +337,8 @@ impl JMAP { return Ok(()); } let reference_cid = self.inner.snowflake_id.past_id(period).ok_or_else(|| { - trc::Cause::Unexpected + trc::StoreCause::Unexpected + .into_err() .caused_by(trc::location!()) .ctx(trc::Key::Reason, "Failed to generate reference cid.") })?; diff --git a/crates/jmap/src/email/import.rs b/crates/jmap/src/email/import.rs index c154181c..07c7bbbc 100644 --- a/crates/jmap/src/email/import.rs +++ b/crates/jmap/src/email/import.rs @@ -129,7 +129,7 @@ impl JMAP { response.created.append(id, email.into()); } Err(mut err) => match err.as_ref() { - trc::Cause::OverQuota => { + trc::Cause::Limit(trc::LimitCause::Quota) => { response.not_created.append( id, SetError::new(SetErrorType::OverQuota) diff --git a/crates/jmap/src/email/ingest.rs b/crates/jmap/src/email/ingest.rs index 2e08ba5c..2c9bada6 100644 --- a/crates/jmap/src/email/ingest.rs +++ b/crates/jmap/src/email/ingest.rs @@ -84,7 +84,7 @@ impl JMAP { .await .caused_by(trc::location!())? { - return Err(trc::Cause::OverQuota.into_err()); + return Err(trc::LimitCause::Quota.into_err()); } // Parse message @@ -224,7 +224,10 @@ impl JMAP { } } Err(EncryptMessageError::Error(err)) => { - trc::bail!(trc::Cause::Crypto.caused_by(trc::location!()).reason(err)); + trc::bail!(trc::StoreCause::Crypto + .into_err() + .caused_by(trc::location!()) + .reason(err)); } _ => unreachable!(), } @@ -494,7 +497,7 @@ impl JMAP { match self.core.storage.data.write(batch.build()).await { Ok(_) => return Ok(Some(thread_id)), - Err(err) if err.matches(trc::Cause::AssertValue) && try_count < MAX_RETRIES => { + Err(err) if err.is_assertion_failure() && try_count < MAX_RETRIES => { let backoff = rand::thread_rng().gen_range(50..=300); tokio::time::sleep(Duration::from_millis(backoff)).await; try_count += 1; diff --git a/crates/jmap/src/email/set.rs b/crates/jmap/src/email/set.rs index 9990ec41..e491a41a 100644 --- a/crates/jmap/src/email/set.rs +++ b/crates/jmap/src/email/set.rs @@ -726,7 +726,7 @@ impl JMAP { Ok(message) => { response.created.insert(id, message.into()); } - Err(err) if err.matches(trc::Cause::OverQuota) => { + Err(err) if err.matches(trc::Cause::Limit(trc::LimitCause::Quota)) => { response.not_created.append( id, SetError::new(SetErrorType::OverQuota) @@ -962,7 +962,7 @@ impl JMAP { // Add to updated list response.updated.append(id, None); } - Err(err) if err.matches(trc::Cause::AssertValue) => { + Err(err) if err.is_assertion_failure() => { response.not_updated.append( id, SetError::forbidden().with_description( diff --git a/crates/jmap/src/mailbox/set.rs b/crates/jmap/src/mailbox/set.rs index d95187a7..da94a808 100644 --- a/crates/jmap/src/mailbox/set.rs +++ b/crates/jmap/src/mailbox/set.rs @@ -128,7 +128,7 @@ impl JMAP { ctx.mailbox_ids.insert(document_id); ctx.response.created(id, document_id); } - Err(err) if err.matches(trc::Cause::AssertValue) => { + Err(err) if err.is_assertion_failure() => { ctx.response.not_created.append( id, SetError::forbidden().with_description( @@ -221,7 +221,7 @@ impl JMAP { Ok(_) => { changes.log_update(Collection::Mailbox, document_id); } - Err(err) if err.matches(trc::Cause::AssertValue) => { + Err(err) if err.is_assertion_failure() => { ctx.response.not_updated.append(id, SetError::forbidden().with_description( "Another process modified this mailbox, please try again.", )); @@ -391,7 +391,7 @@ impl JMAP { Collection::Email, Id::from_parts(thread_id, message_id), ), - Err(err) if err.matches(trc::Cause::AssertValue) => { + Err(err) if err.is_assertion_failure() => { return Ok(Err(SetError::forbidden().with_description( concat!( "Another process modified a message in this mailbox ", @@ -469,7 +469,7 @@ impl JMAP { changes.log_delete(Collection::Mailbox, document_id); Ok(Ok(did_remove_emails)) } - Err(err) if err.matches(trc::Cause::AssertValue) => Ok(Err(SetError::forbidden() + Err(err) if err.is_assertion_failure() => Ok(Err(SetError::forbidden() .with_description(concat!( "Another process modified this mailbox ", "while deleting it, please try again." diff --git a/crates/jmap/src/push/get.rs b/crates/jmap/src/push/get.rs index a5f9268a..c3d35eaa 100644 --- a/crates/jmap/src/push/get.rs +++ b/crates/jmap/src/push/get.rs @@ -128,7 +128,8 @@ impl JMAP { }) .await? .ok_or_else(|| { - trc::Cause::NotFound + trc::StoreCause::NotFound + .into_err() .caused_by(trc::location!()) .document_id(document_id) })?; @@ -138,7 +139,7 @@ impl JMAP { .get(&Property::Expires) .and_then(|p| p.as_date()) .ok_or_else(|| { - trc::Cause::Unexpected + trc::StoreCause::Unexpected .caused_by(trc::location!()) .document_id(document_id) })? @@ -174,7 +175,7 @@ impl JMAP { .remove(&Property::Value) .and_then(|p| p.try_unwrap_string()) .ok_or_else(|| { - trc::Cause::Unexpected + trc::StoreCause::Unexpected .caused_by(trc::location!()) .document_id(document_id) })?; @@ -183,7 +184,7 @@ impl JMAP { .remove(&Property::Url) .and_then(|p| p.try_unwrap_string()) .ok_or_else(|| { - trc::Cause::Unexpected + trc::StoreCause::Unexpected .caused_by(trc::location!()) .document_id(document_id) })?; diff --git a/crates/jmap/src/services/index.rs b/crates/jmap/src/services/index.rs index 22f9de05..9e712c9d 100644 --- a/crates/jmap/src/services/index.rs +++ b/crates/jmap/src/services/index.rs @@ -222,7 +222,7 @@ impl JMAP { .set(event.value_class(), (now() + INDEX_LOCK_EXPIRY).serialize()); match self.core.storage.data.write(batch.build()).await { Ok(_) => true, - Err(err) if err.matches(trc::Cause::AssertValue) => { + Err(err) if err.is_assertion_failure() => { tracing::trace!( context = "queue", event = "locked", diff --git a/crates/jmap/src/services/ingest.rs b/crates/jmap/src/services/ingest.rs index 49021621..8df7aea0 100644 --- a/crates/jmap/src/services/ingest.rs +++ b/crates/jmap/src/services/ingest.rs @@ -140,7 +140,7 @@ impl JMAP { } } Err(mut err) => match err.as_ref() { - trc::Cause::OverQuota => { + trc::Cause::Limit(trc::LimitCause::Quota) => { *status = DeliveryResult::TemporaryFailure { reason: "Mailbox over quota.".into(), } diff --git a/crates/jmap/src/sieve/get.rs b/crates/jmap/src/sieve/get.rs index 4e4c6d51..a1882ace 100644 --- a/crates/jmap/src/sieve/get.rs +++ b/crates/jmap/src/sieve/get.rs @@ -192,7 +192,8 @@ impl JMAP { ) .await? .ok_or_else(|| { - trc::Cause::NotFound + trc::StoreCause::NotFound + .into_err() .caused_by(trc::location!()) .document_id(document_id) })?; @@ -205,7 +206,8 @@ impl JMAP { .and_then(|v| v.as_blob_id()) .and_then(|v| (v.section.as_ref()?.size, v).into()) .ok_or_else(|| { - trc::Cause::NotFound + trc::StoreCause::NotFound + .into_err() .caused_by(trc::location!()) .document_id(document_id) })?; @@ -215,7 +217,8 @@ impl JMAP { .get_blob(&blob_id.hash, 0..usize::MAX) .await? .ok_or_else(|| { - trc::Cause::NotFound + trc::StoreCause::NotFound + .into_err() .caused_by(trc::location!()) .document_id(document_id) })?; @@ -230,7 +233,8 @@ impl JMAP { // Deserialization failed, probably because the script compiler version changed match self.core.sieve.untrusted_compiler.compile( script_bytes.get(0..script_offset).ok_or_else(|| { - trc::Cause::NotFound + trc::StoreCause::NotFound + .into_err() .caused_by(trc::location!()) .document_id(document_id) })?, @@ -274,7 +278,7 @@ impl JMAP { Ok((sieve.inner, new_script_object)) } - Err(error) => Err(trc::Cause::Unexpected + Err(error) => Err(trc::StoreCause::Unexpected .caused_by(trc::location!()) .reason(error) .details("Failed to compile Sieve script")), diff --git a/crates/jmap/src/sieve/set.rs b/crates/jmap/src/sieve/set.rs index 3512b950..7569c788 100644 --- a/crates/jmap/src/sieve/set.rs +++ b/crates/jmap/src/sieve/set.rs @@ -159,7 +159,8 @@ impl JMAP { .inner .blob_id() .ok_or_else(|| { - trc::Cause::NotFound + trc::StoreCause::NotFound + .into_err() .caused_by(trc::location!()) .document_id(document_id) })? @@ -225,7 +226,7 @@ impl JMAP { changes.log_update(Collection::SieveScript, document_id); match self.core.storage.data.write(batch.build()).await { Ok(_) => (), - Err(err) if err.matches(trc::Cause::AssertValue) => { + Err(err) if err.is_assertion_failure() => { ctx.response.not_updated.append(id, SetError::forbidden().with_description( "Another process modified this sieve, please try again.", )); @@ -336,7 +337,8 @@ impl JMAP { ) .await? .ok_or_else(|| { - trc::Cause::NotFound + trc::StoreCause::NotFound + .into_err() .caused_by(trc::location!()) .document_id(document_id) })?; @@ -354,7 +356,8 @@ impl JMAP { // Delete record let mut batch = BatchBuilder::new(); let blob_id = obj.inner.blob_id().ok_or_else(|| { - trc::Cause::NotFound + trc::StoreCause::NotFound + .into_err() .caused_by(trc::location!()) .document_id(document_id) })?; @@ -616,7 +619,7 @@ impl JMAP { if !changed_ids.is_empty() { match self.core.storage.data.write(batch.build()).await { Ok(_) => (), - Err(err) if err.matches(trc::Cause::AssertValue) => { + Err(err) if err.is_assertion_failure() => { return Ok(vec![]); } Err(err) => { diff --git a/crates/jmap/src/vacation/set.rs b/crates/jmap/src/vacation/set.rs index 749e5ac5..791e39f1 100644 --- a/crates/jmap/src/vacation/set.rs +++ b/crates/jmap/src/vacation/set.rs @@ -206,7 +206,11 @@ impl JMAP { == Some(&Value::Bool(true)); value }) - .ok_or_else(|| trc::Cause::NotFound.caused_by(trc::location!()))? + .ok_or_else(|| { + trc::StoreCause::NotFound + .into_err() + .caused_by(trc::location!()) + })? .into() } else { None @@ -250,7 +254,8 @@ impl JMAP { if let Some(current) = obj.current() { let current_blob_id = current.inner.blob_id().ok_or_else(|| { - trc::Cause::NotFound + trc::StoreCause::NotFound + .into_err() .caused_by(trc::location!()) .document_id(document_id.unwrap_or(u32::MAX)) })?; @@ -429,7 +434,7 @@ impl JMAP { Ok(script) } - Err(err) => Err(trc::Cause::Unexpected + Err(err) => Err(trc::StoreCause::Unexpected .caused_by(trc::location!()) .reason(err) .details("Vacation Sieve Script failed to compile.")), diff --git a/crates/jmap/src/websocket/stream.rs b/crates/jmap/src/websocket/stream.rs index 852a9e21..2a138425 100644 --- a/crates/jmap/src/websocket/stream.rs +++ b/crates/jmap/src/websocket/stream.rs @@ -75,23 +75,16 @@ impl JMAP { self.core.jmap.request_max_size, ) { Ok(WebSocketMessage::Request(request)) => { - match self + let response = self .handle_request( request.request, access_token.clone(), &instance, ) - .await - { - Ok(response) => { - WebSocketResponse::from_response(response, request.id) - .to_json() - } - Err(err) => { - WebSocketRequestError::from_error(err, request.id) - .to_json() - } - } + .await; + + WebSocketResponse::from_response(response, request.id) + .to_json() } Ok(WebSocketMessage::PushEnable(push_enable)) => { change_types = if !push_enable.data_types.is_empty() { @@ -105,7 +98,11 @@ impl JMAP { change_types = Bitmap::new(); continue; } - Err(err) => err.to_json(), + Err(err) => { + let todo = "fix"; + //err.to_json() + todo!() + }, }; if let Err(err) = stream.send(Message::Text(response)).await { tracing::debug!(parent: &span, error = ?err, "Failed to send text message"); diff --git a/crates/jmap/src/websocket/upgrade.rs b/crates/jmap/src/websocket/upgrade.rs index 5c47e0eb..b2494995 100644 --- a/crates/jmap/src/websocket/upgrade.rs +++ b/crates/jmap/src/websocket/upgrade.rs @@ -8,14 +8,13 @@ use std::sync::Arc; use common::listener::ServerInstance; use http_body_util::{BodyExt, Full}; -use hyper::{body::Bytes, Response, StatusCode}; +use hyper::{body::Bytes, Response}; use hyper_util::rt::TokioIo; -use jmap_proto::error::request::RequestError; use tokio_tungstenite::WebSocketStream; use tungstenite::{handshake::derive_accept_key, protocol::Role}; use crate::{ - api::{http::ToHttpResponse, HttpRequest, HttpResponse}, + api::{HttpRequest, HttpResponse}, auth::AccessToken, JMAP, }; @@ -26,7 +25,7 @@ impl JMAP { req: HttpRequest, access_token: Arc<AccessToken>, instance: Arc<ServerInstance>, - ) -> HttpResponse { + ) -> trc::Result<HttpResponse> { let headers = req.headers(); if headers .get(hyper::header::CONNECTION) @@ -37,12 +36,13 @@ impl JMAP { .and_then(|h| h.to_str().ok()) != Some("websocket") { - return RequestError::blank( - StatusCode::BAD_REQUEST.as_u16(), - "WebSocket upgrade failed", - "Missing or Invalid Connection or Upgrade headers.", - ) - .into_http_response(); + return Err(trc::ResourceCause::BadParameters + .into_err() + .details("WebSocket upgrade failed") + .ctx( + trc::Key::Reason, + "Missing or Invalid Connection or Upgrade headers.", + )); } let derived_key = match ( headers @@ -54,12 +54,13 @@ impl JMAP { ) { (Some(key), Some("13")) => derive_accept_key(key.as_bytes()), _ => { - return RequestError::blank( - StatusCode::BAD_REQUEST.as_u16(), - "WebSocket upgrade failed", - "Missing or Invalid Sec-WebSocket-Key headers.", - ) - .into_http_response(); + return Err(trc::ResourceCause::BadParameters + .into_err() + .details("WebSocket upgrade failed") + .ctx( + trc::Key::Reason, + "Missing or Invalid Sec-WebSocket-Key headers.", + )); } }; @@ -87,7 +88,7 @@ impl JMAP { } }); - Response::builder() + Ok(Response::builder() .status(hyper::StatusCode::SWITCHING_PROTOCOLS) .header(hyper::header::CONNECTION, "upgrade") .header(hyper::header::UPGRADE, "websocket") @@ -98,6 +99,6 @@ impl JMAP { .map_err(|never| match never {}) .boxed(), ) - .unwrap() + .unwrap()) } } diff --git a/crates/managesieve/Cargo.toml b/crates/managesieve/Cargo.toml index c9510411..eb3801f9 100644 --- a/crates/managesieve/Cargo.toml +++ b/crates/managesieve/Cargo.toml @@ -13,6 +13,7 @@ directory = { path = "../directory" } common = { path = "../common" } store = { path = "../store" } utils = { path = "../utils" } +trc = { path = "../trc" } mail-parser = { version = "0.9", features = ["full_encoding", "ludicrous_mode"] } mail-send = { version = "0.4", default-features = false, features = ["cram-md5", "ring", "tls12"] } sieve-rs = { version = "0.5" } diff --git a/crates/managesieve/src/core/client.rs b/crates/managesieve/src/core/client.rs index 53791536..71e33d5b 100644 --- a/crates/managesieve/src/core/client.rs +++ b/crates/managesieve/src/core/client.rs @@ -4,16 +4,17 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::listener::SessionStream; +use common::listener::{SessionResult, SessionStream}; use imap_proto::receiver::{self, Request}; use jmap_proto::types::{collection::Collection, property::Property}; use store::query::Filter; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; +use trc::AddContext; -use super::{Command, ResponseCode, ResponseType, Session, State, StatusResponse}; +use super::{Command, ResponseCode, SerializeResponse, Session, State, StatusResponse}; impl<T: SessionStream> Session<T> { - pub async fn ingest(&mut self, bytes: &[u8]) -> Result<bool, ()> { + pub async fn ingest(&mut self, bytes: &[u8]) -> SessionResult { /*let tmp = "dd"; for line in String::from_utf8_lossy(bytes).split("\r\n") { println!("<- {:?}", &line[..std::cmp::min(line.len(), 100)]); @@ -29,8 +30,11 @@ impl<T: SessionStream> Session<T> { Ok(request) => { requests.push(request); } - Err(response) => { - self.write(&response.into_bytes()).await?; + Err(err) => { + if let Err(err) = self.write_error(err).await { + tracing::error!(parent: &self.span, event = "error", error = ?err); + return SessionResult::Close; + } } }, Err(receiver::Error::NeedsMoreData) => { @@ -41,15 +45,21 @@ impl<T: SessionStream> Session<T> { break; } Err(receiver::Error::Error { response }) => { - self.write(&StatusResponse::no(response.message).into_bytes()) - .await?; + if let Err(err) = self + .write(&StatusResponse::no(response.message).into_bytes()) + .await + { + tracing::error!(parent: &self.span, event = "error", error = ?err); + return SessionResult::Close; + } break; } } } for request in requests { - match match request.command { + let command = request.command; + match match command { Command::ListScripts => self.handle_listscripts().await, Command::PutScript => self.handle_putscript(request).await, Command::SetActive => self.handle_setactive(request).await, @@ -60,39 +70,46 @@ impl<T: SessionStream> Session<T> { Command::HaveSpace => self.handle_havespace(request).await, Command::Capability => self.handle_capability("").await, Command::Authenticate => self.handle_authenticate(request).await, - Command::StartTls => { - self.write(b"OK Begin TLS negotiation now\r\n").await?; - return Ok(false); - } + Command::StartTls => self.handle_start_tls().await, Command::Logout => self.handle_logout().await, Command::Noop => self.handle_noop(request).await, Command::Unauthenticate => self.handle_unauthenticate().await, } { Ok(response) => { - self.write(&response).await?; + if let Err(err) = self.write(&response).await { + tracing::error!(parent: &self.span, event = "error", error = ?err); + return SessionResult::Close; + } + + match command { + Command::Logout => return SessionResult::Close, + Command::StartTls => return SessionResult::UpgradeTls, + _ => (), + } } Err(err) => { - let disconnect = matches!(err.rtype, ResponseType::Bye | ResponseType::Ok); - self.write(&err.into_bytes()).await?; - if disconnect { - return Err(()); + if let Err(err) = self.write_error(err).await { + tracing::error!(parent: &self.span, event = "error", error = ?err); + return SessionResult::Close; } } } } if let Some(needs_literal) = needs_literal { - self.write(format!("OK Ready for {} bytes.\r\n", needs_literal).as_bytes()) - .await?; + if let Err(err) = self + .write(format!("OK Ready for {} bytes.\r\n", needs_literal).as_bytes()) + .await + { + tracing::error!(parent: &self.span, event = "error", error = ?err); + return SessionResult::Close; + } } - Ok(true) + SessionResult::Continue } - async fn validate_request( - &self, - command: Request<Command>, - ) -> Result<Request<Command>, StatusResponse> { + async fn validate_request(&self, command: Request<Command>) -> trc::Result<Request<Command>> { match &command.command { Command::Capability | Command::Logout | Command::Noop => Ok(command), Command::Authenticate => { @@ -100,18 +117,24 @@ impl<T: SessionStream> Session<T> { if self.stream.is_tls() || self.jmap.core.imap.allow_plain_auth { Ok(command) } else { - Err(StatusResponse::no("Cannot authenticate over plain-text.") - .with_code(ResponseCode::EncryptNeeded)) + Err(trc::Cause::ManageSieve + .into_err() + .code(ResponseCode::EncryptNeeded) + .details("Cannot authenticate over plain-text.")) } } else { - Err(StatusResponse::no("Already authenticated.")) + Err(trc::Cause::ManageSieve + .into_err() + .details("Already authenticated.")) } } Command::StartTls => { if !self.stream.is_tls() { Ok(command) } else { - Err(StatusResponse::no("Already in TLS mode.")) + Err(trc::Cause::ManageSieve + .into_err() + .details("Already in TLS mode.")) } } Command::HaveSpace @@ -125,7 +148,7 @@ impl<T: SessionStream> Session<T> { | Command::Unauthenticate => { if let State::Authenticated { access_token, .. } = &self.state { if let Some(rate) = &self.jmap.core.imap.rate_requests { - match self + if self .jmap .core .storage @@ -136,18 +159,22 @@ impl<T: SessionStream> Session<T> { true, ) .await + .caused_by(trc::location!())? + .is_none() { - Ok(None) => Ok(command), - Ok(Some(_)) => Err(StatusResponse::no("Too many requests") - .with_code(ResponseCode::TryLater)), - Err(_) => Err(StatusResponse::no("Internal server error") - .with_code(ResponseCode::TryLater)), + Ok(command) + } else { + Err(trc::LimitCause::TooManyRequests + .into_err() + .code(ResponseCode::TryLater)) } } else { Ok(command) } } else { - Err(StatusResponse::no("Not authenticated.")) + Err(trc::Cause::ManageSieve + .into_err() + .details("Not authenticated.")) } } } @@ -156,54 +183,51 @@ impl<T: SessionStream> Session<T> { impl<T: AsyncWrite + AsyncRead + Unpin> Session<T> { #[inline(always)] - pub async fn write(&mut self, bytes: &[u8]) -> Result<(), ()> { - let err = match self.stream.write_all(bytes).await { - Ok(_) => match self.stream.flush().await { - Ok(_) => { - tracing::trace!(parent: &self.span, - event = "write", - data = std::str::from_utf8(bytes).unwrap_or_default() , - size = bytes.len()); - return Ok(()); - } - Err(err) => err, - }, - Err(err) => err, - }; - - tracing::debug!(parent: &self.span, - event = "error", - "Failed to write to stream: {:?}", err); - Err(()) + pub async fn write(&mut self, bytes: &[u8]) -> trc::Result<()> { + self.stream + .write_all(bytes) + .await + .map_err(|err| trc::Cause::Network.reason(err).caused_by(trc::location!()))?; + self.stream + .flush() + .await + .map_err(|err| trc::Cause::Network.reason(err).caused_by(trc::location!()))?; + + tracing::trace!(parent: &self.span, + event = "write", + data = std::str::from_utf8(bytes).unwrap_or_default() , + size = bytes.len()); + + Ok(()) + } + + pub async fn write_error(&mut self, error: trc::Error) -> trc::Result<()> { + tracing::error!(parent: &self.span, event = "error", error = ?error); + self.write(&error.serialize()).await } #[inline(always)] - pub async fn read(&mut self, bytes: &mut [u8]) -> Result<usize, ()> { - match self.stream.read(bytes).await { - Ok(len) => { - tracing::trace!(parent: &self.span, - event = "read", - data = bytes - .get(0..len) - .and_then(|bytes| std::str::from_utf8(bytes).ok()) - .unwrap_or("[invalid UTF8]"), - size = len); - Ok(len) - } - Err(err) => { - tracing::trace!( - parent: &self.span, - event = "error", - "Failed to read from stream: {:?}", err - ); - Err(()) - } - } + pub async fn read(&mut self, bytes: &mut [u8]) -> trc::Result<usize> { + let len = self + .stream + .read(bytes) + .await + .map_err(|err| trc::Cause::Network.reason(err).caused_by(trc::location!()))?; + + tracing::trace!(parent: &self.span, + event = "read", + data = bytes + .get(0..len) + .and_then(|bytes| std::str::from_utf8(bytes).ok()) + .unwrap_or("[invalid UTF8]"), + size = len); + + Ok(len) } } impl<T: AsyncWrite + AsyncRead> Session<T> { - pub async fn get_script_id(&self, account_id: u32, name: &str) -> Result<u32, StatusResponse> { + pub async fn get_script_id(&self, account_id: u32, name: &str) -> trc::Result<u32> { self.jmap .core .storage @@ -214,11 +238,13 @@ impl<T: AsyncWrite + AsyncRead> Session<T> { vec![Filter::eq(Property::Name, name)], ) .await - .map_err(|_| StatusResponse::database_failure()) + .caused_by(trc::location!()) .and_then(|results| { results.results.min().ok_or_else(|| { - StatusResponse::no("There is no script by that name") - .with_code(ResponseCode::NonExistent) + trc::Cause::ManageSieve + .into_err() + .code(ResponseCode::NonExistent) + .reason("There is no script by that name") }) }) } diff --git a/crates/managesieve/src/core/mod.rs b/crates/managesieve/src/core/mod.rs index 822f3fc4..db30c63c 100644 --- a/crates/managesieve/src/core/mod.rs +++ b/crates/managesieve/src/core/mod.rs @@ -159,15 +159,38 @@ impl ResponseCode { ResponseCode::Warnings => b"WARNINGS", }); } + + pub fn as_str(&self) -> &'static str { + match self { + ResponseCode::AuthTooWeak => "AUTH-TOO-WEAK", + ResponseCode::EncryptNeeded => "ENCRYPT-NEEDED", + ResponseCode::Quota => "QUOTA", + ResponseCode::QuotaMaxScripts => "QUOTA/MAXSCRIPTS", + ResponseCode::QuotaMaxSize => "QUOTA/MAXSIZE", + ResponseCode::Referral => "REFERRAL", + ResponseCode::Sasl => "SASL", + ResponseCode::TransitionNeeded => "TRANSITION-NEEDED", + ResponseCode::TryLater => "TRYLATER", + ResponseCode::Active => "ACTIVE", + ResponseCode::NonExistent => "NONEXISTENT", + ResponseCode::AlreadyExists => "ALREADYEXISTS", + ResponseCode::Tag(_) => "TAG", + ResponseCode::Warnings => "WARNINGS", + } + } } impl ResponseType { pub fn serialize(&self, buf: &mut Vec<u8>) { - buf.extend_from_slice(match self { - ResponseType::Ok => b"OK", - ResponseType::No => b"NO", - ResponseType::Bye => b"BYE", - }); + buf.extend_from_slice(self.as_str().as_bytes()); + } + + pub fn as_str(&self) -> &'static str { + match self { + ResponseType::Ok => "OK", + ResponseType::No => "NO", + ResponseType::Bye => "BYE", + } } } @@ -234,3 +257,47 @@ impl StatusResponse { } } } + +pub trait SerializeResponse { + fn serialize(&self) -> Vec<u8>; +} + +impl SerializeResponse for trc::Error { + fn serialize(&self) -> Vec<u8> { + let todo = "serialize messages properly in all protocols"; + let mut buf = Vec::with_capacity(64); + buf.extend_from_slice(self.value_as_str(trc::Key::Type).unwrap_or("NO").as_bytes()); + if let Some(code) = self.value_as_str(trc::Key::Code) { + buf.extend_from_slice(b" ("); + buf.extend_from_slice(code.as_bytes()); + buf.push(b')'); + } + if let Some(message) = self + .value_as_str(trc::Key::Details) + .or_else(|| self.value_as_str(trc::Key::Reason)) + { + buf.extend_from_slice(b" \""); + for ch in message.as_bytes() { + if [b'\"', b'\\'].contains(ch) { + buf.push(b'\\'); + } + buf.push(*ch); + } + buf.push(b'\"'); + } + buf.extend_from_slice(b"\r\n"); + buf + } +} + +impl From<ResponseCode> for trc::Value { + fn from(value: ResponseCode) -> Self { + trc::Value::Static(value.as_str()) + } +} + +impl From<ResponseType> for trc::Value { + fn from(value: ResponseType) -> Self { + trc::Value::Static(value.as_str()) + } +} diff --git a/crates/managesieve/src/core/session.rs b/crates/managesieve/src/core/session.rs index da8c50b2..78b927e1 100644 --- a/crates/managesieve/src/core/session.rs +++ b/crates/managesieve/src/core/session.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::listener::{SessionData, SessionManager, SessionStream}; +use common::listener::{SessionData, SessionManager, SessionResult, SessionStream}; use imap_proto::receiver::{self, Receiver}; use jmap::JMAP; use tokio_rustls::server::TlsStream; @@ -76,11 +76,11 @@ impl<T: SessionStream> Session<T> { Ok(Ok(bytes_read)) => { if bytes_read > 0 { match self.ingest(&buf[..bytes_read]).await { - Ok(true) => (), - Ok(false) => { + SessionResult::Continue => (), + SessionResult::UpgradeTls => { return true; } - Err(_) => { + SessionResult::Close => { break; } } diff --git a/crates/managesieve/src/op/authenticate.rs b/crates/managesieve/src/op/authenticate.rs index a65bafcd..aa9f10bf 100644 --- a/crates/managesieve/src/op/authenticate.rs +++ b/crates/managesieve/src/op/authenticate.rs @@ -7,7 +7,6 @@ use common::{ config::server::ServerProtocol, listener::{limiter::ConcurrencyLimiter, SessionStream}, - AuthFailureReason, AuthResult, }; use imap::op::authenticate::{decode_challenge_oauth, decode_challenge_plain}; use imap_proto::{ @@ -22,14 +21,16 @@ use std::sync::Arc; use crate::core::{Command, Session, State, StatusResponse}; impl<T: SessionStream> Session<T> { - pub async fn handle_authenticate(&mut self, request: Request<Command>) -> crate::op::OpResult { + pub async fn handle_authenticate(&mut self, request: Request<Command>) -> trc::Result<Vec<u8>> { if request.tokens.is_empty() { - return Err(StatusResponse::no("Authentication mechanism missing.")); + return Err(trc::AuthCause::Error + .into_err() + .details("Authentication mechanism missing.")); } let mut tokens = request.tokens.into_iter(); - let mechanism = - Mechanism::parse(&tokens.next().unwrap().unwrap_bytes()).map_err(StatusResponse::no)?; + let mechanism = Mechanism::parse(&tokens.next().unwrap().unwrap_bytes()) + .map_err(|err| trc::AuthCause::Error.into_err().details(err))?; let mut params: Vec<String> = tokens .filter_map(|token| token.unwrap_string().ok()) .collect(); @@ -37,14 +38,18 @@ impl<T: SessionStream> Session<T> { let credentials = match mechanism { Mechanism::Plain | Mechanism::OAuthBearer => { if !params.is_empty() { - let challenge = base64_decode(params.pop().unwrap().as_bytes()) - .ok_or_else(|| StatusResponse::no("Failed to decode challenge."))?; + let challenge = + base64_decode(params.pop().unwrap().as_bytes()).ok_or_else(|| { + trc::AuthCause::Error + .into_err() + .details("Failed to decode challenge.") + })?; (if mechanism == Mechanism::Plain { decode_challenge_plain(&challenge) } else { decode_challenge_oauth(&challenge) } - .map_err(StatusResponse::no))? + .map_err(|err| trc::AuthCause::Error.into_err().details(err)))? } else { self.receiver.request = receiver::Request { tag: String::new(), @@ -56,106 +61,63 @@ impl<T: SessionStream> Session<T> { } } _ => { - return Err(StatusResponse::no( - "Authentication mechanism not supported.", - )) + return Err(trc::AuthCause::Error + .into_err() + .details("Authentication mechanism not supported.")) } }; // Throttle authentication requests - if self - .jmap - .is_auth_allowed_soft(&self.remote_addr) - .await - .is_err() - { - tracing::debug!(parent: &self.span, - event = "disconnect", - "Too many authentication attempts, disconnecting.", - ); - return Err(StatusResponse::bye( - "Too many authentication requests from this IP address.", - )); - } + self.jmap.is_auth_allowed_soft(&self.remote_addr).await?; // Authenticate let mut is_totp_error = false; let access_token = match credentials { Credentials::Plain { username, secret } | Credentials::XOauth2 { username, secret } => { - match self - .jmap + self.jmap .authenticate_plain( &username, &secret, self.remote_addr, ServerProtocol::ManageSieve, ) - .await - { - AuthResult::Success(token) => Some(token), - AuthResult::Failure( - AuthFailureReason::InvalidCredentials | AuthFailureReason::InternalError(_), - ) => None, - AuthResult::Failure(AuthFailureReason::MissingTotp) => { - is_totp_error = true; - None - } - AuthResult::Failure(AuthFailureReason::Banned) => { - return Err(StatusResponse::bye( - "Too many authentication requests from this IP address.", - )) - } - } + .await? } Credentials::OAuthBearer { token } => { - match self + let (account_id, _, _) = self .jmap .validate_access_token("access_token", &token) - .await - { - Ok((account_id, _, _)) => self.jmap.get_access_token(account_id).await, - Err(err) => { - tracing::debug!( - parent: &self.span, - context = "authenticate", - err = err, - "Failed to validate access token." - ); - None - } - } + .await?; + self.jmap.get_access_token(account_id).await? } }; - if let Some(access_token) = access_token { - // Enforce concurrency limits - let in_flight = match self - .get_concurrency_limiter(access_token.primary_id()) - .map(|limiter| limiter.concurrent_requests.is_allowed()) - { - Some(Some(limiter)) => Some(limiter), - None => None, - Some(None) => { - tracing::debug!(parent: &self.span, - event = "disconnect", - "Too many concurrent connection.", - ); - return Err(StatusResponse::bye("Too many concurrent connections.")); - } - }; + // Enforce concurrency limits + let in_flight = match self + .get_concurrency_limiter(access_token.primary_id()) + .map(|limiter| limiter.concurrent_requests.is_allowed()) + { + Some(Some(limiter)) => Some(limiter), + None => None, + Some(None) => { + return Err(trc::LimitCause::ConcurrentRequest.into_err()); + } + }; + + // Cache access token + let access_token = Arc::new(access_token); + self.jmap.cache_access_token(access_token.clone()); - // Cache access token - let access_token = Arc::new(access_token); - self.jmap.cache_access_token(access_token.clone()); + // Create session + self.state = State::Authenticated { + access_token, + in_flight, + }; - // Create session - self.state = State::Authenticated { - access_token, - in_flight, - }; + let todo = "implement this"; - Ok(StatusResponse::ok("Authentication successful").into_bytes()) - } else { + Ok(StatusResponse::ok("Authentication successful").into_bytes()) + /*} else { match &self.state { State::NotAuthenticated { auth_failures } if *auth_failures < self.jmap.core.imap.max_auth_failures => @@ -163,12 +125,13 @@ impl<T: SessionStream> Session<T> { self.state = State::NotAuthenticated { auth_failures: auth_failures + 1, }; - Ok(StatusResponse::no(if is_totp_error { - "Missing TOTP code, try with 'secret$totp_code'." - } else { - "Authentication failed." - }) - .into_bytes()) + Err(trc::Cause::Authentication + .into_err() + .details(if is_totp_error { + "Missing TOTP code, try with 'secret$totp_code'." + } else { + "Authentication failed." + })) } _ => { tracing::debug!( @@ -179,10 +142,10 @@ impl<T: SessionStream> Session<T> { Err(StatusResponse::bye("Too many authentication failures")) } } - } + }*/ } - pub async fn handle_unauthenticate(&mut self) -> super::OpResult { + pub async fn handle_unauthenticate(&mut self) -> trc::Result<Vec<u8>> { self.state = State::NotAuthenticated { auth_failures: 0 }; Ok(StatusResponse::ok("Unauthenticate successful.").into_bytes()) diff --git a/crates/managesieve/src/op/capability.rs b/crates/managesieve/src/op/capability.rs index 45d7273a..82c2e092 100644 --- a/crates/managesieve/src/op/capability.rs +++ b/crates/managesieve/src/op/capability.rs @@ -10,7 +10,7 @@ use jmap_proto::request::capability::Capabilities; use crate::core::{Session, StatusResponse}; impl<T: SessionStream> Session<T> { - pub async fn handle_capability(&self, message: &'static str) -> super::OpResult { + pub async fn handle_capability(&self, message: &'static str) -> trc::Result<Vec<u8>> { let mut response = Vec::with_capacity(128); response.extend_from_slice(b"\"IMPLEMENTATION\" \"Stalwart ManageSieve\"\r\n"); response.extend_from_slice(b"\"VERSION\" \"1.0\"\r\n"); diff --git a/crates/managesieve/src/op/checkscript.rs b/crates/managesieve/src/op/checkscript.rs index 4fb0717b..5d0bd9ca 100644 --- a/crates/managesieve/src/op/checkscript.rs +++ b/crates/managesieve/src/op/checkscript.rs @@ -10,9 +10,11 @@ use tokio::io::{AsyncRead, AsyncWrite}; use crate::core::{Command, Session, StatusResponse}; impl<T: AsyncRead + AsyncWrite> Session<T> { - pub async fn handle_checkscript(&mut self, request: Request<Command>) -> super::OpResult { + pub async fn handle_checkscript(&mut self, request: Request<Command>) -> trc::Result<Vec<u8>> { if request.tokens.is_empty() { - return Err(StatusResponse::no("Expected script as a parameter.")); + return Err(trc::Cause::ManageSieve + .into_err() + .details("Expected script as a parameter.")); } self.jmap @@ -21,6 +23,6 @@ impl<T: AsyncRead + AsyncWrite> Session<T> { .untrusted_compiler .compile(&request.tokens.into_iter().next().unwrap().unwrap_bytes()) .map(|_| StatusResponse::ok("Script is valid.").into_bytes()) - .map_err(|err| StatusResponse::no(err.to_string())) + .map_err(|err| trc::Cause::ManageSieve.into_err().details(err.to_string())) } } diff --git a/crates/managesieve/src/op/deletescript.rs b/crates/managesieve/src/op/deletescript.rs index 8d322f3a..f2ad1048 100644 --- a/crates/managesieve/src/op/deletescript.rs +++ b/crates/managesieve/src/op/deletescript.rs @@ -8,34 +8,45 @@ use imap_proto::receiver::Request; use jmap_proto::types::collection::Collection; use store::write::log::ChangeLogBuilder; use tokio::io::{AsyncRead, AsyncWrite}; +use trc::AddContext; use crate::core::{Command, ResponseCode, Session, StatusResponse}; impl<T: AsyncRead + AsyncWrite> Session<T> { - pub async fn handle_deletescript(&mut self, request: Request<Command>) -> super::OpResult { + pub async fn handle_deletescript(&mut self, request: Request<Command>) -> trc::Result<Vec<u8>> { let name = request .tokens .into_iter() .next() .and_then(|s| s.unwrap_string().ok()) - .ok_or_else(|| StatusResponse::no("Expected script name as a parameter."))?; + .ok_or_else(|| { + trc::Cause::ManageSieve + .into_err() + .details("Expected script name as a parameter.") + })?; let account_id = self.state.access_token().primary_id(); let document_id = self.get_script_id(account_id, &name).await?; if self .jmap .sieve_script_delete(account_id, document_id, true) - .await? + .await + .caused_by(trc::location!())? { // Write changes let mut changelog = ChangeLogBuilder::new(); changelog.log_delete(Collection::SieveScript, document_id); - self.jmap.commit_changes(account_id, changelog).await?; + self.jmap + .commit_changes(account_id, changelog) + .await + .caused_by(trc::location!())?; Ok(StatusResponse::ok("Deleted.").into_bytes()) } else { - Err(StatusResponse::no("You may not delete an active script") - .with_code(ResponseCode::Active)) + Err(trc::Cause::ManageSieve + .into_err() + .details("You may not delete an active script") + .code(ResponseCode::Active)) } } } diff --git a/crates/managesieve/src/op/getscript.rs b/crates/managesieve/src/op/getscript.rs index 5988502f..ac3056aa 100644 --- a/crates/managesieve/src/op/getscript.rs +++ b/crates/managesieve/src/op/getscript.rs @@ -11,17 +11,22 @@ use jmap_proto::{ types::{collection::Collection, property::Property, value::Value}, }; use tokio::io::{AsyncRead, AsyncWrite}; +use trc::AddContext; use crate::core::{Command, ResponseCode, Session, StatusResponse}; impl<T: AsyncRead + AsyncWrite> Session<T> { - pub async fn handle_getscript(&mut self, request: Request<Command>) -> super::OpResult { + pub async fn handle_getscript(&mut self, request: Request<Command>) -> trc::Result<Vec<u8>> { let name = request .tokens .into_iter() .next() .and_then(|s| s.unwrap_string().ok()) - .ok_or_else(|| StatusResponse::no("Expected script name as a parameter."))?; + .ok_or_else(|| { + trc::Cause::ManageSieve + .into_err() + .details("Expected script name as a parameter.") + })?; let account_id = self.state.access_token().primary_id(); let document_id = self.get_script_id(account_id, &name).await?; let (blob_section, blob_hash) = self @@ -32,21 +37,32 @@ impl<T: AsyncRead + AsyncWrite> Session<T> { document_id, Property::Value, ) - .await? + .await + .caused_by(trc::location!())? .ok_or_else(|| { - StatusResponse::no("Script not found").with_code(ResponseCode::NonExistent) + trc::Cause::ManageSieve + .into_err() + .details("Script not found") + .code(ResponseCode::NonExistent) })? .blob_id() .and_then(|id| (id.section.as_ref()?.clone(), id.hash.clone()).into()) .ok_or_else(|| { - StatusResponse::no("Failed to retrieve blobId").with_code(ResponseCode::TryLater) + trc::Cause::ManageSieve + .into_err() + .details("Failed to retrieve blobId") + .code(ResponseCode::TryLater) })?; let script = self .jmap .get_blob_section(&blob_hash, &blob_section) - .await? + .await + .caused_by(trc::location!())? .ok_or_else(|| { - StatusResponse::no("Script blob not found").with_code(ResponseCode::NonExistent) + trc::Cause::ManageSieve + .into_err() + .details("Script blob not found") + .code(ResponseCode::NonExistent) })?; debug_assert_eq!(script.len(), blob_section.size); diff --git a/crates/managesieve/src/op/havespace.rs b/crates/managesieve/src/op/havespace.rs index a8237f68..5534b8d1 100644 --- a/crates/managesieve/src/op/havespace.rs +++ b/crates/managesieve/src/op/havespace.rs @@ -6,22 +6,35 @@ use imap_proto::receiver::Request; use tokio::io::{AsyncRead, AsyncWrite}; +use trc::AddContext; use crate::core::{Command, ResponseCode, Session, StatusResponse}; impl<T: AsyncRead + AsyncWrite> Session<T> { - pub async fn handle_havespace(&mut self, request: Request<Command>) -> crate::op::OpResult { + pub async fn handle_havespace(&mut self, request: Request<Command>) -> trc::Result<Vec<u8>> { let mut tokens = request.tokens.into_iter(); let name = tokens .next() .and_then(|s| s.unwrap_string().ok()) - .ok_or_else(|| StatusResponse::no("Expected script name as a parameter."))?; + .ok_or_else(|| { + trc::Cause::ManageSieve + .into_err() + .details("Expected script name as a parameter.") + })?; let size: usize = tokens .next() .and_then(|s| s.unwrap_string().ok()) - .ok_or_else(|| StatusResponse::no("Expected script size as a parameter."))? + .ok_or_else(|| { + trc::Cause::ManageSieve + .into_err() + .details("Expected script size as a parameter.") + })? .parse::<usize>() - .map_err(|_| StatusResponse::no("Invalid size parameter."))?; + .map_err(|_| { + trc::Cause::ManageSieve + .into_err() + .details("Invalid size parameter.") + })?; // Validate name let access_token = self.state.access_token(); @@ -30,12 +43,20 @@ impl<T: AsyncRead + AsyncWrite> Session<T> { // Validate quota if access_token.quota == 0 - || size as i64 + self.jmap.get_used_quota(account_id).await? + || size as i64 + + self + .jmap + .get_used_quota(account_id) + .await + .caused_by(trc::location!())? <= access_token.quota as i64 { Ok(StatusResponse::ok("").into_bytes()) } else { - Err(StatusResponse::no("Quota exceeded.").with_code(ResponseCode::QuotaMaxSize)) + Err(trc::Cause::ManageSieve + .into_err() + .details("Quota exceeded.") + .code(ResponseCode::QuotaMaxSize)) } } } diff --git a/crates/managesieve/src/op/listscripts.rs b/crates/managesieve/src/op/listscripts.rs index a4dbe622..93e9653b 100644 --- a/crates/managesieve/src/op/listscripts.rs +++ b/crates/managesieve/src/op/listscripts.rs @@ -9,16 +9,18 @@ use jmap_proto::{ types::{collection::Collection, property::Property, value::Value}, }; use tokio::io::{AsyncRead, AsyncWrite}; +use trc::AddContext; use crate::core::{Session, StatusResponse}; impl<T: AsyncRead + AsyncWrite> Session<T> { - pub async fn handle_listscripts(&mut self) -> super::OpResult { + pub async fn handle_listscripts(&mut self) -> trc::Result<Vec<u8>> { let account_id = self.state.access_token().primary_id(); let document_ids = self .jmap .get_document_ids(account_id, Collection::SieveScript) - .await? + .await + .caused_by(trc::location!())? .unwrap_or_default(); if document_ids.is_empty() { @@ -36,7 +38,8 @@ impl<T: AsyncRead + AsyncWrite> Session<T> { document_id, Property::Value, ) - .await? + .await + .caused_by(trc::location!())? { response.push(b'\"'); if let Some(name) = script.get(&Property::Name).as_string() { diff --git a/crates/managesieve/src/op/logout.rs b/crates/managesieve/src/op/logout.rs index 60e68df2..05550827 100644 --- a/crates/managesieve/src/op/logout.rs +++ b/crates/managesieve/src/op/logout.rs @@ -9,11 +9,12 @@ use tokio::io::{AsyncRead, AsyncWrite}; use crate::core::{Session, StatusResponse}; impl<T: AsyncRead + AsyncWrite> Session<T> { - pub async fn handle_logout(&mut self) -> super::OpResult { - Err(StatusResponse::ok(concat!( + pub async fn handle_logout(&mut self) -> trc::Result<Vec<u8>> { + Ok(StatusResponse::ok(concat!( "Stalwart ManageSieve v", env!("CARGO_PKG_VERSION"), " bids you farewell." - ))) + )) + .into_bytes()) } } diff --git a/crates/managesieve/src/op/mod.rs b/crates/managesieve/src/op/mod.rs index 3412f3c7..ff3808f4 100644 --- a/crates/managesieve/src/op/mod.rs +++ b/crates/managesieve/src/op/mod.rs @@ -4,9 +4,9 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use jmap_proto::error::method::MethodError; +use common::listener::SessionStream; -use crate::core::StatusResponse; +use crate::core::{Session, StatusResponse}; pub mod authenticate; pub mod capability; @@ -21,10 +21,8 @@ pub mod putscript; pub mod renamescript; pub mod setactive; -impl From<MethodError> for StatusResponse { - fn from(_: MethodError) -> Self { - StatusResponse::database_failure() +impl<T: SessionStream> Session<T> { + pub async fn handle_start_tls(&self) -> trc::Result<Vec<u8>> { + Ok(StatusResponse::ok("Begin TLS negotiation now").into_bytes()) } } - -pub type OpResult = std::result::Result<Vec<u8>, StatusResponse>; diff --git a/crates/managesieve/src/op/noop.rs b/crates/managesieve/src/op/noop.rs index 7137d08e..c1d13669 100644 --- a/crates/managesieve/src/op/noop.rs +++ b/crates/managesieve/src/op/noop.rs @@ -10,7 +10,7 @@ use tokio::io::{AsyncRead, AsyncWrite}; use crate::core::{Command, ResponseCode, Session, StatusResponse}; impl<T: AsyncRead + AsyncWrite> Session<T> { - pub async fn handle_noop(&mut self, request: Request<Command>) -> super::OpResult { + pub async fn handle_noop(&mut self, request: Request<Command>) -> trc::Result<Vec<u8>> { Ok(if let Some(tag) = request .tokens .into_iter() diff --git a/crates/managesieve/src/op/putscript.rs b/crates/managesieve/src/op/putscript.rs index 2e16021f..07847ece 100644 --- a/crates/managesieve/src/op/putscript.rs +++ b/crates/managesieve/src/op/putscript.rs @@ -17,21 +17,30 @@ use store::{ BlobClass, }; use tokio::io::{AsyncRead, AsyncWrite}; +use trc::AddContext; use crate::core::{Command, ResponseCode, Session, StatusResponse}; impl<T: AsyncRead + AsyncWrite> Session<T> { - pub async fn handle_putscript(&mut self, request: Request<Command>) -> super::OpResult { + pub async fn handle_putscript(&mut self, request: Request<Command>) -> trc::Result<Vec<u8>> { let mut tokens = request.tokens.into_iter(); let name = tokens .next() .and_then(|s| s.unwrap_string().ok()) - .ok_or_else(|| StatusResponse::no("Expected script name as a parameter."))? + .ok_or_else(|| { + trc::Cause::ManageSieve + .into_err() + .details("Expected script name as a parameter.") + })? .trim() .to_string(); let mut script_bytes = tokens .next() - .ok_or_else(|| StatusResponse::no("Expected script as a parameter."))? + .ok_or_else(|| { + trc::Cause::ManageSieve + .into_err() + .details("Expected script as a parameter.") + })? .unwrap_bytes(); let script_size = script_bytes.len() as i64; @@ -45,22 +54,28 @@ impl<T: AsyncRead + AsyncWrite> Session<T> { access_token.quota as i64, script_bytes.len() as i64, ) - .await? + .await + .caused_by(trc::location!())? { - return Err(StatusResponse::no("Quota exceeded.").with_code(ResponseCode::Quota)); + return Err(trc::Cause::ManageSieve + .into_err() + .details("Quota exceeded.") + .code(ResponseCode::Quota)); } if self .jmap .get_document_ids(account_id, Collection::SieveScript) - .await? + .await + .caused_by(trc::location!())? .map(|ids| ids.len() as usize) .unwrap_or(0) > self.jmap.core.jmap.sieve_max_scripts { - return Err( - StatusResponse::no("Too many scripts.").with_code(ResponseCode::QuotaMaxScripts) - ); + return Err(trc::Cause::ManageSieve + .into_err() + .details("Too many scripts.") + .code(ResponseCode::QuotaMaxScripts)); } // Compile script @@ -76,9 +91,12 @@ impl<T: AsyncRead + AsyncWrite> Session<T> { } Err(err) => { return Err(if let ErrorType::ScriptTooLong = &err.error_type() { - StatusResponse::no(err.to_string()).with_code(ResponseCode::QuotaMaxSize) + trc::Cause::ManageSieve + .into_err() + .details(err.to_string()) + .code(ResponseCode::QuotaMaxSize) } else { - StatusResponse::no(err.to_string()) + trc::Cause::ManageSieve.into_err().details(err.to_string()) }); } } @@ -94,20 +112,27 @@ impl<T: AsyncRead + AsyncWrite> Session<T> { document_id, Property::Value, ) - .await? + .await + .caused_by(trc::location!())? .ok_or_else(|| { - StatusResponse::no("Script not found").with_code(ResponseCode::NonExistent) + trc::Cause::ManageSieve + .into_err() + .details("Script not found") + .code(ResponseCode::NonExistent) })?; let prev_blob_id = script.inner.blob_id().ok_or_else(|| { - StatusResponse::no("Internal error while obtaining blobId") - .with_code(ResponseCode::TryLater) + trc::Cause::ManageSieve + .into_err() + .details("Internal error while obtaining blobId") + .code(ResponseCode::TryLater) })?; // Write script blob let blob_id = BlobId::new( self.jmap .put_blob(account_id, &script_bytes, false) - .await? + .await + .caused_by(trc::location!())? .hash, BlobClass::Linked { account_id, @@ -152,7 +177,10 @@ impl<T: AsyncRead + AsyncWrite> Session<T> { .with_property(Property::BlobId, Value::BlobId(blob_id)), ), ); - self.jmap.write_batch(batch).await?; + self.jmap + .write_batch(batch) + .await + .caused_by(trc::location!())?; } else { // Write script blob let blob_id = BlobId::new( @@ -190,25 +218,28 @@ impl<T: AsyncRead + AsyncWrite> Session<T> { .with_property(Property::BlobId, Value::BlobId(blob_id)), ), ); - self.jmap.write_batch(batch).await?; + self.jmap + .write_batch(batch) + .await + .caused_by(trc::location!())?; } Ok(StatusResponse::ok("Success.").into_bytes()) } - pub async fn validate_name( - &self, - account_id: u32, - name: &str, - ) -> Result<Option<u32>, StatusResponse> { + pub async fn validate_name(&self, account_id: u32, name: &str) -> trc::Result<Option<u32>> { if name.is_empty() { - Err(StatusResponse::no("Script name cannot be empty.")) + Err(trc::Cause::ManageSieve + .into_err() + .details("Script name cannot be empty.")) } else if name.len() > self.jmap.core.jmap.sieve_max_script_name { - Err(StatusResponse::no("Script name is too long.")) + Err(trc::Cause::ManageSieve + .into_err() + .details("Script name is too long.")) } else if name.eq_ignore_ascii_case("vacation") { - Err(StatusResponse::no( - "The 'vacation' name is reserved, please use a different name.", - )) + Err(trc::Cause::ManageSieve + .into_err() + .details("The 'vacation' name is reserved, please use a different name.")) } else { Ok(self .jmap @@ -217,7 +248,8 @@ impl<T: AsyncRead + AsyncWrite> Session<T> { Collection::SieveScript, vec![Filter::eq(Property::Name, name)], ) - .await? + .await + .caused_by(trc::location!())? .results .min()) } diff --git a/crates/managesieve/src/op/renamescript.rs b/crates/managesieve/src/op/renamescript.rs index d29d8e8f..48871e68 100644 --- a/crates/managesieve/src/op/renamescript.rs +++ b/crates/managesieve/src/op/renamescript.rs @@ -7,28 +7,36 @@ use imap_proto::receiver::Request; use jmap::sieve::set::SCHEMA; use jmap_proto::{ - error::method::MethodError, object::{index::ObjectIndexBuilder, Object}, types::{collection::Collection, property::Property, value::Value}, }; use store::write::{assert::HashedValue, log::ChangeLogBuilder, BatchBuilder}; use tokio::io::{AsyncRead, AsyncWrite}; +use trc::AddContext; use crate::core::{Command, ResponseCode, Session, StatusResponse}; impl<T: AsyncRead + AsyncWrite> Session<T> { - pub async fn handle_renamescript(&mut self, request: Request<Command>) -> super::OpResult { + pub async fn handle_renamescript(&mut self, request: Request<Command>) -> trc::Result<Vec<u8>> { let mut tokens = request.tokens.into_iter(); let name = tokens .next() .and_then(|s| s.unwrap_string().ok()) - .ok_or_else(|| StatusResponse::no("Expected old script name as a parameter."))? + .ok_or_else(|| { + trc::Cause::ManageSieve + .into_err() + .details("Expected old script name as a parameter.") + })? .trim() .to_string(); let new_name = tokens .next() .and_then(|s| s.unwrap_string().ok()) - .ok_or_else(|| StatusResponse::no("Expected new script name as a parameter."))? + .ok_or_else(|| { + trc::Cause::ManageSieve + .into_err() + .details("Expected new script name as a parameter.") + })? .trim() .to_string(); @@ -39,10 +47,10 @@ impl<T: AsyncRead + AsyncWrite> Session<T> { let account_id = self.state.access_token().primary_id(); let document_id = self.get_script_id(account_id, &name).await?; if self.validate_name(account_id, &new_name).await?.is_some() { - return Err(StatusResponse::no(format!( - "A sieve script with name '{name}' already exists.", - )) - .with_code(ResponseCode::AlreadyExists)); + return Err(trc::Cause::ManageSieve + .into_err() + .details(format!("A sieve script with name '{name}' already exists.",)) + .code(ResponseCode::AlreadyExists)); } // Obtain script values @@ -54,9 +62,13 @@ impl<T: AsyncRead + AsyncWrite> Session<T> { document_id, Property::Value, ) - .await? + .await + .caused_by(trc::location!())? .ok_or_else(|| { - StatusResponse::no("Script not found").with_code(ResponseCode::NonExistent) + trc::Cause::ManageSieve + .into_err() + .details("Script not found") + .code(ResponseCode::NonExistent) })?; // Write record @@ -71,22 +83,16 @@ impl<T: AsyncRead + AsyncWrite> Session<T> { .with_changes(Object::with_capacity(1).with_property(Property::Name, new_name)), ); if !batch.is_empty() { - match self.jmap.write_batch(batch).await { - Ok(_) => { - let mut changelog = ChangeLogBuilder::new(); - changelog.log_update(Collection::SieveScript, document_id); - self.jmap.commit_changes(account_id, changelog).await?; - } - Err(MethodError::ServerUnavailable) => { - return Err(StatusResponse::no( - "Another process modified this script, please try again.", - ) - .with_code(ResponseCode::TryLater)); - } - Err(_) => { - return Err(StatusResponse::database_failure()); - } - } + self.jmap + .write_batch(batch) + .await + .caused_by(trc::location!())?; + let mut changelog = ChangeLogBuilder::new(); + changelog.log_update(Collection::SieveScript, document_id); + self.jmap + .commit_changes(account_id, changelog) + .await + .caused_by(trc::location!())?; } Ok(StatusResponse::ok("Success.").into_bytes()) diff --git a/crates/managesieve/src/op/setactive.rs b/crates/managesieve/src/op/setactive.rs index 82dd51ac..5cad960e 100644 --- a/crates/managesieve/src/op/setactive.rs +++ b/crates/managesieve/src/op/setactive.rs @@ -8,17 +8,22 @@ use imap_proto::receiver::Request; use jmap_proto::types::collection::Collection; use store::write::log::ChangeLogBuilder; use tokio::io::{AsyncRead, AsyncWrite}; +use trc::AddContext; use crate::core::{Command, Session, StatusResponse}; impl<T: AsyncRead + AsyncWrite> Session<T> { - pub async fn handle_setactive(&mut self, request: Request<Command>) -> super::OpResult { + pub async fn handle_setactive(&mut self, request: Request<Command>) -> trc::Result<Vec<u8>> { let name = request .tokens .into_iter() .next() .and_then(|s| s.unwrap_string().ok()) - .ok_or_else(|| StatusResponse::no("Expected script name as a parameter."))?; + .ok_or_else(|| { + trc::Cause::ManageSieve + .into_err() + .details("Expected script name as a parameter.") + })?; // De/activate script let account_id = self.state.access_token().primary_id(); @@ -32,7 +37,8 @@ impl<T: AsyncRead + AsyncWrite> Session<T> { None }, ) - .await?; + .await + .caused_by(trc::location!())?; // Write changes if !changes.is_empty() { @@ -40,7 +46,10 @@ impl<T: AsyncRead + AsyncWrite> Session<T> { for (document_id, _) in changes { changelog.log_update(Collection::SieveScript, document_id); } - self.jmap.commit_changes(account_id, changelog).await?; + self.jmap + .commit_changes(account_id, changelog) + .await + .caused_by(trc::location!())?; } Ok(StatusResponse::ok("Success").into_bytes()) } diff --git a/crates/pop3/src/client.rs b/crates/pop3/src/client.rs index 6f5dd035..1bf7f5b5 100644 --- a/crates/pop3/src/client.rs +++ b/crates/pop3/src/client.rs @@ -4,8 +4,9 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::listener::SessionStream; +use common::listener::{SessionResult, SessionStream}; use mail_send::Credentials; +use trc::AddContext; use crate::{ protocol::{request::Error, response::Response, Command, Mechanism}, @@ -13,7 +14,7 @@ use crate::{ }; impl<T: SessionStream> Session<T> { - pub async fn ingest(&mut self, bytes: &[u8]) -> Result<bool, ()> { + pub async fn ingest(&mut self, bytes: &[u8]) -> SessionResult { /*let tmp = "dd"; for line in String::from_utf8_lossy(bytes).split("\r\n") { println!("<- {:?}", &line[..std::cmp::min(line.len(), 100)]); @@ -46,20 +47,21 @@ impl<T: SessionStream> Session<T> { break; } Err(Error::Parse(err)) => { - requests.push(Err(err)); + requests.push(Err(trc::Cause::Pop3.into_err().details(err))); } } } for request in requests { - match request { + let mut result = None; + let maybe_err = match request { Ok(command) => match self.validate_request(command).await { Ok(command) => match command { Command::User { name } => { if let State::NotAuthenticated { username, .. } = &mut self.state { let response = format!("{name} is a valid mailbox"); *username = Some(name); - self.write_ok(response).await?; + self.write_ok(response).await } else { unreachable!(); } @@ -75,30 +77,21 @@ impl<T: SessionStream> Session<T> { username, secret: string, }) - .await?; + .await } Command::Quit => { - self.handle_quit().await?; - } - Command::Stat => self.handle_stat().await?, - Command::List { msg } => { - self.handle_list(msg).await?; - } - Command::Retr { msg } => { - self.handle_fetch(msg, None).await?; - } - Command::Dele { msg } => self.handle_dele(vec![msg]).await?, - Command::DeleMany { msgs } => self.handle_dele(msgs).await?, - Command::Top { msg, n } => { - self.handle_fetch(msg, n.into()).await?; - } - Command::Uidl { msg } => self.handle_uidl(msg).await?, - Command::Noop => { - self.write_ok("NOOP").await?; - } - Command::Rset => { - self.handle_rset().await?; - } + result = SessionResult::Close.into(); + self.handle_quit().await + } + Command::Stat => self.handle_stat().await, + Command::List { msg } => self.handle_list(msg).await, + Command::Retr { msg } => self.handle_fetch(msg, None).await, + Command::Dele { msg } => self.handle_dele(vec![msg]).await, + Command::DeleMany { msgs } => self.handle_dele(msgs).await, + Command::Top { msg, n } => self.handle_fetch(msg, n.into()).await, + Command::Uidl { msg } => self.handle_uidl(msg).await, + Command::Noop => self.write_ok("NOOP").await, + Command::Rset => self.handle_rset().await, Command::Capa => { let mechanisms = if self.stream.is_tls() || self.jmap.core.imap.allow_plain_auth { @@ -114,39 +107,48 @@ impl<T: SessionStream> Session<T> { } .serialize(), ) - .await?; + .await } Command::Stls => { - self.write_ok("Begin TLS negotiation now").await?; - return Ok(false); - } - Command::Utf8 => { - self.write_ok("UTF8 enabled").await?; + result = SessionResult::UpgradeTls.into(); + self.write_ok("Begin TLS negotiation now").await } + Command::Utf8 => self.write_ok("UTF8 enabled").await, Command::Auth { mechanism, params } => { - self.handle_sasl(mechanism, params).await?; + self.handle_sasl(mechanism, params).await } Command::Apop { .. } => { - self.write_err("APOP not supported.").await?; + self.write_err( + trc::Cause::Pop3.into_err().details("APOP not supported."), + ) + .await } }, - Err(err) => { - self.write_err(err).await?; - } + Err(err) => self.write_err(err).await, }, - Err(err) => { - self.write_err(err).await?; + Err(err) => self.write_err(err).await, + }; + + if let Err(err) = maybe_err { + tracing::error!(parent: &self.span, "Error: {:?}", err); + if err.matches(trc::Cause::Network) { + return SessionResult::Close; + } else if let Err(err) = self.write_err(err).await { + tracing::error!(parent: &self.span, "Error: {:?}", err); + return SessionResult::Close; } + } else if let Some(result) = result { + return result; } } - Ok(true) + SessionResult::Continue } async fn validate_request( &self, command: Command<String, Mechanism>, - ) -> Result<Command<String, Mechanism>, &'static str> { + ) -> trc::Result<Command<String, Mechanism>> { match &command { Command::Capa | Command::Quit | Command::Noop => Ok(command), Command::Auth { @@ -161,27 +163,35 @@ impl<T: SessionStream> Session<T> { if !matches!(command, Command::Pass { .. }) || username.is_some() { Ok(command) } else { - Err("Username was not provided.") + Err(trc::Cause::Pop3 + .into_err() + .details("Username was not provided.")) } } else { - Err("Cannot authenticate over plain-text.") + Err(trc::Cause::Pop3 + .into_err() + .details("Cannot authenticate over plain-text.")) } } else { - Err("Already authenticated.") + Err(trc::Cause::Pop3 + .into_err() + .details("Already authenticated.")) } } Command::Auth { .. } => { if let State::NotAuthenticated { .. } = &self.state { Ok(command) } else { - Err("Already authenticated.") + Err(trc::Cause::Pop3 + .into_err() + .details("Already authenticated.")) } } Command::Stls => { if !self.stream.is_tls() { Ok(command) } else { - Err("Already in TLS mode.") + Err(trc::Cause::Pop3.into_err().details("Already in TLS mode.")) } } @@ -196,7 +206,7 @@ impl<T: SessionStream> Session<T> { | Command::Rset => { if let State::Authenticated { mailbox, .. } = &self.state { if let Some(rate) = &self.jmap.core.imap.rate_requests { - match self + if self .jmap .core .storage @@ -207,16 +217,18 @@ impl<T: SessionStream> Session<T> { true, ) .await + .caused_by(trc::location!())? + .is_none() { - Ok(None) => Ok(command), - Ok(Some(_)) => Err("Too many requests"), - Err(_) => Err("Internal server error"), + Ok(command) + } else { + Err(trc::LimitCause::TooManyRequests.into_err()) } } else { Ok(command) } } else { - Err("Not authenticated.") + Err(trc::Cause::Pop3.into_err().details("Not authenticated.")) } } } diff --git a/crates/pop3/src/mailbox.rs b/crates/pop3/src/mailbox.rs index 157164f3..b3e1d06a 100644 --- a/crates/pop3/src/mailbox.rs +++ b/crates/pop3/src/mailbox.rs @@ -9,13 +9,13 @@ use std::collections::BTreeMap; use common::listener::SessionStream; use jmap::mailbox::{UidMailbox, INBOX_ID}; use jmap_proto::{ - error::method::MethodError, object::Object, types::{collection::Collection, property::Property, value::Value}, }; use store::{ ahash::AHashMap, write::key::DeserializeBigEndian, IndexKey, IterateParams, Serialize, U32_LEN, }; +use trc::AddContext; use crate::Session; @@ -46,7 +46,8 @@ impl<T: SessionStream> Session<T> { Property::MailboxIds, INBOX_ID, ) - .await? + .await + .caused_by(trc::location!())? .unwrap_or_default(); if message_ids.is_empty() { @@ -57,7 +58,10 @@ impl<T: SessionStream> Session<T> { let mut message_sizes = AHashMap::new(); // Obtain UID validity - self.jmap.mailbox_get_or_create(account_id).await?; + self.jmap + .mailbox_get_or_create(account_id) + .await + .caused_by(trc::location!())?; let uid_validity = self .jmap .get_property::<Object<Value>>( @@ -66,16 +70,15 @@ impl<T: SessionStream> Session<T> { INBOX_ID, &Property::Value, ) - .await? + .await + .caused_by(trc::location!())? .and_then(|obj| obj.get(&Property::Cid).as_uint()) .ok_or_else(|| { - tracing::debug!(event = "error", - context = "store", - account_id = account_id, - collection = ?Collection::Mailbox, - mailbox_id = INBOX_ID, - "Failed to obtain uid validity"); - MethodError::ServerPartialFail + trc::StoreCause::Unexpected + .caused_by(trc::location!()) + .details("Failed to obtain UID validity") + .account_id(account_id) + .document_id(INBOX_ID) }) .map(|v| v as u32)?; @@ -115,13 +118,7 @@ impl<T: SessionStream> Session<T> { }, ) .await - .map_err(|err| { - tracing::error!(context = "fetch_mailbox", - reason = ?err, - "Failed to iterate message sizes"); - - MethodError::ServerPartialFail - })?; + .caused_by(trc::location!())?; // Sort by UID for (message_id, uid_mailbox) in self @@ -132,7 +129,8 @@ impl<T: SessionStream> Session<T> { &message_ids, Property::MailboxIds, ) - .await? + .await + .caused_by(trc::location!())? .into_iter() { // Make sure the message is still in Inbox diff --git a/crates/pop3/src/op/authenticate.rs b/crates/pop3/src/op/authenticate.rs index 123f0dd9..93bc2422 100644 --- a/crates/pop3/src/op/authenticate.rs +++ b/crates/pop3/src/op/authenticate.rs @@ -7,7 +7,6 @@ use common::{ config::server::ServerProtocol, listener::{limiter::ConcurrencyLimiter, SessionStream}, - AuthFailureReason, AuthResult, }; use imap::op::authenticate::{decode_challenge_oauth, decode_challenge_plain}; use jmap::auth::rate_limit::ConcurrencyLimiters; @@ -25,11 +24,11 @@ impl<T: SessionStream> Session<T> { &mut self, mechanism: Mechanism, mut params: Vec<String>, - ) -> Result<(), ()> { + ) -> trc::Result<()> { match mechanism { Mechanism::Plain | Mechanism::OAuthBearer => { if !params.is_empty() { - let result = base64_decode(params.pop().unwrap().as_bytes()) + let credentials = base64_decode(params.pop().unwrap().as_bytes()) .ok_or("Failed to decode challenge.") .and_then(|challenge| { if mechanism == Mechanism::Plain { @@ -37,12 +36,10 @@ impl<T: SessionStream> Session<T> { } else { decode_challenge_oauth(&challenge) } - }); + }) + .map_err(|err| trc::AuthCause::Error.into_err().details(err))?; - match result { - Ok(credentials) => self.handle_auth(credentials).await, - Err(err) => self.write_err(err).await, - } + self.handle_auth(credentials).await } else { // TODO: This hack is temporary until the SASL library is developed self.receiver.state = request::State::Argument { @@ -57,111 +54,58 @@ impl<T: SessionStream> Session<T> { self.write_bytes("+\r\n").await } } - _ => { - self.write_err("Authentication mechanism not supported.") - .await - } + _ => Err(trc::AuthCause::Error + .into_err() + .details("Authentication mechanism not supported.")), } } - pub async fn handle_auth(&mut self, credentials: Credentials<String>) -> Result<(), ()> { + pub async fn handle_auth(&mut self, credentials: Credentials<String>) -> trc::Result<()> { // Throttle authentication requests - if self - .jmap - .is_auth_allowed_soft(&self.remote_addr) - .await - .is_err() - { - tracing::debug!(parent: &self.span, - event = "disconnect", - "Too many authentication attempts, disconnecting.", - ); - - self.write_err("Too many authentication requests from this IP address.") - .await?; - return Err(()); - } + let todo = "disconnect in all protocols when this error is returned"; + self.jmap.is_auth_allowed_soft(&self.remote_addr).await?; // Authenticate let mut is_totp_error = false; let access_token = match credentials { Credentials::Plain { username, secret } | Credentials::XOauth2 { username, secret } => { - match self - .jmap + self.jmap .authenticate_plain(&username, &secret, self.remote_addr, ServerProtocol::Pop3) - .await - { - AuthResult::Success(token) => Some(token), - AuthResult::Failure( - AuthFailureReason::InvalidCredentials | AuthFailureReason::InternalError(_), - ) => None, - AuthResult::Failure(AuthFailureReason::MissingTotp) => { - is_totp_error = true; - None - } - AuthResult::Failure(AuthFailureReason::Banned) => { - self.write_err("Too many authentication requests from this IP address.") - .await?; - return Err(()); - } - } + .await? } Credentials::OAuthBearer { token } => { - match self + let (account_id, _, _) = self .jmap .validate_access_token("access_token", &token) - .await - { - Ok((account_id, _, _)) => self.jmap.get_access_token(account_id).await, - Err(err) => { - tracing::debug!( - parent: &self.span, - context = "authenticate", - err = err, - "Failed to validate access token." - ); - None - } - } + .await?; + self.jmap.get_access_token(account_id).await? } }; - if let Some(access_token) = access_token { - // Enforce concurrency limits - let in_flight = match self - .get_concurrency_limiter(access_token.primary_id()) - .map(|limiter| limiter.concurrent_requests.is_allowed()) - { - Some(Some(limiter)) => Some(limiter), - None => None, - Some(None) => { - tracing::debug!(parent: &self.span, - event = "disconnect", - "Too many concurrent connection.", - ); - self.write_err("Too many concurrent connections.").await?; - return Err(()); - } - }; + // Enforce concurrency limits + let in_flight = match self + .get_concurrency_limiter(access_token.primary_id()) + .map(|limiter| limiter.concurrent_requests.is_allowed()) + { + Some(Some(limiter)) => Some(limiter), + None => None, + Some(None) => { + return Err(trc::LimitCause::ConcurrentRequest.into_err()); + } + }; - // Cache access token - let access_token = Arc::new(access_token); - self.jmap.cache_access_token(access_token.clone()); + // Cache access token + let access_token = Arc::new(access_token); + self.jmap.cache_access_token(access_token.clone()); - // Fetch mailbox - match self.fetch_mailbox(access_token.primary_id()).await { - Ok(mailbox) => { - // Create session - self.state = State::Authenticated { in_flight, mailbox }; + // Fetch mailbox + let mailbox = self.fetch_mailbox(access_token.primary_id()).await?; - self.write_ok("Authentication successful").await - } - Err(_) => { - self.write_err("Temporary server failure").await?; - Err(()) - } - } - } else { + // Create session + let todo = "fix below"; + self.state = State::Authenticated { in_flight, mailbox }; + self.write_ok("Authentication successful").await + /*} else { match &self.state { State::NotAuthenticated { auth_failures, @@ -188,7 +132,7 @@ impl<T: SessionStream> Session<T> { Err(()) } } - } + }*/ } pub fn get_concurrency_limiter(&self, account_id: u32) -> Option<Arc<ConcurrencyLimiters>> { diff --git a/crates/pop3/src/op/delete.rs b/crates/pop3/src/op/delete.rs index 7f127b0a..fba19ddc 100644 --- a/crates/pop3/src/op/delete.rs +++ b/crates/pop3/src/op/delete.rs @@ -7,11 +7,12 @@ use common::listener::SessionStream; use jmap_proto::types::{state::StateChange, type_state::DataType}; use store::roaring::RoaringBitmap; +use trc::AddContext; -use crate::{Session, State}; +use crate::{protocol::response::Response, Session, State}; impl<T: SessionStream> Session<T> { - pub async fn handle_dele(&mut self, msgs: Vec<u32>) -> Result<(), ()> { + pub async fn handle_dele(&mut self, msgs: Vec<u32>) -> trc::Result<()> { let mailbox = self.state.mailbox_mut(); let mut response = Vec::new(); @@ -33,7 +34,7 @@ impl<T: SessionStream> Session<T> { self.write_bytes(response).await } - pub async fn handle_rset(&mut self) -> Result<(), ()> { + pub async fn handle_rset(&mut self) -> trc::Result<()> { let mut count = 0; let mailbox = self.state.mailbox_mut(); for message in &mut mailbox.messages { @@ -45,7 +46,7 @@ impl<T: SessionStream> Session<T> { self.write_ok(format!("{count} messages undeleted")).await } - pub async fn handle_quit(&mut self) -> Result<(), ()> { + pub async fn handle_quit(&mut self) -> trc::Result<()> { if let State::Authenticated { mailbox, .. } = &self.state { let mut deleted = RoaringBitmap::new(); for message in &mailbox.messages { @@ -56,39 +57,38 @@ impl<T: SessionStream> Session<T> { if !deleted.is_empty() { let num_deleted = deleted.len(); - match self + let (changes, not_deleted) = self .jmap .emails_tombstone(mailbox.account_id, deleted) .await - { - Ok((changes, not_deleted)) => { - if !changes.is_empty() { - if let Ok(change_id) = - self.jmap.commit_changes(mailbox.account_id, changes).await - { - self.jmap - .broadcast_state_change( - StateChange::new(mailbox.account_id) - .with_change(DataType::Email, change_id) - .with_change(DataType::Mailbox, change_id) - .with_change(DataType::Thread, change_id), - ) - .await; - } - } - if not_deleted.is_empty() { - self.write_ok(format!( - "Stalwart POP3 bids you farewell ({num_deleted} messages deleted)." - )) - .await?; - } else { - self.write_err("Some messages could not be deleted").await?; - } - } - Err(_) => { - self.write_err("Failed to delete messages").await?; + .caused_by(trc::location!())?; + + if !changes.is_empty() { + if let Ok(change_id) = + self.jmap.commit_changes(mailbox.account_id, changes).await + { + self.jmap + .broadcast_state_change( + StateChange::new(mailbox.account_id) + .with_change(DataType::Email, change_id) + .with_change(DataType::Mailbox, change_id) + .with_change(DataType::Thread, change_id), + ) + .await; } } + if not_deleted.is_empty() { + self.write_ok(format!( + "Stalwart POP3 bids you farewell ({num_deleted} messages deleted)." + )) + .await?; + } else { + self.write_bytes( + Response::Err::<u32>("Some messages could not be deleted".into()) + .serialize(), + ) + .await?; + } } else { self.write_ok("Stalwart POP3 bids you farewell (no messages deleted).") .await?; @@ -97,6 +97,6 @@ impl<T: SessionStream> Session<T> { self.write_ok("Stalwart POP3 bids you farewell.").await?; } - Err(()) + Ok(()) } } diff --git a/crates/pop3/src/op/fetch.rs b/crates/pop3/src/op/fetch.rs index 19b48fe0..91f8043d 100644 --- a/crates/pop3/src/op/fetch.rs +++ b/crates/pop3/src/op/fetch.rs @@ -8,14 +8,15 @@ use common::listener::SessionStream; use jmap::email::metadata::MessageMetadata; use jmap_proto::types::{collection::Collection, property::Property}; use store::write::Bincode; +use trc::AddContext; use crate::{protocol::response::Response, Session}; impl<T: SessionStream> Session<T> { - pub async fn handle_fetch(&mut self, msg: u32, lines: Option<u32>) -> Result<(), ()> { + pub async fn handle_fetch(&mut self, msg: u32, lines: Option<u32>) -> trc::Result<()> { let mailbox = self.state.mailbox(); if let Some(message) = mailbox.messages.get(msg.saturating_sub(1) as usize) { - match self + if let Some(metadata) = self .jmap .get_property::<Bincode<MessageMetadata>>( mailbox.account_id, @@ -24,38 +25,39 @@ impl<T: SessionStream> Session<T> { &Property::BodyStructure, ) .await + .caused_by(trc::location!())? { - Ok(Some(metadata)) => { - match self - .jmap - .get_blob(&metadata.inner.blob_hash, 0..usize::MAX) - .await - { - Ok(Some(bytes)) => { - self.write_bytes( - Response::Message::<u32> { - bytes, - lines: lines.unwrap_or(0), - } - .serialize(), - ) - .await + if let Some(bytes) = self + .jmap + .get_blob(&metadata.inner.blob_hash, 0..usize::MAX) + .await + .caused_by(trc::location!())? + { + self.write_bytes( + Response::Message::<u32> { + bytes, + lines: lines.unwrap_or(0), } - _ => { - self.write_err( - "Failed to fetch message. Perhaps another session deleted it?", - ) - .await - } - } - } - _ => { - self.write_err("Failed to fetch message. Perhaps another session deleted it?") - .await + .serialize(), + ) + .await + } else { + Err(trc::Cause::Pop3 + .into_err() + .details("Failed to fetch message. Perhaps another session deleted it?") + .caused_by(trc::location!())) } + } else { + Err(trc::Cause::Pop3 + .into_err() + .details("Failed to fetch message. Perhaps another session deleted it?") + .caused_by(trc::location!())) } } else { - self.write_err("No such message").await + Err(trc::Cause::Pop3 + .into_err() + .details("No such message.") + .caused_by(trc::location!())) } } } diff --git a/crates/pop3/src/op/list.rs b/crates/pop3/src/op/list.rs index 0ee40097..ced3014d 100644 --- a/crates/pop3/src/op/list.rs +++ b/crates/pop3/src/op/list.rs @@ -9,13 +9,16 @@ use common::listener::SessionStream; use crate::{protocol::response::Response, Session}; impl<T: SessionStream> Session<T> { - pub async fn handle_list(&mut self, msg: Option<u32>) -> Result<(), ()> { + pub async fn handle_list(&mut self, msg: Option<u32>) -> trc::Result<()> { let mailbox = self.state.mailbox(); if let Some(msg) = msg { if let Some(message) = mailbox.messages.get(msg.saturating_sub(1) as usize) { self.write_ok(format!("{} {}", msg, message.size)).await } else { - self.write_err("No such message").await + Err(trc::Cause::Pop3 + .into_err() + .details("No such message.") + .caused_by(trc::location!())) } } else { self.write_bytes( @@ -26,14 +29,17 @@ impl<T: SessionStream> Session<T> { } } - pub async fn handle_uidl(&mut self, msg: Option<u32>) -> Result<(), ()> { + pub async fn handle_uidl(&mut self, msg: Option<u32>) -> trc::Result<()> { let mailbox = self.state.mailbox(); if let Some(msg) = msg { if let Some(message) = mailbox.messages.get(msg.saturating_sub(1) as usize) { self.write_ok(format!("{} {}{}", msg, mailbox.uid_validity, message.uid)) .await } else { - self.write_err("No such message").await + Err(trc::Cause::Pop3 + .into_err() + .details("No such message.") + .caused_by(trc::location!())) } } else { self.write_bytes( @@ -50,7 +56,7 @@ impl<T: SessionStream> Session<T> { } } - pub async fn handle_stat(&mut self) -> Result<(), ()> { + pub async fn handle_stat(&mut self) -> trc::Result<()> { let mailbox = self.state.mailbox(); self.write_ok(format!("{} {}", mailbox.total, mailbox.size)) .await diff --git a/crates/pop3/src/protocol/response.rs b/crates/pop3/src/protocol/response.rs index f1f9ec69..2b026a8f 100644 --- a/crates/pop3/src/protocol/response.rs +++ b/crates/pop3/src/protocol/response.rs @@ -146,6 +146,25 @@ impl Mechanism { } } +pub trait SerializeResponse { + fn serialize(&self) -> Vec<u8>; +} + +impl SerializeResponse for trc::Error { + fn serialize(&self) -> Vec<u8> { + let todo = "serialize messages properly in all protocols"; + let message = self + .value_as_str(trc::Key::Details) + .or_else(|| self.value_as_str(trc::Key::Reason)) + .unwrap_or("Internal Server Error"); + let mut buf = Vec::with_capacity(message.len() + 6); + buf.extend_from_slice(b"-ERR "); + buf.extend_from_slice(message.as_bytes()); + buf.extend_from_slice(b"\r\n"); + buf + } +} + #[cfg(test)] mod tests { diff --git a/crates/pop3/src/session.rs b/crates/pop3/src/session.rs index ff3f785d..9db33f56 100644 --- a/crates/pop3/src/session.rs +++ b/crates/pop3/src/session.rs @@ -6,12 +6,15 @@ use std::borrow::Cow; -use common::listener::{SessionData, SessionManager, SessionStream}; +use common::listener::{SessionData, SessionManager, SessionResult, SessionStream}; use jmap::JMAP; use tokio_rustls::server::TlsStream; use crate::{ - protocol::{request::Parser, response::Response}, + protocol::{ + request::Parser, + response::{Response, SerializeResponse}, + }, Pop3SessionManager, Session, State, SERVER_GREETING, }; @@ -77,11 +80,11 @@ impl<T: SessionStream> Session<T> { Ok(Ok(bytes_read)) => { if bytes_read > 0 { match self.ingest(&buf[..bytes_read]).await { - Ok(true) => (), - Ok(false) => { + SessionResult::Continue => (), + SessionResult::UpgradeTls => { return true; } - Err(_) => { + SessionResult::Close => { tracing::debug!(parent: &self.span, event = "disconnect", "Disconnecting client."); break; } @@ -129,7 +132,7 @@ impl<T: SessionStream> Session<T> { } impl<T: SessionStream> Session<T> { - pub async fn write_bytes(&mut self, bytes: impl AsRef<[u8]>) -> Result<(), ()> { + pub async fn write_bytes(&mut self, bytes: impl AsRef<[u8]>) -> trc::Result<()> { let bytes = bytes.as_ref(); /*for line in String::from_utf8_lossy(bytes.as_ref()).split("\r\n") { let c = println!("{}", line); @@ -141,22 +144,23 @@ impl<T: SessionStream> Session<T> { size = bytes.len() ); - if let Err(err) = self.stream.write_all(bytes.as_ref()).await { - tracing::trace!(parent: &self.span, "Failed to write to stream: {}", err); - Err(()) - } else { - let _ = self.stream.flush().await; - Ok(()) - } + self.stream + .write_all(bytes.as_ref()) + .await + .map_err(|err| trc::Cause::Network.reason(err).caused_by(trc::location!()))?; + self.stream + .flush() + .await + .map_err(|err| trc::Cause::Network.reason(err).caused_by(trc::location!())) } - pub async fn write_ok(&mut self, message: impl Into<Cow<'static, str>>) -> Result<(), ()> { + pub async fn write_ok(&mut self, message: impl Into<Cow<'static, str>>) -> trc::Result<()> { self.write_bytes(Response::Ok::<u32>(message.into()).serialize()) .await } - pub async fn write_err(&mut self, message: impl Into<Cow<'static, str>>) -> Result<(), ()> { - self.write_bytes(Response::Err::<u32>(message.into()).serialize()) - .await + pub async fn write_err(&mut self, err: trc::Error) -> trc::Result<()> { + tracing::error!(parent: &self.span, "POP3 error: {}", err); + self.write_bytes(err.serialize()).await } } diff --git a/crates/smtp/src/inbound/auth.rs b/crates/smtp/src/inbound/auth.rs index ec33d866..052235e2 100644 --- a/crates/smtp/src/inbound/auth.rs +++ b/crates/smtp/src/inbound/auth.rs @@ -196,7 +196,7 @@ impl<T: SessionStream> Session<T> { return Ok(false); } Err(err) => match err.as_ref() { - trc::Cause::Authentication => { + trc::Cause::Auth(trc::AuthCause::Failed) => { tracing::debug!( parent: &self.span, context = "auth", @@ -208,7 +208,7 @@ impl<T: SessionStream> Session<T> { .auth_error(b"535 5.7.8 Authentication credentials invalid.\r\n") .await; } - trc::Cause::MissingTotp => { + trc::Cause::Auth(trc::AuthCause::MissingTotp) => { tracing::debug!( parent: &self.span, context = "auth", @@ -222,7 +222,7 @@ impl<T: SessionStream> Session<T> { ) .await; } - trc::Cause::Banned => { + trc::Cause::Auth(trc::AuthCause::Banned) => { tracing::debug!( parent: &self.span, context = "auth", diff --git a/crates/smtp/src/inbound/vrfy.rs b/crates/smtp/src/inbound/vrfy.rs index dfc722b7..bf6126fa 100644 --- a/crates/smtp/src/inbound/vrfy.rs +++ b/crates/smtp/src/inbound/vrfy.rs @@ -57,7 +57,7 @@ impl<T: SessionStream> Session<T> { event = "temp-fail", address = &address); - if !err.matches(trc::Cause::Unsupported) { + if !err.matches(trc::Cause::Store(trc::StoreCause::NotSupported)) { self.write(b"252 2.4.3 Unable to verify address at this time.\r\n") .await } else { @@ -122,7 +122,7 @@ impl<T: SessionStream> Session<T> { event = "temp-fail", address = &address); - if !err.matches(trc::Cause::Unsupported) { + if !err.matches(trc::Cause::Store(trc::StoreCause::NotSupported)) { self.write(b"252 2.4.3 Unable to expand mailing list at this time.\r\n") .await } else { diff --git a/crates/smtp/src/queue/spool.rs b/crates/smtp/src/queue/spool.rs index a6758ff1..e3ea8f10 100644 --- a/crates/smtp/src/queue/spool.rs +++ b/crates/smtp/src/queue/spool.rs @@ -127,7 +127,7 @@ impl SMTP { ); match self.core.storage.data.write(batch.build()).await { Ok(_) => Some(event), - Err(err) if err.matches(trc::Cause::AssertValue) => { + Err(err) if err.is_assertion_failure() => { tracing::debug!( context = "queue", event = "locked", diff --git a/crates/smtp/src/reporting/scheduler.rs b/crates/smtp/src/reporting/scheduler.rs index 3b198928..7000f376 100644 --- a/crates/smtp/src/reporting/scheduler.rs +++ b/crates/smtp/src/reporting/scheduler.rs @@ -173,7 +173,7 @@ impl SMTP { ); match self.core.storage.data.write(batch.build()).await { Ok(_) => true, - Err(err) if err.matches(trc::Cause::AssertValue) => { + Err(err) if err.is_assertion_failure() => { tracing::debug!( context = "queue", event = "locked", diff --git a/crates/store/src/backend/elastic/mod.rs b/crates/store/src/backend/elastic/mod.rs index 99cb998d..abcf1ba0 100644 --- a/crates/store/src/backend/elastic/mod.rs +++ b/crates/store/src/backend/elastic/mod.rs @@ -111,7 +111,7 @@ impl ElasticSearchStore { .exists(IndicesExistsParts::Index(&[INDEX_NAMES[0]])) .send() .await - .map_err(|err| trc::Cause::ElasticSearch.reason(err))?; + .map_err(|err| trc::StoreCause::ElasticSearch.reason(err))?; if exists.status_code() == StatusCode::NOT_FOUND { let response = self @@ -183,11 +183,11 @@ pub(crate) async fn assert_success(response: Result<Response, Error>) -> trc::Re if status.is_success() { Ok(response) } else { - Err(trc::Cause::ElasticSearch + Err(trc::StoreCause::ElasticSearch .reason(response.text().await.unwrap_or_default()) .ctx(trc::Key::Code, status.as_u16())) } } - Err(err) => Err(trc::Cause::ElasticSearch.reason(err)), + Err(err) => Err(trc::StoreCause::ElasticSearch.reason(err)), } } diff --git a/crates/store/src/backend/elastic/query.rs b/crates/store/src/backend/elastic/query.rs index b2448f7d..624ae8a5 100644 --- a/crates/store/src/backend/elastic/query.rs +++ b/crates/store/src/backend/elastic/query.rs @@ -107,14 +107,14 @@ impl ElasticSearchStore { let json: Value = response .json() .await - .map_err(|err| trc::Cause::ElasticSearch.reason(err))?; + .map_err(|err| trc::StoreCause::ElasticSearch.reason(err))?; let mut results = RoaringBitmap::new(); for hit in json["hits"]["hits"].as_array().ok_or_else(|| { - trc::Cause::ElasticSearch.reason("Invalid response from ElasticSearch") + trc::StoreCause::ElasticSearch.reason("Invalid response from ElasticSearch") })? { results.insert(hit["_source"]["document_id"].as_u64().ok_or_else(|| { - trc::Cause::ElasticSearch.reason("Invalid response from ElasticSearch") + trc::StoreCause::ElasticSearch.reason("Invalid response from ElasticSearch") })? as u32); } diff --git a/crates/store/src/backend/foundationdb/mod.rs b/crates/store/src/backend/foundationdb/mod.rs index 3ea02014..52a28f7f 100644 --- a/crates/store/src/backend/foundationdb/mod.rs +++ b/crates/store/src/backend/foundationdb/mod.rs @@ -77,7 +77,7 @@ impl TimedTransaction { #[inline(always)] fn into_error(error: FdbError) -> trc::Error { - trc::Cause::FoundationDB + trc::StoreCause::FoundationDB .reason(error.message()) .ctx(trc::Key::Code, error.code()) } diff --git a/crates/store/src/backend/foundationdb/write.rs b/crates/store/src/backend/foundationdb/write.rs index 4a498995..9fd10f75 100644 --- a/crates/store/src/backend/foundationdb/write.rs +++ b/crates/store/src/backend/foundationdb/write.rs @@ -94,7 +94,7 @@ impl FdbStore { *key.last_mut().unwrap() += 1; } else { trx.cancel(); - return Err(trc::Cause::FoundationDB.ctx( + return Err(trc::StoreCause::FoundationDB.ctx( trc::Key::Reason, "Value is too large", )); @@ -257,7 +257,7 @@ impl FdbStore { if !matches { trx.cancel(); - return Err(trc::Cause::AssertValue.into()); + return Err(trc::StoreCause::AssertValue.into()); } } } diff --git a/crates/store/src/backend/fs/mod.rs b/crates/store/src/backend/fs/mod.rs index f2695f2e..bc522c1b 100644 --- a/crates/store/src/backend/fs/mod.rs +++ b/crates/store/src/backend/fs/mod.rs @@ -57,7 +57,7 @@ impl FsStore { Ok(m) => m.len() as usize, Err(_) => return Ok(None), }; - let mut blob = File::open(&blob_path).await?; + let mut blob = File::open(&blob_path).await.map_err(into_error)?; Ok(Some(if range.start != 0 || range.end != usize::MAX { let from_offset = if range.start < blob_size { @@ -68,13 +68,15 @@ impl FsStore { let mut buf = vec![0; (std::cmp::min(range.end, blob_size) - from_offset) as usize]; if from_offset > 0 { - blob.seek(SeekFrom::Start(from_offset as u64)).await?; + blob.seek(SeekFrom::Start(from_offset as u64)) + .await + .map_err(into_error)?; } - blob.read_exact(&mut buf).await?; + blob.read_exact(&mut buf).await.map_err(into_error)?; buf } else { let mut buf = Vec::with_capacity(blob_size as usize); - blob.read_to_end(&mut buf).await?; + blob.read_to_end(&mut buf).await.map_err(into_error)?; buf })) } @@ -86,10 +88,12 @@ impl FsStore { .await .map_or(true, |m| m.len() as usize != data.len()) { - fs::create_dir_all(blob_path.parent().unwrap()).await?; - let mut blob_file = File::create(&blob_path).await?; - blob_file.write_all(data).await?; - blob_file.flush().await?; + fs::create_dir_all(blob_path.parent().unwrap()) + .await + .map_err(into_error)?; + let mut blob_file = File::create(&blob_path).await.map_err(into_error)?; + blob_file.write_all(data).await.map_err(into_error)?; + blob_file.flush().await.map_err(into_error)?; } Ok(()) @@ -98,7 +102,7 @@ impl FsStore { pub(crate) async fn delete_blob(&self, key: &[u8]) -> trc::Result<bool> { let blob_path = self.build_path(key); if fs::metadata(&blob_path).await.is_ok() { - fs::remove_file(&blob_path).await?; + fs::remove_file(&blob_path).await.map_err(into_error)?; Ok(true) } else { Ok(false) @@ -115,3 +119,7 @@ impl FsStore { path } } + +fn into_error(err: std::io::Error) -> trc::Error { + trc::StoreCause::Filesystem.reason(err) +} diff --git a/crates/store/src/backend/mysql/mod.rs b/crates/store/src/backend/mysql/mod.rs index 2a3d2d90..fa28d1bc 100644 --- a/crates/store/src/backend/mysql/mod.rs +++ b/crates/store/src/backend/mysql/mod.rs @@ -20,5 +20,5 @@ pub struct MysqlStore { #[inline(always)] fn into_error(err: impl Display) -> trc::Error { - trc::Cause::MySQL.reason(err) + trc::StoreCause::MySQL.reason(err) } diff --git a/crates/store/src/backend/mysql/write.rs b/crates/store/src/backend/mysql/write.rs index 5c7e7c08..ca20ba20 100644 --- a/crates/store/src/backend/mysql/write.rs +++ b/crates/store/src/backend/mysql/write.rs @@ -46,7 +46,7 @@ impl MysqlStore { && start.elapsed() < MAX_COMMIT_TIME => {} Err(CommitError::Retry) => { if retry_count > MAX_COMMIT_ATTEMPTS || start.elapsed() > MAX_COMMIT_TIME { - return Err(trc::Cause::AssertValue.into()); + return Err(trc::StoreCause::AssertValue.into()); } } Err(CommitError::Mysql(err)) => { @@ -135,7 +135,7 @@ impl MysqlStore { Ok(_) => { if exists.is_some() && trx.affected_rows() == 0 { trx.rollback().await?; - return Err(trc::Cause::AssertValue.into_err().into()); + return Err(trc::StoreCause::AssertValue.into_err().into()); } } Err(err) => { @@ -308,7 +308,7 @@ impl MysqlStore { .unwrap_or_else(|| (false, assert_value.is_none())); if !matches { trx.rollback().await?; - return Err(trc::Cause::AssertValue.into_err().into()); + return Err(trc::StoreCause::AssertValue.into_err().into()); } asserted_values.insert(key, exists); } diff --git a/crates/store/src/backend/postgres/mod.rs b/crates/store/src/backend/postgres/mod.rs index 81031e5d..63093154 100644 --- a/crates/store/src/backend/postgres/mod.rs +++ b/crates/store/src/backend/postgres/mod.rs @@ -21,5 +21,5 @@ pub struct PostgresStore { #[inline(always)] fn into_error(err: impl Display) -> trc::Error { - trc::Cause::PostgreSQL.reason(err) + trc::StoreCause::PostgreSQL.reason(err) } diff --git a/crates/store/src/backend/postgres/write.rs b/crates/store/src/backend/postgres/write.rs index 04b19d32..aaf4e825 100644 --- a/crates/store/src/backend/postgres/write.rs +++ b/crates/store/src/backend/postgres/write.rs @@ -50,7 +50,7 @@ impl PostgresStore { ) if retry_count < MAX_COMMIT_ATTEMPTS && start.elapsed() < MAX_COMMIT_TIME => {} Some(&SqlState::UNIQUE_VIOLATION) => { - return Err(trc::Cause::AssertValue.into()); + return Err(trc::StoreCause::AssertValue.into()); } _ => return Err(into_error(err)), }, @@ -59,7 +59,7 @@ impl PostgresStore { if retry_count > MAX_COMMIT_ATTEMPTS || start.elapsed() > MAX_COMMIT_TIME { - return Err(trc::Cause::AssertValue.into()); + return Err(trc::StoreCause::AssertValue.into()); } } } @@ -148,7 +148,7 @@ impl PostgresStore { .await? == 0 { - return Err(trc::Cause::AssertValue.into_err().into()); + return Err(trc::StoreCause::AssertValue.into_err().into()); } } ValueOp::AtomicAdd(by) => { @@ -322,7 +322,7 @@ impl PostgresStore { }) .unwrap_or_else(|| (false, assert_value.is_none())); if !matches { - return Err(trc::Cause::AssertValue.into_err().into()); + return Err(trc::StoreCause::AssertValue.into_err().into()); } asserted_values.insert(key, exists); } diff --git a/crates/store/src/backend/redis/mod.rs b/crates/store/src/backend/redis/mod.rs index 479b2c5e..6ce007e2 100644 --- a/crates/store/src/backend/redis/mod.rs +++ b/crates/store/src/backend/redis/mod.rs @@ -184,5 +184,5 @@ fn build_pool<M: Manager>( #[inline(always)] fn into_error(err: impl Display) -> trc::Error { - trc::Cause::Redis.reason(err) + trc::StoreCause::Redis.reason(err) } diff --git a/crates/store/src/backend/redis/pool.rs b/crates/store/src/backend/redis/pool.rs index b8f01863..c322584e 100644 --- a/crates/store/src/backend/redis/pool.rs +++ b/crates/store/src/backend/redis/pool.rs @@ -21,7 +21,7 @@ impl managed::Manager for RedisConnectionManager { .await { Ok(conn) => conn.map_err(into_error), - Err(_) => Err(trc::Cause::Timeout.into()), + Err(_) => Err(trc::StoreCause::Redis.ctx(trc::Key::Details, "Connection Timeout")), } } @@ -44,7 +44,7 @@ impl managed::Manager for RedisClusterConnectionManager { async fn create(&self) -> Result<ClusterConnection, trc::Error> { match tokio::time::timeout(self.timeout, self.client.get_async_connection()).await { Ok(conn) => conn.map_err(into_error), - Err(_) => Err(trc::Cause::Timeout.into()), + Err(_) => Err(trc::StoreCause::Redis.ctx(trc::Key::Details, "Connection Timeout")), } } diff --git a/crates/store/src/backend/rocksdb/mod.rs b/crates/store/src/backend/rocksdb/mod.rs index 33d273a9..78b80e86 100644 --- a/crates/store/src/backend/rocksdb/mod.rs +++ b/crates/store/src/backend/rocksdb/mod.rs @@ -38,5 +38,5 @@ pub struct RocksDbStore { #[inline(always)] fn into_error(err: rocksdb::Error) -> trc::Error { - trc::Cause::RocksDB.reason(err) + trc::StoreCause::RocksDB.reason(err) } diff --git a/crates/store/src/backend/rocksdb/write.rs b/crates/store/src/backend/rocksdb/write.rs index 8639e2b7..d8c47b2b 100644 --- a/crates/store/src/backend/rocksdb/write.rs +++ b/crates/store/src/backend/rocksdb/write.rs @@ -308,7 +308,7 @@ impl<'x> RocksDBTransaction<'x> { if !matches { txn.rollback()?; - return Err(CommitError::Internal(trc::Cause::AssertValue.into())); + return Err(CommitError::Internal(trc::StoreCause::AssertValue.into())); } } } diff --git a/crates/store/src/backend/s3/mod.rs b/crates/store/src/backend/s3/mod.rs index 70b314e1..dee52205 100644 --- a/crates/store/src/backend/s3/mod.rs +++ b/crates/store/src/backend/s3/mod.rs @@ -90,7 +90,7 @@ impl S3Store { match response.status_code() { 200..=299 => Ok(Some(response.to_vec())), 404 => Ok(None), - code => Err(trc::Cause::S3 + code => Err(trc::StoreCause::S3 .reason(String::from_utf8_lossy(response.as_slice())) .ctx(trc::Key::Code, code)), } @@ -105,7 +105,7 @@ impl S3Store { match response.status_code() { 200..=299 => Ok(()), - code => Err(trc::Cause::S3 + code => Err(trc::StoreCause::S3 .reason(String::from_utf8_lossy(response.as_slice())) .ctx(trc::Key::Code, code)), } @@ -121,7 +121,7 @@ impl S3Store { match response.status_code() { 200..=299 => Ok(true), 404 => Ok(false), - code => Err(trc::Cause::S3 + code => Err(trc::StoreCause::S3 .reason(String::from_utf8_lossy(response.as_slice())) .ctx(trc::Key::Code, code)), } @@ -142,5 +142,5 @@ impl S3Store { #[inline(always)] fn into_error(err: impl Display) -> trc::Error { - trc::Cause::S3.reason(err) + trc::StoreCause::S3.reason(err) } diff --git a/crates/store/src/backend/sqlite/mod.rs b/crates/store/src/backend/sqlite/mod.rs index 6cd07502..3bc632f8 100644 --- a/crates/store/src/backend/sqlite/mod.rs +++ b/crates/store/src/backend/sqlite/mod.rs @@ -24,5 +24,5 @@ pub struct SqliteStore { #[inline(always)] fn into_error(err: impl Display) -> trc::Error { - trc::Cause::SQLite.reason(err) + trc::StoreCause::SQLite.reason(err) } diff --git a/crates/store/src/backend/sqlite/write.rs b/crates/store/src/backend/sqlite/write.rs index c5050462..c07f42df 100644 --- a/crates/store/src/backend/sqlite/write.rs +++ b/crates/store/src/backend/sqlite/write.rs @@ -245,7 +245,7 @@ impl SqliteStore { .unwrap_or_else(|| assert_value.is_none()); if !matches { trx.rollback().map_err(into_error)?; - return Err(trc::Cause::AssertValue.into()); + return Err(trc::StoreCause::AssertValue.into()); } } } diff --git a/crates/store/src/dispatch/blob.rs b/crates/store/src/dispatch/blob.rs index f774b855..68303596 100644 --- a/crates/store/src/dispatch/blob.rs +++ b/crates/store/src/dispatch/blob.rs @@ -30,7 +30,7 @@ impl BlobStore { Store::MySQL(store) => store.get_blob(key, read_range).await, #[cfg(feature = "rocks")] Store::RocksDb(store) => store.get_blob(key, read_range).await, - Store::None => Err(trc::Cause::NotConfigured.into()), + Store::None => Err(trc::StoreCause::NotConfigured.into()), }, BlobBackend::Fs(store) => store.get_blob(key, read_range).await, #[cfg(feature = "s3")] @@ -47,14 +47,14 @@ impl BlobStore { data.get(..data.len() - 1).unwrap_or_default(), ) .map_err(|err| { - trc::Cause::Decompress + trc::StoreCause::Decompress .reason(err) .ctx(trc::Key::Key, key) .ctx(trc::Key::CausedBy, trc::location!()) })? } Some(data) => { - trc::error!(BlobMissingMarker, Details = key); + let todo = "log"; data } None => return Ok(None), @@ -96,7 +96,7 @@ impl BlobStore { Store::MySQL(store) => store.put_blob(key, data.as_ref()).await, #[cfg(feature = "rocks")] Store::RocksDb(store) => store.put_blob(key, data.as_ref()).await, - Store::None => Err(trc::Cause::NotConfigured.into()), + Store::None => Err(trc::StoreCause::NotConfigured.into()), }, BlobBackend::Fs(store) => store.put_blob(key, data.as_ref()).await, #[cfg(feature = "s3")] @@ -118,7 +118,7 @@ impl BlobStore { Store::MySQL(store) => store.delete_blob(key).await, #[cfg(feature = "rocks")] Store::RocksDb(store) => store.delete_blob(key).await, - Store::None => Err(trc::Cause::NotConfigured.into()), + Store::None => Err(trc::StoreCause::NotConfigured.into()), }, BlobBackend::Fs(store) => store.delete_blob(key).await, #[cfg(feature = "s3")] diff --git a/crates/store/src/dispatch/lookup.rs b/crates/store/src/dispatch/lookup.rs index 8ab3aaed..afe02835 100644 --- a/crates/store/src/dispatch/lookup.rs +++ b/crates/store/src/dispatch/lookup.rs @@ -32,7 +32,7 @@ impl LookupStore { LookupStore::Store(Store::PostgreSQL(store)) => store.query(query, ¶ms).await, #[cfg(feature = "mysql")] LookupStore::Store(Store::MySQL(store)) => store.query(query, ¶ms).await, - _ => Err(trc::Cause::Unsupported.into_err()), + _ => Err(trc::StoreCause::NotSupported.into_err()), }; trc::trace!( @@ -42,7 +42,7 @@ impl LookupStore { Result = &result, ); - result.caused_by( trc::location!()) + result.caused_by(trc::location!()) } pub async fn key_set( @@ -76,9 +76,9 @@ impl LookupStore { ) .await .map(|_| ()), - LookupStore::Memory(_) => Err(trc::Cause::Unsupported.into_err()), + LookupStore::Memory(_) => Err(trc::StoreCause::NotSupported.into_err()), } - .caused_by( trc::location!()) + .caused_by(trc::location!()) } pub async fn counter_incr( @@ -125,10 +125,10 @@ impl LookupStore { #[cfg(feature = "redis")] LookupStore::Redis(store) => store.key_incr(key, value, expires).await, LookupStore::Query(_) | LookupStore::Memory(_) => { - Err(trc::Cause::Unsupported.into_err()) + Err(trc::StoreCause::NotSupported.into_err()) } } - .caused_by( trc::location!()) + .caused_by(trc::location!()) } pub async fn key_delete(&self, key: Vec<u8>) -> trc::Result<()> { @@ -144,10 +144,10 @@ impl LookupStore { #[cfg(feature = "redis")] LookupStore::Redis(store) => store.key_delete(key).await, LookupStore::Query(_) | LookupStore::Memory(_) => { - Err(trc::Cause::Unsupported.into_err()) + Err(trc::StoreCause::NotSupported.into_err()) } } - .caused_by( trc::location!()) + .caused_by(trc::location!()) } pub async fn counter_delete(&self, key: Vec<u8>) -> trc::Result<()> { @@ -163,10 +163,10 @@ impl LookupStore { #[cfg(feature = "redis")] LookupStore::Redis(store) => store.key_delete(key).await, LookupStore::Query(_) | LookupStore::Memory(_) => { - Err(trc::Cause::Unsupported.into_err()) + Err(trc::StoreCause::NotSupported.into_err()) } } - .caused_by( trc::location!()) + .caused_by(trc::location!()) } pub async fn key_get<T: Deserialize + From<Value<'static>> + std::fmt::Debug + 'static>( @@ -197,7 +197,7 @@ impl LookupStore { .get(std::str::from_utf8(&key).unwrap_or_default()) .map(|value| T::from(value.clone()))), } - .caused_by( trc::location!()) + .caused_by(trc::location!()) } pub async fn counter_get(&self, key: Vec<u8>) -> trc::Result<i64> { @@ -212,10 +212,10 @@ impl LookupStore { #[cfg(feature = "redis")] LookupStore::Redis(store) => store.counter_get(key).await, LookupStore::Query(_) | LookupStore::Memory(_) => { - Err(trc::Cause::Unsupported.into_err()) + Err(trc::StoreCause::NotSupported.into_err()) } } - .caused_by( trc::location!()) + .caused_by(trc::location!()) } pub async fn key_exists(&self, key: Vec<u8>) -> trc::Result<bool> { @@ -240,7 +240,7 @@ impl LookupStore { .get(std::str::from_utf8(&key).unwrap_or_default()) .is_some()), } - .caused_by( trc::location!()) + .caused_by(trc::location!()) } pub async fn is_rate_allowed( @@ -261,12 +261,9 @@ impl LookupStore { let requests = if !soft_check { self.counter_incr(bucket, 1, expires_in.into(), true) .await - .caused_by( trc::location!())? + .caused_by(trc::location!())? } else { - self.counter_get(bucket) - .await - .caused_by( trc::location!())? - + 1 + self.counter_get(bucket).await.caused_by(trc::location!())? + 1 }; if requests <= rate.requests as i64 { @@ -289,11 +286,11 @@ impl LookupStore { let mut expired_counters = Vec::new(); store .iterate(IterateParams::new(from_key, to_key), |key, value| { - let expiry = value.deserialize_be_u64(0).caused_by( trc::location!())?; + let expiry = value.deserialize_be_u64(0).caused_by(trc::location!())?; if expiry == 0 { if value .deserialize_be_u64(U64_LEN) - .caused_by( trc::location!())? + .caused_by(trc::location!())? <= current_time { expired_counters.push(key.to_vec()); @@ -304,7 +301,7 @@ impl LookupStore { Ok(true) }) .await - .caused_by( trc::location!())?; + .caused_by(trc::location!())?; if !expired_keys.is_empty() { let mut batch = BatchBuilder::new(); @@ -317,7 +314,7 @@ impl LookupStore { store .write(batch.build()) .await - .caused_by( trc::location!())?; + .caused_by(trc::location!())?; batch = BatchBuilder::new(); } } @@ -325,7 +322,7 @@ impl LookupStore { store .write(batch.build()) .await - .caused_by( trc::location!())?; + .caused_by(trc::location!())?; } } @@ -344,7 +341,7 @@ impl LookupStore { store .write(batch.build()) .await - .caused_by( trc::location!())?; + .caused_by(trc::location!())?; batch = BatchBuilder::new(); } } @@ -352,7 +349,7 @@ impl LookupStore { store .write(batch.build()) .await - .caused_by( trc::location!())?; + .caused_by(trc::location!())?; } } } @@ -383,7 +380,7 @@ impl<T: Deserialize> Deserialize for LookupValue<T> { Ok(if expires > now() { LookupValue::Value( T::deserialize(bytes.get(U64_LEN..).unwrap_or_default()) - .caused_by( trc::location!())?, + .caused_by(trc::location!())?, ) } else { LookupValue::None diff --git a/crates/store/src/dispatch/store.rs b/crates/store/src/dispatch/store.rs index 8981a5da..badccd41 100644 --- a/crates/store/src/dispatch/store.rs +++ b/crates/store/src/dispatch/store.rs @@ -43,9 +43,9 @@ impl Store { Self::MySQL(store) => store.get_value(key).await, #[cfg(feature = "rocks")] Self::RocksDb(store) => store.get_value(key).await, - Self::None => Err(trc::Cause::NotConfigured.into()), + Self::None => Err(trc::StoreCause::NotConfigured.into()), } - .caused_by( trc::location!()) + .caused_by(trc::location!()) } pub async fn get_bitmap( @@ -63,9 +63,9 @@ impl Store { Self::MySQL(store) => store.get_bitmap(key).await, #[cfg(feature = "rocks")] Self::RocksDb(store) => store.get_bitmap(key).await, - Self::None => Err(trc::Cause::NotConfigured.into()), + Self::None => Err(trc::StoreCause::NotConfigured.into()), } - .caused_by( trc::location!()) + .caused_by(trc::location!()) } pub async fn get_bitmaps_intersection( @@ -74,7 +74,7 @@ impl Store { ) -> trc::Result<Option<RoaringBitmap>> { let mut result: Option<RoaringBitmap> = None; for key in keys { - if let Some(bitmap) = self.get_bitmap(key).await.caused_by( trc::location!())? { + if let Some(bitmap) = self.get_bitmap(key).await.caused_by(trc::location!())? { if let Some(result) = &mut result { result.bitand_assign(&bitmap); if result.is_empty() { @@ -106,9 +106,9 @@ impl Store { Self::MySQL(store) => store.iterate(params, cb).await, #[cfg(feature = "rocks")] Self::RocksDb(store) => store.iterate(params, cb).await, - Self::None => Err(trc::Cause::NotConfigured.into()), + Self::None => Err(trc::StoreCause::NotConfigured.into()), } - .caused_by( trc::location!()) + .caused_by(trc::location!()) } pub async fn get_counter( @@ -126,9 +126,9 @@ impl Store { Self::MySQL(store) => store.get_counter(key).await, #[cfg(feature = "rocks")] Self::RocksDb(store) => store.get_counter(key).await, - Self::None => Err(trc::Cause::NotConfigured.into()), + Self::None => Err(trc::StoreCause::NotConfigured.into()), } - .caused_by( trc::location!()) + .caused_by(trc::location!()) } pub async fn write(&self, batch: Batch) -> trc::Result<AssignedIds> { @@ -189,9 +189,9 @@ impl Store { Self::MySQL(store) => store.write(batch).await, #[cfg(feature = "rocks")] Self::RocksDb(store) => store.write(batch).await, - Self::None => Err(trc::Cause::NotConfigured.into()), + Self::None => Err(trc::StoreCause::NotConfigured.into()), } - .caused_by( trc::location!())?; + .caused_by(trc::location!())?; for (key, class, document_id, set) in bitmaps { let mut bitmaps = BITMAPS.lock(); @@ -231,7 +231,7 @@ impl Store { Self::MySQL(store) => store.write(batch).await, #[cfg(feature = "rocks")] Self::RocksDb(store) => store.write(batch).await, - Self::None => Err(trc::Cause::NotConfigured.into()), + Self::None => Err(trc::StoreCause::NotConfigured.into()), } } @@ -246,7 +246,7 @@ impl Store { })), ) .await - .caused_by( trc::location!())?; + .caused_by(trc::location!())?; self.delete_range( ValueKey::from(ValueClass::Report(ReportClass::Tls { id: 0, expires: 0 })), ValueKey::from(ValueClass::Report(ReportClass::Tls { @@ -255,7 +255,7 @@ impl Store { })), ) .await - .caused_by( trc::location!())?; + .caused_by(trc::location!())?; self.delete_range( ValueKey::from(ValueClass::Report(ReportClass::Arf { id: 0, expires: 0 })), ValueKey::from(ValueClass::Report(ReportClass::Arf { @@ -264,7 +264,7 @@ impl Store { })), ) .await - .caused_by( trc::location!())?; + .caused_by(trc::location!())?; match self { #[cfg(feature = "sqlite")] @@ -277,9 +277,9 @@ impl Store { Self::MySQL(store) => store.purge_store().await, #[cfg(feature = "rocks")] Self::RocksDb(store) => store.purge_store().await, - Self::None => Err(trc::Cause::NotConfigured.into()), + Self::None => Err(trc::StoreCause::NotConfigured.into()), } - .caused_by( trc::location!()) + .caused_by(trc::location!()) } pub async fn delete_range(&self, from: impl Key, to: impl Key) -> trc::Result<()> { @@ -294,9 +294,9 @@ impl Store { Self::MySQL(store) => store.delete_range(from, to).await, #[cfg(feature = "rocks")] Self::RocksDb(store) => store.delete_range(from, to).await, - Self::None => Err(trc::Cause::NotConfigured.into()), + Self::None => Err(trc::StoreCause::NotConfigured.into()), } - .caused_by( trc::location!()) + .caused_by(trc::location!()) } pub async fn delete_documents( @@ -352,7 +352,7 @@ impl Store { }, ) .await - .caused_by( trc::location!())?; + .caused_by(trc::location!())?; // Remove keys let mut batch = BatchBuilder::new(); @@ -361,7 +361,7 @@ impl Store { if batch.ops.len() >= 1000 { self.write(std::mem::take(&mut batch).build()) .await - .caused_by( trc::location!())?; + .caused_by(trc::location!())?; } batch.ops.push(Operation::Value { class: ValueClass::Any(AnyClass { subspace, key }), @@ -372,7 +372,7 @@ impl Store { if !batch.is_empty() { self.write(batch.build()) .await - .caused_by( trc::location!())?; + .caused_by(trc::location!())?; } Ok(()) @@ -397,7 +397,7 @@ impl Store { }, ) .await - .caused_by( trc::location!())?; + .caused_by(trc::location!())?; } for (from_class, to_class) in [ @@ -429,7 +429,7 @@ impl Store { }, ) .await - .caused_by( trc::location!())?; + .caused_by(trc::location!())?; } Ok(()) @@ -447,9 +447,9 @@ impl Store { Self::MySQL(store) => store.get_blob(key, range).await, #[cfg(feature = "rocks")] Self::RocksDb(store) => store.get_blob(key, range).await, - Self::None => Err(trc::Cause::NotConfigured.into()), + Self::None => Err(trc::StoreCause::NotConfigured.into()), } - .caused_by( trc::location!()) + .caused_by(trc::location!()) } pub async fn put_blob(&self, key: &[u8], data: &[u8]) -> trc::Result<()> { @@ -464,9 +464,9 @@ impl Store { Self::MySQL(store) => store.put_blob(key, data).await, #[cfg(feature = "rocks")] Self::RocksDb(store) => store.put_blob(key, data).await, - Self::None => Err(trc::Cause::NotConfigured.into()), + Self::None => Err(trc::StoreCause::NotConfigured.into()), } - .caused_by( trc::location!()) + .caused_by(trc::location!()) } pub async fn delete_blob(&self, key: &[u8]) -> trc::Result<bool> { @@ -481,9 +481,9 @@ impl Store { Self::MySQL(store) => store.delete_blob(key).await, #[cfg(feature = "rocks")] Self::RocksDb(store) => store.delete_blob(key).await, - Self::None => Err(trc::Cause::NotConfigured.into()), + Self::None => Err(trc::StoreCause::NotConfigured.into()), } - .caused_by( trc::location!()) + .caused_by(trc::location!()) } #[cfg(feature = "test_mode")] @@ -568,7 +568,7 @@ impl Store { self.iterate( IterateParams::new(from_key, to_key).ascending().no_values(), |key, _| { - let account_id = key.deserialize_be_u32(0).caused_by( trc::location!())?; + let account_id = key.deserialize_be_u32(0).caused_by(trc::location!())?; if account_id != last_account_id { last_account_id = account_id; batch.with_account_id(account_id); @@ -582,7 +582,7 @@ impl Store { .unwrap(), until: key .deserialize_be_u64(key.len() - U64_LEN) - .caused_by( trc::location!())?, + .caused_by(trc::location!())?, }), op: ValueOp::Clear, }); @@ -607,7 +607,7 @@ impl Store { let mut expired_counters = Vec::new(); self.iterate(IterateParams::new(from_key, to_key), |key, value| { - let expiry = value.deserialize_be_u64(0).caused_by( trc::location!())?; + let expiry = value.deserialize_be_u64(0).caused_by(trc::location!())?; if expiry == 0 { expired_counters.push(key.to_vec()); } else if expiry != u64::MAX { diff --git a/crates/store/src/query/acl.rs b/crates/store/src/query/acl.rs index bd6f0d16..53d5c088 100644 --- a/crates/store/src/query/acl.rs +++ b/crates/store/src/query/acl.rs @@ -75,7 +75,7 @@ impl Store { }, ) .await - .caused_by( trc::location!()) + .caused_by(trc::location!()) .map(|_| results) } @@ -108,7 +108,7 @@ impl Store { }, ) .await - .caused_by( trc::location!())?; + .caused_by(trc::location!())?; // Remove permissions let mut batch = BatchBuilder::new(); @@ -118,7 +118,7 @@ impl Store { if batch.ops.len() >= 1000 { self.write(batch.build()) .await - .caused_by( trc::location!())?; + .caused_by(trc::location!())?; batch = BatchBuilder::new(); batch.with_account_id(account_id); last_collection = u8::MAX; @@ -136,7 +136,7 @@ impl Store { if !batch.is_empty() { self.write(batch.build()) .await - .caused_by( trc::location!())?; + .caused_by(trc::location!())?; } Ok(()) @@ -149,7 +149,7 @@ impl Deserialize for AclItem { to_account_id: bytes.deserialize_be_u32(U32_LEN)?, to_collection: *bytes .get(U32_LEN * 2) - .ok_or_else(|| trc::Cause::DataCorruption.caused_by(trc::location!()))?, + .ok_or_else(|| trc::StoreCause::DataCorruption.caused_by(trc::location!()))?, to_document_id: bytes.deserialize_be_u32((U32_LEN * 2) + 1)?, permissions: 0, }) diff --git a/crates/store/src/write/key.rs b/crates/store/src/write/key.rs index 22ff00e0..fff9a816 100644 --- a/crates/store/src/write/key.rs +++ b/crates/store/src/write/key.rs @@ -102,13 +102,13 @@ impl DeserializeBigEndian for &[u8] { fn deserialize_be_u32(&self, index: usize) -> trc::Result<u32> { self.get(index..index + U32_LEN) .ok_or_else(|| { - trc::Cause::DataCorruption + trc::StoreCause::DataCorruption .caused_by(trc::location!()) .ctx(trc::Key::Value, *self) }) .and_then(|bytes| { bytes.try_into().map_err(|_| { - trc::Cause::DataCorruption + trc::StoreCause::DataCorruption .caused_by(trc::location!()) .ctx(trc::Key::Value, *self) }) @@ -119,13 +119,13 @@ impl DeserializeBigEndian for &[u8] { fn deserialize_be_u64(&self, index: usize) -> trc::Result<u64> { self.get(index..index + U64_LEN) .ok_or_else(|| { - trc::Cause::DataCorruption + trc::StoreCause::DataCorruption .caused_by(trc::location!()) .ctx(trc::Key::Value, *self) }) .and_then(|bytes| { bytes.try_into().map_err(|_| { - trc::Cause::DataCorruption + trc::StoreCause::DataCorruption .caused_by(trc::location!()) .ctx(trc::Key::Value, *self) }) @@ -644,7 +644,7 @@ impl Deserialize for ReportEvent { .and_then(|domain| std::str::from_utf8(domain).ok()) .map(|s| s.to_string()) .ok_or_else(|| { - trc::Cause::DataCorruption + trc::StoreCause::DataCorruption .caused_by(trc::location!()) .ctx(trc::Key::Key, key) })?, diff --git a/crates/store/src/write/mod.rs b/crates/store/src/write/mod.rs index b014637a..f59fdc0f 100644 --- a/crates/store/src/write/mod.rs +++ b/crates/store/src/write/mod.rs @@ -346,7 +346,7 @@ impl Deserialize for String { impl Deserialize for u64 { fn deserialize(bytes: &[u8]) -> trc::Result<Self> { Ok(u64::from_be_bytes(bytes.try_into().map_err(|_| { - trc::Cause::DataCorruption.caused_by(trc::location!()) + trc::StoreCause::DataCorruption.caused_by(trc::location!()) })?)) } } @@ -354,7 +354,7 @@ impl Deserialize for u64 { impl Deserialize for i64 { fn deserialize(bytes: &[u8]) -> trc::Result<Self> { Ok(i64::from_be_bytes(bytes.try_into().map_err(|_| { - trc::Cause::DataCorruption.caused_by(trc::location!()) + trc::StoreCause::DataCorruption.caused_by(trc::location!()) })?)) } } @@ -362,7 +362,7 @@ impl Deserialize for i64 { impl Deserialize for u32 { fn deserialize(bytes: &[u8]) -> trc::Result<Self> { Ok(u32::from_be_bytes(bytes.try_into().map_err(|_| { - trc::Cause::DataCorruption.caused_by(trc::location!()) + trc::StoreCause::DataCorruption.caused_by(trc::location!()) })?)) } } @@ -456,12 +456,12 @@ impl<T: DeserializeFrom + Sync + Send> Deserialize for Vec<T> { let mut bytes = bytes.iter(); let len: usize = bytes .next_leb128() - .ok_or_else(|| trc::Cause::DataCorruption.caused_by(trc::location!()))?; + .ok_or_else(|| trc::StoreCause::DataCorruption.caused_by(trc::location!()))?; let mut list = Vec::with_capacity(len); for _ in 0..len { list.push( T::deserialize_from(&mut bytes) - .ok_or_else(|| trc::Cause::DataCorruption.caused_by(trc::location!()))?, + .ok_or_else(|| trc::StoreCause::DataCorruption.caused_by(trc::location!()))?, ); } Ok(list) @@ -686,13 +686,13 @@ impl<T: serde::Serialize + serde::de::DeserializeOwned + Sized + Sync + Send> De fn deserialize(bytes: &[u8]) -> trc::Result<Self> { lz4_flex::decompress_size_prepended(bytes) .map_err(|err| { - trc::Cause::Decompress + trc::StoreCause::Decompress .caused_by(trc::location!()) .reason(err) }) .and_then(|result| { bincode::deserialize(&result).map_err(|err| { - trc::Cause::DataCorruption + trc::StoreCause::DataCorruption .caused_by(trc::location!()) .reason(err) }) @@ -724,7 +724,7 @@ impl AssignedIds { pub fn get_document_id(&self, idx: usize) -> trc::Result<u32> { self.document_ids.get(idx).copied().ok_or_else(|| { - trc::Cause::Unexpected + trc::StoreCause::Unexpected .caused_by(trc::location!()) .ctx(trc::Key::Reason, "No document ids were created") }) @@ -736,7 +736,7 @@ impl AssignedIds { pub fn last_document_id(&self) -> trc::Result<u32> { self.document_ids.last().copied().ok_or_else(|| { - trc::Cause::Unexpected + trc::StoreCause::Unexpected .caused_by(trc::location!()) .ctx(trc::Key::Reason, "No document ids were created") }) @@ -744,7 +744,7 @@ impl AssignedIds { pub fn last_counter_id(&self) -> trc::Result<i64> { self.counter_ids.last().copied().ok_or_else(|| { - trc::Cause::Unexpected + trc::StoreCause::Unexpected .caused_by(trc::location!()) .ctx(trc::Key::Reason, "No document ids were created") }) diff --git a/crates/trc/src/conv.rs b/crates/trc/src/conv.rs index 298428e8..f65eadd8 100644 --- a/crates/trc/src/conv.rs +++ b/crates/trc/src/conv.rs @@ -83,18 +83,24 @@ impl From<Error> for Value { } } -impl From<ErrorKind> for Value { - fn from(value: ErrorKind) -> Self { - Self::ErrorKind(value) - } -} - impl From<Cause> for Error { fn from(value: Cause) -> Self { Error::new(value) } } +impl From<StoreCause> for Error { + fn from(value: StoreCause) -> Self { + Error::new(Cause::Store(value)) + } +} + +impl From<AuthCause> for Error { + fn from(value: AuthCause) -> Self { + Error::new(Cause::Auth(value)) + } +} + impl From<Protocol> for Value { fn from(value: Protocol) -> Self { Self::Protocol(value) @@ -152,52 +158,32 @@ where } } -impl From<std::io::Error> for Error { - fn from(err: std::io::Error) -> Self { - Cause::Io - .ctx(Key::Reason, err.kind()) - .ctx(Key::Details, err.to_string()) +impl Cause { + pub fn from_io_error(self, err: std::io::Error) -> Error { + self.reason(err).details("I/O error") } -} -impl From<serde_json::Error> for Error { - fn from(err: serde_json::Error) -> Self { - Cause::Deserialize - .reason(err) - .details("JSON deserialization failed") + pub fn from_json_error(self, err: serde_json::Error) -> Error { + self.reason(err).details("JSON deserialization failed") } -} -impl From<base64::DecodeError> for Error { - fn from(err: base64::DecodeError) -> Self { - Cause::DataCorruption - .reason(err) - .details("Base64 decoding failed") + pub fn from_base64_error(self, err: base64::DecodeError) -> Error { + self.reason(err).details("Base64 decoding failed") } -} -impl From<reqwest::Error> for Error { - fn from(err: reqwest::Error) -> Self { - Cause::Http - .into_err() + pub fn from_http_error(self, err: reqwest::Error) -> Error { + self.into_err() .ctx_opt(Key::Url, err.url().map(|url| url.as_ref().to_string())) .ctx_opt(Key::Code, err.status().map(|status| status.as_u16())) .reason(err) } -} -impl From<bincode::Error> for Error { - fn from(value: bincode::Error) -> Self { - Cause::Deserialize - .reason(value) - .details("Bincode deserialization failed") + pub fn from_bincode_error(self, err: bincode::Error) -> Error { + self.reason(err).details("Bincode deserialization failed") } -} -impl From<reqwest::header::ToStrError> for Error { - fn from(value: reqwest::header::ToStrError) -> Self { - Cause::Http - .reason(value) + pub fn from_http_str_error(self, err: reqwest::header::ToStrError) -> Error { + self.reason(err) .details("Failed to convert header to string") } } @@ -206,17 +192,21 @@ pub trait AssertSuccess where Self: Sized, { - fn assert_success(self) -> impl std::future::Future<Output = crate::Result<Self>> + Send; + fn assert_success( + self, + cause: Cause, + ) -> impl std::future::Future<Output = crate::Result<Self>> + Send; } impl AssertSuccess for reqwest::Response { - async fn assert_success(self) -> crate::Result<Self> { + async fn assert_success(self, cause: Cause) -> crate::Result<Self> { let status = self.status(); if status.is_success() { Ok(self) } else { - Err(Cause::Http + Err(cause .ctx(Key::Code, status.as_u16()) + .details("HTTP request failed") .ctx_opt(Key::Reason, self.text().await.ok())) } } diff --git a/crates/trc/src/imple.rs b/crates/trc/src/imple.rs index 8278ccac..2596d35a 100644 --- a/crates/trc/src/imple.rs +++ b/crates/trc/src/imple.rs @@ -129,7 +129,7 @@ where } pub fn corrupted_key(key: &[u8], value: Option<&[u8]>, caused_by: &'static str) -> Error { - Cause::DataCorruption + Cause::Store(StoreCause::DataCorruption) .ctx(Key::Key, key) .ctx_opt(Key::Value, value) .ctx(Key::CausedBy, caused_by) @@ -139,17 +139,17 @@ where impl Cause { #[inline(always)] pub fn ctx(self, key: Key, value: impl Into<Value>) -> Error { - Error::new(self).ctx(key, value) + self.into_err().ctx(key, value) } #[inline(always)] pub fn caused_by(self, error: impl Into<Value>) -> Error { - Error::new(self).caused_by(error) + self.into_err().caused_by(error) } #[inline(always)] pub fn reason(self, error: impl Display) -> Error { - Error::new(self).reason(error) + self.into_err().reason(error) } #[inline(always)] @@ -158,11 +158,155 @@ impl Cause { } } +impl StoreCause { + #[inline(always)] + pub fn ctx(self, key: Key, value: impl Into<Value>) -> Error { + self.into_err().ctx(key, value) + } + + #[inline(always)] + pub fn caused_by(self, error: impl Into<Value>) -> Error { + self.into_err().caused_by(error) + } + + #[inline(always)] + pub fn reason(self, error: impl Display) -> Error { + self.into_err().reason(error) + } + + #[inline(always)] + pub fn into_err(self) -> Error { + Error::new(Cause::Store(self)) + } +} + +impl AuthCause { + #[inline(always)] + pub fn ctx(self, key: Key, value: impl Into<Value>) -> Error { + self.into_err().ctx(key, value) + } + + #[inline(always)] + pub fn caused_by(self, error: impl Into<Value>) -> Error { + self.into_err().caused_by(error) + } + + #[inline(always)] + pub fn reason(self, error: impl Display) -> Error { + self.into_err().reason(error) + } + + #[inline(always)] + pub fn into_err(self) -> Error { + Error::new(Cause::Auth(self)) + } +} + +impl ManageCause { + #[inline(always)] + pub fn ctx(self, key: Key, value: impl Into<Value>) -> Error { + self.into_err().ctx(key, value) + } + + #[inline(always)] + pub fn caused_by(self, error: impl Into<Value>) -> Error { + self.into_err().caused_by(error) + } + + #[inline(always)] + pub fn reason(self, error: impl Display) -> Error { + self.into_err().reason(error) + } + + #[inline(always)] + pub fn into_err(self) -> Error { + Error::new(Cause::Manage(self)) + } +} + +impl JmapCause { + #[inline(always)] + pub fn ctx(self, key: Key, value: impl Into<Value>) -> Error { + self.into_err().ctx(key, value) + } + + #[inline(always)] + pub fn caused_by(self, error: impl Into<Value>) -> Error { + self.into_err().caused_by(error) + } + + #[inline(always)] + pub fn reason(self, error: impl Display) -> Error { + self.into_err().reason(error) + } + + #[inline(always)] + pub fn into_err(self) -> Error { + Error::new(Cause::Jmap(self)) + } +} + +impl LimitCause { + #[inline(always)] + pub fn ctx(self, key: Key, value: impl Into<Value>) -> Error { + self.into_err().ctx(key, value) + } + + #[inline(always)] + pub fn caused_by(self, error: impl Into<Value>) -> Error { + self.into_err().caused_by(error) + } + + #[inline(always)] + pub fn reason(self, error: impl Display) -> Error { + self.into_err().reason(error) + } + + #[inline(always)] + pub fn into_err(self) -> Error { + Error::new(Cause::Limit(self)) + } +} + +impl ResourceCause { + #[inline(always)] + pub fn ctx(self, key: Key, value: impl Into<Value>) -> Error { + self.into_err().ctx(key, value) + } + + #[inline(always)] + pub fn caused_by(self, error: impl Into<Value>) -> Error { + self.into_err().caused_by(error) + } + + #[inline(always)] + pub fn reason(self, error: impl Display) -> Error { + self.into_err().reason(error) + } + + #[inline(always)] + pub fn into_err(self) -> Error { + Error::new(Cause::Resource(self)) + } +} + impl Error { #[inline(always)] pub fn wrap(self, cause: Cause) -> Self { Error::new(cause).caused_by(self) } + + #[inline(always)] + pub fn is_assertion_failure(&self) -> bool { + self.inner == Cause::Store(StoreCause::AssertValue) + } + + pub fn is_jmap_method_error(&self) -> bool { + !matches!( + self.inner, + Cause::Jmap(JmapCause::UnknownCapability | JmapCause::NotJSON | JmapCause::NotRequest) + ) + } } impl Value { diff --git a/crates/trc/src/lib.rs b/crates/trc/src/lib.rs index 08b15440..351960ba 100644 --- a/crates/trc/src/lib.rs +++ b/crates/trc/src/lib.rs @@ -8,10 +8,7 @@ pub mod conv; pub mod imple; pub mod macros; -use std::{ - io::ErrorKind, - net::{IpAddr, Ipv4Addr, Ipv6Addr}, -}; +use std::net::{IpAddr, Ipv4Addr, Ipv6Addr}; pub type Result<T> = std::result::Result<T, Error>; pub type Error = Context<Cause, ERROR_CONTEXT_SIZE>; @@ -33,7 +30,6 @@ pub enum Value { Ipv6(Box<Ipv6Addr>), Protocol(Protocol), Error(Box<Error>), - ErrorKind(ErrorKind), Array(Vec<Value>), #[default] None, @@ -79,53 +75,124 @@ pub enum Event { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Cause { + Store(StoreCause), + Jmap(JmapCause), + Imap, + ManageSieve, + Pop3, + Smtp, + Thread, + Fetch, + Acme, + Dns, + Ingest, + Network, + Limit(LimitCause), + Manage(ManageCause), + Auth(AuthCause), + Purge, + Configuration, + Resource(ResourceCause), +} + +/* + + Http, + Crypto, + Timeout, + Configuration, + Unknown, + +*/ + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum StoreCause { + AssertValue, + BlobMissingMarker, FoundationDB, MySQL, PostgreSQL, RocksDB, SQLite, + Ldap, ElasticSearch, Redis, S3, - Io, - Imap, - Smtp, - Ldap, - BlobMissingMarker, - Unknown, - Purge, - AssertValue, - Timeout, - Thread, + Filesystem, Pool, DataCorruption, Decompress, Deserialize, + NotFound, NotConfigured, - Unsupported, + NotSupported, Unexpected, + Crypto, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum JmapCause { + // Method errors + InvalidArguments, + RequestTooLarge, + StateMismatch, + AnchorNotFound, + UnsupportedFilter, + UnsupportedSort, + UnknownMethod, + InvalidResultReference, + Forbidden, + AccountNotFound, + AccountNotSupportedByMethod, + AccountReadOnly, + NotFound, + CannotCalculateChanges, + UnknownDataType, + + // Request errors + UnknownCapability, + NotJSON, + NotRequest, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum LimitCause { + SizeRequest, + SizeUpload, + CallsIn, + ConcurrentRequest, //RequestError::limit(RequestLimitError::ConcurrentRequest) StatusResponse::bye("Too many concurrent IMAP connections.").into_bytes(), + ConcurrentUpload, //RequestError::limit(RequestLimitError::ConcurrentUpload) + Quota, + BlobQuota, //RequestError::over_blob_quota + TooManyRequests, //RequestError::too_many_requests() + disconnect imap StatusResponse::bye("Too many authentication requests from this IP address.") +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ManageCause { MissingParameter, Invalid, AlreadyExists, + AssertFailed, NotFound, - Configuration, - Fetch, - Acme, - Http, - Crypto, - Dns, - Authentication, + NotSupported, + Error, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AuthCause { + Failed, MissingTotp, - Jmap, - OverQuota, - OverBlobQuota, //RequestError::over_blob_quota - Ingest, - Network, - TooManyRequests, //RequestError::too_many_requests() + disconnect imap StatusResponse::bye("Too many authentication requests from this IP address.") - TooManyConcurrentRequests, //RequestError::limit(RequestLimitError::ConcurrentRequest) StatusResponse::bye("Too many concurrent IMAP connections.").into_bytes(), - TooManyConcurrentUploads, //RequestError::limit(RequestLimitError::ConcurrentUpload) - TooManyAuthAttempts, //RequestError::too_many_auth_attempts() + disconnect imap + TooManyAttempts, //RequestError::too_many_auth_attempts() + disconnect imap Banned, + Invalid, + Error, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ResourceCause { + NotFound, + BadParameters, + Error, } /* diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 47e52e76..1fd16a7e 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -27,6 +27,7 @@ imap_proto = { path = "../crates/imap-proto" } pop3 = { path = "../crates/pop3", features = ["test_mode"] } smtp = { path = "../crates/smtp", features = ["test_mode"] } common = { path = "../crates/common", features = ["test_mode"] } +trc = { path = "../crates/trc" } managesieve = { path = "../crates/managesieve", features = ["test_mode"] } smtp-proto = { version = "0.1" } mail-send = { version = "0.4", default-features = false, features = ["cram-md5", "ring", "tls12"] } diff --git a/tests/src/directory/internal.rs b/tests/src/directory/internal.rs index 2d42cf31..7efc2017 100644 --- a/tests/src/directory/internal.rs +++ b/tests/src/directory/internal.rs @@ -10,7 +10,7 @@ use directory::{ lookup::DirectoryStore, manage::ManageDirectory, PrincipalField, PrincipalUpdate, PrincipalValue, }, - DirectoryError, ManagementError, Principal, QueryBy, Type, + Principal, QueryBy, Type, }; use jmap_proto::types::collection::Collection; use mail_send::Credentials; diff --git a/tests/src/directory/mod.rs b/tests/src/directory/mod.rs index d6efc3e2..2b36b8a2 100644 --- a/tests/src/directory/mod.rs +++ b/tests/src/directory/mod.rs @@ -5,7 +5,7 @@ */ pub mod imap; -pub mod internal; +//pub mod internal; pub mod ldap; pub mod smtp; pub mod sql; @@ -392,6 +392,8 @@ pub fn dummy_tls_acceptor() -> Arc<TlsAcceptor> { let cert_file = &mut BufReader::new(CERT.as_bytes()); let key_file = &mut BufReader::new(PK.as_bytes()); + let todo = "fix interkal"; + // convert files to key/cert objects let cert_chain = certs(cert_file).map(|r| r.unwrap()).collect(); let mut keys: Vec<PrivateKeyDer> = pkcs8_private_keys(key_file) diff --git a/tests/src/directory/smtp.rs b/tests/src/directory/smtp.rs index 07b1a8a6..87aa5c37 100644 --- a/tests/src/directory/smtp.rs +++ b/tests/src/directory/smtp.rs @@ -7,7 +7,7 @@ use std::sync::Arc; use common::listener::limiter::{ConcurrencyLimiter, InFlight}; -use directory::{DirectoryError, QueryBy}; +use directory::QueryBy; use mail_parser::decoders::base64::base64_decode; use mail_send::Credentials; use tokio::{ @@ -87,13 +87,23 @@ async fn lmtp_directory() { .into(), Item::Verify(v) => match core.vrfy(&handle, v).await { Ok(v) => v.into(), - Err(DirectoryError::Unsupported) => LookupResult::False, - Err(e) => panic!("Unexpected error: {e:?}"), + Err(e) => { + if e.matches(trc::StoreCause::NotSupported) { + LookupResult::False + } else { + panic!("Unexpected error: {e:?}") + } + } }, Item::Expand(v) => match core.expn(&handle, v).await { Ok(v) => v.into(), - Err(DirectoryError::Unsupported) => LookupResult::False, - Err(e) => panic!("Unexpected error: {e:?}"), + Err(e) => { + if e.matches(trc::StoreCause::NotSupported) { + LookupResult::False + } else { + panic!("Unexpected error: {e:?}") + } + } }, }; @@ -121,13 +131,23 @@ async fn lmtp_directory() { .into(), Item::Verify(v) => match core.vrfy(&handle, v).await { Ok(v) => v.into(), - Err(DirectoryError::Unsupported) => LookupResult::False, - Err(e) => panic!("Unexpected error: {e:?}"), + Err(e) => { + if e.matches(trc::StoreCause::NotSupported) { + LookupResult::False + } else { + panic!("Unexpected error: {e:?}") + } + } }, Item::Expand(v) => match core.expn(&handle, v).await { Ok(v) => v.into(), - Err(DirectoryError::Unsupported) => LookupResult::False, - Err(e) => panic!("Unexpected error: {e:?}"), + Err(e) => { + if e.matches(trc::StoreCause::NotSupported) { + LookupResult::False + } else { + panic!("Unexpected error: {e:?}") + } + } }, }; diff --git a/tests/src/jmap/thread_merge.rs b/tests/src/jmap/thread_merge.rs index cec7dbbd..2531ad9c 100644 --- a/tests/src/jmap/thread_merge.rs +++ b/tests/src/jmap/thread_merge.rs @@ -10,10 +10,7 @@ use crate::{ jmap::{assert_is_empty, mailbox::destroy_all_mailboxes}, store::deflate_test_resource, }; -use jmap::{ - email::ingest::{IngestEmail, IngestSource}, - IngestError, -}; +use jmap::email::ingest::{IngestEmail, IngestSource}; use jmap_client::{email, mailbox::Role}; use jmap_proto::types::{collection::Collection, id::Id}; use mail_parser::{mailbox::mbox::MessageIterator, MessageParser}; @@ -256,21 +253,16 @@ async fn test_multi_thread(params: &mut JMAPTest) { .await { Ok(_) => break, - Err(IngestError::Temporary) if retry_count < 10 => { - //println!("Retrying ingest for {}...", message.from()); - let backoff = rand::thread_rng().gen_range(50..=300); - tokio::time::sleep(Duration::from_millis(backoff)).await; - retry_count += 1; - continue; - } - Err(IngestError::Permanent { .. }) => { - panic!( - "Failed to ingest message: {:?} {}", - message.from(), - String::from_utf8_lossy(message.contents()) - ); + Err(err) => { + if err.is_assertion_failure() && retry_count < 10 { + //println!("Retrying ingest for {}...", message.from()); + let backoff = rand::thread_rng().gen_range(50..=300); + tokio::time::sleep(Duration::from_millis(backoff)).await; + retry_count += 1; + continue; + } + panic!("Failed to ingest message: {:?}", err); } - Err(err) => panic!("Failed to ingest message: {:?}", err), } } })); diff --git a/tests/src/jmap/webhooks.rs b/tests/src/jmap/webhooks.rs index bffcf98d..7ac11622 100644 --- a/tests/src/jmap/webhooks.rs +++ b/tests/src/jmap/webhooks.rs @@ -140,7 +140,7 @@ pub fn spawn_mock_webhook_endpoint() -> Arc<MockWebhookEndpoint> { //let c = print!("rejected webhook: {}", serde_json::to_string_pretty(&request).unwrap()); Ok::<_, hyper::Error>( - RequestError::not_found().into_http_response() + Err(trc::ResourceCause::NotFound.into_err()) ) } |