Improved error handling (part 3)

This commit is contained in:
mdecimus 2024-07-17 18:33:22 +02:00
parent e74b29189a
commit d2ad44cf9f
166 changed files with 2806 additions and 2692 deletions

10
Cargo.lock generated
View file

@ -3623,6 +3623,7 @@ dependencies = [
"tokio",
"tokio-rustls 0.26.0",
"tracing",
"trc",
"utils",
]
@ -5572,9 +5573,9 @@ checksum = "ece8e78b2f38ec51c51f5d475df0a7187ba5111b2a28bdc761ee05b075d40a71"
[[package]]
name = "scc"
version = "2.1.1"
version = "2.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76ad2bbb0ae5100a07b7a6f2ed7ab5fd0045551a4c507989b7a620046ea3efdc"
checksum = "a4465c22496331e20eb047ff46e7366455bc01c0c02015c4a376de0b2cd3a1af"
dependencies = [
"sdd",
]
@ -5627,9 +5628,9 @@ dependencies = [
[[package]]
name = "sdd"
version = "0.2.0"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b84345e4c9bd703274a082fb80caaa99b7612be48dfaa1dd9266577ec412309d"
checksum = "1e806d6633ef141556fef75e345275e35652e9c045bbbc21e6ecfce3e9aa2638"
[[package]]
name = "seahash"
@ -6416,6 +6417,7 @@ dependencies = [
"tokio-rustls 0.26.0",
"tracing",
"tracing-subscriber",
"trc",
"utils",
]

View file

@ -142,7 +142,7 @@ impl Default for MailAuthConfig {
},
iprev: IpRevAuthConfig {
verify: IfBlock::new::<VerifyStrategy>(
"auth.ipref.verify",
"auth.iprev.verify",
[("local_port == 25", "relaxed")],
#[cfg(not(feature = "test_mode"))]
"disable",

View file

@ -255,7 +255,7 @@ impl Core {
}
Ok(None) => Ok(()),
Err(err) => {
if err.matches(trc::Cause::MissingTotp) {
if err.matches(trc::Cause::Auth(trc::AuthCause::MissingTotp)) {
return Err(err);
} else {
Err(err)
@ -331,7 +331,7 @@ impl Core {
.await;
}
Err(trc::Cause::Authentication.into())
Err(trc::AuthCause::Failed.into())
};
}
}
@ -377,7 +377,7 @@ impl Core {
.await;
}
Err(trc::Cause::Banned.into())
Err(trc::AuthCause::Banned.into())
} else {
// Send webhook event
if self.has_webhook_subscribers(WebhookType::AuthFailure) {
@ -394,7 +394,7 @@ impl Core {
.await;
}
Err(trc::Cause::Authentication.into())
Err(trc::AuthCause::Failed.into())
}
} else {
// Send webhook event
@ -411,7 +411,7 @@ impl Core {
)
.await;
}
Err(trc::Cause::Authentication.into())
Err(trc::AuthCause::Failed.into())
}
}
}

View file

@ -75,7 +75,12 @@ impl Core {
{
URL_SAFE_NO_PAD
.decode(content.as_bytes())
.map_err(Into::into)
.map_err(|err| {
trc::Cause::Acme
.caused_by(trc::location!())
.reason(err)
.details("failed to decode certificate")
})
.map(Some)
} else {
Ok(None)

View file

@ -60,7 +60,7 @@ impl Account {
I: IntoIterator<Item = &'a S>,
{
let key_pair = EcdsaKeyPair::from_pkcs8(ALG, key_pair, &SystemRandom::new())
.map_err(|err| trc::Cause::Crypto.reason(err).caused_by(trc::location!()))?;
.map_err(|err| trc::Cause::Acme.reason(err).caused_by(trc::location!()))?;
let contact: Vec<&'a str> = contact.into_iter().map(AsRef::<str>::as_ref).collect();
let payload = json!({
"termsOfServiceAgreed": true,
@ -97,13 +97,19 @@ impl Account {
)?;
let response = https(url.as_ref(), Method::POST, Some(body)).await?;
let location = get_header(&response, "Location").ok();
let body = response.text().await?;
let body = response
.text()
.await
.map_err(|err| trc::Cause::Acme.from_http_error(err))?;
Ok((location, body))
}
pub async fn new_order(&self, domains: Vec<String>) -> trc::Result<(String, Order)> {
let domains: Vec<Identifier> = domains.into_iter().map(Identifier::Dns).collect();
let payload = format!("{{\"identifiers\":{}}}", serde_json::to_string(&domains)?);
let payload = format!(
"{{\"identifiers\":{}}}",
serde_json::to_string(&domains).map_err(|err| trc::Cause::Acme.from_json_error(err))?
);
let response = self.request(&self.directory.new_order, &payload).await?;
let url = response.0.ok_or(
trc::Cause::Acme
@ -111,13 +117,14 @@ impl Account {
.details("Missing header")
.ctx(trc::Key::Id, "Location"),
)?;
let order = serde_json::from_str(&response.1)?;
let order = serde_json::from_str(&response.1)
.map_err(|err| trc::Cause::Acme.from_json_error(err))?;
Ok((url, order))
}
pub async fn auth(&self, url: impl AsRef<str>) -> trc::Result<Auth> {
let response = self.request(url, "").await?;
serde_json::from_str(&response.1).map_err(Into::into)
serde_json::from_str(&response.1).map_err(|err| trc::Cause::Acme.from_json_error(err))
}
pub async fn challenge(&self, url: impl AsRef<str>) -> trc::Result<()> {
@ -126,13 +133,13 @@ impl Account {
pub async fn order(&self, url: impl AsRef<str>) -> trc::Result<Order> {
let response = self.request(&url, "").await?;
serde_json::from_str(&response.1).map_err(Into::into)
serde_json::from_str(&response.1).map_err(|err| trc::Cause::Acme.from_json_error(err))
}
pub async fn finalize(&self, url: impl AsRef<str>, csr: Vec<u8>) -> trc::Result<Order> {
let payload = format!("{{\"csr\":\"{}\"}}", URL_SAFE_NO_PAD.encode(csr));
let response = self.request(&url, &payload).await?;
serde_json::from_str(&response.1).map_err(Into::into)
serde_json::from_str(&response.1).map_err(|err| trc::Cause::Acme.from_json_error(err))
}
pub async fn certificate(&self, url: impl AsRef<str>) -> trc::Result<String> {
@ -155,12 +162,12 @@ impl Account {
params.alg = &PKCS_ECDSA_P256_SHA256;
params.custom_extensions = vec![CustomExtension::new_acme_identifier(key_auth.as_ref())];
let cert = Certificate::from_params(params)
.map_err(|err| trc::Cause::Crypto.caused_by(trc::location!()).reason(err))?;
.map_err(|err| trc::Cause::Acme.caused_by(trc::location!()).reason(err))?;
Ok(Bincode::new(SerializedCert {
certificate: cert
.serialize_der()
.map_err(|err| trc::Cause::Crypto.caused_by(trc::location!()).reason(err))?,
.map_err(|err| trc::Cause::Acme.caused_by(trc::location!()).reason(err))?,
private_key: cert.serialize_private_key_der(),
})
.serialize())
@ -183,9 +190,14 @@ pub struct Directory {
impl Directory {
pub async fn discover(url: impl AsRef<str>) -> trc::Result<Self> {
Ok(serde_json::from_str(
&https(url, Method::GET, None).await?.text().await?,
)?)
serde_json::from_str(
&https(url, Method::GET, None)
.await?
.text()
.await
.map_err(|err| trc::Cause::Acme.from_http_error(err))?,
)
.map_err(|err| trc::Cause::Acme.from_json_error(err))
}
pub async fn nonce(&self) -> trc::Result<String> {
get_header(
@ -286,7 +298,10 @@ async fn https(
);
}
let mut request = builder.build()?.request(method, url);
let mut request = builder
.build()
.map_err(|err| trc::Cause::Acme.from_http_error(err))?
.request(method, url);
if let Some(body) = body {
request = request
@ -294,12 +309,20 @@ async fn https(
.body(body);
}
request.send().await?.assert_success().await
request
.send()
.await
.map_err(|err| trc::Cause::Acme.from_http_error(err))?
.assert_success(trc::Cause::Acme)
.await
}
fn get_header(response: &Response, header: &'static str) -> trc::Result<String> {
match response.headers().get_all(header).iter().last() {
Some(value) => Ok(value.to_str()?.to_string()),
Some(value) => Ok(value
.to_str()
.map_err(|err| trc::Cause::Acme.from_http_str_error(err))?
.to_string()),
None => Err(trc::Cause::Acme
.caused_by(trc::location!())
.details("Missing header")

View file

@ -23,14 +23,15 @@ pub(crate) fn sign(
let combined = format!("{}.{}", &protected, &payload);
let signature = key
.sign(&SystemRandom::new(), combined.as_bytes())
.map_err(|err| trc::Cause::Crypto.caused_by(trc::location!()).reason(err))?;
.map_err(|err| trc::Cause::Acme.caused_by(trc::location!()).reason(err))?;
let signature = URL_SAFE_NO_PAD.encode(signature.as_ref());
let body = Body {
protected,
payload,
signature,
};
Ok(serde_json::to_string(&body)?)
serde_json::to_string(&body).map_err(|err| trc::Cause::Acme.from_json_error(err))
}
pub(crate) fn key_authorization(key: &EcdsaKeyPair, token: &str) -> trc::Result<String> {
@ -84,7 +85,8 @@ impl<'a> Protected<'a> {
nonce,
url,
};
let protected = serde_json::to_vec(&protected)?;
let protected =
serde_json::to_vec(&protected).map_err(|err| trc::Cause::Acme.from_json_error(err))?;
Ok(URL_SAFE_NO_PAD.encode(protected))
}
}
@ -119,7 +121,8 @@ impl Jwk {
x: &self.x,
y: &self.y,
};
let json = serde_json::to_vec(&jwk_thumb)?;
let json =
serde_json::to_vec(&jwk_thumb).map_err(|err| trc::Cause::Acme.from_json_error(err))?;
let hash = digest(&SHA256, &json);
Ok(URL_SAFE_NO_PAD.encode(hash))
}

View file

@ -87,7 +87,7 @@ impl Core {
params.distinguished_name = DistinguishedName::new();
params.alg = &PKCS_ECDSA_P256_SHA256;
let cert = rcgen::Certificate::from_params(params)
.map_err(|err| trc::Cause::Crypto.caused_by(trc::location!()).reason(err))?;
.map_err(|err| trc::Cause::Acme.caused_by(trc::location!()).reason(err))?;
let (order_url, mut order) = account.new_order(provider.domains.clone()).await?;
loop {
@ -122,7 +122,7 @@ impl Core {
}
}
if order.status == OrderStatus::Processing {
return Err(trc::Cause::Timeout
return Err(trc::Cause::Acme
.caused_by(trc::location!())
.details("Order processing timed out"));
}
@ -135,9 +135,9 @@ impl Core {
"Sending CSR"
);
let csr = cert.serialize_request_der().map_err(|err| {
trc::Cause::Crypto.caused_by(trc::location!()).reason(err)
})?;
let csr = cert
.serialize_request_der()
.map_err(|err| trc::Cause::Acme.caused_by(trc::location!()).reason(err))?;
order = account.finalize(order.finalize, csr).await?
}
OrderStatus::Valid { certificate } => {
@ -165,7 +165,7 @@ impl Core {
"Invalid order"
);
return Err(trc::Cause::Invalid.into_err().details("Invalid ACME order"));
return Err(trc::Cause::Acme.into_err().details("Invalid ACME order"));
}
}
}
@ -194,8 +194,9 @@ impl Core {
.iter()
.find(|c| c.typ == challenge_type)
.ok_or(
trc::Cause::MissingParameter
trc::Cause::Acme
.into_err()
.details("Missing Parameter")
.ctx(trc::Key::Id, challenge_type.as_str()),
)?;
@ -342,8 +343,9 @@ impl Core {
}
AuthStatus::Valid => return Ok(()),
_ => {
return Err(trc::Cause::Authentication
return Err(trc::Cause::Acme
.into_err()
.details("Authentication error")
.ctx(trc::Key::Status, auth.status.as_str()))
}
};
@ -373,24 +375,25 @@ impl Core {
return Ok(());
}
_ => {
return Err(trc::Cause::Authentication
return Err(trc::Cause::Acme
.into_err()
.details("Authentication error")
.ctx(trc::Key::Status, auth.status.as_str()))
}
}
}
Err(trc::Cause::Authentication
Err(trc::Cause::Acme
.into_err()
.details("Too many attempts")
.details("Too many authentication attempts")
.ctx(trc::Key::Id, domain))
}
}
fn parse_cert(pem: &[u8]) -> trc::Result<(CertifiedKey, [DateTime<Utc>; 2])> {
let mut pems = pem::parse_many(pem)
.map_err(|err| trc::Cause::Crypto.reason(err).caused_by(trc::location!()))?;
.map_err(|err| trc::Cause::Acme.reason(err).caused_by(trc::location!()))?;
if pems.len() < 2 {
return Err(trc::Cause::Crypto
return Err(trc::Cause::Acme
.caused_by(trc::location!())
.ctx(trc::Key::Size, pems.len())
.details("Too few PEMs"));
@ -399,7 +402,7 @@ fn parse_cert(pem: &[u8]) -> trc::Result<(CertifiedKey, [DateTime<Utc>; 2])> {
pems.remove(0).contents(),
))) {
Ok(pk) => pk,
Err(err) => return Err(trc::Cause::Crypto.reason(err).caused_by(trc::location!())),
Err(err) => return Err(trc::Cause::Acme.reason(err).caused_by(trc::location!())),
};
let cert_chain: Vec<CertificateDer> = pems
.into_iter()
@ -414,7 +417,7 @@ fn parse_cert(pem: &[u8]) -> trc::Result<(CertifiedKey, [DateTime<Utc>; 2])> {
.unwrap_or_default()
})
}
Err(err) => return Err(trc::Cause::Crypto.reason(err).caused_by(trc::location!())),
Err(err) => return Err(trc::Cause::Acme.reason(err).caused_by(trc::location!())),
};
let cert = CertifiedKey::new(cert_chain, pk);
Ok((cert, validity))

View file

@ -77,6 +77,13 @@ pub trait SessionStream: AsyncRead + AsyncWrite + Unpin + 'static + Sync + Send
fn tls_version_and_cipher(&self) -> (Cow<'static, str>, Cow<'static, str>);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SessionResult {
Continue,
Close,
UpgradeTls,
}
pub trait SessionManager: Sync + Send + 'static + Clone {
fn spawn<T: SessionStream>(
&self,

View file

@ -1117,19 +1117,19 @@ pub(super) trait DeserializeBytes {
impl DeserializeBytes for &[u8] {
fn range(&self, range: Range<usize>) -> trc::Result<&[u8]> {
self.get(range.start..std::cmp::min(range.end, self.len()))
.ok_or_else(|| trc::Cause::DataCorruption.caused_by(trc::location!()))
.ok_or_else(|| trc::StoreCause::DataCorruption.caused_by(trc::location!()))
}
fn deserialize_u8(&self, offset: usize) -> trc::Result<u8> {
self.get(offset)
.copied()
.ok_or_else(|| trc::Cause::DataCorruption.caused_by(trc::location!()))
.ok_or_else(|| trc::StoreCause::DataCorruption.caused_by(trc::location!()))
}
fn deserialize_leb128<U: Leb128_>(&self) -> trc::Result<U> {
self.read_leb128::<U>()
.map(|(v, _)| v)
.ok_or_else(|| trc::Cause::DataCorruption.caused_by(trc::location!()))
.ok_or_else(|| trc::StoreCause::DataCorruption.caused_by(trc::location!()))
}
}

View file

@ -316,7 +316,8 @@ impl ConfigManager {
.await
.map_err(|err| {
trc::Cause::Configuration
.caused_by(trc::Error::from(err))
.reason(err)
.details("Failed to write local configuration")
.ctx(trc::Key::Path, self.cfg_local_path.display().to_string())
})
}

View file

@ -36,7 +36,7 @@ impl WebAdminManager {
}
}
pub async fn get(&self, path: &str) -> io::Result<Resource<Vec<u8>>> {
pub async fn get(&self, path: &str) -> trc::Result<Resource<Vec<u8>>> {
let routes = self.routes.load();
if let Some(resource) = routes.get(path).or_else(|| routes.get("index.html")) {
tokio::fs::read(&resource.contents)
@ -45,6 +45,12 @@ impl WebAdminManager {
content_type: resource.content_type,
contents,
})
.map_err(|err| {
trc::ResourceCause::Error
.reason(err)
.ctx(trc::Key::Path, path.to_string())
.caused_by(trc::location!())
})
} else {
Ok(Resource::default())
}
@ -52,42 +58,46 @@ impl WebAdminManager {
pub async fn unpack(&self, blob_store: &BlobStore) -> trc::Result<()> {
// Delete any existing bundles
self.bundle_path.clean().await?;
self.bundle_path.clean().await.map_err(unpack_error)?;
// Obtain webadmin bundle
let bundle = blob_store
.get_blob(WEBADMIN_KEY, 0..usize::MAX)
.await?
.ok_or_else(|| {
trc::Cause::NotFound
trc::ResourceCause::NotFound
.caused_by(trc::location!())
.details("Webadmin bundle not found")
})?;
// Uncompress
let mut bundle = zip::ZipArchive::new(Cursor::new(bundle)).map_err(|err| {
trc::Cause::Decompress
trc::ResourceCause::Error
.caused_by(trc::location!())
.reason(err)
.details("Failed to decompress webadmin bundle")
})?;
let mut routes = AHashMap::new();
for i in 0..bundle.len() {
let (file_name, contents) = {
let mut file = bundle.by_index(i).map_err(|err| {
trc::Cause::Decompress
trc::ResourceCause::Error
.caused_by(trc::location!())
.reason(err)
.details("Failed to read file from webadmin bundle")
})?;
if file.is_dir() {
continue;
}
let mut contents = Vec::new();
file.read_to_end(&mut contents)?;
file.read_to_end(&mut contents).map_err(unpack_error)?;
(file.name().to_string(), contents)
};
let path = self.bundle_path.path.join(format!("{i:02}"));
tokio::fs::write(&path, contents).await?;
tokio::fs::write(&path, contents)
.await
.map_err(unpack_error)?;
let resource = Resource {
content_type: match file_name
@ -164,6 +174,12 @@ impl TempDir {
}
}
fn unpack_error(err: std::io::Error) -> trc::Error {
trc::ResourceCause::Error
.reason(err)
.details("Failed to unpack webadmin bundle")
}
impl Default for WebAdminManager {
fn default() -> Self {
Self::new()

View file

@ -38,7 +38,7 @@ impl ImapDirectory {
AUTH_XOAUTH2
}
_ => {
trc::bail!(trc::Cause::Unsupported
trc::bail!(trc::StoreCause::NotSupported
.ctx(
trc::Key::Reason,
"IMAP server does not offer any supported auth mechanisms."
@ -58,32 +58,32 @@ impl ImapDirectory {
},
}
} else {
Err(trc::Cause::Unsupported
Err(trc::StoreCause::NotSupported
.caused_by(trc::location!())
.protocol(trc::Protocol::Imap))
}
}
pub async fn email_to_ids(&self, _address: &str) -> trc::Result<Vec<u32>> {
Err(trc::Cause::Unsupported
Err(trc::StoreCause::NotSupported
.caused_by(trc::location!())
.protocol(trc::Protocol::Imap))
}
pub async fn rcpt(&self, _address: &str) -> trc::Result<bool> {
Err(trc::Cause::Unsupported
Err(trc::StoreCause::NotSupported
.caused_by(trc::location!())
.protocol(trc::Protocol::Imap))
}
pub async fn vrfy(&self, _address: &str) -> trc::Result<Vec<String>> {
Err(trc::Cause::Unsupported
Err(trc::StoreCause::NotSupported
.caused_by(trc::location!())
.protocol(trc::Protocol::Imap))
}
pub async fn expn(&self, _address: &str) -> trc::Result<Vec<String>> {
Err(trc::Cause::Unsupported
Err(trc::StoreCause::NotSupported
.caused_by(trc::location!())
.protocol(trc::Protocol::Imap))
}

View file

@ -122,7 +122,7 @@ impl ManageDirectory for Store {
return Ok(account_id);
}
Err(err) => {
if err.matches(trc::Cause::AssertValue) && try_count < 3 {
if err.is_assertion_failure() && try_count < 3 {
try_count += 1;
continue;
} else {
@ -422,7 +422,7 @@ impl ManageDirectory for Store {
continue;
}
}
return Err(trc::Cause::Unsupported.caused_by(trc::location!()));
return Err(trc::ManageCause::NotSupported.caused_by(trc::location!()));
}
(
PrincipalAction::Set,
@ -762,7 +762,7 @@ impl ManageDirectory for Store {
}
_ => {
return Err(trc::Cause::Unsupported.caused_by(trc::location!()));
return Err(trc::StoreCause::NotSupported.caused_by(trc::location!()));
}
}
}
@ -1055,18 +1055,28 @@ impl From<Principal<String>> for Principal<u32> {
}
}
fn err_missing(field: impl Into<trc::Value>) -> trc::Error {
trc::Cause::MissingParameter.ctx(trc::Key::Key, field)
pub fn err_missing(field: impl Into<trc::Value>) -> trc::Error {
trc::ManageCause::MissingParameter.ctx(trc::Key::Key, field)
}
fn err_exists(field: impl Into<trc::Value>, value: impl Into<trc::Value>) -> trc::Error {
trc::Cause::AlreadyExists
pub fn err_exists(field: impl Into<trc::Value>, value: impl Into<trc::Value>) -> trc::Error {
trc::ManageCause::AlreadyExists
.ctx(trc::Key::Key, field)
.ctx(trc::Key::Value, value)
}
fn not_found(value: impl Into<trc::Value>) -> trc::Error {
trc::Cause::NotFound.ctx(trc::Key::Key, value)
pub fn not_found(value: impl Into<trc::Value>) -> trc::Error {
trc::ManageCause::NotFound.ctx(trc::Key::Key, value)
}
pub fn unsupported(details: impl Into<trc::Value>) -> trc::Error {
trc::ManageCause::NotSupported.ctx(trc::Key::Details, details)
}
pub fn error(details: impl Into<trc::Value>, reason: Option<impl Into<trc::Value>>) -> trc::Error {
trc::ManageCause::Error
.ctx(trc::Key::Details, details)
.ctx_opt(trc::Key::Reason, reason)
}
impl From<PrincipalField> for trc::Value {

View file

@ -58,7 +58,7 @@ impl Serialize for &Principal<u32> {
impl Deserialize for Principal<u32> {
fn deserialize(bytes: &[u8]) -> trc::Result<Self> {
deserialize(bytes).ok_or_else(|| {
trc::Cause::DataCorruption
trc::StoreCause::DataCorruption
.caused_by(trc::location!())
.ctx(trc::Key::Value, bytes)
})
@ -79,12 +79,12 @@ impl Deserialize for PrincipalIdType {
let mut bytes = bytes_.iter();
Ok(PrincipalIdType {
account_id: bytes.next_leb128().ok_or_else(|| {
trc::Cause::DataCorruption
trc::StoreCause::DataCorruption
.caused_by(trc::location!())
.ctx(trc::Key::Value, bytes_)
})?,
typ: Type::from_u8(*bytes.next().ok_or_else(|| {
trc::Cause::DataCorruption
trc::StoreCause::DataCorruption
.caused_by(trc::location!())
.ctx(trc::Key::Value, bytes_)
})?),

View file

@ -79,7 +79,7 @@ impl LdapDirectory {
{
Ok(Some(principal)) => principal,
Err(err)
if err.matches(trc::Cause::Ldap)
if err.matches(trc::Cause::Store(trc::StoreCause::Ldap))
&& err
.value(trc::Key::Code)
.and_then(|v| v.to_uint())

View file

@ -21,14 +21,14 @@ impl SmtpDirectory {
.authenticate(credentials)
.await
} else {
Err(trc::Cause::Unsupported
Err(trc::StoreCause::NotSupported
.caused_by(trc::location!())
.protocol(trc::Protocol::Smtp))
}
}
pub async fn email_to_ids(&self, _address: &str) -> trc::Result<Vec<u32>> {
Err(trc::Cause::Unsupported
Err(trc::StoreCause::NotSupported
.caused_by(trc::location!())
.protocol(trc::Protocol::Smtp))
}
@ -64,7 +64,7 @@ impl SmtpDirectory {
Ok(true)
}
Severity::PermanentNegativeCompletion => Ok(false),
_ => Err(trc::Cause::Unexpected
_ => Err(trc::StoreCause::Unexpected
.ctx(trc::Key::Protocol, trc::Protocol::Smtp)
.ctx(trc::Key::Code, reply.code())
.ctx(trc::Key::Details, reply.message)),
@ -127,10 +127,10 @@ impl SmtpClient {
.split('\n')
.map(|p| p.to_string())
.collect::<Vec<String>>()),
code @ (550 | 551 | 553 | 500 | 502) => Err(trc::Cause::Unsupported
code @ (550 | 551 | 553 | 500 | 502) => Err(trc::StoreCause::NotSupported
.ctx(trc::Key::Protocol, trc::Protocol::Smtp)
.ctx(trc::Key::Code, code)),
code => Err(trc::Cause::Unexpected
code => Err(trc::StoreCause::Unexpected
.ctx(trc::Key::Protocol, trc::Protocol::Smtp)
.ctx(trc::Key::Code, code)
.ctx(trc::Key::Details, reply.message)),

View file

@ -58,7 +58,11 @@ impl<T: serde::Serialize + serde::de::DeserializeOwned> Principal<T> {
// Token needs to validate with at least one of the TOTP secrets
is_totp_verified = TOTP::from_url(secret)
.map_err(|err| trc::Cause::Invalid.reason(err).details(secret.to_string()))?
.map_err(|err| {
trc::AuthCause::Invalid
.reason(err)
.details(secret.to_string())
})?
.check_current(totp_token)
.unwrap_or(false);
}
@ -82,7 +86,7 @@ impl<T: serde::Serialize + serde::de::DeserializeOwned> Principal<T> {
// Only let the client know if the TOTP code is missing
// if the password is correct
Err(trc::Cause::MissingTotp.into_err())
Err(trc::AuthCause::MissingTotp.into_err())
} else {
// Return the TOTP verification status
@ -124,7 +128,9 @@ async fn verify_hash_prefix(hashed_secret: &str, secret: &str) -> trc::Result<bo
.ok();
}
Err(err) => {
tx.send(Err(trc::Cause::Invalid.reason(err).details(hashed_secret)))
tx.send(Err(trc::AuthCause::Invalid
.reason(err)
.details(hashed_secret)))
.ok();
}
});
@ -149,7 +155,7 @@ async fn verify_hash_prefix(hashed_secret: &str, secret: &str) -> trc::Result<bo
// MD5 based hash
Ok(md5_crypt::verify(secret, hashed_secret))
} else {
Err(trc::Cause::Invalid
Err(trc::AuthCause::Invalid
.into_err()
.details(hashed_secret.to_string()))
}
@ -250,12 +256,12 @@ pub async fn verify_secret_hash(hashed_secret: &str, secret: &str) -> trc::Resul
}
}
"PLAIN" | "plain" | "CLEAR" | "clear" => Ok(hashed_secret == secret),
_ => Err(trc::Cause::Invalid
_ => Err(trc::AuthCause::Invalid
.ctx(trc::Key::Reason, "Unsupported algorithm")
.details(hashed_secret.to_string())),
}
} else {
Err(trc::Cause::Invalid
Err(trc::AuthCause::Invalid
.into_err()
.details(hashed_secret.to_string()))
}

View file

@ -158,10 +158,10 @@ impl IntoError for PoolError<LdapError> {
fn into_error(self) -> trc::Error {
match self {
PoolError::Backend(error) => error.into_error(),
PoolError::Timeout(_) => {
trc::Cause::Timeout.ctx(trc::Key::Protocol, trc::Protocol::Ldap)
}
err => trc::Cause::Pool
PoolError::Timeout(_) => trc::StoreCause::Pool
.ctx(trc::Key::Protocol, trc::Protocol::Ldap)
.details("Connection timed out"),
err => trc::StoreCause::Pool
.ctx(trc::Key::Protocol, trc::Protocol::Ldap)
.reason(err),
}
@ -172,10 +172,10 @@ impl IntoError for PoolError<ImapError> {
fn into_error(self) -> trc::Error {
match self {
PoolError::Backend(error) => error.into_error(),
PoolError::Timeout(_) => {
trc::Cause::Timeout.ctx(trc::Key::Protocol, trc::Protocol::Imap)
}
err => trc::Cause::Pool
PoolError::Timeout(_) => trc::StoreCause::Pool
.ctx(trc::Key::Protocol, trc::Protocol::Imap)
.details("Connection timed out"),
err => trc::StoreCause::Pool
.ctx(trc::Key::Protocol, trc::Protocol::Imap)
.reason(err),
}
@ -186,10 +186,10 @@ impl IntoError for PoolError<mail_send::Error> {
fn into_error(self) -> trc::Error {
match self {
PoolError::Backend(error) => error.into_error(),
PoolError::Timeout(_) => {
trc::Cause::Timeout.ctx(trc::Key::Protocol, trc::Protocol::Smtp)
}
err => trc::Cause::Pool
PoolError::Timeout(_) => trc::StoreCause::Pool
.ctx(trc::Key::Protocol, trc::Protocol::Smtp)
.details("Connection timed out"),
err => trc::StoreCause::Pool
.ctx(trc::Key::Protocol, trc::Protocol::Smtp)
.reason(err),
}
@ -211,9 +211,11 @@ impl IntoError for mail_send::Error {
impl IntoError for LdapError {
fn into_error(self) -> trc::Error {
if let LdapError::LdapResult { result } = &self {
trc::Cause::Ldap.ctx(trc::Key::Code, result.rc).reason(self)
trc::StoreCause::Ldap
.ctx(trc::Key::Code, result.rc)
.reason(self)
} else {
trc::Cause::Ldap.reason(self)
trc::StoreCause::Ldap.reason(self)
}
}
}

View file

@ -250,7 +250,7 @@ impl<T: SessionStream> Session<T> {
}) {
acl
} else {
return Err(trc::Cause::DataCorruption
return Err(trc::StoreCause::DataCorruption
.into_err()
.id(arguments.tag)
.ctx(trc::Key::Reason, "Invalid mailbox ACL")

View file

@ -116,7 +116,7 @@ impl<T: SessionStream> SessionData<T> {
last_change_id = Some(email.change_id);
}
Err(err) => {
return Err(if err.matches(trc::Cause::OverQuota) {
return Err(if err.matches(trc::Cause::Limit(trc::LimitCause::Quota)) {
err.details("Disk quota exceeded.")
.code(ResponseCode::OverQuota)
} else {

View file

@ -25,7 +25,7 @@ impl<T: SessionStream> Session<T> {
if !args.params.is_empty() {
let challenge = base64_decode(args.params.pop().unwrap().as_bytes())
.ok_or_else(|| {
trc::Cause::Authentication
trc::AuthCause::Error
.into_err()
.details("Failed to decode challenge.")
.id(args.tag.clone())
@ -38,7 +38,7 @@ impl<T: SessionStream> Session<T> {
decode_challenge_oauth(&challenge)
}
.map_err(|err| {
trc::Cause::Authentication
trc::AuthCause::Error
.into_err()
.details(err)
.id(args.tag.clone())
@ -55,7 +55,7 @@ impl<T: SessionStream> Session<T> {
self.write_bytes(b"+ \"\"\r\n".to_vec()).await
}
}
_ => Err(trc::Cause::Authentication
_ => Err(trc::AuthCause::Error
.into_err()
.details("Authentication mechanism not supported.")
.id(args.tag)
@ -96,7 +96,7 @@ impl<T: SessionStream> Session<T> {
Some(Some(limiter)) => Some(limiter),
None => None,
Some(None) => {
return Err(trc::Cause::TooManyConcurrentRequests.into());
return Err(trc::LimitCause::ConcurrentRequest.into_err());
}
};

View file

@ -239,7 +239,7 @@ impl<T: SessionStream> SessionData<T> {
changelog.log_child_update(Collection::Mailbox, mailbox_id.mailbox_id);
}
Err(err) => {
if !err.matches(trc::Cause::AssertValue) {
if !err.is_assertion_failure() {
return Err(err.caused_by(trc::location!()));
}
}

View file

@ -516,7 +516,7 @@ impl<T: SessionStream> SessionData<T> {
changelog.log_update(Collection::Email, id);
}
Err(err) => {
if !err.matches(trc::Cause::AssertValue) {
if !err.is_assertion_failure() {
return Err(err.id(arguments.tag));
}
}

View file

@ -239,7 +239,7 @@ impl<T: SessionStream> SessionData<T> {
.await?
.and_then(|obj| obj.get(&Property::Cid).as_uint())
.ok_or_else(|| {
trc::Cause::Unexpected
trc::StoreCause::Unexpected
.into_err()
.details("Mailbox unavailable")
.ctx(trc::Key::Reason, "Failed to obtain uid validity")

View file

@ -288,7 +288,7 @@ impl<T: SessionStream> SessionData<T> {
});
}
}
Err(err) if err.matches(trc::Cause::AssertValue) => {
Err(err) if err.is_assertion_failure() => {
if try_count < MAX_RETRIES {
try_count += 1;
continue;

View file

@ -136,7 +136,9 @@ impl From<MethodError> for trc::Error {
),
};
trc::Cause::Jmap
let todo = "fix";
trc::JmapCause::RequestTooLarge
.ctx(trc::Key::Type, typ)
.ctx(trc::Key::Details, description)
}
@ -184,7 +186,11 @@ impl Serialize for MethodErrorWrapper {
{
let mut map = serializer.serialize_map(2.into())?;
let (error_type, description) = if self.0.matches(trc::Cause::Jmap) {
let todo = "fix";
let (error_type, description) = if self
.0
.matches(trc::Cause::Jmap(trc::JmapCause::RequestTooLarge))
{
(
self.0
.value(trc::Key::Type)

View file

@ -5,8 +5,7 @@
*/
use crate::{
error::method::MethodError,
parser::{json::Parser, Error, Ignore, JsonObjectParser, Token},
parser::{json::Parser, Ignore, JsonObjectParser, Token},
request::{method::MethodObject, RequestProperty},
types::{id::Id, property::Property, state::State},
};
@ -55,7 +54,7 @@ pub enum RequestArguments {
}
impl JsonObjectParser for ChangesRequest {
fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self>
fn parse(parser: &mut Parser<'_>) -> trc::Result<Self>
where
Self: Sized,
{
@ -68,10 +67,9 @@ impl JsonObjectParser for ChangesRequest {
MethodObject::EmailSubmission => RequestArguments::EmailSubmission,
MethodObject::Quota => RequestArguments::Quota,
_ => {
return Err(Error::Method(MethodError::UnknownMethod(format!(
"{}/changes",
parser.ctx
))))
return Err(trc::JmapCause::UnknownMethod
.into_err()
.details(format!("{}/changes", parser.ctx)))
}
},
account_id: Id::default(),

View file

@ -8,9 +8,9 @@ use serde::Serialize;
use utils::map::vec_map::VecMap;
use crate::{
error::{method::MethodError, set::SetError},
error::set::SetError,
object::Object,
parser::{json::Parser, Error, JsonObjectParser, Token},
parser::{json::Parser, JsonObjectParser, Token},
request::{method::MethodObject, reference::MaybeReference, RequestProperty},
types::{
blob::BlobId,
@ -88,7 +88,7 @@ pub enum RequestArguments {
}
impl JsonObjectParser for CopyRequest<RequestArguments> {
fn parse(parser: &mut Parser) -> crate::parser::Result<Self>
fn parse(parser: &mut Parser) -> trc::Result<Self>
where
Self: Sized,
{
@ -96,10 +96,9 @@ impl JsonObjectParser for CopyRequest<RequestArguments> {
arguments: match &parser.ctx {
MethodObject::Email => RequestArguments::Email,
_ => {
return Err(Error::Method(MethodError::UnknownMethod(format!(
"{}/copy",
parser.ctx
))))
return Err(trc::JmapCause::UnknownMethod
.into_err()
.details(format!("{}/copy", parser.ctx)))
}
},
account_id: Id::default(),
@ -159,7 +158,7 @@ impl JsonObjectParser for CopyRequest<RequestArguments> {
}
impl JsonObjectParser for CopyBlobRequest {
fn parse(parser: &mut Parser) -> crate::parser::Result<Self>
fn parse(parser: &mut Parser) -> trc::Result<Self>
where
Self: Sized,
{

View file

@ -7,7 +7,7 @@
use crate::{
error::method::MethodError,
object::{blob, email, Object},
parser::{json::Parser, Error, JsonObjectParser, Token},
parser::{json::Parser, JsonObjectParser, Token},
request::{
method::MethodObject,
reference::{MaybeReference, ResultReference},
@ -55,7 +55,7 @@ pub struct GetResponse {
}
impl JsonObjectParser for GetRequest<RequestArguments> {
fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self>
fn parse(parser: &mut Parser<'_>) -> trc::Result<Self>
where
Self: Sized,
{
@ -73,10 +73,9 @@ impl JsonObjectParser for GetRequest<RequestArguments> {
MethodObject::Blob => RequestArguments::Blob(Default::default()),
MethodObject::Quota => RequestArguments::Quota,
_ => {
return Err(Error::Method(MethodError::UnknownMethod(format!(
"{}/get",
parser.ctx
))))
return Err(trc::JmapCause::UnknownMethod
.into_err()
.details(format!("{}/get", parser.ctx)))
}
},
account_id: Id::default(),
@ -130,11 +129,7 @@ impl JsonObjectParser for GetRequest<RequestArguments> {
}
impl RequestPropertyParser for RequestArguments {
fn parse(
&mut self,
parser: &mut Parser,
property: RequestProperty,
) -> crate::parser::Result<bool> {
fn parse(&mut self, parser: &mut Parser, property: RequestProperty) -> trc::Result<bool> {
match self {
RequestArguments::Email(arguments) => arguments.parse(parser, property),
RequestArguments::Blob(arguments) => arguments.parse(parser, property),

View file

@ -66,7 +66,7 @@ pub struct ImportEmailResponse {
}
impl JsonObjectParser for ImportEmailRequest {
fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self>
fn parse(parser: &mut Parser<'_>) -> trc::Result<Self>
where
Self: Sized,
{
@ -104,7 +104,7 @@ impl JsonObjectParser for ImportEmailRequest {
}
impl JsonObjectParser for ImportEmail {
fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self>
fn parse(parser: &mut Parser<'_>) -> trc::Result<Self>
where
Self: Sized,
{

View file

@ -39,7 +39,7 @@ pub struct BlobInfo {
}
impl JsonObjectParser for BlobLookupRequest {
fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self>
fn parse(parser: &mut Parser<'_>) -> trc::Result<Self>
where
Self: Sized,
{

View file

@ -44,7 +44,7 @@ pub struct ParseEmailResponse {
}
impl JsonObjectParser for ParseEmailRequest {
fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self>
fn parse(parser: &mut Parser<'_>) -> trc::Result<Self>
where
Self: Sized,
{

View file

@ -9,9 +9,8 @@ use std::fmt::Display;
use store::fts::{FilterItem, FilterType, FtsFilter};
use crate::{
error::method::MethodError,
object::{email, mailbox},
parser::{json::Parser, Error, Ignore, JsonObjectParser, Token},
parser::{json::Parser, Ignore, JsonObjectParser, Token},
request::{method::MethodObject, RequestProperty, RequestPropertyParser},
types::{date::UTCDate, id::Id, keyword::Keyword, state::State},
};
@ -151,7 +150,7 @@ pub enum RequestArguments {
}
impl JsonObjectParser for QueryRequest<RequestArguments> {
fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self>
fn parse(parser: &mut Parser<'_>) -> trc::Result<Self>
where
Self: Sized,
{
@ -164,10 +163,9 @@ impl JsonObjectParser for QueryRequest<RequestArguments> {
MethodObject::Principal => RequestArguments::Principal,
MethodObject::Quota => RequestArguments::Quota,
_ => {
return Err(Error::Method(MethodError::UnknownMethod(format!(
"{}/query",
parser.ctx
))))
return Err(trc::JmapCause::UnknownMethod
.into_err()
.details(format!("{}/query", parser.ctx)))
}
},
filter: vec![],
@ -243,7 +241,7 @@ impl JsonObjectParser for QueryRequest<RequestArguments> {
}
}
pub fn parse_filter(parser: &mut Parser) -> crate::parser::Result<Vec<Filter>> {
pub fn parse_filter(parser: &mut Parser) -> trc::Result<Vec<Filter>> {
let mut filter = vec![Filter::Close];
let mut pos_stack = vec![0];
@ -453,9 +451,9 @@ pub fn parse_filter(parser: &mut Parser) -> crate::parser::Result<Vec<Filter>> {
break;
}
} else {
return Err(Error::Method(MethodError::InvalidArguments(
"Malformed filter".to_string(),
)));
return Err(trc::JmapCause::InvalidArguments
.into_err()
.details("Malformed filter"));
}
}
Token::ArrayEnd => {
@ -471,7 +469,7 @@ pub fn parse_filter(parser: &mut Parser) -> crate::parser::Result<Vec<Filter>> {
Ok(filter)
}
pub fn parse_sort(parser: &mut Parser) -> crate::parser::Result<Vec<Comparator>> {
pub fn parse_sort(parser: &mut Parser) -> trc::Result<Vec<Comparator>> {
let mut sort = vec![];
loop {
@ -527,7 +525,7 @@ pub fn parse_sort(parser: &mut Parser) -> crate::parser::Result<Vec<Comparator>>
}
impl JsonObjectParser for SortProperty {
fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self>
fn parse(parser: &mut Parser<'_>) -> trc::Result<Self>
where
Self: Sized,
{
@ -667,11 +665,7 @@ impl Display for SortProperty {
}
impl RequestPropertyParser for RequestArguments {
fn parse(
&mut self,
parser: &mut Parser,
property: RequestProperty,
) -> crate::parser::Result<bool> {
fn parse(&mut self, parser: &mut Parser, property: RequestProperty) -> trc::Result<bool> {
match self {
RequestArguments::Email(args) => args.parse(parser, property),
RequestArguments::Mailbox(args) => args.parse(parser, property),

View file

@ -5,8 +5,7 @@
*/
use crate::{
error::method::MethodError,
parser::{json::Parser, Error, Ignore, JsonObjectParser, Token},
parser::{json::Parser, Ignore, JsonObjectParser, Token},
request::{method::MethodObject, RequestProperty, RequestPropertyParser},
types::{id::Id, state::State},
};
@ -60,7 +59,7 @@ impl AddedItem {
}
impl JsonObjectParser for QueryChangesRequest {
fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self>
fn parse(parser: &mut Parser<'_>) -> trc::Result<Self>
where
Self: Sized,
{
@ -71,10 +70,9 @@ impl JsonObjectParser for QueryChangesRequest {
MethodObject::EmailSubmission => RequestArguments::EmailSubmission,
MethodObject::Quota => RequestArguments::Quota,
_ => {
return Err(Error::Method(MethodError::UnknownMethod(format!(
"{}/queryChanges",
parser.ctx
))))
return Err(trc::JmapCause::UnknownMethod
.into_err()
.details(format!("{}/queryChanges", parser.ctx)))
}
},
filter: vec![],

View file

@ -48,7 +48,7 @@ pub struct SearchSnippet {
}
impl JsonObjectParser for GetSearchSnippetRequest {
fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self>
fn parse(parser: &mut Parser<'_>) -> trc::Result<Self>
where
Self: Sized,
{

View file

@ -13,7 +13,7 @@ use crate::{
set::{InvalidProperty, SetError},
},
object::{email_submission, mailbox, sieve, Object},
parser::{json::Parser, Error, JsonObjectParser, Token},
parser::{json::Parser, JsonObjectParser, Token},
request::{
method::MethodObject,
reference::{MaybeReference, ResultReference},
@ -99,7 +99,7 @@ pub struct SetResponse {
}
impl JsonObjectParser for SetRequest<RequestArguments> {
fn parse(parser: &mut Parser) -> crate::parser::Result<Self>
fn parse(parser: &mut Parser) -> trc::Result<Self>
where
Self: Sized,
{
@ -115,10 +115,9 @@ impl JsonObjectParser for SetRequest<RequestArguments> {
MethodObject::VacationResponse => RequestArguments::VacationResponse,
MethodObject::SieveScript => RequestArguments::SieveScript(Default::default()),
_ => {
return Err(Error::Method(MethodError::UnknownMethod(format!(
"{}/set",
parser.ctx
))))
return Err(trc::JmapCause::UnknownMethod
.into_err()
.details(format!("{}/set", parser.ctx)))
}
},
account_id: Id::default(),
@ -168,7 +167,7 @@ impl JsonObjectParser for SetRequest<RequestArguments> {
}
impl JsonObjectParser for Object<SetValue> {
fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self>
fn parse(parser: &mut Parser<'_>) -> trc::Result<Self>
where
Self: Sized,
{
@ -385,11 +384,7 @@ impl<T: Into<AnyId>> From<Vec<MaybeReference<T, String>>> for SetValue {
}
impl RequestPropertyParser for RequestArguments {
fn parse(
&mut self,
parser: &mut Parser,
property: RequestProperty,
) -> crate::parser::Result<bool> {
fn parse(&mut self, parser: &mut Parser, property: RequestProperty) -> trc::Result<bool> {
match self {
RequestArguments::Mailbox(args) => args.parse(parser, property),
RequestArguments::EmailSubmission(args) => args.parse(parser, property),

View file

@ -64,7 +64,7 @@ pub struct BlobUploadResponseObject {
}
impl JsonObjectParser for BlobUploadRequest {
fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self>
fn parse(parser: &mut Parser<'_>) -> trc::Result<Self>
where
Self: Sized,
{
@ -96,7 +96,7 @@ impl JsonObjectParser for BlobUploadRequest {
}
impl JsonObjectParser for UploadObject {
fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self>
fn parse(parser: &mut Parser<'_>) -> trc::Result<Self>
where
Self: Sized,
{
@ -140,7 +140,7 @@ impl JsonObjectParser for UploadObject {
}
impl JsonObjectParser for DataSourceObject {
fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self>
fn parse(parser: &mut Parser<'_>) -> trc::Result<Self>
where
Self: Sized,
{

View file

@ -27,7 +27,7 @@ pub struct ValidateSieveScriptResponse {
}
impl JsonObjectParser for ValidateSieveScriptRequest {
fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self>
fn parse(parser: &mut Parser<'_>) -> trc::Result<Self>
where
Self: Sized,
{

View file

@ -20,7 +20,7 @@ impl RequestPropertyParser for GetArguments {
&mut self,
parser: &mut Parser,
property: RequestProperty,
) -> crate::parser::Result<bool> {
) -> trc::Result<bool> {
match &property.hash[0] {
0x7465_7366_666f => {
self.offset = parser

View file

@ -29,7 +29,7 @@ impl RequestPropertyParser for GetArguments {
&mut self,
parser: &mut Parser,
property: RequestProperty,
) -> crate::parser::Result<bool> {
) -> trc::Result<bool> {
match (&property.hash[0], &property.hash[1]) {
(0x7365_6974_7265_706f_7250_7964_6f62, _) => {
self.body_properties = <Option<Vec<Property>>>::parse(parser)?;
@ -66,7 +66,7 @@ impl RequestPropertyParser for QueryArguments {
&mut self,
parser: &mut Parser,
property: RequestProperty,
) -> crate::parser::Result<bool> {
) -> trc::Result<bool> {
if property.hash[0] == 0x0073_6461_6572_6854_6573_7061_6c6c_6f63 {
self.collapse_threads = parser
.next_token::<Ignore>()?

View file

@ -25,7 +25,7 @@ impl RequestPropertyParser for SetArguments {
&mut self,
parser: &mut Parser,
property: RequestProperty,
) -> crate::parser::Result<bool> {
) -> trc::Result<bool> {
if property.hash[0] == 0x4565_7461_6470_5573_7365_6363_7553_6e6f
&& property.hash[1] == 0x6c69_616d
{

View file

@ -25,7 +25,7 @@ impl RequestPropertyParser for SetArguments {
&mut self,
parser: &mut Parser,
property: RequestProperty,
) -> crate::parser::Result<bool> {
) -> trc::Result<bool> {
if property.hash[0] == 0x4565_766f_6d65_5279_6f72_7473_6544_6e6f
&& property.hash[1] == 0x0073_6c69_616d
{
@ -44,7 +44,7 @@ impl RequestPropertyParser for QueryArguments {
&mut self,
parser: &mut Parser,
property: RequestProperty,
) -> crate::parser::Result<bool> {
) -> trc::Result<bool> {
match &property.hash[0] {
0x6565_7254_7341_7472_6f73 => {
self.sort_as_tree = parser

View file

@ -124,7 +124,7 @@ impl Serialize for Value {
impl Deserialize for Value {
fn deserialize(bytes: &[u8]) -> trc::Result<Self> {
Self::deserialize_from(&mut bytes.iter()).ok_or_else(|| {
trc::Cause::DataCorruption
trc::StoreCause::DataCorruption
.caused_by(trc::location!())
.ctx(trc::Key::Value, bytes)
})
@ -148,7 +148,7 @@ impl Serialize for &Object<Value> {
impl Deserialize for Object<Value> {
fn deserialize(bytes: &[u8]) -> trc::Result<Self> {
Object::deserialize_from(&mut bytes.iter()).ok_or_else(|| {
trc::Cause::DataCorruption
trc::StoreCause::DataCorruption
.caused_by(trc::location!())
.ctx(trc::Key::Value, bytes)
})

View file

@ -21,7 +21,7 @@ impl RequestPropertyParser for SetArguments {
&mut self,
parser: &mut Parser,
property: RequestProperty,
) -> crate::parser::Result<bool> {
) -> trc::Result<bool> {
if property.hash[0] == 0x7461_7669_7463_4173_7365_6363_7553_6e6f
&& property.hash[1] == 0x0074_7069_7263_5365
{

View file

@ -6,7 +6,7 @@
use utils::codec::{base32_custom::BASE32_INVERSE, leb128::Leb128Iterator};
use super::{json::Parser, Error};
use super::json::Parser;
#[derive(Debug)]
pub struct JsonBase32Reader<'x, 'y> {
@ -38,7 +38,7 @@ impl<'x, 'y> JsonBase32Reader<'x, 'y> {
}
}
pub fn error(&mut self) -> Error {
pub fn error(&mut self) -> trc::Error {
self.bytes.error_value()
}
}

View file

@ -14,7 +14,7 @@ use utils::map::{
use super::{json::Parser, Ignore, JsonObjectParser, Token};
impl JsonObjectParser for u64 {
fn parse(parser: &mut Parser<'_>) -> super::Result<Self>
fn parse(parser: &mut Parser<'_>) -> trc::Result<Self>
where
Self: Sized,
{
@ -36,7 +36,7 @@ impl JsonObjectParser for u64 {
}
impl JsonObjectParser for u128 {
fn parse(parser: &mut Parser<'_>) -> super::Result<Self>
fn parse(parser: &mut Parser<'_>) -> trc::Result<Self>
where
Self: Sized,
{
@ -58,7 +58,7 @@ impl JsonObjectParser for u128 {
}
impl JsonObjectParser for String {
fn parse(parser: &mut Parser<'_>) -> super::Result<Self>
fn parse(parser: &mut Parser<'_>) -> trc::Result<Self>
where
Self: Sized,
{
@ -177,7 +177,7 @@ impl JsonObjectParser for String {
}
impl<T: JsonObjectParser + Eq> JsonObjectParser for Vec<T> {
fn parse(parser: &mut Parser<'_>) -> super::Result<Self>
fn parse(parser: &mut Parser<'_>) -> trc::Result<Self>
where
Self: Sized,
{
@ -197,7 +197,7 @@ impl<T: JsonObjectParser + Eq> JsonObjectParser for Vec<T> {
}
impl<T: JsonObjectParser + Eq> JsonObjectParser for Option<Vec<T>> {
fn parse(parser: &mut Parser<'_>) -> super::Result<Self>
fn parse(parser: &mut Parser<'_>) -> trc::Result<Self>
where
Self: Sized,
{
@ -221,7 +221,7 @@ impl<T: JsonObjectParser + Eq> JsonObjectParser for Option<Vec<T>> {
}
impl<T: JsonObjectParser + Eq + BitmapItem> JsonObjectParser for Bitmap<T> {
fn parse(parser: &mut Parser<'_>) -> super::Result<Self>
fn parse(parser: &mut Parser<'_>) -> trc::Result<Self>
where
Self: Sized,
{
@ -245,7 +245,7 @@ impl<T: JsonObjectParser + Eq + BitmapItem> JsonObjectParser for Bitmap<T> {
}
impl<K: JsonObjectParser + Eq + Display, V: JsonObjectParser> JsonObjectParser for VecMap<K, V> {
fn parse(parser: &mut Parser<'_>) -> super::Result<Self>
fn parse(parser: &mut Parser<'_>) -> trc::Result<Self>
where
Self: Sized,
{
@ -263,7 +263,7 @@ impl<K: JsonObjectParser + Eq + Display, V: JsonObjectParser> JsonObjectParser f
impl<K: JsonObjectParser + Eq + Display, V: JsonObjectParser> JsonObjectParser
for Option<VecMap<K, V>>
{
fn parse(parser: &mut Parser<'_>) -> super::Result<Self>
fn parse(parser: &mut Parser<'_>) -> trc::Result<Self>
where
Self: Sized,
{
@ -284,7 +284,7 @@ impl<K: JsonObjectParser + Eq + Display, V: JsonObjectParser> JsonObjectParser
}
impl JsonObjectParser for bool {
fn parse(parser: &mut Parser<'_>) -> super::Result<Self>
fn parse(parser: &mut Parser<'_>) -> trc::Result<Self>
where
Self: Sized,
{
@ -297,7 +297,7 @@ impl JsonObjectParser for bool {
}
impl JsonObjectParser for Ignore {
fn parse(parser: &mut Parser<'_>) -> super::Result<Self>
fn parse(parser: &mut Parser<'_>) -> trc::Result<Self>
where
Self: Sized,
{

View file

@ -6,9 +6,9 @@
use std::{fmt::Display, iter::Peekable, slice::Iter};
use crate::{error::method::MethodError, request::method::MethodObject};
use crate::request::method::MethodObject;
use super::{Error, Ignore, JsonObjectParser, Token};
use super::{Ignore, JsonObjectParser, Token};
const MAX_NESTED_LEVELS: u32 = 16;
@ -40,25 +40,33 @@ impl<'x> Parser<'x> {
}
}
pub fn error(&self, message: &str) -> Error {
format!("{message} at position {}.", self.pos).into()
pub fn error(&self, message: &str) -> trc::Error {
trc::JmapCause::NotJSON
.into_err()
.details(format!("{message} at position {}.", self.pos))
}
pub fn error_unterminated(&self) -> Error {
format!("Unterminated string at position {pos}.", pos = self.pos).into()
pub fn error_unterminated(&self) -> trc::Error {
trc::JmapCause::NotJSON.into_err().details(format!(
"Unterminated string at position {pos}.",
pos = self.pos
))
}
pub fn error_utf8(&self) -> Error {
format!("Invalid UTF-8 sequence at position {pos}.", pos = self.pos).into()
pub fn error_utf8(&self) -> trc::Error {
trc::JmapCause::NotJSON.into_err().details(format!(
"Invalid UTF-8 sequence at position {pos}.",
pos = self.pos
))
}
pub fn error_value(&mut self) -> Error {
pub fn error_value(&mut self) -> trc::Error {
if self.is_eof || self.skip_string() {
Error::Method(MethodError::InvalidArguments(format!(
trc::JmapCause::InvalidArguments.into_err().details(format!(
"Invalid value {:?} at position {}.",
String::from_utf8_lossy(self.bytes[self.pos_marker..self.pos - 1].as_ref()),
self.pos
)))
))
} else {
self.error_unterminated()
}
@ -76,7 +84,7 @@ impl<'x> Parser<'x> {
}
#[inline(always)]
pub fn next_unescaped(&mut self) -> super::Result<Option<u8>> {
pub fn next_unescaped(&mut self) -> trc::Result<Option<u8>> {
match self.next_char() {
Some(b'"') => {
self.is_eof = true;
@ -112,7 +120,7 @@ impl<'x> Parser<'x> {
false
}
pub fn next_token<T: JsonObjectParser>(&mut self) -> super::Result<Token<T>> {
pub fn next_token<T: JsonObjectParser>(&mut self) -> trc::Result<Token<T>> {
let mut next_ch = self.next_ch.take().or_else(|| self.next_char());
while let Some(mut ch) = next_ch {
@ -263,9 +271,7 @@ impl<'x> Parser<'x> {
Err(self.error("Unexpected EOF"))
}
pub fn next_dict_key<T: JsonObjectParser + Display + Eq>(
&mut self,
) -> super::Result<Option<T>> {
pub fn next_dict_key<T: JsonObjectParser + Display + Eq>(&mut self) -> trc::Result<Option<T>> {
loop {
match self.next_token::<T>()? {
Token::String(k) => {
@ -281,11 +287,7 @@ impl<'x> Parser<'x> {
}
}
pub fn skip_token(
&mut self,
start_depth_array: u32,
start_depth_dict: u32,
) -> super::Result<()> {
pub fn skip_token(&mut self, start_depth_array: u32, start_depth_dict: u32) -> trc::Result<()> {
while {
self.next_token::<Ignore>()?;
start_depth_array != self.depth_array || start_depth_dict != self.depth_dict

View file

@ -6,8 +6,6 @@
use std::fmt::Display;
use crate::error::{method::MethodError, request::RequestError};
use self::json::Parser;
pub mod base32;
@ -32,31 +30,23 @@ pub enum Token<T> {
impl<T: PartialEq> Eq for Token<T> {}
pub trait JsonObjectParser {
fn parse(parser: &mut Parser<'_>) -> Result<Self>
fn parse(parser: &mut Parser<'_>) -> trc::Result<Self>
where
Self: Sized;
}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug)]
pub enum Error {
Request(RequestError),
Method(MethodError),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Ignore {}
impl<T: Eq> Token<T> {
pub fn unwrap_string(self, property: &str) -> Result<T> {
pub fn unwrap_string(self, property: &str) -> trc::Result<T> {
match self {
Token::String(s) => Ok(s),
token => Err(token.error(property, "string")),
}
}
pub fn unwrap_string_or_null(self, property: &str) -> Result<Option<T>> {
pub fn unwrap_string_or_null(self, property: &str) -> trc::Result<Option<T>> {
match self {
Token::String(s) => Ok(Some(s)),
Token::Null => Ok(None),
@ -64,14 +54,14 @@ impl<T: Eq> Token<T> {
}
}
pub fn unwrap_bool(self, property: &str) -> Result<bool> {
pub fn unwrap_bool(self, property: &str) -> trc::Result<bool> {
match self {
Token::Boolean(v) => Ok(v),
token => Err(token.error(property, "boolean")),
}
}
pub fn unwrap_bool_or_null(self, property: &str) -> Result<Option<bool>> {
pub fn unwrap_bool_or_null(self, property: &str) -> trc::Result<Option<bool>> {
match self {
Token::Boolean(v) => Ok(Some(v)),
Token::Null => Ok(None),
@ -79,7 +69,7 @@ impl<T: Eq> Token<T> {
}
}
pub fn unwrap_usize_or_null(self, property: &str) -> Result<Option<usize>> {
pub fn unwrap_usize_or_null(self, property: &str) -> trc::Result<Option<usize>> {
match self {
Token::Integer(v) if v >= 0 => Ok(Some(v as usize)),
Token::Float(v) if v >= 0.0 => Ok(Some(v as usize)),
@ -88,7 +78,7 @@ impl<T: Eq> Token<T> {
}
}
pub fn unwrap_uint_or_null(self, property: &str) -> Result<Option<u64>> {
pub fn unwrap_uint_or_null(self, property: &str) -> trc::Result<Option<u64>> {
match self {
Token::Integer(v) if v >= 0 => Ok(Some(v as u64)),
Token::Float(v) if v >= 0.0 => Ok(Some(v as u64)),
@ -97,7 +87,7 @@ impl<T: Eq> Token<T> {
}
}
pub fn unwrap_int_or_null(self, property: &str) -> Result<Option<i64>> {
pub fn unwrap_int_or_null(self, property: &str) -> trc::Result<Option<i64>> {
match self {
Token::Integer(v) => Ok(Some(v)),
Token::Float(v) => Ok(Some(v as i64)),
@ -106,7 +96,7 @@ impl<T: Eq> Token<T> {
}
}
pub fn unwrap_ints_or_null(self, property: &str) -> Result<Option<i32>> {
pub fn unwrap_ints_or_null(self, property: &str) -> trc::Result<Option<i32>> {
match self {
Token::Integer(v) => Ok(Some(v as i32)),
Token::Float(v) => Ok(Some(v as i32)),
@ -115,7 +105,7 @@ impl<T: Eq> Token<T> {
}
}
pub fn assert(self, token: Token<T>) -> Result<()> {
pub fn assert(self, token: Token<T>) -> trc::Result<()> {
if self == token {
Ok(())
} else {
@ -123,22 +113,22 @@ impl<T: Eq> Token<T> {
}
}
pub fn assert_jmap(self, token: Token<T>) -> Result<()> {
pub fn assert_jmap(self, token: Token<T>) -> trc::Result<()> {
if self == token {
Ok(())
} else {
Err(Error::Request(RequestError::not_request(format!(
Err(trc::JmapCause::NotRequest.into_err().details(format!(
"Invalid JMAP request: expected '{token}', got '{self}'."
))))
)))
}
}
pub fn error(&self, property: &str, expected: &str) -> Error {
Error::Method(MethodError::InvalidArguments(if !property.is_empty() {
pub fn error(&self, property: &str, expected: &str) -> trc::Error {
trc::JmapCause::InvalidArguments.into_err().details(if !property.is_empty() {
format!("Invalid argument for '{property:?}': expected '{expected}', got '{self}'.",)
} else {
format!("Invalid argument: expected '{expected}', got '{self}'.")
}))
})
}
}
@ -148,18 +138,6 @@ impl Display for Ignore {
}
}
impl From<String> for Error {
fn from(s: String) -> Self {
Error::Request(RequestError::not_json(&s))
}
}
impl From<&str> for Error {
fn from(s: &str) -> Self {
Error::Request(RequestError::not_json(s))
}
}
impl<T> Display for Token<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {

View file

@ -7,8 +7,7 @@
use utils::map::vec_map::VecMap;
use crate::{
error::request::RequestError,
parser::{json::Parser, Error, JsonObjectParser},
parser::{json::Parser, JsonObjectParser},
response::serialize::serialize_hex,
types::{id::Id, type_state::DataType},
};
@ -315,7 +314,7 @@ impl WebSocketCapabilities {
}
impl JsonObjectParser for Capability {
fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self>
fn parse(parser: &mut Parser<'_>) -> trc::Result<Self>
where
Self: Sized,
{
@ -343,18 +342,19 @@ impl JsonObjectParser for Capability {
0x0061_746f_7571 => Ok(Capability::Quota),
_ => Err(parser.error_capability()),
},
Err(Error::Method(_)) => Err(parser.error_capability()),
Err(err @ Error::Request(_)) => Err(err),
Err(err) if err.is_jmap_method_error() => Err(parser.error_capability()),
Err(err) => Err(err),
}
}
}
impl<'x> Parser<'x> {
fn error_capability(&mut self) -> Error {
fn error_capability(&mut self) -> trc::Error {
if self.is_eof || self.skip_string() {
Error::Request(RequestError::unknown_capability(&String::from_utf8_lossy(
self.bytes[self.pos_marker..self.pos - 1].as_ref(),
)))
trc::JmapCause::UnknownCapability.into_err().details(
String::from_utf8_lossy(self.bytes[self.pos_marker..self.pos - 1].as_ref())
.into_owned(),
)
} else {
self.error_unterminated()
}

View file

@ -15,7 +15,7 @@ pub struct Echo {
}
impl JsonObjectParser for Echo {
fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self>
fn parse(parser: &mut Parser<'_>) -> trc::Result<Self>
where
Self: Sized,
{

View file

@ -48,7 +48,7 @@ pub enum MethodFunction {
}
impl JsonObjectParser for MethodName {
fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self>
fn parse(parser: &mut Parser<'_>) -> trc::Result<Self>
where
Self: Sized,
{

View file

@ -77,7 +77,7 @@ pub enum RequestMethod {
}
impl JsonObjectParser for RequestProperty {
fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self>
fn parse(parser: &mut Parser<'_>) -> trc::Result<Self>
where
Self: Sized,
{
@ -116,5 +116,5 @@ pub trait RequestPropertyParser {
&mut self,
parser: &mut Parser,
property: RequestProperty,
) -> crate::parser::Result<bool>;
) -> trc::Result<bool>;
}

View file

@ -7,10 +7,6 @@
use std::collections::HashMap;
use crate::{
error::{
method::MethodError,
request::{RequestError, RequestLimitError},
},
method::{
changes::ChangesRequest,
copy::{CopyBlobRequest, CopyRequest},
@ -25,7 +21,7 @@ use crate::{
upload::BlobUploadRequest,
validate::ValidateSieveScriptRequest,
},
parser::{json::Parser, Error, Ignore, JsonObjectParser, Token},
parser::{json::Parser, Ignore, JsonObjectParser, Token},
types::any_id::AnyId,
};
@ -37,7 +33,7 @@ use super::{
};
impl Request {
pub fn parse(json: &[u8], max_calls: usize, max_size: usize) -> Result<Self, RequestError> {
pub fn parse(json: &[u8], max_calls: usize, max_size: usize) -> trc::Result<Self> {
if json.len() <= max_size {
let mut request = Request {
using: 0,
@ -54,10 +50,12 @@ impl Request {
if found_valid_keys {
Ok(request)
} else {
Err(RequestError::not_request("Invalid JMAP request"))
Err(trc::JmapCause::NotRequest
.into_err()
.details("Invalid JMAP request"))
}
} else {
Err(RequestError::limit(RequestLimitError::SizeRequest))
Err(trc::LimitCause::SizeRequest.into_err())
}
}
@ -66,7 +64,7 @@ impl Request {
parser: &mut Parser,
max_calls: usize,
key: u128,
) -> Result<bool, RequestError> {
) -> trc::Result<bool> {
match key {
0x0067_6e69_7375 => {
parser.next_token::<Ignore>()?.assert(Token::ArrayStart)?;
@ -77,7 +75,7 @@ impl Request {
}
Token::Comma => (),
Token::ArrayEnd => break,
token => return Err(token.error("capability", &token.to_string()).into()),
token => return Err(token.error("capability", &token.to_string())),
}
}
Ok(true)
@ -92,20 +90,28 @@ impl Request {
Token::Comma => continue,
Token::ArrayEnd => break,
_ => {
return Err(RequestError::not_request("Invalid JMAP request"));
return Err(trc::JmapCause::NotRequest
.into_err()
.details("Invalid JMAP request"));
}
};
if self.method_calls.len() < max_calls {
let method_name = match parser.next_token::<MethodName>() {
Ok(Token::String(method)) => method,
Ok(_) => {
return Err(RequestError::not_request("Invalid JMAP request"));
return Err(trc::JmapCause::NotRequest
.into_err()
.details("Invalid JMAP request"));
}
Err(Error::Method(MethodError::InvalidArguments(_))) => {
Err(err)
if err.matches(trc::Cause::Jmap(
trc::JmapCause::InvalidArguments,
)) =>
{
MethodName::error()
}
Err(err) => {
return Err(err.into());
return Err(err);
}
};
parser.next_token::<Ignore>()?.assert_jmap(Token::Comma)?;
@ -169,19 +175,19 @@ impl Request {
(MethodFunction::Echo, MethodObject::Core) => {
Echo::parse(parser).map(RequestMethod::Echo)
}
_ => Err(Error::Method(MethodError::UnknownMethod(
method_name.to_string(),
))),
_ => Err(trc::JmapCause::UnknownMethod
.into_err()
.details(method_name.to_string())),
};
let method = match method {
Ok(method) => method,
Err(Error::Method(err)) => {
Err(err) if !err.is_jmap_method_error() => {
parser.skip_token(start_depth_array, start_depth_dict)?;
RequestMethod::Error(err.into())
RequestMethod::Error(err)
}
Err(err) => {
return Err(err.into());
return Err(err);
}
};
@ -196,7 +202,7 @@ impl Request {
name: method_name,
});
} else {
return Err(RequestError::limit(RequestLimitError::CallsIn));
return Err(trc::LimitCause::CallsIn.into_err());
}
}
Ok(true)
@ -221,15 +227,6 @@ impl Request {
}
}
impl From<Error> for RequestError {
fn from(value: Error) -> Self {
match value {
Error::Request(err) => err,
Error::Method(err) => RequestError::not_request(err.to_string()),
}
}
}
#[cfg(test)]
mod tests {
use crate::request::Request;

View file

@ -7,8 +7,7 @@
use std::fmt::Display;
use crate::{
error::method::MethodError,
parser::{json::Parser, Error, JsonObjectParser, Token},
parser::{json::Parser, JsonObjectParser, Token},
types::{id::Id, pointer::JSONPointer},
};
@ -45,7 +44,7 @@ impl<V, R> MaybeReference<V, R> {
}
impl JsonObjectParser for ResultReference {
fn parse(parser: &mut Parser) -> crate::parser::Result<Self>
fn parse(parser: &mut Parser) -> trc::Result<Self>
where
Self: Sized,
{
@ -81,15 +80,15 @@ impl JsonObjectParser for ResultReference {
path,
})
} else {
Err(Error::Method(MethodError::InvalidResultReference(
"Missing required fields".into(),
)))
Err(trc::JmapCause::InvalidResultReference
.into_err()
.details("Missing required fields"))
}
}
}
impl<T: JsonObjectParser> JsonObjectParser for MaybeReference<T, String> {
fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self>
fn parse(parser: &mut Parser<'_>) -> trc::Result<Self>
where
Self: Sized,
{

View file

@ -8,7 +8,7 @@ use std::{borrow::Cow, collections::HashMap};
use crate::{
error::request::{RequestError, RequestErrorType, RequestLimitError},
parser::{json::Parser, Error, JsonObjectParser, Token},
parser::{json::Parser, JsonObjectParser, Token},
request::Call,
response::{serialize::serialize_hex, Response, ResponseMethod},
types::{any_id::AnyId, id::Id, state::State, type_state::DataType},
@ -108,11 +108,7 @@ enum MessageType {
}
impl WebSocketMessage {
pub fn parse(
json: &[u8],
max_calls: usize,
max_size: usize,
) -> Result<Self, WebSocketRequestError> {
pub fn parse(json: &[u8], max_calls: usize, max_size: usize) -> trc::Result<Self> {
if json.len() <= max_size {
let mut message_type = MessageType::None;
let mut request = WebSocketRequest {
@ -174,10 +170,12 @@ impl WebSocketMessage {
MessageType::PushDisable if !found_request_keys && !found_push_keys => {
Ok(WebSocketMessage::PushDisable)
}
_ => Err(RequestError::not_request("Invalid WebSocket JMAP request").into()),
_ => Err(trc::JmapCause::NotRequest
.into_err()
.details("Invalid WebSocket JMAP request")),
}
} else {
Err(RequestError::limit(RequestLimitError::SizeRequest).into())
Err(trc::LimitCause::SizeRequest.into_err())
}
}
}
@ -205,12 +203,6 @@ impl From<RequestError> for WebSocketRequestError {
}
}
impl From<Error> for WebSocketRequestError {
fn from(value: Error) -> Self {
RequestError::from(value).into()
}
}
impl WebSocketResponse {
pub fn from_response(response: Response, request_id: Option<String>) -> Self {
Self {

View file

@ -27,7 +27,7 @@ pub enum Acl {
}
impl JsonObjectParser for Acl {
fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self>
fn parse(parser: &mut Parser<'_>) -> trc::Result<Self>
where
Self: Sized,
{

View file

@ -96,7 +96,7 @@ impl From<&AnyId> for Value {
}
impl JsonObjectParser for AnyId {
fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self>
fn parse(parser: &mut Parser<'_>) -> trc::Result<Self>
where
Self: Sized,
{

View file

@ -38,7 +38,7 @@ pub struct BlobSection {
}
impl JsonObjectParser for BlobId {
fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self>
fn parse(parser: &mut Parser<'_>) -> trc::Result<Self>
where
Self: Sized,
{

View file

@ -24,7 +24,7 @@ pub struct UTCDate {
}
impl JsonObjectParser for UTCDate {
fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self>
fn parse(parser: &mut Parser<'_>) -> trc::Result<Self>
where
Self: Sized,
{

View file

@ -24,7 +24,7 @@ impl Default for Id {
}
impl JsonObjectParser for Id {
fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self>
fn parse(parser: &mut Parser<'_>) -> trc::Result<Self>
where
Self: Sized,
{

View file

@ -61,7 +61,7 @@ pub enum Keyword {
}
impl JsonObjectParser for Keyword {
fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self>
fn parse(parser: &mut Parser<'_>) -> trc::Result<Self>
where
Self: Sized,
{

View file

@ -29,7 +29,7 @@ pub enum MaybeUnparsable<V> {
}
impl<V: JsonObjectParser> JsonObjectParser for MaybeUnparsable<V> {
fn parse(parser: &mut Parser) -> crate::parser::Result<Self> {
fn parse(parser: &mut Parser) -> trc::Result<Self> {
match V::parse(parser) {
Ok(value) => Ok(MaybeUnparsable::Value(value)),
Err(_) if parser.is_eof || parser.skip_string() => Ok(MaybeUnparsable::ParseError(

View file

@ -26,7 +26,7 @@ enum TokenType {
}
impl JsonObjectParser for JSONPointer {
fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self>
fn parse(parser: &mut Parser<'_>) -> trc::Result<Self>
where
Self: Sized,
{

View file

@ -10,7 +10,7 @@ use mail_parser::HeaderName;
use serde::Serialize;
use store::write::{DeserializeFrom, SerializeInto};
use crate::parser::{json::Parser, Error, JsonObjectParser};
use crate::parser::{json::Parser, JsonObjectParser};
use super::{acl::Acl, id::Id, keyword::Keyword, value::Value};
@ -153,7 +153,7 @@ pub trait IntoProperty: Eq + Display {
}
impl JsonObjectParser for Property {
fn parse(parser: &mut Parser) -> crate::parser::Result<Self> {
fn parse(parser: &mut Parser) -> trc::Result<Self> {
let mut first_char = 0;
let mut hash = 0;
let mut shift = 0;
@ -190,7 +190,7 @@ impl JsonObjectParser for Property {
}
impl JsonObjectParser for SetProperty {
fn parse(parser: &mut Parser) -> crate::parser::Result<Self> {
fn parse(parser: &mut Parser) -> trc::Result<Self> {
let mut first_char = 0;
let mut hash = 0;
let mut shift = 0;
@ -251,7 +251,7 @@ impl JsonObjectParser for SetProperty {
Ok(id) => {
patch.push(Value::Id(id));
}
Err(Error::Method(_)) => {
Err(err) if err.is_jmap_method_error() => {
property = parser.invalid_property()?;
}
Err(err) => {
@ -262,7 +262,7 @@ impl JsonObjectParser for SetProperty {
Ok(keyword) => {
patch.push(Value::Keyword(keyword));
}
Err(Error::Method(_)) => {
Err(err) if err.is_jmap_method_error() => {
property = parser.invalid_property()?;
}
Err(err) => {
@ -290,7 +290,7 @@ impl JsonObjectParser for SetProperty {
Ok(acl) => {
patch.push(Value::UnsignedInt(acl as u64));
}
Err(Error::Method(_)) => {
Err(err) if err.is_jmap_method_error() => {
property = parser.invalid_property()?;
}
Err(err) => {
@ -469,7 +469,7 @@ fn parse_property(first_char: u8, hash: u128) -> Option<Property> {
})
}
fn parse_header_property(parser: &mut Parser) -> crate::parser::Result<Property> {
fn parse_header_property(parser: &mut Parser) -> trc::Result<Property> {
let hdr_start_pos = parser.pos;
let mut has_next = false;
@ -553,7 +553,7 @@ fn parse_sub_property(
parser: &mut Parser,
first_char: u8,
parent_hash: u128,
) -> crate::parser::Result<Property> {
) -> trc::Result<Property> {
let mut hash = 0;
let mut shift = 0;
@ -585,7 +585,7 @@ fn parse_sub_property(
}
impl JsonObjectParser for ObjectProperty {
fn parse(parser: &mut Parser) -> crate::parser::Result<Self> {
fn parse(parser: &mut Parser) -> trc::Result<Self> {
let mut first_char = 0;
let mut hash = 0;
let mut shift = 0;
@ -707,7 +707,7 @@ impl JsonObjectParser for ObjectProperty {
}
impl<'x> Parser<'x> {
fn invalid_property(&mut self) -> crate::parser::Result<Property> {
fn invalid_property(&mut self) -> trc::Result<Property> {
if self.is_eof || self.skip_string() {
Ok(Property::_T(
String::from_utf8_lossy(self.bytes[self.pos_marker..self.pos - 1].as_ref())

View file

@ -72,7 +72,7 @@ impl From<Option<ChangeId>> for State {
}
impl JsonObjectParser for State {
fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self>
fn parse(parser: &mut Parser<'_>) -> trc::Result<Self>
where
Self: Sized,
{

View file

@ -85,7 +85,7 @@ impl From<DataType> for u64 {
}
impl JsonObjectParser for DataType {
fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self>
fn parse(parser: &mut Parser<'_>) -> trc::Result<Self>
where
Self: Sized,
{

View file

@ -78,7 +78,7 @@ impl Value {
pub fn parse<K: JsonObjectParser + IntoProperty, V: JsonObjectParser + IntoValue>(
token: Token<V>,
parser: &mut Parser<'_>,
) -> crate::parser::Result<Self> {
) -> trc::Result<Self> {
Ok(match token {
Token::String(v) => v.into_value(),
Token::DictStart => {
@ -114,7 +114,7 @@ impl Value {
pub fn from_property(
parser: &mut Parser<'_>,
property: &Property,
) -> crate::parser::Result<Self> {
) -> trc::Result<Self> {
match &property {
Property::BlobId => Ok(parser
.next_token::<BlobId>()?
@ -327,7 +327,7 @@ impl Value {
}
impl<T: JsonObjectParser + Display + Eq> JsonObjectParser for SetValueMap<T> {
fn parse(parser: &mut Parser<'_>) -> crate::parser::Result<Self>
fn parse(parser: &mut Parser<'_>) -> trc::Result<Self>
where
Self: Sized,
{

View file

@ -8,7 +8,6 @@ use std::fmt::Write;
use common::manager::webadmin::Resource;
use directory::QueryBy;
use jmap_proto::error::request::RequestError;
use quick_xml::events::Event;
use quick_xml::Reader;
use utils::url_params::UrlParams;
@ -18,22 +17,15 @@ use crate::{api::http::ToHttpResponse, JMAP};
use super::{HttpRequest, HttpResponse};
impl JMAP {
pub async fn handle_autoconfig_request(&self, req: &HttpRequest) -> HttpResponse {
pub async fn handle_autoconfig_request(&self, req: &HttpRequest) -> trc::Result<HttpResponse> {
// Obtain parameters
let params = UrlParams::new(req.uri().query());
let emailaddress = params
.get("emailaddress")
.unwrap_or_default()
.to_lowercase();
let (account_name, server_name, domain) =
match self.autoconfig_parameters(&emailaddress).await {
Ok(result) => result,
Err(err) => return err.into_http_response(),
};
let services = match self.core.storage.config.get_services().await {
Ok(services) => services,
Err(err) => return err.into_http_response(),
};
let (account_name, server_name, domain) = self.autoconfig_parameters(&emailaddress).await?;
let services = self.core.storage.config.get_services().await?;
// Build XML response
let mut config = String::with_capacity(1024);
@ -75,30 +67,27 @@ impl JMAP {
);
config.push_str("</clientConfig>\n");
Resource {
Ok(Resource {
content_type: "application/xml; charset=utf-8",
contents: config.into_bytes(),
}
.into_http_response()
.into_http_response())
}
pub async fn handle_autodiscover_request(&self, body: Option<Vec<u8>>) -> HttpResponse {
pub async fn handle_autodiscover_request(
&self,
body: Option<Vec<u8>>,
) -> trc::Result<HttpResponse> {
// Obtain parameters
let emailaddress = match parse_autodiscover_request(body.as_deref().unwrap_or_default()) {
Ok(emailaddress) => emailaddress,
Err(err) => {
return RequestError::blank(400, "Failed to parse autodiscover request", err)
.into_http_response()
}
};
let (account_name, server_name, _) = match self.autoconfig_parameters(&emailaddress).await {
Ok(result) => result,
Err(err) => return err.into_http_response(),
};
let services = match self.core.storage.config.get_services().await {
Ok(services) => services,
Err(err) => return err.into_http_response(),
};
let emailaddress = parse_autodiscover_request(body.as_deref().unwrap_or_default())
.map_err(|err| {
trc::ResourceCause::BadParameters
.into_err()
.details("Failed to parse autodiscover request")
.ctx(trc::Key::Reason, err)
})?;
let (account_name, server_name, _) = self.autoconfig_parameters(&emailaddress).await?;
let services = self.core.storage.config.get_services().await?;
// Build XML response
let mut config = String::with_capacity(1024);
@ -158,36 +147,35 @@ impl JMAP {
let _ = writeln!(&mut config, "\t</Response>");
let _ = writeln!(&mut config, "</Autodiscover>");
Resource {
Ok(Resource {
content_type: "application/xml; charset=utf-8",
contents: config.into_bytes(),
}
.into_http_response()
.into_http_response())
}
async fn autoconfig_parameters<'x>(
&self,
emailaddress: &'x str,
) -> Result<(String, String, &'x str), RequestError> {
let domain = if let Some((_, domain)) = emailaddress.rsplit_once('@') {
domain
} else {
return Err(RequestError::invalid_parameters());
};
) -> trc::Result<(String, String, &'x str)> {
let (_, domain) = emailaddress.rsplit_once('@').ok_or_else(|| {
trc::ResourceCause::BadParameters
.into_err()
.details("Missing domain in email address")
})?;
// Obtain server name
let server_name = if let Ok(Some(server_name)) = self
let server_name = self
.core
.storage
.config
.get("lookup.default.hostname")
.await
{
server_name
} else {
tracing::error!("Autoconfig request failed: Server name not configured");
return Err(RequestError::internal_server_error());
};
.await?
.ok_or_else(|| {
trc::Cause::Configuration
.into_err()
.details("Server name not configured")
})?;
// Find the account name by e-mail address
let mut account_name = emailaddress.to_string();

View file

@ -14,12 +14,12 @@ use hyper::{
body::{Bytes, Frame},
header, StatusCode,
};
use jmap_proto::{error::request::RequestError, types::type_state::DataType};
use jmap_proto::types::type_state::DataType;
use utils::map::bitmap::Bitmap;
use crate::{auth::AccessToken, JMAP, LONG_SLUMBER};
use super::{http::ToHttpResponse, HttpRequest, HttpResponse, StateChangeResponse};
use super::{HttpRequest, HttpResponse, StateChangeResponse};
struct Ping {
interval: Duration,
@ -32,7 +32,7 @@ impl JMAP {
&self,
req: HttpRequest,
access_token: Arc<AccessToken>,
) -> HttpResponse {
) -> trc::Result<HttpResponse> {
// Parse query
let mut ping = 0;
let mut types = Bitmap::default();
@ -49,7 +49,7 @@ impl JMAP {
} else if let Ok(type_state) = DataType::try_from(type_state) {
types.insert(type_state);
} else {
return RequestError::invalid_parameters().into_http_response();
return Err(trc::ResourceCause::BadParameters.into_err());
}
}
}
@ -58,13 +58,13 @@ impl JMAP {
close_after_state = true;
}
"no" => {}
_ => return RequestError::invalid_parameters().into_http_response(),
_ => return Err(trc::ResourceCause::BadParameters.into_err()),
},
"ping" => match value.parse::<u32>() {
Ok(value) => {
ping = value;
}
Err(_) => return RequestError::invalid_parameters().into_http_response(),
Err(_) => return Err(trc::ResourceCause::BadParameters.into_err()),
},
_ => {}
}
@ -92,17 +92,11 @@ impl JMAP {
let throttle = self.core.jmap.event_source_throttle;
// Register with state manager
let mut change_rx = if let Ok(change_rx) = self
let mut change_rx = self
.subscribe_state_manager(access_token.primary_id(), types)
.await
{
let todo = "return error";
change_rx
} else {
return RequestError::internal_server_error().into_http_response();
};
.await?;
hyper::Response::builder()
Ok(hyper::Response::builder()
.status(StatusCode::OK)
.header(header::CONTENT_TYPE, "text/event-stream")
.header(header::CACHE_CONTROL, "no-store")
@ -160,6 +154,6 @@ impl JMAP {
};
}
})))
.unwrap()
.unwrap())
}
}

View file

@ -22,7 +22,7 @@ use hyper::{
};
use hyper_util::rt::TokioIo;
use jmap_proto::{
error::request::{RequestError, RequestLimitError},
error::request::RequestError,
request::{capability::Session, Request},
response::Response,
types::{blob::BlobId, id::Id},
@ -51,28 +51,19 @@ impl JMAP {
&self,
mut req: HttpRequest,
session: HttpSessionData,
) -> HttpResponse {
) -> trc::Result<HttpResponse> {
let mut path = req.uri().path().split('/');
path.next();
match path.next().unwrap_or_default() {
"jmap" => {
// Authenticate request
let (_in_flight, access_token) =
match self.authenticate_headers(&req, session.remote_ip).await {
Ok(session) => session,
Err(err) => {
return if req.method() != Method::OPTIONS {
err.into_http_response()
} else {
StatusCode::NO_CONTENT.into_http_response()
}
}
};
match (path.next().unwrap_or_default(), req.method()) {
("", &Method::POST) => {
return match fetch_body(
// Authenticate request
let (_in_flight, access_token) =
self.authenticate_headers(&req, session.remote_ip).await?;
let request = fetch_body(
&mut req,
if !access_token.is_super_user() {
self.core.jmap.upload_max_size
@ -81,7 +72,7 @@ impl JMAP {
},
)
.await
.ok_or_else(|| RequestError::limit(RequestLimitError::SizeRequest))
.ok_or_else(|| trc::LimitCause::SizeRequest.into_err())
.and_then(|bytes| {
//let c = println!("<- {}", String::from_utf8_lossy(&bytes));
@ -90,34 +81,25 @@ impl JMAP {
self.core.jmap.request_max_calls,
self.core.jmap.request_max_size,
)
}) {
Ok(request) => {
match self
.handle_request(request, access_token, &session.instance)
.await
{
Ok(response) => {
/*let c = println!(
"-> {}",
serde_json::to_string_pretty(&response).unwrap()
);*/
})?;
response.into_http_response()
}
Err(err) => err.into_http_response(),
}
}
Err(err) => err.into_http_response(),
};
return Ok(self
.handle_request(request, access_token, &session.instance)
.await
.into_http_response());
}
("download", &Method::GET) => {
// Authenticate request
let (_in_flight, access_token) =
self.authenticate_headers(&req, session.remote_ip).await?;
if let (Some(_), Some(blob_id), Some(name)) = (
path.next().and_then(|p| Id::from_bytes(p.as_bytes())),
path.next().and_then(BlobId::from_base32),
path.next(),
) {
return match self.blob_download(&blob_id, &access_token).await {
Ok(Some(blob)) => DownloadResponse {
return match self.blob_download(&blob_id, &access_token).await? {
Some(blob) => Ok(DownloadResponse {
filename: name.to_string(),
content_type: req
.uri()
@ -130,15 +112,16 @@ impl JMAP {
.unwrap_or("application/octet-stream".to_string()),
blob,
}
.into_http_response(),
Ok(None) => RequestError::not_found().into_http_response(),
Err(_) => {
RequestError::internal_server_error().into_http_response()
}
.into_http_response()),
None => Err(trc::ResourceCause::NotFound.into_err()),
};
}
}
("upload", &Method::POST) => {
// Authenticate request
let (_in_flight, access_token) =
self.authenticate_headers(&req, session.remote_ip).await?;
if let Some(account_id) =
path.next().and_then(|p| Id::from_bytes(p.as_bytes()))
{
@ -152,32 +135,34 @@ impl JMAP {
)
.await
{
Some(bytes) => {
match self
.blob_upload(
account_id,
req.headers()
.get(CONTENT_TYPE)
.and_then(|h| h.to_str().ok())
.unwrap_or("application/octet-stream"),
&bytes,
access_token,
)
.await
{
Ok(response) => response.into_http_response(),
Err(err) => err.into_http_response(),
}
}
None => RequestError::limit(RequestLimitError::SizeUpload)
.into_http_response(),
Some(bytes) => Ok(self
.blob_upload(
account_id,
req.headers()
.get(CONTENT_TYPE)
.and_then(|h| h.to_str().ok())
.unwrap_or("application/octet-stream"),
&bytes,
access_token,
)
.await?
.into_http_response()),
None => Err(trc::LimitCause::SizeUpload.into_err()),
};
}
}
("eventsource", &Method::GET) => {
return self.handle_event_source(req, access_token).await
// Authenticate request
let (_in_flight, access_token) =
self.authenticate_headers(&req, session.remote_ip).await?;
return self.handle_event_source(req, access_token).await;
}
("ws", &Method::GET) => {
// Authenticate request
let (_in_flight, access_token) =
self.authenticate_headers(&req, session.remote_ip).await?;
return self
.upgrade_websocket_connection(
req,
@ -187,7 +172,7 @@ impl JMAP {
.await;
}
(_, &Method::OPTIONS) => {
return StatusCode::NO_CONTENT.into_http_response();
return Ok(StatusCode::NO_CONTENT.into_http_response());
}
_ => (),
}
@ -196,31 +181,24 @@ impl JMAP {
("jmap", &Method::GET) => {
// Authenticate request
let (_in_flight, access_token) =
match self.authenticate_headers(&req, session.remote_ip).await {
Ok(session) => session,
Err(err) => return err.into_http_response(),
};
self.authenticate_headers(&req, session.remote_ip).await?;
return match self
return Ok(self
.handle_session_resource(
session.resolve_url(&self.core).await,
access_token,
)
.await
{
Ok(session) => session.into_http_response(),
Err(err) => err.into_http_response(),
};
.await?
.into_http_response());
}
("oauth-authorization-server", &Method::GET) => {
// Limit anonymous requests
return match self.is_anonymous_allowed(&session.remote_ip).await {
Ok(_) => JsonResponse::new(OAuthMetadata::new(
session.resolve_url(&self.core).await,
))
.into_http_response(),
Err(err) => err.into_http_response(),
};
self.is_anonymous_allowed(&session.remote_ip).await?;
return Ok(JsonResponse::new(OAuthMetadata::new(
session.resolve_url(&self.core).await,
))
.into_http_response());
}
("acme-challenge", &Method::GET) if self.core.has_acme_http_providers() => {
if let Some(token) = path.next() {
@ -229,27 +207,26 @@ impl JMAP {
.storage
.lookup
.key_get::<String>(format!("acme:{token}").into_bytes())
.await
.await?
{
Ok(Some(proof)) => Resource {
Some(proof) => Ok(Resource {
content_type: "text/plain",
contents: proof.into_bytes(),
}
.into_http_response(),
Ok(None) => RequestError::not_found().into_http_response(),
Err(err) => err.into_http_response(),
.into_http_response()),
None => Err(trc::ResourceCause::NotFound.into_err()),
};
}
}
("mta-sts.txt", &Method::GET) => {
if let Some(policy) = self.core.build_mta_sts_policy() {
return Resource {
return Ok(Resource {
content_type: "text/plain",
contents: policy.to_string().into_bytes(),
}
.into_http_response();
.into_http_response());
} else {
return RequestError::not_found().into_http_response();
return Err(trc::ResourceCause::NotFound.into_err());
}
}
("mail-v1.xml", &Method::GET) => {
@ -263,46 +240,40 @@ impl JMAP {
}
}
(_, &Method::OPTIONS) => {
return StatusCode::NO_CONTENT.into_http_response();
return Ok(StatusCode::NO_CONTENT.into_http_response());
}
_ => (),
},
"auth" => match (path.next().unwrap_or_default(), req.method()) {
("device", &Method::POST) => {
return match self.is_anonymous_allowed(&session.remote_ip).await {
Ok(_) => {
self.handle_device_auth(&mut req, session.resolve_url(&self.core).await)
.await
}
Err(err) => err.into_http_response(),
}
self.is_anonymous_allowed(&session.remote_ip).await?;
return self
.handle_device_auth(&mut req, session.resolve_url(&self.core).await)
.await;
}
("token", &Method::POST) => {
return match self.is_anonymous_allowed(&session.remote_ip).await {
Ok(_) => self.handle_token_request(&mut req).await,
Err(err) => err.into_http_response(),
}
self.is_anonymous_allowed(&session.remote_ip).await?;
return self.handle_token_request(&mut req).await;
}
(_, &Method::OPTIONS) => {
return StatusCode::NO_CONTENT.into_http_response();
return Ok(StatusCode::NO_CONTENT.into_http_response());
}
_ => (),
},
"api" => {
// Allow CORS preflight requests
if req.method() == Method::OPTIONS {
return StatusCode::NO_CONTENT.into_http_response();
return Ok(StatusCode::NO_CONTENT.into_http_response());
}
// Authenticate user
return match self.authenticate_headers(&req, session.remote_ip).await {
Ok((_, access_token)) => {
let body = fetch_body(&mut req, 1024 * 1024).await;
self.handle_api_manage_request(&req, body, access_token)
.await
}
Err(err) => err.into_http_response(),
};
let (_, access_token) = self.authenticate_headers(&req, session.remote_ip).await?;
let body = fetch_body(&mut req, 1024 * 1024).await;
return self
.handle_api_manage_request(&req, body, access_token)
.await;
}
"mail" => {
if req.method() == Method::GET
@ -321,43 +292,45 @@ impl JMAP {
}
}
"robots.txt" => {
return Resource {
return Ok(Resource {
content_type: "text/plain",
contents: b"User-agent: *\nDisallow: /\n".to_vec(),
}
.into_http_response();
.into_http_response());
}
"healthz" => match path.next().unwrap_or_default() {
"live" => {
return StatusCode::OK.into_http_response();
return Ok(StatusCode::OK.into_http_response());
}
"ready" => {
return {
return Ok({
if !self.core.storage.data.is_none() {
StatusCode::OK
} else {
StatusCode::SERVICE_UNAVAILABLE
}
}
.into_http_response();
.into_http_response());
}
_ => (),
},
_ => {
let path = req.uri().path();
return match self
let resource = self
.inner
.webadmin
.get(path.strip_prefix('/').unwrap_or(path))
.await
{
Ok(resource) if !resource.is_empty() => resource.into_http_response(),
Err(err) => err.into_http_response(),
_ => RequestError::not_found().into_http_response(),
.await?;
return if !resource.is_empty() {
Ok(resource.into_http_response())
} else {
Err(trc::ResourceCause::NotFound.into_err())
};
}
}
RequestError::not_found().into_http_response()
Err(trc::ResourceCause::NotFound.into_err())
}
}
@ -403,7 +376,7 @@ impl JmapInstance {
};
// Parse HTTP request
let mut response = jmap
let mut response = match jmap
.parse_http_request(
req,
HttpSessionData {
@ -415,7 +388,19 @@ impl JmapInstance {
is_tls,
},
)
.await;
.await
{
Ok(response) => response,
Err(err) => {
tracing::error!(
parent: &span,
event = "error",
context = "http",
reason = %err,
);
err.into_http_response()
}
};
// Add custom headers
if !jmap.core.jmap.http_headers.is_empty() {
@ -534,7 +519,7 @@ impl ToHttpResponse for trc::Error {
}
}
impl ToHttpResponse for std::io::Error {
/*impl ToHttpResponse for std::io::Error {
fn into_http_response(self) -> HttpResponse {
tracing::error!(context = "i/o", error = %self, "I/O error");
@ -551,7 +536,7 @@ impl ToHttpResponse for serde_json::Error {
)
.into_http_response()
}
}
}*/
impl<T: serde::Serialize> JsonResponse<T> {
pub fn new(inner: T) -> Self {

View file

@ -7,8 +7,8 @@
use std::str::FromStr;
use common::config::smtp::auth::simple_pem_parse;
use directory::backend::internal::manage;
use hyper::Method;
use jmap_proto::error::request::RequestError;
use mail_auth::{
common::crypto::{Ed25519Key, RsaKey, Sha256},
dkim::generate::DkimKeyPair,
@ -22,10 +22,7 @@ use serde_json::json;
use store::write::now;
use crate::{
api::{
http::ToHttpResponse, management::ManagementApiError, HttpRequest, HttpResponse,
JsonResponse,
},
api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse},
JMAP,
};
@ -51,22 +48,21 @@ impl JMAP {
req: &HttpRequest,
path: Vec<&str>,
body: Option<Vec<u8>>,
) -> HttpResponse {
) -> trc::Result<HttpResponse> {
match *req.method() {
Method::GET => self.handle_get_public_key(path).await,
Method::POST => self.handle_create_signature(body).await,
_ => RequestError::not_found().into_http_response(),
_ => Err(trc::ResourceCause::NotFound.into_err()),
}
}
async fn handle_get_public_key(&self, path: Vec<&str>) -> HttpResponse {
async fn handle_get_public_key(&self, path: Vec<&str>) -> trc::Result<HttpResponse> {
let signature_id = match path.get(1) {
Some(signature_id) => decode_path_element(signature_id),
None => {
return RequestError::not_found().into_http_response();
return Err(trc::ResourceCause::NotFound.into_err());
}
};
let todo = "bubble up error and log them";
let (pk, algo) = match (
self.core
@ -82,27 +78,26 @@ impl JMAP {
.map(|algo| algo.and_then(|algo| algo.parse::<Algorithm>().ok())),
) {
(Ok(Some(pk)), Ok(Some(algorithm))) => (pk, algorithm),
(Err(err), _) | (_, Err(err)) => return err.into_http_response(),
_ => return RequestError::not_found().into_http_response(),
(Err(err), _) | (_, Err(err)) => return Err(err.caused_by(trc::location!())),
_ => return Err(trc::ResourceCause::NotFound.into_err()),
};
match obtain_dkim_public_key(algo, &pk) {
Ok(data) => JsonResponse::new(json!({
Ok(data) => Ok(JsonResponse::new(json!({
"data": data,
}))
.into_http_response(),
Err(err) => ManagementApiError::Other {
details: err.into(),
}
.into_http_response(),
.into_http_response()),
Err(details) => Err(manage::error(details, None::<u32>)),
}
}
async fn handle_create_signature(&self, body: Option<Vec<u8>>) -> HttpResponse {
async fn handle_create_signature(&self, body: Option<Vec<u8>>) -> trc::Result<HttpResponse> {
let request =
match serde_json::from_slice::<DkimSignature>(body.as_deref().unwrap_or_default()) {
Ok(request) => request,
Err(err) => return err.into_http_response(),
Err(err) => {
return Err(trc::Cause::Resource(trc::ResourceCause::BadParameters).reason(err))
}
};
let algo_str = match request.algorithm {
@ -127,35 +122,27 @@ impl JMAP {
});
// Make sure the signature does not exist already
match self
if let Some(value) = self
.core
.storage
.config
.get(&format!("signature.{id}.private-key"))
.await
.await?
{
Ok(None) => (),
Ok(Some(value)) => {
return ManagementApiError::FieldAlreadyExists {
field: format!("signature.{id}.private-key").into(),
value: value.into(),
}
.into_http_response();
}
Err(err) => return err.into_http_response(),
return Err(manage::err_exists(
format!("signature.{id}.private-key"),
value,
));
}
// Create signature
match self
.create_dkim_key(request.algorithm, id, request.domain, selector)
.await
{
Ok(_) => JsonResponse::new(json!({
"data": (),
}))
.into_http_response(),
Err(err) => err.into_http_response(),
}
self.create_dkim_key(request.algorithm, id, request.domain, selector)
.await?;
Ok(JsonResponse::new(json!({
"data": (),
}))
.into_http_response())
}
async fn create_dkim_key(
@ -177,7 +164,10 @@ impl JMAP {
Algorithm::Rsa => DkimKeyPair::generate_rsa(2048),
Algorithm::Ed25519 => DkimKeyPair::generate_ed25519(),
}
.map_err(|err| trc::Cause::Crypto.reason(err).caused_by(trc::location!()))?
.map_err(|err| {
manage::error("Failed to generate key", err.to_string().into())
.caused_by(trc::location!())
})?
.private_key(),
)
.unwrap_or_default()

View file

@ -7,7 +7,6 @@
use directory::backend::internal::manage::ManageDirectory;
use hyper::Method;
use jmap_proto::error::request::RequestError;
use serde::{Deserialize, Serialize};
use serde_json::json;
use sha1::Digest;
@ -35,7 +34,11 @@ struct DnsRecord {
}
impl JMAP {
pub async fn handle_manage_domain(&self, req: &HttpRequest, path: Vec<&str>) -> HttpResponse {
pub async fn handle_manage_domain(
&self,
req: &HttpRequest,
path: Vec<&str>,
) -> trc::Result<HttpResponse> {
match (path.get(1), req.method()) {
(None, &Method::GET) => {
// List domains
@ -44,82 +47,78 @@ impl JMAP {
let page: usize = params.parse("page").unwrap_or(0);
let limit: usize = params.parse("limit").unwrap_or(0);
match self.core.storage.data.list_domains(filter).await {
Ok(domains) => {
let (total, domains) = if limit > 0 {
let offset = page.saturating_sub(1) * limit;
(
domains.len(),
domains.into_iter().skip(offset).take(limit).collect(),
)
} else {
(domains.len(), domains)
};
let domains = self.core.storage.data.list_domains(filter).await?;
let (total, domains) = if limit > 0 {
let offset = page.saturating_sub(1) * limit;
(
domains.len(),
domains.into_iter().skip(offset).take(limit).collect(),
)
} else {
(domains.len(), domains)
};
JsonResponse::new(json!({
"data": {
"items": domains,
"total": total,
},
}))
.into_http_response()
}
Err(err) => err.into_http_response(),
}
Ok(JsonResponse::new(json!({
"data": {
"items": domains,
"total": total,
},
}))
.into_http_response())
}
(Some(domain), &Method::GET) => {
// Obtain DNS records
let domain = decode_path_element(domain);
match self.build_dns_records(domain.as_ref()).await {
Ok(records) => JsonResponse::new(json!({
"data": records,
}))
.into_http_response(),
Err(err) => err.into_http_response(),
}
Ok(JsonResponse::new(json!({
"data": self.build_dns_records(domain.as_ref()).await?,
}))
.into_http_response())
}
(Some(domain), &Method::POST) => {
// Create domain
let domain = decode_path_element(domain);
match self.core.storage.data.create_domain(domain.as_ref()).await {
Ok(_) => {
// Set default domain name if missing
if matches!(
self.core.storage.config.get("lookup.default.domain").await,
Ok(None)
) {
if let Err(err) = self
.core
.storage
.config
.set([("lookup.default.domain", domain.as_ref())])
.await
{
tracing::error!("Failed to set default domain name: {}", err);
}
}
JsonResponse::new(json!({
"data": (),
}))
.into_http_response()
}
Err(err) => err.into_http_response(),
self.core
.storage
.data
.create_domain(domain.as_ref())
.await?;
// Set default domain name if missing
if self
.core
.storage
.config
.get("lookup.default.domain")
.await?
.is_none()
{
self.core
.storage
.config
.set([("lookup.default.domain", domain.as_ref())])
.await?;
}
Ok(JsonResponse::new(json!({
"data": (),
}))
.into_http_response())
}
(Some(domain), &Method::DELETE) => {
// Delete domain
let domain = decode_path_element(domain);
match self.core.storage.data.delete_domain(domain.as_ref()).await {
Ok(_) => JsonResponse::new(json!({
"data": (),
}))
.into_http_response(),
Err(err) => err.into_http_response(),
}
self.core
.storage
.data
.delete_domain(domain.as_ref())
.await?;
Ok(JsonResponse::new(json!({
"data": (),
}))
.into_http_response())
}
_ => RequestError::not_found().into_http_response(),
_ => Err(trc::ResourceCause::NotFound.into_err()),
}
}

View file

@ -14,10 +14,11 @@ use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use common::enterprise::undelete::DeletedBlob;
use directory::backend::internal::manage::ManageDirectory;
use hyper::Method;
use jmap_proto::{error::request::RequestError, types::collection::Collection};
use jmap_proto::types::collection::Collection;
use mail_parser::{DateTime, MessageParser};
use serde_json::json;
use store::write::{BatchBuilder, BlobOp, ValueClass};
use trc::AddContext;
use utils::{url_params::UrlParams, BlobHash};
use crate::{
@ -53,10 +54,10 @@ impl JMAP {
req: &HttpRequest,
path: Vec<&str>,
body: Option<Vec<u8>>,
) -> HttpResponse {
) -> trc::Result<HttpResponse> {
match path.get(1).copied().unwrap_or_default() {
"undelete" => self.handle_undelete_api_request(req, path, body).await,
_ => RequestError::not_found().into_http_response(),
_ => Err(trc::ResourceCause::NotFound.into_err()),
}
}
@ -65,72 +66,76 @@ impl JMAP {
req: &HttpRequest,
path: Vec<&str>,
body: Option<Vec<u8>>,
) -> HttpResponse {
) -> trc::Result<HttpResponse> {
match (path.get(2).copied(), req.method()) {
(Some(account_name), &Method::GET) => {
match self.core.storage.data.get_account_id(account_name).await {
Ok(Some(account_id)) => match self.core.list_deleted(account_id).await {
Ok(mut deleted) => {
let params = UrlParams::new(req.uri().query());
let limit = params.parse::<usize>("limit").unwrap_or_default();
let mut offset = params
.parse::<usize>("page")
.unwrap_or_default()
.saturating_sub(1)
* limit;
let account_id = self
.core
.storage
.data
.get_account_id(account_name)
.await?
.ok_or_else(|| trc::ResourceCause::NotFound.into_err())?;
let mut deleted = self.core.list_deleted(account_id).await?;
// Sort ascending by deleted_at
let total = deleted.len();
deleted.sort_by(|a, b| a.deleted_at.cmp(&b.deleted_at));
let mut results =
Vec::with_capacity(if limit > 0 { limit } else { total });
let params = UrlParams::new(req.uri().query());
let limit = params.parse::<usize>("limit").unwrap_or_default();
let mut offset = params
.parse::<usize>("page")
.unwrap_or_default()
.saturating_sub(1)
* limit;
for blob in deleted {
if offset == 0 {
results.push(DeletedBlob {
hash: URL_SAFE_NO_PAD.encode(blob.hash.as_slice()),
size: blob.size,
deleted_at: DateTime::from_timestamp(
blob.deleted_at as i64,
)
.to_rfc3339(),
expires_at: DateTime::from_timestamp(
blob.expires_at as i64,
)
.to_rfc3339(),
collection: Collection::from(blob.collection).to_string(),
});
if results.len() == limit {
break;
}
} else {
offset -= 1;
}
}
// Sort ascending by deleted_at
let total = deleted.len();
deleted.sort_by(|a, b| a.deleted_at.cmp(&b.deleted_at));
let mut results = Vec::with_capacity(if limit > 0 { limit } else { total });
JsonResponse::new(json!({
"data":{
"items": results,
"total": total,
},
}))
.into_http_response()
for blob in deleted {
if offset == 0 {
results.push(DeletedBlob {
hash: URL_SAFE_NO_PAD.encode(blob.hash.as_slice()),
size: blob.size,
deleted_at: DateTime::from_timestamp(blob.deleted_at as i64)
.to_rfc3339(),
expires_at: DateTime::from_timestamp(blob.expires_at as i64)
.to_rfc3339(),
collection: Collection::from(blob.collection).to_string(),
});
if results.len() == limit {
break;
}
Err(err) => err.into_http_response(),
},
Ok(None) => RequestError::not_found().into_http_response(),
Err(err) => err.into_http_response(),
} else {
offset -= 1;
}
}
Ok(JsonResponse::new(json!({
"data":{
"items": results,
"total": total,
},
}))
.into_http_response())
}
(Some(account_name), &Method::POST) => {
match self.core.storage.data.get_account_id(account_name).await {
Ok(Some(account_id)) => {
match serde_json::from_slice::<Vec<UndeleteRequest<String, String, String>>>(
body.as_deref().unwrap_or_default(),
)
.ok()
.and_then(|request| {
request.into_iter().map(|request| {
let account_id = self
.core
.storage
.data
.get_account_id(account_name)
.await?
.ok_or_else(|| trc::ResourceCause::NotFound.into_err())?;
let requests =
serde_json::from_slice::<Vec<UndeleteRequest<String, String, String>>>(
body.as_deref().unwrap_or_default(),
)
.ok()
.and_then(|request| {
request
.into_iter()
.map(|request| {
UndeleteRequest {
hash: BlobHash::try_from_hash_slice(
URL_SAFE_NO_PAD
@ -139,99 +144,100 @@ impl JMAP {
.as_slice(),
)
.ok()?,
collection: Collection::from_str(
request.collection.as_str(),
)
.ok()?,
collection: Collection::from_str(request.collection.as_str())
.ok()?,
time: DateTime::parse_rfc3339(request.time.as_str())?
.to_timestamp(),
cancel_deletion: if let Some(cancel_deletion) = request.cancel_deletion {
cancel_deletion: if let Some(cancel_deletion) =
request.cancel_deletion
{
DateTime::parse_rfc3339(cancel_deletion.as_str())?
.to_timestamp().into()
.to_timestamp()
.into()
} else {
None
}
},
}
.into()
}).collect::<Option<Vec<_>>>()
}) {
Some(requests) => {
let mut results = Vec::with_capacity(requests.len());
let mut batch = BatchBuilder::new();
batch.with_account_id(account_id);
for request in requests {
match request.collection {
Collection::Email => {
match self.get_blob(&request.hash, 0..usize::MAX).await
{
Ok(Some(bytes)) => {
match self
.email_ingest(IngestEmail {
raw_message: &bytes,
message: MessageParser::new().parse(&bytes),
account_id,
account_quota: 0,
mailbox_ids: vec![INBOX_ID],
keywords: vec![],
received_at: (request.time as u64).into(),
source: IngestSource::Smtp,
encrypt: false,
})
.await
{
Ok(_) => {
results.push(UndeleteResponse::Success);
if let Some(cancel_deletion) = request.cancel_deletion {
batch.clear(ValueClass::Blob(BlobOp::Reserve { hash: request.hash, until: cancel_deletion as u64 }));
}
},
Err(mut err) if err.matches(trc::Cause::Ingest) => {
results.push(UndeleteResponse::Error { reason: err.take_value(trc::Key::Reason)
.and_then(|v| v.into_string())
.unwrap().into_owned() });
}
Err(_) => {
return RequestError::internal_server_error().into_http_response();
},
}
}
Ok(None) => {
results.push(UndeleteResponse::NotFound);
},
Err(_) => {
return RequestError::internal_server_error().into_http_response();
},
})
.collect::<Option<Vec<_>>>()
})
.ok_or_else(|| trc::ResourceCause::BadParameters.into_err())?;
let mut results = Vec::with_capacity(requests.len());
let mut batch = BatchBuilder::new();
batch.with_account_id(account_id);
for request in requests {
match request.collection {
Collection::Email => {
match self.get_blob(&request.hash, 0..usize::MAX).await? {
Some(bytes) => {
match self
.email_ingest(IngestEmail {
raw_message: &bytes,
message: MessageParser::new().parse(&bytes),
account_id,
account_quota: 0,
mailbox_ids: vec![INBOX_ID],
keywords: vec![],
received_at: (request.time as u64).into(),
source: IngestSource::Smtp,
encrypt: false,
})
.await
{
Ok(_) => {
results.push(UndeleteResponse::Success);
if let Some(cancel_deletion) = request.cancel_deletion {
batch.clear(ValueClass::Blob(BlobOp::Reserve {
hash: request.hash,
until: cancel_deletion as u64,
}));
}
}
_ => {
Err(mut err) if err.matches(trc::Cause::Ingest) => {
results.push(UndeleteResponse::Error {
reason: "Unsupported collection".to_string(),
reason: err
.take_value(trc::Key::Reason)
.and_then(|v| v.into_string())
.unwrap()
.into_owned(),
});
}
Err(err) => {
return Err(err.caused_by(trc::location!()));
}
}
}
// Commit batch
if !batch.is_empty() {
match self.core.storage.data.write(batch.build()).await {
Ok(_) => (),
Err(err) => return err.into_http_response(),
}
None => {
results.push(UndeleteResponse::NotFound);
}
JsonResponse::new(json!({
"data": results,
}))
.into_http_response()
},
None => RequestError::invalid_parameters().into_http_response(),
}
}
_ => {
results.push(UndeleteResponse::Error {
reason: "Unsupported collection".to_string(),
});
}
}
Ok(None) => RequestError::not_found().into_http_response(),
Err(err) => err.into_http_response(),
}
// Commit batch
if !batch.is_empty() {
self.core
.storage
.data
.write(batch.build())
.await
.caused_by(trc::location!())?;
}
Ok(JsonResponse::new(json!({
"data": results,
}))
.into_http_response())
}
_ => RequestError::not_found().into_http_response(),
_ => Err(trc::ResourceCause::NotFound.into_err()),
}
}
}

View file

@ -5,6 +5,7 @@ use std::{
};
use chrono::DateTime;
use directory::backend::internal::manage;
use rev_lines::RevLines;
use serde::Serialize;
use serde_json::json;
@ -16,8 +17,6 @@ use crate::{
JMAP,
};
use super::ManagementApiError;
#[derive(Serialize)]
struct LogEntry {
timestamp: String,
@ -26,18 +25,15 @@ struct LogEntry {
}
impl JMAP {
pub async fn handle_view_logs(&self, req: &HttpRequest) -> HttpResponse {
pub async fn handle_view_logs(&self, req: &HttpRequest) -> trc::Result<HttpResponse> {
// Obtain log file path
let path = match self.core.storage.config.get("tracer.log.path").await {
Ok(Some(path)) => path,
Ok(None) => {
return ManagementApiError::Unsupported {
details: "Tracer log path not configured".into(),
}
.into_http_response()
}
Err(err) => return err.into_http_response(),
};
let path = self
.core
.storage
.config
.get("tracer.log.path")
.await?
.ok_or_else(|| manage::unsupported("Tracer log path not configured"))?;
let params = UrlParams::new(req.uri().query());
let filter = params.get("filter").unwrap_or_default().to_string();
@ -51,25 +47,23 @@ impl JMAP {
let _ = tx.send(read_log_files(path, &filter, offset, limit));
});
match rx.await {
Ok(result) => match result {
Ok((total, items)) => JsonResponse::new(json!({
"data": {
"items": items,
"total": total,
},
}))
.into_http_response(),
Err(err) => err.into_http_response(),
let (total, items) = rx
.await
.map_err(|err| trc::Cause::Thread.reason(err).caused_by(trc::location!()))?
.map_err(|err| {
trc::ManageCause::Error
.reason(err)
.details("Failed to read log files")
.caused_by(trc::location!())
})?;
Ok(JsonResponse::new(json!({
"data": {
"items": items,
"total": total,
},
Err(_) => {
tracing::warn!(context = "view_logs", event = "error", "Thread join error");
ManagementApiError::Other {
details: "Thread join error".into(),
}
.into_http_response()
}
}
}))
.into_http_response())
}
}

View file

@ -19,11 +19,11 @@ pub mod stores;
use std::{borrow::Cow, sync::Arc};
use directory::backend::internal::manage;
use hyper::Method;
use jmap_proto::error::request::RequestError;
use serde::Serialize;
use super::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse};
use super::{HttpRequest, HttpResponse};
use crate::{auth::AccessToken, JMAP};
#[derive(Serialize)]
@ -57,7 +57,7 @@ impl JMAP {
req: &HttpRequest,
body: Option<Vec<u8>>,
access_token: Arc<AccessToken>,
) -> HttpResponse {
) -> trc::Result<HttpResponse> {
let path = req.uri().path().split('/').skip(2).collect::<Vec<_>>();
let is_superuser = access_token.is_super_user();
@ -76,10 +76,7 @@ impl JMAP {
}
"sieve" if is_superuser => self.handle_run_sieve(req, path, body).await,
"restart" if is_superuser && req.method() == Method::GET => {
ManagementApiError::Unsupported {
details: "Restart is not yet supported".into(),
}
.into_http_response()
Err(manage::unsupported("Restart is not yet supported"))
}
"oauth" => self.handle_oauth_api_request(access_token, body).await,
"account" => match (path.get(1).copied().unwrap_or_default(), req.method()) {
@ -89,7 +86,7 @@ impl JMAP {
("auth", &Method::POST) => {
self.handle_account_auth_post(req, access_token, body).await
}
_ => RequestError::not_found().into_http_response(),
_ => Err(trc::ResourceCause::NotFound.into_err()),
},
// SPDX-SnippetBegin
@ -109,34 +106,13 @@ impl JMAP {
if self.core.is_enterprise_edition() {
self.handle_enterprise_api_request(req, path, body).await
} else {
ManagementApiError::Unsupported {
details: "This feature is only available in the Enterprise version".into(),
}
.into_http_response()
Err(manage::unsupported(
"This feature is only available in the Enterprise version",
))
}
}
// SPDX-SnippetEnd
_ => RequestError::not_found().into_http_response(),
}
}
}
impl ToHttpResponse for ManagementApiError {
fn into_http_response(self) -> super::HttpResponse {
JsonResponse::new(self).into_http_response()
}
}
impl From<Cow<'static, str>> for ManagementApiError {
fn from(details: Cow<'static, str>) -> Self {
ManagementApiError::Other { details }
}
}
impl From<String> for ManagementApiError {
fn from(details: String) -> Self {
ManagementApiError::Other {
details: details.into(),
_ => Err(trc::ResourceCause::NotFound.into_err()),
}
}
}

View file

@ -8,14 +8,14 @@ use std::sync::Arc;
use directory::{
backend::internal::{
lookup::DirectoryStore, manage::ManageDirectory, PrincipalAction, PrincipalField,
PrincipalUpdate, PrincipalValue, SpecialSecrets,
lookup::DirectoryStore,
manage::{self, ManageDirectory},
PrincipalAction, PrincipalField, PrincipalUpdate, PrincipalValue, SpecialSecrets,
},
DirectoryInner, Principal, QueryBy, Type,
};
use hyper::{header, Method, StatusCode};
use jmap_proto::error::request::RequestError;
use hyper::{header, Method};
use serde_json::json;
use utils::url_params::UrlParams;
@ -25,7 +25,7 @@ use crate::{
JMAP,
};
use super::{decode_path_element, ManagementApiError};
use super::decode_path_element;
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct PrincipalResponse {
@ -80,47 +80,41 @@ impl JMAP {
req: &HttpRequest,
path: Vec<&str>,
body: Option<Vec<u8>>,
) -> HttpResponse {
) -> trc::Result<HttpResponse> {
match (path.get(1), req.method()) {
(None, &Method::POST) => {
// Make sure the current directory supports updates
if let Some(response) = self.assert_supported_directory() {
return response;
}
self.assert_supported_directory()?;
// Create principal
match serde_json::from_slice::<PrincipalResponse>(
let principal = serde_json::from_slice::<PrincipalResponse>(
body.as_deref().unwrap_or_default(),
) {
Ok(principal) => {
match self
.core
.storage
.data
.create_account(
Principal {
id: principal.id,
typ: principal.typ,
quota: principal.quota,
name: principal.name,
secrets: principal.secrets,
emails: principal.emails,
member_of: principal.member_of,
description: principal.description,
},
principal.members,
)
.await
{
Ok(account_id) => JsonResponse::new(json!({
"data": account_id,
}))
.into_http_response(),
Err(err) => into_directory_response(err),
}
}
Err(err) => err.into_http_response(),
}
)
.map_err(|err| {
trc::Cause::Resource(trc::ResourceCause::BadParameters).from_json_error(err)
})?;
Ok(JsonResponse::new(json!({
"data": self
.core
.storage
.data
.create_account(
Principal {
id: principal.id,
typ: principal.typ,
quota: principal.quota,
name: principal.name,
secrets: principal.secrets,
emails: principal.emails,
member_of: principal.member_of,
description: principal.description,
},
principal.members,
)
.await?,
}))
.into_http_response())
}
(None, &Method::GET) => {
// List principal ids
@ -130,187 +124,137 @@ impl JMAP {
let page: usize = params.parse("page").unwrap_or(0);
let limit: usize = params.parse("limit").unwrap_or(0);
match self.core.storage.data.list_accounts(filter, typ).await {
Ok(accounts) => {
let (total, accounts) = if limit > 0 {
let offset = page.saturating_sub(1) * limit;
(
accounts.len(),
accounts.into_iter().skip(offset).take(limit).collect(),
)
} else {
(accounts.len(), accounts)
};
let accounts = self.core.storage.data.list_accounts(filter, typ).await?;
let (total, accounts) = if limit > 0 {
let offset = page.saturating_sub(1) * limit;
(
accounts.len(),
accounts.into_iter().skip(offset).take(limit).collect(),
)
} else {
(accounts.len(), accounts)
};
JsonResponse::new(json!({
"data": {
"items": accounts,
"total": total,
},
}))
.into_http_response()
}
Err(err) => into_directory_response(err),
}
Ok(JsonResponse::new(json!({
"data": {
"items": accounts,
"total": total,
},
}))
.into_http_response())
}
(Some(name), method) => {
// Fetch, update or delete principal
let name = decode_path_element(name);
let account_id = match self.core.storage.data.get_account_id(name.as_ref()).await {
Ok(Some(account_id)) => account_id,
Ok(None) => {
return RequestError::blank(
StatusCode::NOT_FOUND.as_u16(),
"Not found",
"Account not found.",
)
.into_http_response();
}
Err(err) => {
return into_directory_response(err);
}
};
let account_id = self
.core
.storage
.data
.get_account_id(name.as_ref())
.await?
.ok_or_else(|| trc::ManageCause::NotFound.into_err())?;
match *method {
Method::GET => {
let result = match self
let principal = self
.core
.storage
.data
.query(QueryBy::Id(account_id), true)
.await
{
Ok(Some(principal)) => {
self.core.storage.data.map_group_ids(principal).await
}
Ok(None) => {
return RequestError::blank(
StatusCode::NOT_FOUND.as_u16(),
"Not found",
"Account not found.",
)
.into_http_response()
}
Err(err) => Err(err),
};
.await?
.ok_or_else(|| trc::ManageCause::NotFound.into_err())?;
let principal = self.core.storage.data.map_group_ids(principal).await?;
match result {
Ok(principal) => {
// Obtain quota usage
let mut principal = PrincipalResponse::from(principal);
principal.used_quota =
self.get_used_quota(account_id).await.unwrap_or_default()
as u64;
// Obtain quota usage
let mut principal = PrincipalResponse::from(principal);
principal.used_quota = self.get_used_quota(account_id).await? as u64;
// Obtain member names
for member_id in self
.core
.storage
.data
.get_members(account_id)
.await
.unwrap_or_default()
{
if let Ok(Some(member_principal)) = self
.core
.storage
.data
.query(QueryBy::Id(member_id), false)
.await
{
principal.members.push(member_principal.name);
}
}
JsonResponse::new(json!({
"data": principal,
}))
.into_http_response()
// Obtain member names
for member_id in self.core.storage.data.get_members(account_id).await? {
if let Some(member_principal) = self
.core
.storage
.data
.query(QueryBy::Id(member_id), false)
.await?
{
principal.members.push(member_principal.name);
}
Err(err) => into_directory_response(err),
}
Ok(JsonResponse::new(json!({
"data": principal,
}))
.into_http_response())
}
Method::DELETE => {
// Remove FTS index
if let Err(err) = self.core.storage.fts.remove_all(account_id).await {
return err.into_http_response();
}
self.core.storage.fts.remove_all(account_id).await;
// Delete account
match self
.core
self.core
.storage
.data
.delete_account(QueryBy::Id(account_id))
.await
{
Ok(_) => {
// Remove entries from cache
self.inner.sessions.retain(|_, id| id.item != account_id);
.await?;
// Remove entries from cache
self.inner.sessions.retain(|_, id| id.item != account_id);
JsonResponse::new(json!({
"data": (),
}))
.into_http_response()
}
Err(err) => into_directory_response(err),
}
Ok(JsonResponse::new(json!({
"data": (),
}))
.into_http_response())
}
Method::PATCH => {
match serde_json::from_slice::<Vec<PrincipalUpdate>>(
let changes = serde_json::from_slice::<Vec<PrincipalUpdate>>(
body.as_deref().unwrap_or_default(),
) {
Ok(changes) => {
// Make sure the current directory supports updates
if let Some(response) = self.assert_supported_directory() {
if changes.iter().any(|change| {
!matches!(
change.field,
PrincipalField::Quota | PrincipalField::Description
)
}) {
return response;
}
}
let is_password_change = changes
.iter()
.any(|change| matches!(change.field, PrincipalField::Secrets));
)
.map_err(|err| {
trc::Cause::Resource(trc::ResourceCause::BadParameters)
.from_json_error(err)
})?;
match self
.core
.storage
.data
.update_account(QueryBy::Id(account_id), changes)
.await
{
Ok(_) => {
if is_password_change {
// Remove entries from cache
self.inner
.sessions
.retain(|_, id| id.item != account_id);
}
JsonResponse::new(json!({
"data": (),
}))
.into_http_response()
}
Err(err) => into_directory_response(err),
}
}
Err(err) => err.into_http_response(),
// Make sure the current directory supports updates
if changes.iter().any(|change| {
!matches!(
change.field,
PrincipalField::Quota | PrincipalField::Description
)
}) {
self.assert_supported_directory()?;
}
let is_password_change = changes
.iter()
.any(|change| matches!(change.field, PrincipalField::Secrets));
self.core
.storage
.data
.update_account(QueryBy::Id(account_id), changes)
.await?;
if is_password_change {
// Remove entries from cache
self.inner.sessions.retain(|_, id| id.item != account_id);
}
Ok(JsonResponse::new(json!({
"data": (),
}))
.into_http_response())
}
_ => RequestError::not_found().into_http_response(),
_ => Err(trc::ResourceCause::NotFound.into_err()),
}
}
_ => RequestError::not_found().into_http_response(),
_ => Err(trc::ResourceCause::NotFound.into_err()),
}
}
pub async fn handle_account_auth_get(&self, access_token: Arc<AccessToken>) -> HttpResponse {
pub async fn handle_account_auth_get(
&self,
access_token: Arc<AccessToken>,
) -> trc::Result<HttpResponse> {
let mut response = AccountAuthResponse {
otp_auth: false,
is_admin: access_token.is_super_user(),
@ -318,35 +262,29 @@ impl JMAP {
};
if access_token.primary_id() != u32::MAX {
match self
let principal = self
.core
.storage
.directory
.query(QueryBy::Id(access_token.primary_id()), false)
.await
{
Ok(Some(principal)) => {
for secret in principal.secrets {
if secret.is_otp_auth() {
response.otp_auth = true;
} else if let Some((app_name, _)) =
secret.strip_prefix("$app$").and_then(|s| s.split_once('$'))
{
response.app_passwords.push(app_name.to_string());
}
}
.await?
.ok_or_else(|| trc::ManageCause::NotFound.into_err())?;
for secret in principal.secrets {
if secret.is_otp_auth() {
response.otp_auth = true;
} else if let Some((app_name, _)) =
secret.strip_prefix("$app$").and_then(|s| s.split_once('$'))
{
response.app_passwords.push(app_name.to_string());
}
Ok(None) => {
return RequestError::not_found().into_http_response();
}
Err(err) => return into_directory_response(err),
}
}
JsonResponse::new(json!({
Ok(JsonResponse::new(json!({
"data": response,
}))
.into_http_response()
.into_http_response())
}
pub async fn handle_account_auth_post(
@ -354,16 +292,18 @@ impl JMAP {
req: &HttpRequest,
access_token: Arc<AccessToken>,
body: Option<Vec<u8>>,
) -> HttpResponse {
) -> trc::Result<HttpResponse> {
// Parse request
let requests = match serde_json::from_slice::<Vec<AccountAuthRequest>>(
body.as_deref().unwrap_or_default(),
) {
Ok(request) => request,
Err(err) => return err.into_http_response(),
};
let requests =
serde_json::from_slice::<Vec<AccountAuthRequest>>(body.as_deref().unwrap_or_default())
.map_err(|err| {
trc::Cause::Resource(trc::ResourceCause::BadParameters).from_json_error(err)
})?;
if requests.is_empty() {
return RequestError::invalid_parameters().into_http_response();
return Err(trc::Cause::Resource(trc::ResourceCause::BadParameters)
.into_err()
.details("Empty request"));
}
// Make sure the user authenticated using Basic auth
@ -380,50 +320,41 @@ impl JMAP {
.and_then(|h| h.to_str().ok())
.map_or(true, |header| !header.to_lowercase().starts_with("basic "))
{
return ManagementApiError::Other {
details: "Password changes only allowed using Basic auth".into(),
}
.into_http_response();
return Err(manage::error(
"Password changes only allowed using Basic auth",
None::<u32>,
));
}
// Handle Fallback admin password changes
if access_token.is_super_user() && access_token.primary_id() == u32::MAX {
match requests.into_iter().next().unwrap() {
AccountAuthRequest::SetPassword { password } => {
return match self
.core
self.core
.storage
.config
.set([("authentication.fallback-admin.secret", password)])
.await
{
Ok(_) => {
// Remove entries from cache
self.inner.sessions.retain(|_, id| id.item != u32::MAX);
.await?;
JsonResponse::new(json!({
"data": (),
}))
.into_http_response()
}
Err(err) => err.into_http_response(),
};
// Remove entries from cache
self.inner.sessions.retain(|_, id| id.item != u32::MAX);
return Ok(JsonResponse::new(json!({
"data": (),
}))
.into_http_response());
}
_ => {
return ManagementApiError::Other {
details:
"Fallback administrator accounts do not support 2FA or AppPasswords"
.into(),
}
.into_http_response()
return Err(manage::error(
"Fallback administrator accounts do not support 2FA or AppPasswords",
None::<u32>,
));
}
}
}
// Make sure the current directory supports updates
if let Some(response) = self.assert_supported_directory() {
return response;
}
self.assert_supported_directory()?;
// Build actions
let mut actions = Vec::with_capacity(requests.len());
@ -459,42 +390,38 @@ impl JMAP {
}
// Update password
match self
.core
self.core
.storage
.data
.update_account(QueryBy::Id(access_token.primary_id()), actions)
.await
{
Ok(_) => {
// Remove entries from cache
self.inner
.sessions
.retain(|_, id| id.item != access_token.primary_id());
.await?;
JsonResponse::new(json!({
"data": (),
}))
.into_http_response()
}
Err(err) => into_directory_response(err),
}
// Remove entries from cache
self.inner
.sessions
.retain(|_, id| id.item != access_token.primary_id());
Ok(JsonResponse::new(json!({
"data": (),
}))
.into_http_response())
}
pub fn assert_supported_directory(&self) -> Option<HttpResponse> {
ManagementApiError::UnsupportedDirectoryOperation {
class: match &self.core.storage.directory.store {
DirectoryInner::Internal(_) => return None,
DirectoryInner::Ldap(_) => "LDAP",
DirectoryInner::Sql(_) => "SQL",
DirectoryInner::Imap(_) => "IMAP",
DirectoryInner::Smtp(_) => "SMTP",
DirectoryInner::Memory(_) => "In-Memory",
}
.into(),
}
.into_http_response()
.into()
pub fn assert_supported_directory(&self) -> trc::Result<()> {
let todo = "update webadmin";
let class = match &self.core.storage.directory.store {
DirectoryInner::Internal(_) => return Ok(()),
DirectoryInner::Ldap(_) => "LDAP",
DirectoryInner::Sql(_) => "SQL",
DirectoryInner::Imap(_) => "IMAP",
DirectoryInner::Smtp(_) => "SMTP",
DirectoryInner::Memory(_) => "In-Memory",
};
Err(manage::unsupported(format!(
"Requested action is unsupported for {class} directories.",
)))
}
}
@ -515,7 +442,8 @@ impl From<Principal<String>> for PrincipalResponse {
}
}
fn into_directory_response(mut error: trc::Error) -> HttpResponse {
/*
fn into_directory_response(mut error: trc::Error) -> trc::Result<HttpResponse> {
let response = match error.as_ref() {
trc::Cause::MissingParameter => ManagementApiError::FieldMissing {
field: error
@ -533,7 +461,7 @@ fn into_directory_response(mut error: trc::Error) -> HttpResponse {
.and_then(|v| v.into_string())
.unwrap_or_default(),
},
trc::Cause::NotFound => ManagementApiError::NotFound {
trc::StoreCause::NotFound.into_err() => ManagementApiError::NotFound {
item: error
.take_value(trc::Key::Key)
.and_then(|v| v.into_string())
@ -559,3 +487,4 @@ fn into_directory_response(mut error: trc::Error) -> HttpResponse {
JsonResponse::new(response).into_http_response()
}
*/

View file

@ -8,7 +8,6 @@ use std::str::FromStr;
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use hyper::Method;
use jmap_proto::error::request::RequestError;
use mail_auth::{
dmarc::URI,
mta_sts::ReportUri,
@ -104,7 +103,11 @@ pub enum Report {
}
impl JMAP {
pub async fn handle_manage_queue(&self, req: &HttpRequest, path: Vec<&str>) -> HttpResponse {
pub async fn handle_manage_queue(
&self,
req: &HttpRequest,
path: Vec<&str>,
) -> trc::Result<HttpResponse> {
let params = UrlParams::new(req.uri().query());
match (
@ -138,8 +141,7 @@ impl JMAP {
let mut offset = page.saturating_sub(1) * limit;
let mut total = 0;
let mut total_returned = 0;
let _ = self
.core
self.core
.storage
.data
.iterate(
@ -193,9 +195,9 @@ impl JMAP {
Ok(max_total == 0 || total < max_total)
},
)
.await;
.await?;
if values {
Ok(if values {
JsonResponse::new(json!({
"data":{
"items": result_values,
@ -210,7 +212,7 @@ impl JMAP {
},
}))
}
.into_http_response()
.into_http_response())
}
("messages", Some(queue_id), &Method::GET) => {
if let Some(message) = self
@ -218,12 +220,12 @@ impl JMAP {
.read_message(queue_id.parse().unwrap_or_default())
.await
{
JsonResponse::new(json!({
Ok(JsonResponse::new(json!({
"data": Message::from(&message),
}))
.into_http_response()
.into_http_response())
} else {
RequestError::not_found().into_http_response()
Err(trc::ResourceCause::NotFound.into_err())
}
}
("messages", Some(queue_id), &Method::PATCH) => {
@ -265,12 +267,12 @@ impl JMAP {
let _ = self.smtp.inner.queue_tx.send(queue::Event::Reload).await;
}
JsonResponse::new(json!({
Ok(JsonResponse::new(json!({
"data": found,
}))
.into_http_response()
.into_http_response())
} else {
RequestError::not_found().into_http_response()
Err(trc::ResourceCause::NotFound.into_err())
}
}
("messages", Some(queue_id), &Method::DELETE) => {
@ -345,12 +347,12 @@ impl JMAP {
found = true;
}
JsonResponse::new(json!({
Ok(JsonResponse::new(json!({
"data": found,
}))
.into_http_response()
.into_http_response())
} else {
RequestError::not_found().into_http_response()
Err(trc::ResourceCause::NotFound.into_err())
}
}
("reports", None, &Method::GET) => {
@ -387,8 +389,7 @@ impl JMAP {
let mut offset = page.saturating_sub(1) * limit;
let mut total = 0;
let mut total_returned = 0;
let _ = self
.core
self.core
.storage
.data
.iterate(
@ -422,15 +423,15 @@ impl JMAP {
Ok(max_total == 0 || total < max_total)
},
)
.await;
.await?;
JsonResponse::new(json!({
Ok(JsonResponse::new(json!({
"data": {
"items": result,
"total": total,
},
}))
.into_http_response()
.into_http_response())
}
("reports", Some(report_id), &Method::GET) => {
let mut result = None;
@ -438,20 +439,20 @@ impl JMAP {
match report_id {
QueueClass::DmarcReportHeader(event) => {
let mut rua = Vec::new();
if let Ok(Some(report)) = self
if let Some(report) = self
.smtp
.generate_dmarc_aggregate_report(&event, &mut rua, None)
.await
.await?
{
result = Report::dmarc(event, report, rua).into();
}
}
QueueClass::TlsReportHeader(event) => {
let mut rua = Vec::new();
if let Ok(Some(report)) = self
if let Some(report) = self
.smtp
.generate_tls_aggregate_report(&[event.clone()], &mut rua, None)
.await
.await?
{
result = Report::tls(event, report, rua).into();
}
@ -461,12 +462,12 @@ impl JMAP {
}
if let Some(result) = result {
JsonResponse::new(json!({
Ok(JsonResponse::new(json!({
"data": result,
}))
.into_http_response()
.into_http_response())
} else {
RequestError::not_found().into_http_response()
Err(trc::ResourceCause::NotFound.into_err())
}
}
("reports", Some(report_id), &Method::DELETE) => {
@ -481,15 +482,15 @@ impl JMAP {
_ => (),
}
JsonResponse::new(json!({
Ok(JsonResponse::new(json!({
"data": true,
}))
.into_http_response()
.into_http_response())
} else {
RequestError::not_found().into_http_response()
Err(trc::ResourceCause::NotFound.into_err())
}
}
_ => RequestError::not_found().into_http_response(),
_ => Err(trc::ResourceCause::NotFound.into_err()),
}
}
}

View file

@ -5,7 +5,6 @@
*/
use hyper::Method;
use jmap_proto::error::request::RequestError;
use serde_json::json;
use utils::url_params::UrlParams;
@ -16,107 +15,88 @@ use crate::{
};
impl JMAP {
pub async fn handle_manage_reload(&self, req: &HttpRequest, path: Vec<&str>) -> HttpResponse {
pub async fn handle_manage_reload(
&self,
req: &HttpRequest,
path: Vec<&str>,
) -> trc::Result<HttpResponse> {
match (path.get(1).copied(), req.method()) {
(Some("lookup"), &Method::GET) => {
match self.core.reload_lookups().await {
Ok(result) => {
// Update core
if let Some(core) = result.new_core {
self.shared_core.store(core.into());
}
JsonResponse::new(json!({
"data": result.config,
}))
.into_http_response()
}
Err(err) => err.into_http_response(),
let result = self.core.reload_lookups().await?;
// Update core
if let Some(core) = result.new_core {
self.shared_core.store(core.into());
}
}
(Some("certificate"), &Method::GET) => match self.core.reload_certificates().await {
Ok(result) => JsonResponse::new(json!({
Ok(JsonResponse::new(json!({
"data": result.config,
}))
.into_http_response(),
Err(err) => err.into_http_response(),
},
.into_http_response())
}
(Some("certificate"), &Method::GET) => Ok(JsonResponse::new(json!({
"data": self.core.reload_certificates().await?.config,
}))
.into_http_response()),
(Some("server.blocked-ip"), &Method::GET) => {
match self.core.reload_blocked_ips().await {
Ok(result) => {
// Increment version counter
self.core.network.blocked_ips.increment_version();
let result = self.core.reload_blocked_ips().await?;
// Increment version counter
self.core.network.blocked_ips.increment_version();
JsonResponse::new(json!({
"data": result.config,
}))
.into_http_response()
}
Err(err) => err.into_http_response(),
}
Ok(JsonResponse::new(json!({
"data": result.config,
}))
.into_http_response())
}
(_, &Method::GET) => {
match self.core.reload().await {
Ok(result) => {
if !UrlParams::new(req.uri().query()).has_key("dry-run") {
if let Some(core) = result.new_core {
// Update core
self.shared_core.store(core.into());
let result = self.core.reload().await?;
if !UrlParams::new(req.uri().query()).has_key("dry-run") {
if let Some(core) = result.new_core {
// Update core
self.shared_core.store(core.into());
// Increment version counter
self.inner.increment_config_version();
}
// Reload ACME
if let Err(err) =
self.inner.housekeeper_tx.send(Event::AcmeReload).await
{
tracing::warn!(
"Failed to send ACME reload event to housekeeper: {}",
err
);
}
}
JsonResponse::new(json!({
"data": result.config,
}))
.into_http_response()
// Increment version counter
self.inner.increment_config_version();
}
// Reload ACME
if let Err(err) = self.inner.housekeeper_tx.send(Event::AcmeReload).await {
tracing::warn!("Failed to send ACME reload event to housekeeper: {}", err);
}
Err(err) => err.into_http_response(),
}
Ok(JsonResponse::new(json!({
"data": result.config,
}))
.into_http_response())
}
_ => RequestError::not_found().into_http_response(),
_ => Err(trc::ResourceCause::NotFound.into_err()),
}
}
pub async fn handle_manage_update(&self, req: &HttpRequest, path: Vec<&str>) -> HttpResponse {
pub async fn handle_manage_update(
&self,
req: &HttpRequest,
path: Vec<&str>,
) -> trc::Result<HttpResponse> {
match (path.get(1).copied(), req.method()) {
(Some("spam-filter"), &Method::GET) => {
match self
.core
.storage
.config
.update_config_resource("spam-filter")
.await
{
Ok(result) => JsonResponse::new(json!({
"data": result,
}))
.into_http_response(),
Err(err) => err.into_http_response(),
}
}
(Some("spam-filter"), &Method::GET) => Ok(JsonResponse::new(json!({
"data": self
.core
.storage
.config
.update_config_resource("spam-filter")
.await?,
}))
.into_http_response()),
(Some("webadmin"), &Method::GET) => {
match self.inner.webadmin.update_and_unpack(&self.core).await {
Ok(_) => JsonResponse::new(json!({
"data": (),
}))
.into_http_response(),
Err(err) => err.into_http_response(),
}
self.inner.webadmin.update_and_unpack(&self.core).await?;
Ok(JsonResponse::new(json!({
"data": (),
}))
.into_http_response())
}
_ => RequestError::not_found().into_http_response(),
_ => Err(trc::ResourceCause::NotFound.into_err()),
}
}
}

View file

@ -5,7 +5,6 @@
*/
use hyper::Method;
use jmap_proto::error::request::RequestError;
use mail_auth::report::{
tlsrpt::{FailureDetails, Policy, TlsReport},
Feedback,
@ -32,7 +31,11 @@ enum ReportType {
}
impl JMAP {
pub async fn handle_manage_reports(&self, req: &HttpRequest, path: Vec<&str>) -> HttpResponse {
pub async fn handle_manage_reports(
&self,
req: &HttpRequest,
path: Vec<&str>,
) -> trc::Result<HttpResponse> {
match (
path.get(1).copied().unwrap_or_default(),
path.get(2).copied().map(decode_path_element),
@ -89,8 +92,7 @@ impl JMAP {
let mut offset = page.saturating_sub(1) * limit;
let mut total = 0;
let mut last_id = 0;
let result = self
.core
self.core
.storage
.data
.iterate(
@ -141,17 +143,15 @@ impl JMAP {
Ok(max_total == 0 || total < max_total)
},
)
.await;
match result {
Ok(_) => JsonResponse::new(json!({
"data": {
"items": results,
"total": total,
},
}))
.into_http_response(),
Err(err) => err.into_http_response(),
}
.await?;
Ok(JsonResponse::new(json!({
"data": {
"items": results,
"total": total,
},
}))
.into_http_response())
}
(class @ ("dmarc" | "tls" | "arf"), Some(report_id), &Method::GET) => {
if let Some(report_id) = parse_incoming_report_id(class, report_id.as_ref()) {
@ -163,14 +163,13 @@ impl JMAP {
.get_value::<Bincode<IncomingReport<TlsReport>>>(ValueKey::from(
ValueClass::Report(report_id),
))
.await
.await?
{
Ok(Some(report)) => JsonResponse::new(json!({
Some(report) => Ok(JsonResponse::new(json!({
"data": report.inner,
}))
.into_http_response(),
Ok(None) => RequestError::not_found().into_http_response(),
Err(err) => err.into_http_response(),
.into_http_response()),
None => Err(trc::ResourceCause::NotFound.into_err()),
},
ReportClass::Dmarc { .. } => match self
.core
@ -179,14 +178,13 @@ impl JMAP {
.get_value::<Bincode<IncomingReport<mail_auth::report::Report>>>(
ValueKey::from(ValueClass::Report(report_id)),
)
.await
.await?
{
Ok(Some(report)) => JsonResponse::new(json!({
Some(report) => Ok(JsonResponse::new(json!({
"data": report.inner,
}))
.into_http_response(),
Ok(None) => RequestError::not_found().into_http_response(),
Err(err) => err.into_http_response(),
.into_http_response()),
None => Err(trc::ResourceCause::NotFound.into_err()),
},
ReportClass::Arf { .. } => match self
.core
@ -195,35 +193,34 @@ impl JMAP {
.get_value::<Bincode<IncomingReport<Feedback>>>(ValueKey::from(
ValueClass::Report(report_id),
))
.await
.await?
{
Ok(Some(report)) => JsonResponse::new(json!({
Some(report) => Ok(JsonResponse::new(json!({
"data": report.inner,
}))
.into_http_response(),
Ok(None) => RequestError::not_found().into_http_response(),
Err(err) => err.into_http_response(),
.into_http_response()),
None => Err(trc::ResourceCause::NotFound.into_err()),
},
}
} else {
RequestError::not_found().into_http_response()
Err(trc::ResourceCause::NotFound.into_err())
}
}
(class @ ("dmarc" | "tls" | "arf"), Some(report_id), &Method::DELETE) => {
if let Some(report_id) = parse_incoming_report_id(class, report_id.as_ref()) {
let mut batch = BatchBuilder::new();
batch.clear(ValueClass::Report(report_id));
let result = self.core.storage.data.write(batch.build()).await.is_ok();
self.core.storage.data.write(batch.build()).await?;
JsonResponse::new(json!({
"data": result,
Ok(JsonResponse::new(json!({
"data": true,
}))
.into_http_response()
.into_http_response())
} else {
RequestError::not_found().into_http_response()
Err(trc::ResourceCause::NotFound.into_err())
}
}
_ => RequestError::not_found().into_http_response(),
_ => Err(trc::ResourceCause::NotFound.into_err()),
}
}
}

View file

@ -5,7 +5,6 @@
*/
use hyper::Method;
use jmap_proto::error::request::RequestError;
use serde_json::json;
use store::ahash::AHashMap;
use utils::{config::ConfigKey, url_params::UrlParams};
@ -15,7 +14,7 @@ use crate::{
JMAP,
};
use super::{decode_path_element, ManagementApiError};
use super::decode_path_element;
#[derive(Debug, serde::Serialize, serde::Deserialize)]
#[serde(tag = "type")]
@ -39,7 +38,7 @@ impl JMAP {
req: &HttpRequest,
path: Vec<&str>,
body: Option<Vec<u8>>,
) -> HttpResponse {
) -> trc::Result<HttpResponse> {
match (path.get(1).copied(), req.method()) {
(Some("group"), &Method::GET) => {
// List settings
@ -71,103 +70,101 @@ impl JMAP {
params.parse::<usize>("page").unwrap_or(0).saturating_sub(1) * limit;
let has_filter = !filter.is_empty();
match self.core.storage.config.list(&prefix, true).await {
Ok(settings) => if !suffix.is_empty() && !settings.is_empty() {
// Obtain record ids
let mut total = 0;
let mut ids = Vec::new();
for (key, _) in &settings {
if let Some(id) = key.strip_suffix(&suffix) {
if !id.is_empty() {
if !has_filter {
if offset == 0 {
if limit == 0 || ids.len() < limit {
ids.push(id);
}
} else {
offset -= 1;
}
total += 1;
} else {
ids.push(id);
}
}
}
}
// Group settings by record id
let mut records = Vec::new();
for id in ids {
let mut record = AHashMap::new();
let prefix = format!("{id}.");
record.insert("_id".to_string(), id.to_string());
for (k, v) in &settings {
if let Some(k) = k.strip_prefix(&prefix) {
if field.map_or(true, |field| field == k) {
record.insert(k.to_string(), v.to_string());
}
} else if record.len() > 1 {
break;
}
}
if has_filter {
if record
.iter()
.any(|(_, v)| v.to_lowercase().contains(&filter))
{
let settings = self.core.storage.config.list(&prefix, true).await?;
if !suffix.is_empty() && !settings.is_empty() {
// Obtain record ids
let mut total = 0;
let mut ids = Vec::new();
for (key, _) in &settings {
if let Some(id) = key.strip_suffix(&suffix) {
if !id.is_empty() {
if !has_filter {
if offset == 0 {
if limit == 0 || records.len() < limit {
records.push(record);
if limit == 0 || ids.len() < limit {
ids.push(id);
}
} else {
offset -= 1;
}
total += 1;
} else {
ids.push(id);
}
} else {
records.push(record);
}
}
}
// Group settings by record id
let mut records = Vec::new();
for id in ids {
let mut record = AHashMap::new();
let prefix = format!("{id}.");
record.insert("_id".to_string(), id.to_string());
for (k, v) in &settings {
if let Some(k) = k.strip_prefix(&prefix) {
if field.map_or(true, |field| field == k) {
record.insert(k.to_string(), v.to_string());
}
} else if record.len() > 1 {
break;
}
}
JsonResponse::new(json!({
"data": {
"total": total,
"items": records,
},
}))
} else {
let total = settings.len();
let items = settings
.into_iter()
.filter_map(|(k, v)| {
if filter.is_empty()
|| k.to_lowercase().contains(&filter)
|| v.to_lowercase().contains(&filter)
{
let k =
k.strip_prefix(&prefix).map(|k| k.to_string()).unwrap_or(k);
Some(json!({
"_id": k,
"_value": v,
}))
if has_filter {
if record
.iter()
.any(|(_, v)| v.to_lowercase().contains(&filter))
{
if offset == 0 {
if limit == 0 || records.len() < limit {
records.push(record);
}
} else {
None
offset -= 1;
}
})
.skip(offset)
.take(if limit == 0 { total } else { limit })
.collect::<Vec<_>>();
JsonResponse::new(json!({
"data": {
"total": total,
"items": items,
},
}))
total += 1;
}
} else {
records.push(record);
}
}
.into_http_response(),
Err(err) => err.into_http_response(),
Ok(JsonResponse::new(json!({
"data": {
"total": total,
"items": records,
},
}))
.into_http_response())
} else {
let total = settings.len();
let items = settings
.into_iter()
.filter_map(|(k, v)| {
if filter.is_empty()
|| k.to_lowercase().contains(&filter)
|| v.to_lowercase().contains(&filter)
{
let k = k.strip_prefix(&prefix).map(|k| k.to_string()).unwrap_or(k);
Some(json!({
"_id": k,
"_value": v,
}))
} else {
None
}
})
.skip(offset)
.take(if limit == 0 { total } else { limit })
.collect::<Vec<_>>();
Ok(JsonResponse::new(json!({
"data": {
"total": total,
"items": items,
},
}))
.into_http_response())
}
}
(Some("list"), &Method::GET) => {
@ -186,25 +183,21 @@ impl JMAP {
let limit: usize = params.parse("limit").unwrap_or(0);
let offset = params.parse::<usize>("page").unwrap_or(0).saturating_sub(1) * limit;
match self.core.storage.config.list(&prefix, true).await {
Ok(settings) => {
let total = settings.len();
let items = settings
.into_iter()
.skip(offset)
.take(if limit == 0 { total } else { limit })
.collect::<AHashMap<_, _>>();
let settings = self.core.storage.config.list(&prefix, true).await?;
let total = settings.len();
let items = settings
.into_iter()
.skip(offset)
.take(if limit == 0 { total } else { limit })
.collect::<AHashMap<_, _>>();
JsonResponse::new(json!({
"data": {
"total": total,
"items": items,
},
}))
.into_http_response()
}
Err(err) => err.into_http_response(),
}
Ok(JsonResponse::new(json!({
"data": {
"total": total,
"items": items,
},
}))
.into_http_response())
}
(Some("keys"), &Method::GET) => {
// Obtain keys
@ -217,19 +210,11 @@ impl JMAP {
.get("prefixes")
.map(|s| s.split(',').collect::<Vec<_>>())
.unwrap_or_default();
let mut err = None;
let mut results = AHashMap::with_capacity(keys.len());
for key in keys {
match self.core.storage.config.get(key).await {
Ok(Some(value)) => {
results.insert(key.to_string(), value);
}
Ok(None) => {}
Err(err_) => {
err = err_.into();
break;
}
if let Some(value) = self.core.storage.config.get(key).await? {
results.insert(key.to_string(), value);
}
}
for prefix in prefixes {
@ -238,134 +223,88 @@ impl JMAP {
} else {
prefix.to_string()
};
match self.core.storage.config.list(&prefix, false).await {
Ok(values) => {
results.extend(values);
}
Err(err_) => {
err = err_.into();
break;
}
}
results.extend(self.core.storage.config.list(&prefix, false).await?);
}
match err {
None => JsonResponse::new(json!({
"data": results,
}))
.into_http_response(),
Some(err) => err.into_http_response(),
}
Ok(JsonResponse::new(json!({
"data": results,
}))
.into_http_response())
}
(Some(prefix), &Method::DELETE) if !prefix.is_empty() => {
let prefix = decode_path_element(prefix);
match self.core.storage.config.clear(prefix.as_ref()).await {
Ok(_) => JsonResponse::new(json!({
"data": (),
}))
.into_http_response(),
Err(err) => err.into_http_response(),
}
self.core.storage.config.clear(prefix.as_ref()).await?;
Ok(JsonResponse::new(json!({
"data": (),
}))
.into_http_response())
}
(None, &Method::POST) => {
match serde_json::from_slice::<Vec<UpdateSettings>>(
let changes = serde_json::from_slice::<Vec<UpdateSettings>>(
body.as_deref().unwrap_or_default(),
) {
Ok(changes) => {
let mut result = Ok(true);
)
.map_err(|err| {
trc::Cause::Resource(trc::ResourceCause::BadParameters).from_json_error(err)
})?;
'next: for change in changes {
match change {
UpdateSettings::Delete { keys } => {
for key in keys {
result =
self.core.storage.config.clear(key).await.map(|_| true);
if result.is_err() {
break 'next;
}
}
}
UpdateSettings::Clear { prefix } => {
result = self
for change in changes {
match change {
UpdateSettings::Delete { keys } => {
for key in keys {
self.core.storage.config.clear(key).await?;
}
}
UpdateSettings::Clear { prefix } => {
self.core.storage.config.clear_prefix(&prefix).await?;
}
UpdateSettings::Insert {
prefix,
values,
assert_empty,
} => {
if assert_empty {
if let Some(prefix) = &prefix {
if !self
.core
.storage
.config
.clear_prefix(&prefix)
.await
.map(|_| true);
if result.is_err() {
break;
.list(&format!("{prefix}."), true)
.await?
.is_empty()
{
return Err(trc::ManageCause::AssertFailed.into_err());
}
}
UpdateSettings::Insert {
prefix,
values,
assert_empty,
} => {
if assert_empty {
if let Some(prefix) = &prefix {
result = self
.core
.storage
.config
.list(&format!("{prefix}."), true)
.await
.map(|items| items.is_empty());
if matches!(result, Ok(false) | Err(_)) {
break;
}
} else if let Some((key, _)) = values.first() {
result = self
.core
.storage
.config
.get(key)
.await
.map(|items| items.is_none());
if matches!(result, Ok(false) | Err(_)) {
break;
}
}
}
result = self
.core
.storage
.config
.set(values.into_iter().map(|(key, value)| ConfigKey {
key: if let Some(prefix) = &prefix {
format!("{prefix}.{key}")
} else {
key
},
value,
}))
.await
.map(|_| true);
if result.is_err() {
break;
} else if let Some((key, _)) = values.first() {
if self.core.storage.config.get(key).await?.is_some() {
return Err(trc::ManageCause::AssertFailed.into_err());
}
}
}
}
match result {
Ok(true) => JsonResponse::new(json!({
"data": (),
}))
.into_http_response(),
Ok(false) => JsonResponse::new(ManagementApiError::AssertFailed)
.into_http_response(),
Err(err) => err.into_http_response(),
self.core
.storage
.config
.set(values.into_iter().map(|(key, value)| ConfigKey {
key: if let Some(prefix) = &prefix {
format!("{prefix}.{key}")
} else {
key
},
value,
}))
.await?;
}
}
Err(err) => err.into_http_response(),
}
Ok(JsonResponse::new(json!({
"data": (),
}))
.into_http_response())
}
_ => RequestError::not_found().into_http_response(),
_ => Err(trc::ResourceCause::NotFound.into_err()),
}
}
}

View file

@ -8,7 +8,6 @@ use std::time::SystemTime;
use common::{scripts::ScriptModification, IntoString};
use hyper::Method;
use jmap_proto::error::request::RequestError;
use serde_json::json;
use sieve::{runtime::Variable, Envelope};
use smtp::scripts::{ScriptParameters, ScriptResult};
@ -42,7 +41,7 @@ impl JMAP {
req: &HttpRequest,
path: Vec<&str>,
body: Option<Vec<u8>>,
) -> HttpResponse {
) -> trc::Result<HttpResponse> {
let script = match (
path.get(1)
.and_then(|name| self.core.sieve.scripts.get(*name))
@ -51,7 +50,7 @@ impl JMAP {
) {
(Some(script), &Method::POST) => script,
_ => {
return RequestError::not_found().into_http_response();
return Err(trc::ResourceCause::NotFound.into_err());
}
};
@ -112,9 +111,9 @@ impl JMAP {
ScriptResult::Discard => Response::Discard,
};
JsonResponse::new(json!({
Ok(JsonResponse::new(json!({
"data": result,
}))
.into_http_response()
.into_http_response())
}
}

View file

@ -8,7 +8,6 @@ use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use common::manager::webadmin::Resource;
use directory::backend::internal::manage::ManageDirectory;
use hyper::Method;
use jmap_proto::error::request::RequestError;
use serde_json::json;
use utils::url_params::UrlParams;
@ -21,7 +20,11 @@ use crate::{
use super::decode_path_element;
impl JMAP {
pub async fn handle_manage_store(&self, req: &HttpRequest, path: Vec<&str>) -> HttpResponse {
pub async fn handle_manage_store(
&self,
req: &HttpRequest,
path: Vec<&str>,
) -> trc::Result<HttpResponse> {
match (
path.get(1).copied(),
path.get(2).copied(),
@ -29,41 +32,36 @@ impl JMAP {
req.method(),
) {
(Some("blobs"), Some(blob_hash), _, &Method::GET) => {
match URL_SAFE_NO_PAD.decode(decode_path_element(blob_hash).as_bytes()) {
Ok(blob_hash) => {
match self
.core
.storage
.blob
.get_blob(&blob_hash, 0..usize::MAX)
.await
{
Ok(Some(contents)) => {
let params = UrlParams::new(req.uri().query());
let offset = params.parse("offset").unwrap_or(0);
let limit = params.parse("limit").unwrap_or(usize::MAX);
let blob_hash = URL_SAFE_NO_PAD
.decode(decode_path_element(blob_hash).as_bytes())
.map_err(|err| {
trc::Cause::Resource(trc::ResourceCause::BadParameters)
.from_base64_error(err)
})?;
let contents = self
.core
.storage
.blob
.get_blob(&blob_hash, 0..usize::MAX)
.await?
.ok_or_else(|| trc::ManageCause::NotFound.into_err())?;
let params = UrlParams::new(req.uri().query());
let offset = params.parse("offset").unwrap_or(0);
let limit = params.parse("limit").unwrap_or(usize::MAX);
let contents = if offset == 0 && limit == usize::MAX {
contents
} else {
contents
.get(offset..std::cmp::min(offset + limit, contents.len()))
.unwrap_or_default()
.to_vec()
};
let contents = if offset == 0 && limit == usize::MAX {
contents
} else {
contents
.get(offset..std::cmp::min(offset + limit, contents.len()))
.unwrap_or_default()
.to_vec()
};
Resource {
content_type: "application/octet-stream",
contents,
}
.into_http_response()
}
Ok(None) => RequestError::not_found().into_http_response(),
Err(err) => err.into_http_response(),
}
}
Err(_) => RequestError::invalid_parameters().into_http_response(),
Ok(Resource {
content_type: "application/octet-stream",
contents,
}
.into_http_response())
}
(Some("purge"), Some("blob"), _, &Method::GET) => {
self.housekeeper_request(Event::Purge(PurgeType::Blobs {
@ -77,7 +75,7 @@ impl JMAP {
if let Some(store) = self.core.storage.stores.get(id) {
store.clone()
} else {
return RequestError::not_found().into_http_response();
return Err(trc::ResourceCause::NotFound.into_err());
}
} else {
self.core.storage.data.clone()
@ -91,7 +89,7 @@ impl JMAP {
if let Some(store) = self.core.storage.lookups.get(id) {
store.clone()
} else {
return RequestError::not_found().into_http_response();
return Err(trc::ResourceCause::NotFound.into_err());
}
} else {
self.core.storage.lookup.clone()
@ -102,17 +100,13 @@ impl JMAP {
}
(Some("purge"), Some("account"), id, &Method::GET) => {
let account_id = if let Some(id) = id {
match self
.core
self.core
.storage
.data
.get_account_id(decode_path_element(id).as_ref())
.await
{
Ok(Some(id)) => id.into(),
Ok(None) => return RequestError::not_found().into_http_response(),
Err(err) => return err.into_http_response(),
}
.await?
.ok_or_else(|| trc::ManageCause::NotFound.into_err())?
.into()
} else {
None
};
@ -120,20 +114,20 @@ impl JMAP {
self.housekeeper_request(Event::Purge(PurgeType::Account(account_id)))
.await
}
_ => RequestError::not_found().into_http_response(),
_ => Err(trc::ResourceCause::NotFound.into_err()),
}
}
async fn housekeeper_request(&self, event: Event) -> HttpResponse {
match self.inner.housekeeper_tx.send(event).await {
Ok(_) => JsonResponse::new(json!({
"data": (),
}))
.into_http_response(),
Err(_) => {
tracing::error!("Failed to send housekeeper event");
RequestError::internal_server_error().into_http_response()
}
}
async fn housekeeper_request(&self, event: Event) -> trc::Result<HttpResponse> {
self.inner.housekeeper_tx.send(event).await.map_err(|err| {
trc::Cause::Thread
.reason(err)
.details("Failed to send housekeeper event")
})?;
Ok(JsonResponse::new(json!({
"data": (),
}))
.into_http_response())
}
}

View file

@ -8,7 +8,7 @@ use std::sync::Arc;
use common::listener::ServerInstance;
use jmap_proto::{
error::{method::MethodError, request::RequestError},
error::method::MethodError,
method::{
get, query,
set::{self},
@ -26,7 +26,7 @@ impl JMAP {
request: Request,
access_token: Arc<AccessToken>,
instance: &Arc<ServerInstance>,
) -> Result<Response, RequestError> {
) -> Response {
let mut response = Response::new(
access_token.state(),
request.created_ids.unwrap_or_default(),
@ -104,7 +104,7 @@ impl JMAP {
response.created_ids.clear();
}
Ok(response)
response
}
async fn handle_method_call(

View file

@ -8,10 +8,10 @@ use std::sync::Arc;
use directory::QueryBy;
use jmap_proto::{
error::request::RequestError,
request::capability::{Capability, Session},
types::{acl::Acl, collection::Collection, id::Id},
};
use trc::AddContext;
use crate::{auth::AccessToken, JMAP};
@ -20,7 +20,7 @@ impl JMAP {
&self,
base_url: String,
access_token: Arc<AccessToken>,
) -> Result<Session, RequestError> {
) -> trc::Result<Session> {
let mut session = Session::new(base_url, &self.core.jmap.capabilities);
session.set_state(access_token.state());
session.set_primary_account(
@ -41,7 +41,8 @@ impl JMAP {
&& self
.shared_documents(&access_token, *id, Collection::Mailbox, Acl::AddItems)
.await
.map_or(true, |ids| ids.is_empty());
.caused_by(trc::location!())?
.is_empty();
session.add_account(
(*id).into(),
@ -50,7 +51,7 @@ impl JMAP {
.directory
.query(QueryBy::Id(*id), false)
.await
.unwrap_or_default()
.caused_by(trc::location!())?
.map(|p| p.name)
.unwrap_or_else(|| Id::from(*id).to_string()),
is_personal,

View file

@ -49,7 +49,7 @@ impl JMAP {
let acl = Bitmap::<Acl>::from(acl_item.permissions);
let collection = Collection::from(acl_item.to_collection);
if !collection.is_valid() {
return Err(trc::Cause::DataCorruption
return Err(trc::StoreCause::DataCorruption
.ctx(trc::Key::Reason, "Corrupted collection found in ACL key.")
.details(format!("{acl_item:?}"))
.account_id(grant_account_id)

View file

@ -48,7 +48,7 @@ impl JMAP {
self.authenticate_plain(&account, &secret, remote_ip, ServerProtocol::Http)
.await?
} else {
return Err(trc::Cause::Authentication
return Err(trc::AuthCause::Error
.into_err()
.details("Failed to decode Basic auth request.")
.id(token)
@ -65,7 +65,7 @@ impl JMAP {
} else {
// Enforce anonymous rate limit
self.is_anonymous_allowed(&remote_ip).await?;
return Err(trc::Cause::Authentication
return Err(trc::AuthCause::Error
.into_err()
.reason("Unsupported authentication mechanism.")
.details(token)
@ -87,7 +87,7 @@ impl JMAP {
// Enforce anonymous rate limit
self.is_anonymous_allowed(&remote_ip).await?;
Err(trc::Cause::Authentication
Err(trc::AuthCause::Error
.into_err()
.details("Missing Authorization header.")
.caused_by(trc::location!()))
@ -147,7 +147,7 @@ impl JMAP {
{
Ok(principal) => Ok(AccessToken::new(principal)),
Err(err) => {
if !err.matches(trc::Cause::MissingTotp) {
if !err.matches(trc::Cause::Auth(trc::AuthCause::MissingTotp)) {
let _ = self.is_auth_allowed_hard(&remote_ip).await;
}
Err(err)
@ -164,7 +164,7 @@ impl JMAP {
.await
{
Ok(Some(principal)) => self.update_access_token(AccessToken::new(principal)).await,
Ok(None) => Err(trc::Cause::Authentication
Ok(None) => Err(trc::AuthCause::Error
.into_err()
.details("Account not found.")
.caused_by(trc::location!())),

View file

@ -6,7 +6,6 @@
use std::sync::Arc;
use hyper::StatusCode;
use rand::distributions::Standard;
use serde_json::json;
use store::{
@ -16,10 +15,7 @@ use store::{
};
use crate::{
api::{
http::ToHttpResponse, management::ManagementApiError, HtmlResponse, HttpRequest,
HttpResponse, JsonResponse,
},
api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse},
auth::{oauth::OAuthStatus, AccessToken},
JMAP,
};
@ -34,155 +30,133 @@ impl JMAP {
&self,
access_token: Arc<AccessToken>,
body: Option<Vec<u8>>,
) -> HttpResponse {
match serde_json::from_slice::<OAuthCodeRequest>(body.as_deref().unwrap_or_default()) {
Ok(request) => {
let response = match request {
OAuthCodeRequest::Code {
client_id,
redirect_uri,
} => {
// Validate clientId
if client_id.len() > CLIENT_ID_MAX_LEN {
return ManagementApiError::Other {
details: "Client ID is invalid.".into(),
}
.into_http_response();
} else if redirect_uri
.as_ref()
.map_or(false, |uri| !uri.starts_with("https://"))
{
return ManagementApiError::Other {
details: "Redirect URI must be HTTPS.".into(),
}
.into_http_response();
}
) -> trc::Result<HttpResponse> {
let request =
serde_json::from_slice::<OAuthCodeRequest>(body.as_deref().unwrap_or_default())
.map_err(|err| {
trc::Cause::Resource(trc::ResourceCause::BadParameters).from_json_error(err)
})?;
// Generate client code
let client_code = thread_rng()
.sample_iter(Alphanumeric)
.take(DEVICE_CODE_LEN)
.map(char::from)
.collect::<String>();
let response = match request {
OAuthCodeRequest::Code {
client_id,
redirect_uri,
} => {
// Validate clientId
if client_id.len() > CLIENT_ID_MAX_LEN {
return Err(trc::ManageCause::Error
.into_err()
.details("Client ID is invalid."));
} else if redirect_uri
.as_ref()
.map_or(false, |uri| !uri.starts_with("https://"))
{
return Err(trc::ManageCause::Error
.into_err()
.details("Redirect URI must be HTTPS."));
}
// Serialize OAuth code
let value = Bincode::new(OAuthCode {
status: OAuthStatus::Authorized,
account_id: access_token.primary_id(),
client_id,
params: redirect_uri.unwrap_or_default(),
})
.serialize();
// Generate client code
let client_code = thread_rng()
.sample_iter(Alphanumeric)
.take(DEVICE_CODE_LEN)
.map(char::from)
.collect::<String>();
// Insert client code
if let Err(err) = self
.core
// Serialize OAuth code
let value = Bincode::new(OAuthCode {
status: OAuthStatus::Authorized,
account_id: access_token.primary_id(),
client_id,
params: redirect_uri.unwrap_or_default(),
})
.serialize();
// Insert client code
self.core
.storage
.lookup
.key_set(
format!("oauth:{client_code}").into_bytes(),
value,
self.core.jmap.oauth_expiry_auth_code.into(),
)
.await?;
#[cfg(not(feature = "enterprise"))]
let is_enterprise = false;
#[cfg(feature = "enterprise")]
let is_enterprise = self.core.is_enterprise_edition();
json!({
"data": {
"code": client_code,
"is_admin": access_token.is_super_user(),
"is_enterprise": is_enterprise,
},
})
}
OAuthCodeRequest::Device { code } => {
let mut success = false;
// Obtain code
if let Some(mut auth_code) = self
.core
.storage
.lookup
.key_get::<Bincode<OAuthCode>>(format!("oauth:{code}").into_bytes())
.await?
{
if auth_code.inner.status == OAuthStatus::Pending {
auth_code.inner.status = OAuthStatus::Authorized;
auth_code.inner.account_id = access_token.primary_id();
let device_code = std::mem::take(&mut auth_code.inner.params);
success = true;
// Delete issued user code
self.core
.storage
.lookup
.key_delete(format!("oauth:{code}").into_bytes())
.await?;
// Update device code status
self.core
.storage
.lookup
.key_set(
format!("oauth:{client_code}").into_bytes(),
value,
format!("oauth:{device_code}").into_bytes(),
auth_code.serialize(),
self.core.jmap.oauth_expiry_auth_code.into(),
)
.await
{
return err.into_http_response();
}
#[cfg(not(feature = "enterprise"))]
let is_enterprise = false;
#[cfg(feature = "enterprise")]
let is_enterprise = self.core.is_enterprise_edition();
json!({
"data": {
"code": client_code,
"is_admin": access_token.is_super_user(),
"is_enterprise": is_enterprise,
},
})
.await?;
}
OAuthCodeRequest::Device { code } => {
let mut success = false;
}
// Obtain code
match self
.core
.storage
.lookup
.key_get::<Bincode<OAuthCode>>(format!("oauth:{code}").into_bytes())
.await
{
Ok(Some(mut auth_code))
if auth_code.inner.status == OAuthStatus::Pending =>
{
auth_code.inner.status = OAuthStatus::Authorized;
auth_code.inner.account_id = access_token.primary_id();
let device_code = std::mem::take(&mut auth_code.inner.params);
success = true;
// Delete issued user code
if let Err(err) = self
.core
.storage
.lookup
.key_delete(format!("oauth:{code}").into_bytes())
.await
{
return err.into_http_response();
}
// Update device code status
if let Err(err) = self
.core
.storage
.lookup
.key_set(
format!("oauth:{device_code}").into_bytes(),
auth_code.serialize(),
self.core.jmap.oauth_expiry_auth_code.into(),
)
.await
{
return err.into_http_response();
}
}
Err(err) => return err.into_http_response(),
_ => (),
}
json!({
"data": success,
})
}
};
JsonResponse::new(response).into_http_response()
json!({
"data": success,
})
}
Err(err) => err.into_http_response(),
}
};
Ok(JsonResponse::new(response).into_http_response())
}
pub async fn handle_device_auth(
&self,
req: &mut HttpRequest,
base_url: impl AsRef<str>,
) -> HttpResponse {
) -> trc::Result<HttpResponse> {
// Parse form
let client_id = match FormData::from_request(req, MAX_POST_LEN)
.await
.map(|mut p| p.remove("client_id"))
{
Ok(Some(client_id)) if client_id.len() < CLIENT_ID_MAX_LEN => client_id,
Err(err) => return err,
_ => {
return HtmlResponse::with_status(
StatusCode::BAD_REQUEST,
"Client ID is invalid.".to_string(),
)
.into_http_response();
}
};
let client_id = FormData::from_request(req, MAX_POST_LEN)
.await?
.remove("client_id")
.filter(|client_id| client_id.len() < CLIENT_ID_MAX_LEN)
.ok_or_else(|| {
trc::ResourceCause::BadParameters
.into_err()
.details("Client ID is missing.")
})?;
// Generate device code
let device_code = thread_rng()
@ -215,8 +189,7 @@ impl JMAP {
.serialize();
// Insert device code
if let Err(err) = self
.core
self.core
.storage
.lookup
.key_set(
@ -224,14 +197,10 @@ impl JMAP {
oauth_code.clone(),
self.core.jmap.oauth_expiry_user_code.into(),
)
.await
{
return err.into_http_response();
}
.await?;
// Insert user code
if let Err(err) = self
.core
self.core
.storage
.lookup
.key_set(
@ -239,14 +208,11 @@ impl JMAP {
oauth_code,
self.core.jmap.oauth_expiry_user_code.into(),
)
.await
{
return err.into_http_response();
}
.await?;
// Build response
let base_url = base_url.as_ref();
JsonResponse::new(DeviceAuthResponse {
Ok(JsonResponse::new(DeviceAuthResponse {
verification_uri: format!("{}/authorize", base_url),
verification_uri_complete: format!("{}/authorize/?code={}", base_url, user_code),
device_code,
@ -254,6 +220,6 @@ impl JMAP {
expires_in: self.core.jmap.oauth_expiry_user_code,
interval: 5,
})
.into_http_response()
.into_http_response())
}
}

View file

@ -6,13 +6,10 @@
use std::collections::HashMap;
use hyper::{header::CONTENT_TYPE, StatusCode};
use hyper::header::CONTENT_TYPE;
use serde::{Deserialize, Serialize};
use crate::api::{
http::{fetch_body, ToHttpResponse},
HtmlResponse, HttpRequest, HttpResponse,
};
use crate::api::{http::fetch_body, HttpRequest};
pub mod auth;
pub mod token;
@ -205,7 +202,7 @@ pub struct FormData {
}
impl FormData {
pub async fn from_request(req: &mut HttpRequest, max_len: usize) -> Result<Self, HttpResponse> {
pub async fn from_request(req: &mut HttpRequest, max_len: usize) -> trc::Result<Self> {
match (
req.headers()
.get(CONTENT_TYPE)
@ -229,11 +226,9 @@ impl FormData {
}
Ok(FormData { fields })
}
_ => Err(HtmlResponse::with_status(
StatusCode::BAD_REQUEST,
"Invalid post request".to_string(),
)
.into_http_response()),
_ => Err(trc::ResourceCause::BadParameters
.into_err()
.details("Invalid post request")),
}
}

View file

@ -30,12 +30,9 @@ use super::{
impl JMAP {
// Token endpoint
pub async fn handle_token_request(&self, req: &mut HttpRequest) -> HttpResponse {
pub async fn handle_token_request(&self, req: &mut HttpRequest) -> trc::Result<HttpResponse> {
// Parse form
let params = match FormData::from_request(req, MAX_POST_LEN).await {
Ok(params) => params,
Err(err) => return err,
};
let params = FormData::from_request(req, MAX_POST_LEN).await?;
let grant_type = params.get("grant_type").unwrap_or_default();
let mut response = TokenResponse::error(ErrorType::InvalidGrant);
@ -52,38 +49,35 @@ impl JMAP {
.storage
.lookup
.key_get::<Bincode<OAuthCode>>(format!("oauth:{code}").into_bytes())
.await
.await?
{
Ok(Some(auth_code)) => {
Some(auth_code) => {
let oauth = auth_code.inner;
if client_id != oauth.client_id || redirect_uri != oauth.params {
TokenResponse::error(ErrorType::InvalidClient)
} else if oauth.status == OAuthStatus::Authorized {
// Mark this token as issued
if let Err(err) = self
.core
self.core
.storage
.lookup
.key_delete(format!("oauth:{code}").into_bytes())
.await
{
return err.into_http_response();
}
.await?;
// Issue token
self.issue_token(oauth.account_id, &oauth.client_id, true)
.await
.map(TokenResponse::Granted)
.unwrap_or_else(|err| {
tracing::error!("Failed to generate OAuth token: {}", err);
TokenResponse::error(ErrorType::InvalidRequest)
})
.map_err(|err| {
trc::AuthCause::Error
.into_err()
.details(err)
.caused_by(trc::location!())
})?
} else {
TokenResponse::error(ErrorType::InvalidGrant)
}
}
Ok(None) => TokenResponse::error(ErrorType::AccessDenied),
Err(err) => return err.into_http_response(),
None => TokenResponse::error(ErrorType::AccessDenied),
}
} else {
TokenResponse::error(ErrorType::InvalidClient)
@ -100,9 +94,9 @@ impl JMAP {
.storage
.lookup
.key_get::<Bincode<OAuthCode>>(format!("oauth:{device_code}").into_bytes())
.await
.await?
{
Ok(Some(auth_code)) => {
Some(auth_code) => {
let oauth = auth_code.inner;
response = if oauth.client_id != client_id {
TokenResponse::error(ErrorType::InvalidClient)
@ -110,27 +104,22 @@ impl JMAP {
match oauth.status {
OAuthStatus::Authorized => {
// Mark this token as issued
if let Err(err) = self
.core
self.core
.storage
.lookup
.key_delete(format!("oauth:{device_code}").into_bytes())
.await
{
return err.into_http_response();
}
.await?;
// Issue token
self.issue_token(oauth.account_id, &oauth.client_id, true)
.await
.map(TokenResponse::Granted)
.unwrap_or_else(|err| {
tracing::error!(
"Failed to generate OAuth token: {}",
err
);
TokenResponse::error(ErrorType::InvalidRequest)
})
.map_err(|err| {
trc::AuthCause::Error
.into_err()
.details(err)
.caused_by(trc::location!())
})?
}
OAuthStatus::Pending => {
TokenResponse::error(ErrorType::AuthorizationPending)
@ -141,37 +130,36 @@ impl JMAP {
}
};
}
Ok(None) => (),
Err(err) => return err.into_http_response(),
None => (),
}
}
} else if grant_type.eq_ignore_ascii_case("refresh_token") {
let todo = "return error";
if let Some(refresh_token) = params.get("refresh_token") {
if let Ok((account_id, client_id, time_left)) = self
let (account_id, client_id, time_left) = self
.validate_access_token("refresh_token", refresh_token)
.await?;
// TODO: implement revoking client ids
response = self
.issue_token(
account_id,
&client_id,
time_left <= self.core.jmap.oauth_expiry_refresh_token_renew,
)
.await
{
// TODO: implement revoking client ids
response = self
.issue_token(
account_id,
&client_id,
time_left <= self.core.jmap.oauth_expiry_refresh_token_renew,
)
.await
.map(TokenResponse::Granted)
.unwrap_or_else(|err| {
tracing::debug!("Failed to refresh OAuth token: {}", err);
TokenResponse::error(ErrorType::InvalidGrant)
});
}
.map(TokenResponse::Granted)
.map_err(|err| {
trc::AuthCause::Error
.into_err()
.details(err)
.caused_by(trc::location!())
})?;
} else {
response = TokenResponse::error(ErrorType::InvalidRequest);
}
}
JsonResponse::with_status(
Ok(JsonResponse::with_status(
if response.is_error() {
StatusCode::BAD_REQUEST
} else {
@ -179,7 +167,7 @@ impl JMAP {
},
response,
)
.into_http_response()
.into_http_response())
}
async fn password_hash(&self, account_id: u32) -> Result<String, &'static str> {
@ -293,7 +281,8 @@ impl JMAP {
) -> trc::Result<(u32, String, u64)> {
// Base64 decode token
let token = base64_decode(token_.as_bytes()).ok_or_else(|| {
trc::Cause::Authentication
trc::AuthCause::Error
.into_err()
.ctx(trc::Key::Reason, "Failed to decode token")
.details(token_.to_string())
})?;
@ -309,7 +298,8 @@ impl JMAP {
.into()
})
.ok_or_else(|| {
trc::Cause::Authentication
trc::AuthCause::Error
.into_err()
.ctx(trc::Key::Reason, "Failed to decode token")
.details(token_.to_string())
})?;
@ -321,14 +311,16 @@ impl JMAP {
.unwrap_or(0)
.saturating_sub(946684800); // Jan 1, 2000
if expiry <= now {
return Err(trc::Cause::Authentication.ctx(trc::Key::Reason, "Token expired"));
return Err(trc::AuthCause::Error
.into_err()
.ctx(trc::Key::Reason, "Token expired"));
}
// Obtain password hash
let password_hash = self
.password_hash(account_id)
.await
.map_err(|err| trc::Cause::Authentication.ctx(trc::Key::Details, err))?;
.map_err(|err| trc::AuthCause::Error.into_err().ctx(trc::Key::Details, err))?;
// Build context
let key = self.core.jmap.oauth_key.clone();
@ -357,7 +349,8 @@ impl JMAP {
&nonce,
)
.map_err(|err| {
trc::Cause::Authentication
trc::AuthCause::Error
.into_err()
.ctx(trc::Key::Details, "Failed to decode token")
.reason(err)
})?;

View file

@ -64,12 +64,12 @@ impl JMAP {
} else if access_token.is_super_user() {
Ok(InFlight::default())
} else {
Err(trc::Cause::TooManyConcurrentRequests.into())
Err(trc::LimitCause::ConcurrentRequest.into_err())
}
} else if access_token.is_super_user() {
Ok(InFlight::default())
} else {
Err(trc::Cause::TooManyRequests.into())
Err(trc::LimitCause::TooManyRequests.into_err())
}
}
@ -84,7 +84,7 @@ impl JMAP {
.caused_by(trc::location!())?
.is_some()
{
return Err(trc::Cause::TooManyRequests.into());
return Err(trc::LimitCause::TooManyRequests.into_err());
}
}
Ok(())
@ -100,7 +100,7 @@ impl JMAP {
} else if access_token.is_super_user() {
Ok(InFlight::default())
} else {
Err(trc::Cause::TooManyConcurrentUploads.into())
Err(trc::LimitCause::ConcurrentUpload.into_err())
}
}
@ -117,7 +117,7 @@ impl JMAP {
.caused_by(trc::location!())?
.is_some()
{
return Err(trc::Cause::TooManyAuthAttempts.into());
return Err(trc::AuthCause::TooManyAttempts.into_err());
}
}
Ok(())
@ -134,7 +134,7 @@ impl JMAP {
.caused_by(trc::location!())?
.is_some()
{
return Err(trc::Cause::TooManyAuthAttempts.into());
return Err(trc::AuthCause::TooManyAttempts.into_err());
}
}
Ok(())

View file

@ -211,7 +211,8 @@ impl JMAP {
&& used.count + 1 > self.core.jmap.upload_tmp_quota_amount))
&& !access_token.is_super_user()
{
let err = Err(trc::Cause::OverBlobQuota
let err = Err(trc::LimitCause::BlobQuota
.into_err()
.ctx(trc::Key::Size, self.core.jmap.upload_tmp_quota_size)
.ctx(trc::Key::Total, self.core.jmap.upload_tmp_quota_amount));

View file

@ -28,7 +28,8 @@ impl JMAP {
pub fn generate_snowflake_id(&self) -> trc::Result<u64> {
self.inner.snowflake_id.generate().ok_or_else(|| {
trc::Cause::Unexpected
trc::StoreCause::Unexpected
.into_err()
.caused_by(trc::location!())
.ctx(trc::Key::Reason, "Failed to generate snowflake id.")
})
@ -57,7 +58,7 @@ impl JMAP {
pub async fn delete_changes(&self, account_id: u32, before: Duration) -> trc::Result<()> {
let reference_cid = self.inner.snowflake_id.past_id(before).ok_or_else(|| {
trc::Cause::Unexpected
trc::StoreCause::Unexpected
.caused_by(trc::location!())
.ctx(trc::Key::Reason, "Failed to generate reference change id.")
})?;

View file

@ -7,15 +7,13 @@
use std::{borrow::Cow, collections::BTreeSet, fmt::Display, io::Cursor, sync::Arc};
use crate::{
api::{http::ToHttpResponse, management::ManagementApiError, HttpResponse, JsonResponse},
api::{http::ToHttpResponse, HttpResponse, JsonResponse},
auth::AccessToken,
JMAP,
};
use aes::cipher::{block_padding::Pkcs7, BlockEncryptMut, KeyIvInit};
use jmap_proto::{
error::request::RequestError,
types::{collection::Collection, property::Property},
};
use directory::backend::internal::manage;
use jmap_proto::types::{collection::Collection, property::Property};
use mail_builder::{encoders::base64::base64_encode_mime, mime::make_boundary};
use mail_parser::{decoders::base64::base64_decode, Message, MessageParser, MimeHeaders};
use openpgp::{
@ -608,12 +606,16 @@ impl Deserialize for EncryptionParams {
fn deserialize(bytes: &[u8]) -> trc::Result<Self> {
let version = *bytes
.first()
.ok_or_else(|| trc::Cause::DataCorruption.caused_by(trc::location!()))?;
.ok_or_else(|| trc::StoreCause::DataCorruption.caused_by(trc::location!()))?;
match version {
1 if bytes.len() > 1 => bincode::deserialize(&bytes[1..])
.map_err(|err| trc::Error::from(err).caused_by(trc::location!())),
1 if bytes.len() > 1 => bincode::deserialize(&bytes[1..]).map_err(|err| {
trc::Cause::Store(trc::StoreCause::Deserialize)
.from_bincode_error(err)
.caused_by(trc::location!())
}),
_ => Err(trc::Cause::Deserialize
_ => Err(trc::StoreCause::Deserialize
.into_err()
.caused_by(trc::location!())
.ctx(trc::Key::Value, version as u64)),
}
@ -627,66 +629,48 @@ impl ToBitmaps for &EncryptionParams {
}
impl JMAP {
pub async fn handle_crypto_get(&self, access_token: Arc<AccessToken>) -> HttpResponse {
match self
pub async fn handle_crypto_get(
&self,
access_token: Arc<AccessToken>,
) -> trc::Result<HttpResponse> {
let params = self
.get_property::<EncryptionParams>(
access_token.primary_id(),
Collection::Principal,
0,
Property::Parameters,
)
.await
{
Ok(params) => {
let ec = params
.map(|params| {
let method = params.method;
let algo = params.algo;
let mut certs = Vec::new();
certs.extend_from_slice(b"-----STALWART CERTIFICATE-----\r\n");
let _ = base64_encode_mime(
&Bincode::new(params).serialize(),
&mut certs,
false,
);
certs.extend_from_slice(b"\r\n");
let certs = String::from_utf8(certs).unwrap_or_default();
.await?;
let ec = params
.map(|params| {
let method = params.method;
let algo = params.algo;
let mut certs = Vec::new();
certs.extend_from_slice(b"-----STALWART CERTIFICATE-----\r\n");
let _ = base64_encode_mime(&Bincode::new(params).serialize(), &mut certs, false);
certs.extend_from_slice(b"\r\n");
let certs = String::from_utf8(certs).unwrap_or_default();
match method {
EncryptionMethod::PGP => EncryptionType::PGP { algo, certs },
EncryptionMethod::SMIME => EncryptionType::SMIME { algo, certs },
}
})
.unwrap_or(EncryptionType::Disabled);
match method {
EncryptionMethod::PGP => EncryptionType::PGP { algo, certs },
EncryptionMethod::SMIME => EncryptionType::SMIME { algo, certs },
}
})
.unwrap_or(EncryptionType::Disabled);
JsonResponse::new(json!({
"data": ec,
}))
.into_http_response()
}
Err(err) => {
tracing::warn!(
context = "store",
event = "error",
reason = ?err,
"Database error while fetching encryption parameters"
);
RequestError::internal_server_error().into_http_response()
}
}
Ok(JsonResponse::new(json!({
"data": ec,
}))
.into_http_response())
}
pub async fn handle_crypto_post(
&self,
access_token: Arc<AccessToken>,
body: Option<Vec<u8>>,
) -> HttpResponse {
let request =
match serde_json::from_slice::<EncryptionType>(body.as_deref().unwrap_or_default()) {
Ok(request) => request,
Err(err) => return err.into_http_response(),
};
) -> trc::Result<HttpResponse> {
let request = serde_json::from_slice::<EncryptionType>(body.as_deref().unwrap_or_default())
.map_err(|err| trc::ResourceCause::BadParameters.into_err().reason(err))?;
let (method, algo, certs) = match request {
EncryptionType::PGP { algo, certs } => (EncryptionMethod::PGP, algo, certs),
@ -699,32 +683,27 @@ impl JMAP {
.with_collection(Collection::Principal)
.update_document(0)
.value(Property::Parameters, (), F_VALUE | F_CLEAR);
return match self.core.storage.data.write(batch.build()).await {
Ok(_) => JsonResponse::new(json!({
"data": (),
}))
.into_http_response(),
Err(err) => err.into_http_response(),
};
self.core.storage.data.write(batch.build()).await?;
return Ok(JsonResponse::new(json!({
"data": (),
}))
.into_http_response());
}
};
// Make sure Encryption is enabled
if !self.core.jmap.encrypt {
return ManagementApiError::Unsupported {
details: "Encryption-at-rest has been disabled by the system administrator".into(),
}
.into_http_response();
return Err(manage::unsupported(
"Encryption-at-rest has been disabled by the system administrator",
));
}
// Parse certificates
let params = match try_parse_certs(method, certs.into_bytes()) {
Ok(certs) => EncryptionParams {
method,
algo,
certs,
},
Err(err) => return ManagementApiError::from(err).into_http_response(),
let params = EncryptionParams {
method,
algo,
certs: try_parse_certs(method, certs.into_bytes())
.map_err(|err| manage::error(err, None::<u32>))?,
};
// Try a test encryption
@ -734,7 +713,7 @@ impl JMAP {
.encrypt(&params)
.await
{
return ManagementApiError::from(message).into_http_response();
return Err(manage::error(message, None::<u32>));
}
// Save encryption params
@ -745,13 +724,12 @@ impl JMAP {
.with_collection(Collection::Principal)
.update_document(0)
.value(Property::Parameters, &params, F_VALUE);
match self.core.storage.data.write(batch.build()).await {
Ok(_) => JsonResponse::new(json!({
"data": num_certs,
}))
.into_http_response(),
Err(err) => err.into_http_response(),
}
self.core.storage.data.write(batch.build()).await?;
Ok(JsonResponse::new(json!({
"data": num_certs,
}))
.into_http_response())
}
}

View file

@ -337,7 +337,8 @@ impl JMAP {
return Ok(());
}
let reference_cid = self.inner.snowflake_id.past_id(period).ok_or_else(|| {
trc::Cause::Unexpected
trc::StoreCause::Unexpected
.into_err()
.caused_by(trc::location!())
.ctx(trc::Key::Reason, "Failed to generate reference cid.")
})?;

View file

@ -129,7 +129,7 @@ impl JMAP {
response.created.append(id, email.into());
}
Err(mut err) => match err.as_ref() {
trc::Cause::OverQuota => {
trc::Cause::Limit(trc::LimitCause::Quota) => {
response.not_created.append(
id,
SetError::new(SetErrorType::OverQuota)

View file

@ -84,7 +84,7 @@ impl JMAP {
.await
.caused_by(trc::location!())?
{
return Err(trc::Cause::OverQuota.into_err());
return Err(trc::LimitCause::Quota.into_err());
}
// Parse message
@ -224,7 +224,10 @@ impl JMAP {
}
}
Err(EncryptMessageError::Error(err)) => {
trc::bail!(trc::Cause::Crypto.caused_by(trc::location!()).reason(err));
trc::bail!(trc::StoreCause::Crypto
.into_err()
.caused_by(trc::location!())
.reason(err));
}
_ => unreachable!(),
}
@ -494,7 +497,7 @@ impl JMAP {
match self.core.storage.data.write(batch.build()).await {
Ok(_) => return Ok(Some(thread_id)),
Err(err) if err.matches(trc::Cause::AssertValue) && try_count < MAX_RETRIES => {
Err(err) if err.is_assertion_failure() && try_count < MAX_RETRIES => {
let backoff = rand::thread_rng().gen_range(50..=300);
tokio::time::sleep(Duration::from_millis(backoff)).await;
try_count += 1;

View file

@ -726,7 +726,7 @@ impl JMAP {
Ok(message) => {
response.created.insert(id, message.into());
}
Err(err) if err.matches(trc::Cause::OverQuota) => {
Err(err) if err.matches(trc::Cause::Limit(trc::LimitCause::Quota)) => {
response.not_created.append(
id,
SetError::new(SetErrorType::OverQuota)
@ -962,7 +962,7 @@ impl JMAP {
// Add to updated list
response.updated.append(id, None);
}
Err(err) if err.matches(trc::Cause::AssertValue) => {
Err(err) if err.is_assertion_failure() => {
response.not_updated.append(
id,
SetError::forbidden().with_description(

View file

@ -128,7 +128,7 @@ impl JMAP {
ctx.mailbox_ids.insert(document_id);
ctx.response.created(id, document_id);
}
Err(err) if err.matches(trc::Cause::AssertValue) => {
Err(err) if err.is_assertion_failure() => {
ctx.response.not_created.append(
id,
SetError::forbidden().with_description(
@ -221,7 +221,7 @@ impl JMAP {
Ok(_) => {
changes.log_update(Collection::Mailbox, document_id);
}
Err(err) if err.matches(trc::Cause::AssertValue) => {
Err(err) if err.is_assertion_failure() => {
ctx.response.not_updated.append(id, SetError::forbidden().with_description(
"Another process modified this mailbox, please try again.",
));
@ -391,7 +391,7 @@ impl JMAP {
Collection::Email,
Id::from_parts(thread_id, message_id),
),
Err(err) if err.matches(trc::Cause::AssertValue) => {
Err(err) if err.is_assertion_failure() => {
return Ok(Err(SetError::forbidden().with_description(
concat!(
"Another process modified a message in this mailbox ",
@ -469,7 +469,7 @@ impl JMAP {
changes.log_delete(Collection::Mailbox, document_id);
Ok(Ok(did_remove_emails))
}
Err(err) if err.matches(trc::Cause::AssertValue) => Ok(Err(SetError::forbidden()
Err(err) if err.is_assertion_failure() => Ok(Err(SetError::forbidden()
.with_description(concat!(
"Another process modified this mailbox ",
"while deleting it, please try again."

View file

@ -128,7 +128,8 @@ impl JMAP {
})
.await?
.ok_or_else(|| {
trc::Cause::NotFound
trc::StoreCause::NotFound
.into_err()
.caused_by(trc::location!())
.document_id(document_id)
})?;
@ -138,7 +139,7 @@ impl JMAP {
.get(&Property::Expires)
.and_then(|p| p.as_date())
.ok_or_else(|| {
trc::Cause::Unexpected
trc::StoreCause::Unexpected
.caused_by(trc::location!())
.document_id(document_id)
})?
@ -174,7 +175,7 @@ impl JMAP {
.remove(&Property::Value)
.and_then(|p| p.try_unwrap_string())
.ok_or_else(|| {
trc::Cause::Unexpected
trc::StoreCause::Unexpected
.caused_by(trc::location!())
.document_id(document_id)
})?;
@ -183,7 +184,7 @@ impl JMAP {
.remove(&Property::Url)
.and_then(|p| p.try_unwrap_string())
.ok_or_else(|| {
trc::Cause::Unexpected
trc::StoreCause::Unexpected
.caused_by(trc::location!())
.document_id(document_id)
})?;

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(err) if err.matches(trc::Cause::AssertValue) => {
Err(err) if err.is_assertion_failure() => {
tracing::trace!(
context = "queue",
event = "locked",

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