Internal directory implementation + Management REST API
This commit is contained in:
303
crates/jmap/src/api/admin.rs
Normal file
303
crates/jmap/src/api/admin.rs
Normal file
@@ -0,0 +1,303 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Stalwart Labs Ltd.
|
||||
*
|
||||
* This file is part of Stalwart Mail Server.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* in the LICENSE file at the top-level directory of this distribution.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* You can be released from the requirements of the AGPLv3 license by
|
||||
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use directory::{
|
||||
backend::internal::{manage::ManageDirectory, PrincipalUpdate},
|
||||
Directory, DirectoryError, ManagementError, Principal, QueryBy, Type,
|
||||
};
|
||||
use http_body_util::combinators::BoxBody;
|
||||
use hyper::{body::Bytes, Method, StatusCode};
|
||||
use jmap_proto::error::request::RequestError;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::JMAP;
|
||||
|
||||
use super::{http::ToHttpResponse, HttpRequest, JsonResponse};
|
||||
|
||||
#[derive(Debug, serde::Serialize, serde::Deserialize)]
|
||||
pub struct PrincipalResponse {
|
||||
pub id: u32,
|
||||
#[serde(rename = "type")]
|
||||
pub typ: Type,
|
||||
pub quota: u32,
|
||||
#[serde(rename = "usedQuota")]
|
||||
pub used_quota: u32,
|
||||
pub name: String,
|
||||
pub emails: Vec<String>,
|
||||
#[serde(rename = "memberOf")]
|
||||
pub member_of: Vec<String>,
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
impl JMAP {
|
||||
pub async fn handle_manage_request(
|
||||
&self,
|
||||
req: &HttpRequest,
|
||||
body: Option<Vec<u8>>,
|
||||
) -> hyper::Response<BoxBody<Bytes, hyper::Error>> {
|
||||
let mut path = req.uri().path().split('/');
|
||||
path.next();
|
||||
path.next();
|
||||
|
||||
match (path.next().unwrap_or(""), path.next(), req.method()) {
|
||||
("principal", None, &Method::POST) => {
|
||||
// Create principal
|
||||
if let Some(principal) =
|
||||
body.and_then(|body| serde_json::from_slice::<Principal<String>>(&body).ok())
|
||||
{
|
||||
match self.store.create_account(principal).await {
|
||||
Ok(account_id) => JsonResponse::new(json!({
|
||||
"accountId": account_id,
|
||||
"status": "success",
|
||||
}))
|
||||
.into_http_response(),
|
||||
Err(err) => map_directory_error(err),
|
||||
}
|
||||
} else {
|
||||
RequestError::blank(
|
||||
StatusCode::BAD_REQUEST.as_u16(),
|
||||
"Invalid parameters",
|
||||
"Failed to deserialize principal object",
|
||||
)
|
||||
.into_http_response()
|
||||
}
|
||||
}
|
||||
("principal", None, &Method::GET) => {
|
||||
// List principal ids
|
||||
let mut from_key = None;
|
||||
let mut limit: usize = 0;
|
||||
|
||||
if let Some(query) = req.uri().query() {
|
||||
for (key, value) in form_urlencoded::parse(query.as_bytes()) {
|
||||
match key.as_ref() {
|
||||
"limit" => {
|
||||
limit = value.parse().unwrap_or_default();
|
||||
}
|
||||
"from" => {
|
||||
from_key = value.into();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match self.store.list_accounts(from_key.as_deref(), limit).await {
|
||||
Ok(accounts) => JsonResponse::new(json!({
|
||||
"status": "success",
|
||||
"data": accounts,
|
||||
}))
|
||||
.into_http_response(),
|
||||
Err(err) => map_directory_error(err),
|
||||
}
|
||||
}
|
||||
("principal", Some(name), method) => {
|
||||
// Fetch, update or delete principal
|
||||
let account_id = match self.store.get_account_id(name).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 map_directory_error(err);
|
||||
}
|
||||
};
|
||||
|
||||
match *method {
|
||||
Method::GET => {
|
||||
let result = match self.store.query(QueryBy::Id(account_id)).await {
|
||||
Ok(Some(principal)) => self.store.map_group_ids(principal).await,
|
||||
Ok(None) => {
|
||||
return RequestError::blank(
|
||||
StatusCode::NOT_FOUND.as_u16(),
|
||||
"Not found",
|
||||
"Account not found.",
|
||||
)
|
||||
.into_http_response()
|
||||
}
|
||||
Err(err) => Err(err),
|
||||
};
|
||||
|
||||
match result {
|
||||
Ok(principal) => {
|
||||
// Obtain quota usage
|
||||
let mut principal = PrincipalResponse::from(principal);
|
||||
principal.used_quota =
|
||||
self.get_used_quota(account_id).await.unwrap_or_default()
|
||||
as u32;
|
||||
|
||||
JsonResponse::new(json!({
|
||||
"status": "success",
|
||||
"data": principal,
|
||||
}))
|
||||
.into_http_response()
|
||||
}
|
||||
Err(err) => map_directory_error(err),
|
||||
}
|
||||
}
|
||||
Method::DELETE => {
|
||||
// Remove FTS index
|
||||
if let Err(err) = self.fts_store.remove_all(account_id).await {
|
||||
tracing::warn!(
|
||||
context = "fts",
|
||||
event = "error",
|
||||
reason = ?err,
|
||||
"Failed to remove FTS index"
|
||||
);
|
||||
return RequestError::blank(
|
||||
StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
|
||||
"Failed to remove FTS index",
|
||||
"Contact the administrator if this problem persists",
|
||||
)
|
||||
.into_http_response();
|
||||
}
|
||||
|
||||
// Delete account
|
||||
match self.store.delete_account(QueryBy::Id(account_id)).await {
|
||||
Ok(_) => JsonResponse::new(json!({
|
||||
"status": "success",
|
||||
}))
|
||||
.into_http_response(),
|
||||
Err(err) => map_directory_error(err),
|
||||
}
|
||||
}
|
||||
Method::PUT => {
|
||||
if let Some(changes) = body.and_then(|body| {
|
||||
serde_json::from_slice::<Vec<PrincipalUpdate>>(&body).ok()
|
||||
}) {
|
||||
match self
|
||||
.store
|
||||
.update_account(QueryBy::Id(account_id), changes)
|
||||
.await
|
||||
{
|
||||
Ok(account_id) => JsonResponse::new(json!({
|
||||
"accountId": account_id,
|
||||
"status": "success",
|
||||
}))
|
||||
.into_http_response(),
|
||||
Err(err) => map_directory_error(err),
|
||||
}
|
||||
} else {
|
||||
RequestError::blank(
|
||||
StatusCode::BAD_REQUEST.as_u16(),
|
||||
"Invalid parameters",
|
||||
"Failed to deserialize modify request",
|
||||
)
|
||||
.into_http_response()
|
||||
}
|
||||
}
|
||||
_ => RequestError::not_found().into_http_response(),
|
||||
}
|
||||
}
|
||||
("store", Some("purge"), &Method::GET) => {
|
||||
match self.store.purge_blobs(self.blob_store.clone()).await {
|
||||
Ok(_) => match self.store.purge_bitmaps().await {
|
||||
Ok(_) => JsonResponse::new(json!({
|
||||
"status": "success",
|
||||
}))
|
||||
.into_http_response(),
|
||||
Err(err) => RequestError::blank(
|
||||
StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
|
||||
"Purge database failed",
|
||||
err.to_string(),
|
||||
)
|
||||
.into_http_response(),
|
||||
},
|
||||
Err(err) => RequestError::blank(
|
||||
StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
|
||||
"Purge blob failed",
|
||||
err.to_string(),
|
||||
)
|
||||
.into_http_response(),
|
||||
}
|
||||
}
|
||||
(path_1 @ ("queue" | "report"), Some(path_2), &Method::GET) => {
|
||||
self.smtp
|
||||
.handle_manage_request(req.uri(), req.method(), path_1, path_2)
|
||||
.await
|
||||
}
|
||||
_ => RequestError::not_found().into_http_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn map_directory_error(err: DirectoryError) -> hyper::Response<BoxBody<Bytes, hyper::Error>> {
|
||||
match err {
|
||||
DirectoryError::Management(err) => {
|
||||
let response = match err {
|
||||
ManagementError::MissingField(details) => json!({
|
||||
"status": "missingField",
|
||||
"details": details,
|
||||
}),
|
||||
ManagementError::NotUniqueField(details) => json!({
|
||||
"status": "notUniqueField",
|
||||
"details": details,
|
||||
}),
|
||||
ManagementError::NotFound(details) => json!({
|
||||
"status": "notFound",
|
||||
"details": details,
|
||||
}),
|
||||
};
|
||||
JsonResponse::new(response).into_http_response()
|
||||
}
|
||||
DirectoryError::Unsupported => JsonResponse::new(json!({
|
||||
"status": "unsupported",
|
||||
"details": "Requested action is unsupported",
|
||||
}))
|
||||
.into_http_response(),
|
||||
err => {
|
||||
tracing::warn!(
|
||||
context = "directory",
|
||||
event = "error",
|
||||
reason = ?err,
|
||||
"Directory error"
|
||||
);
|
||||
|
||||
RequestError::blank(
|
||||
StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
|
||||
"Database error",
|
||||
"Contact the administrator if this problem persists",
|
||||
)
|
||||
.into_http_response()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Principal<String>> for PrincipalResponse {
|
||||
fn from(principal: Principal<String>) -> Self {
|
||||
PrincipalResponse {
|
||||
id: principal.id,
|
||||
typ: principal.typ,
|
||||
quota: principal.quota,
|
||||
name: principal.name,
|
||||
emails: principal.emails,
|
||||
member_of: principal.member_of,
|
||||
description: principal.description,
|
||||
used_quota: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -38,7 +38,6 @@ use jmap_proto::{
|
||||
response::Response,
|
||||
types::{blob::BlobId, id::Id},
|
||||
};
|
||||
use serde_json::Value;
|
||||
use tokio::{
|
||||
io::{AsyncRead, AsyncWrite},
|
||||
net::TcpStream,
|
||||
@@ -274,138 +273,17 @@ pub async fn parse_jmap_request(
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
"admin" => {
|
||||
// Make sure the user is a superuser
|
||||
match jmap.authenticate_headers(&req, remote_ip).await {
|
||||
Ok(Some((_, access_token))) if access_token.is_super_user() => (),
|
||||
let body = match jmap.authenticate_headers(&req, remote_ip).await {
|
||||
Ok(Some((_, access_token))) if access_token.is_super_user() => {
|
||||
fetch_body(&mut req, 8192, &access_token).await
|
||||
}
|
||||
Ok(_) => return RequestError::unauthorized().into_http_response(),
|
||||
Err(err) => return err.into_http_response(),
|
||||
}
|
||||
};
|
||||
|
||||
match (
|
||||
path.next().unwrap_or(""),
|
||||
path.next().unwrap_or(""),
|
||||
req.method(),
|
||||
) {
|
||||
("account", "delete", &Method::GET) => {
|
||||
let todo = true;
|
||||
/*
|
||||
|
||||
// Remove FTS index
|
||||
self.fts_store.remove_all(principal.id).await?;
|
||||
*/
|
||||
todo!()
|
||||
/*return if let Some(account_name) = path.next() {
|
||||
if let Ok(Some(account_id)) = jmap.try_get_account_id(account_name).await {
|
||||
match jmap.delete_account(account_name, account_id).await {
|
||||
Ok(_) => JsonResponse::new(Value::String("success".into()))
|
||||
.into_http_response(),
|
||||
Err(err) => RequestError::blank(
|
||||
StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
|
||||
"Account deletion failed",
|
||||
err.to_string(),
|
||||
)
|
||||
.into_http_response(),
|
||||
}
|
||||
} else {
|
||||
RequestError::blank(
|
||||
StatusCode::NOT_FOUND.as_u16(),
|
||||
"Not found",
|
||||
"Account not found.",
|
||||
)
|
||||
.into_http_response()
|
||||
}
|
||||
} else {
|
||||
RequestError::blank(
|
||||
StatusCode::BAD_REQUEST.as_u16(),
|
||||
"Invalid parameters",
|
||||
"Expected account name",
|
||||
)
|
||||
.into_http_response()
|
||||
};*/
|
||||
}
|
||||
("account", "rename", &Method::GET) => {
|
||||
todo!()
|
||||
/*return if let (Some(account_name), Some(new_account_name)) =
|
||||
(path.next(), path.next())
|
||||
{
|
||||
match (
|
||||
jmap.try_get_account_id(account_name).await,
|
||||
jmap.try_get_account_id(new_account_name).await,
|
||||
) {
|
||||
(Ok(Some(account_id)), Ok(None)) => {
|
||||
match jmap
|
||||
.rename_account(new_account_name, account_name, account_id)
|
||||
.await
|
||||
{
|
||||
Ok(_) => JsonResponse::new(Value::String("success".into()))
|
||||
.into_http_response(),
|
||||
Err(err) => RequestError::blank(
|
||||
StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
|
||||
"Account rename failed",
|
||||
err.to_string(),
|
||||
)
|
||||
.into_http_response(),
|
||||
}
|
||||
}
|
||||
(Ok(None), _) => RequestError::blank(
|
||||
StatusCode::NOT_FOUND.as_u16(),
|
||||
"Not found",
|
||||
"Account not found.",
|
||||
)
|
||||
.into_http_response(),
|
||||
(_, Ok(Some(_))) => RequestError::blank(
|
||||
StatusCode::BAD_REQUEST.as_u16(),
|
||||
"Invalid parameters",
|
||||
"New account name already exists.",
|
||||
)
|
||||
.into_http_response(),
|
||||
_ => RequestError::internal_server_error().into_http_response(),
|
||||
}
|
||||
} else {
|
||||
RequestError::blank(
|
||||
StatusCode::BAD_REQUEST.as_u16(),
|
||||
"Invalid parameters",
|
||||
"Expected old and new account names",
|
||||
)
|
||||
.into_http_response()
|
||||
};*/
|
||||
}
|
||||
("blob", "purge", &Method::GET) => {
|
||||
return match jmap.store.purge_blobs(jmap.blob_store.clone()).await {
|
||||
Ok(_) => {
|
||||
JsonResponse::new(Value::String("success".into())).into_http_response()
|
||||
}
|
||||
Err(err) => RequestError::blank(
|
||||
StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
|
||||
"Purge blob failed",
|
||||
err.to_string(),
|
||||
)
|
||||
.into_http_response(),
|
||||
};
|
||||
}
|
||||
("db", "purge", &Method::GET) => {
|
||||
return match jmap.store.purge_bitmaps().await {
|
||||
Ok(_) => {
|
||||
JsonResponse::new(Value::String("success".into())).into_http_response()
|
||||
}
|
||||
Err(err) => RequestError::blank(
|
||||
StatusCode::INTERNAL_SERVER_ERROR.as_u16(),
|
||||
"Purge database failed",
|
||||
err.to_string(),
|
||||
)
|
||||
.into_http_response(),
|
||||
};
|
||||
}
|
||||
(path_1 @ ("queue" | "report"), path_2, &Method::GET) => {
|
||||
return jmap
|
||||
.smtp
|
||||
.handle_manage_request(req.uri(), req.method(), path_1, path_2)
|
||||
.await;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
return jmap.handle_manage_request(&req, body).await;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ use utils::map::vec_map::VecMap;
|
||||
|
||||
use crate::JMAP;
|
||||
|
||||
pub mod admin;
|
||||
pub mod config;
|
||||
pub mod event_source;
|
||||
pub mod http;
|
||||
|
||||
@@ -214,7 +214,7 @@ impl JMAP {
|
||||
session.add_account(
|
||||
(*id).into(),
|
||||
self.directory
|
||||
.query(QueryBy::id(*id).with_store(&self.store))
|
||||
.query(QueryBy::Id(*id))
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.map(|p| p.name)
|
||||
|
||||
@@ -379,7 +379,7 @@ impl JMAP {
|
||||
{
|
||||
if let Some(principal) = self
|
||||
.directory
|
||||
.query(QueryBy::id(id.document_id()).with_store(&self.store))
|
||||
.query(QueryBy::Id(id.document_id()))
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
{
|
||||
@@ -452,11 +452,7 @@ impl JMAP {
|
||||
async fn map_acl_accounts(&self, mut acl_set: Vec<Value>) -> Result<Vec<Value>, SetError> {
|
||||
for item in &mut acl_set {
|
||||
if let Value::Text(account_name) = item {
|
||||
match self
|
||||
.directory
|
||||
.query(QueryBy::name(account_name).with_store(&self.store))
|
||||
.await
|
||||
{
|
||||
match self.directory.query(QueryBy::Name(account_name)).await {
|
||||
Ok(Some(principal)) => {
|
||||
*item = Value::Id(principal.id.into());
|
||||
}
|
||||
|
||||
@@ -174,13 +174,10 @@ impl JMAP {
|
||||
) -> Option<AccessToken> {
|
||||
match self
|
||||
.directory
|
||||
.query(
|
||||
QueryBy::credentials(&Credentials::Plain {
|
||||
username: username.to_string(),
|
||||
secret: secret.to_string(),
|
||||
})
|
||||
.with_store(&self.store),
|
||||
)
|
||||
.query(QueryBy::Credentials(&Credentials::Plain {
|
||||
username: username.to_string(),
|
||||
secret: secret.to_string(),
|
||||
}))
|
||||
.await
|
||||
{
|
||||
Ok(Some(mut principal)) => {
|
||||
@@ -201,10 +198,7 @@ impl JMAP {
|
||||
pub async fn get_access_token(&self, account_id: u32) -> Option<AccessToken> {
|
||||
// Create access token
|
||||
self.update_access_token(AccessToken::new(
|
||||
self.directory
|
||||
.query(QueryBy::id(account_id).with_store(&self.store))
|
||||
.await
|
||||
.ok()??,
|
||||
self.directory.query(QueryBy::Id(account_id)).await.ok()??,
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ pub struct AccessToken {
|
||||
}
|
||||
|
||||
impl AccessToken {
|
||||
pub fn new(principal: Principal) -> Self {
|
||||
pub fn new(principal: Principal<u32>) -> Self {
|
||||
Self {
|
||||
primary_id: principal.id,
|
||||
member_of: principal.member_of,
|
||||
|
||||
@@ -182,7 +182,7 @@ impl JMAP {
|
||||
) -> Result<TokenResponse, &'static str> {
|
||||
let password_hash = self
|
||||
.directory
|
||||
.query(QueryBy::id(account_id).with_store(&self.store))
|
||||
.query(QueryBy::Id(account_id))
|
||||
.await
|
||||
.map_err(|_| "Temporary lookup error")?
|
||||
.ok_or("Account no longer exists")?
|
||||
@@ -301,7 +301,7 @@ impl JMAP {
|
||||
|
||||
let password_hash = self
|
||||
.directory
|
||||
.query(QueryBy::id(account_id).with_store(&self.store))
|
||||
.query(QueryBy::Id(account_id))
|
||||
.await
|
||||
.map_err(|_| "Temporary lookup error")?
|
||||
.ok_or("Account no longer exists")?
|
||||
|
||||
@@ -75,7 +75,7 @@ impl JMAP {
|
||||
if let Value::Text(email) = identity.get(&Property::Email) {
|
||||
if !self
|
||||
.directory
|
||||
.query(QueryBy::id(account_id).with_store(&self.store))
|
||||
.query(QueryBy::Id(account_id))
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_default()
|
||||
|
||||
@@ -599,7 +599,7 @@ impl JMAP {
|
||||
access_token.quota as i64
|
||||
} else {
|
||||
self.directory
|
||||
.query(QueryBy::id(account_id).with_store(&self.store))
|
||||
.query(QueryBy::Id(account_id))
|
||||
.await
|
||||
.map_err(|err| {
|
||||
tracing::error!(
|
||||
|
||||
@@ -70,7 +70,7 @@ impl JMAP {
|
||||
// Obtain the principal
|
||||
let principal = if let Some(principal) = self
|
||||
.directory
|
||||
.query(QueryBy::id(id.document_id()).with_store(&self.store))
|
||||
.query(QueryBy::Id(id.document_id()))
|
||||
.await
|
||||
.map_err(|_| MethodError::ServerPartialFail)?
|
||||
{
|
||||
|
||||
@@ -49,7 +49,7 @@ impl JMAP {
|
||||
Filter::Name(name) => {
|
||||
if let Some(principal) = self
|
||||
.directory
|
||||
.query(QueryBy::name(name.as_str()).with_store(&self.store))
|
||||
.query(QueryBy::Name(name.as_str()))
|
||||
.await
|
||||
.map_err(|_| MethodError::ServerPartialFail)?
|
||||
{
|
||||
@@ -68,7 +68,7 @@ impl JMAP {
|
||||
let mut ids = RoaringBitmap::new();
|
||||
for id in self
|
||||
.directory
|
||||
.email_to_ids(&email, &self.store)
|
||||
.email_to_ids(&email)
|
||||
.await
|
||||
.map_err(|_| MethodError::ServerPartialFail)?
|
||||
{
|
||||
|
||||
@@ -47,11 +47,7 @@ impl JMAP {
|
||||
let mut recipients = Vec::with_capacity(message.recipients.len());
|
||||
let mut deliver_names = AHashMap::with_capacity(message.recipients.len());
|
||||
for rcpt in &message.recipients {
|
||||
let uids = self
|
||||
.directory
|
||||
.email_to_ids(rcpt, &self.store)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
let uids = self.directory.email_to_ids(rcpt).await.unwrap_or_default();
|
||||
for uid in &uids {
|
||||
deliver_names.insert(*uid, (DeliveryResult::Success, rcpt));
|
||||
}
|
||||
@@ -73,11 +69,7 @@ impl JMAP {
|
||||
.await
|
||||
}
|
||||
Ok(None) => {
|
||||
let account_quota = match self
|
||||
.directory
|
||||
.query(QueryBy::id(*uid).with_store(&self.store))
|
||||
.await
|
||||
{
|
||||
let account_quota = match self.directory.query(QueryBy::Id(*uid)).await {
|
||||
Ok(Some(p)) => p.quota as i64,
|
||||
Ok(None) => 0,
|
||||
Err(_) => {
|
||||
|
||||
@@ -78,11 +78,7 @@ impl JMAP {
|
||||
let mut instance = self.sieve_runtime.filter_parsed(message);
|
||||
|
||||
// Set account name and obtain quota
|
||||
let (account_quota, mail_from) = match self
|
||||
.directory
|
||||
.query(QueryBy::id(account_id).with_store(&self.store))
|
||||
.await
|
||||
{
|
||||
let (account_quota, mail_from) = match self.directory.query(QueryBy::Id(account_id)).await {
|
||||
Ok(Some(p)) => {
|
||||
instance.set_user_full_name(p.description().unwrap_or_else(|| p.name()));
|
||||
(p.quota as i64, p.emails.into_iter().next())
|
||||
|
||||
Reference in New Issue
Block a user