Improved error handling (part 1)

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

22
Cargo.lock generated
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -24,7 +24,7 @@ use crate::{
api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse},
email::ingest::{IngestEmail, IngestSource},
mailbox::INBOX_ID,
IngestError, JMAP,
JMAP,
};
#[derive(serde::Deserialize)]
@ -185,8 +185,10 @@ impl JMAP {
batch.clear(ValueClass::Blob(BlobOp::Reserve { hash: request.hash, until: cancel_deletion as u64 }));
}
},
Err(IngestError::Permanent { reason, .. }) => {
results.push(UndeleteResponse::Error { reason });
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();

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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