This commit is contained in:
mdecimus
2023-11-01 12:08:24 +01:00
parent ee088d5184
commit df45384fcd
94 changed files with 3024 additions and 489 deletions

View File

@@ -25,7 +25,10 @@ use std::ops::Range;
use jmap_proto::{
error::method::MethodError,
types::{acl::Acl, blob::BlobId},
types::{
acl::Acl,
blob::{BlobId, BlobSection},
},
};
use mail_parser::{
decoders::{base64::base64_decode, quoted_printable::quoted_printable_decode},
@@ -79,23 +82,31 @@ impl JMAP {
}
if let Some(section) = &blob_id.section {
Ok(self
.get_blob(
&blob_id.kind,
(section.offset_start as u32)
..(section.offset_start.saturating_add(section.size) as u32),
)
.await?
.and_then(|bytes| match Encoding::from(section.encoding) {
Encoding::None => Some(bytes),
Encoding::Base64 => base64_decode(&bytes),
Encoding::QuotedPrintable => quoted_printable_decode(&bytes),
}))
self.get_blob_section(&blob_id.kind, section).await
} else {
self.get_blob(&blob_id.kind, 0..u32::MAX).await
}
}
pub async fn get_blob_section(
&self,
kind: &BlobKind,
section: &BlobSection,
) -> Result<Option<Vec<u8>>, MethodError> {
Ok(self
.get_blob(
kind,
(section.offset_start as u32)
..(section.offset_start.saturating_add(section.size) as u32),
)
.await?
.and_then(|bytes| match Encoding::from(section.encoding) {
Encoding::None => Some(bytes),
Encoding::Base64 => base64_decode(&bytes),
Encoding::QuotedPrintable => quoted_printable_decode(&bytes),
}))
}
pub async fn get_blob(
&self,
kind: &BlobKind,

288
crates/jmap/src/blob/get.rs Normal file
View File

@@ -0,0 +1,288 @@
/*
* 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 jmap_proto::{
error::method::MethodError,
method::{
get::{GetRequest, GetResponse},
lookup::{BlobInfo, BlobLookupRequest, BlobLookupResponse},
},
object::{blob::GetArguments, Object},
types::{
collection::Collection,
id::Id,
property::{DataProperty, DigestProperty, Property},
type_state::DataType,
value::Value,
MaybeUnparsable,
},
};
use mail_builder::encoders::base64::base64_encode;
use sha1::{Digest, Sha1};
use sha2::{Sha256, Sha512};
use store::BlobKind;
use utils::map::vec_map::VecMap;
use crate::{auth::AccessToken, JMAP};
impl JMAP {
pub async fn blob_get(
&self,
mut request: GetRequest<GetArguments>,
access_token: &AccessToken,
) -> Result<GetResponse, MethodError> {
let ids = request
.unwrap_blob_ids(self.config.get_max_objects)?
.unwrap_or_default();
let properties = request.unwrap_properties(&[
Property::Id,
Property::Data(DataProperty::Default),
Property::Size,
]);
let mut response = GetResponse {
account_id: request.account_id.into(),
state: None,
list: Vec::with_capacity(ids.len()),
not_found: vec![],
};
let range_from = request.arguments.offset.unwrap_or(0);
let range_to = request
.arguments
.length
.map(|length| range_from.saturating_add(length))
.unwrap_or(usize::MAX);
for blob_id in ids {
if let Some(bytes) = self.blob_download(&blob_id, access_token).await? {
let mut blob = Object::with_capacity(properties.len());
let bytes_range = if range_from == 0 && range_to == usize::MAX {
&bytes[..]
} else {
let range_to = if range_to != usize::MAX && range_to > bytes.len() {
blob.append(Property::IsTruncated, true);
bytes.len()
} else {
range_to
};
let bytes_range = bytes.get(range_from..range_to).unwrap_or_default();
bytes_range
};
for property in &properties {
let mut property = property.clone();
let value: Value = match &property {
Property::Id => Value::BlobId(blob_id.clone()),
Property::Size => bytes.len().into(),
Property::Digest(digest) => match digest {
DigestProperty::Sha => {
let mut hasher = Sha1::new();
hasher.update(bytes_range);
String::from_utf8(
base64_encode(&hasher.finalize()[..]).unwrap_or_default(),
)
.unwrap()
}
DigestProperty::Sha256 => {
let mut hasher = Sha256::new();
hasher.update(bytes_range);
String::from_utf8(
base64_encode(&hasher.finalize()[..]).unwrap_or_default(),
)
.unwrap()
}
DigestProperty::Sha512 => {
let mut hasher = Sha512::new();
hasher.update(bytes_range);
String::from_utf8(
base64_encode(&hasher.finalize()[..]).unwrap_or_default(),
)
.unwrap()
}
}
.into(),
Property::Data(data) => match data {
DataProperty::AsText => match std::str::from_utf8(bytes_range) {
Ok(text) => text.to_string().into(),
Err(_) => {
blob.append(Property::IsEncodingProblem, true);
Value::Null
}
},
DataProperty::AsBase64 => {
String::from_utf8(base64_encode(bytes_range).unwrap_or_default())
.unwrap()
.into()
}
DataProperty::Default => match std::str::from_utf8(bytes_range) {
Ok(text) => {
property = Property::Data(DataProperty::AsText);
text.to_string().into()
}
Err(_) => {
property = Property::Data(DataProperty::AsBase64);
blob.append(Property::IsEncodingProblem, true);
String::from_utf8(
base64_encode(bytes_range).unwrap_or_default(),
)
.unwrap()
.into()
}
},
},
_ => Value::Null,
};
blob.append(property, value);
}
// Add result to response
response.list.push(blob);
} else {
response.not_found.push(blob_id.into());
}
}
Ok(response)
}
pub async fn blob_lookup(
&self,
request: BlobLookupRequest,
) -> Result<BlobLookupResponse, MethodError> {
let mut include_email = false;
let mut include_mailbox = false;
let mut include_thread = false;
let type_names = request
.type_names
.into_iter()
.map(|tn| match tn {
MaybeUnparsable::Value(value) => {
match &value {
DataType::Email => {
include_email = true;
}
DataType::Mailbox => {
include_mailbox = true;
}
DataType::Thread => {
include_thread = true;
}
_ => (),
}
Ok(value)
}
MaybeUnparsable::ParseError(_) => Err(MethodError::UnknownDataType),
})
.collect::<Result<Vec<_>, _>>()?;
let req_account_id = request.account_id.document_id();
let mut response = BlobLookupResponse {
account_id: request.account_id,
list: Vec::with_capacity(request.ids.len()),
not_found: vec![],
};
for id in request.ids {
match id {
MaybeUnparsable::Value(id) => {
let mut matched_ids = VecMap::new();
match &id.kind {
BlobKind::Linked {
account_id,
collection,
document_id,
} if *account_id == req_account_id => {
if *account_id != req_account_id {
response.not_found.push(MaybeUnparsable::Value(id));
continue;
}
match DataType::try_from(Collection::from(*collection)) {
Ok(data_type) if type_names.contains(&data_type) => {
matched_ids.append(data_type, vec![Id::from(*document_id)]);
}
_ => (),
}
}
BlobKind::LinkedMaildir {
account_id,
document_id,
} if *account_id == req_account_id => {
if include_email || include_thread {
if let Some(thread_id) = self
.get_property::<u32>(
req_account_id,
Collection::Email,
*document_id,
Property::ThreadId,
)
.await?
{
if include_email {
matched_ids.append(
DataType::Email,
vec![Id::from_parts(thread_id, *document_id)],
);
}
if include_thread {
matched_ids
.append(DataType::Thread, vec![Id::from(thread_id)]);
}
}
}
if include_mailbox {
if let Some(mailboxes) = self
.get_property::<Vec<u32>>(
req_account_id,
Collection::Email,
*document_id,
Property::MailboxIds,
)
.await?
{
matched_ids.append(
DataType::Mailbox,
mailboxes.into_iter().map(Id::from).collect::<Vec<_>>(),
);
}
}
}
BlobKind::Temporary { account_id, .. } if *account_id == req_account_id => {
}
_ => {
response.not_found.push(MaybeUnparsable::Value(id));
continue;
}
}
response.list.push(BlobInfo { id, matched_ids });
}
_ => response.not_found.push(id),
}
}
Ok(response)
}
}

View File

@@ -25,6 +25,7 @@ use jmap_proto::types::{blob::BlobId, id::Id};
pub mod copy;
pub mod download;
pub mod get;
pub mod upload;
#[derive(Debug, serde::Serialize)]

View File

@@ -24,7 +24,11 @@
use std::sync::Arc;
use jmap_proto::{
error::{method::MethodError, request::RequestError},
error::{method::MethodError, request::RequestError, set::SetError},
method::upload::{
BlobUploadRequest, BlobUploadResponse, BlobUploadResponseObject, DataSourceObject,
},
request::reference::MaybeReference,
types::{blob::BlobId, id::Id},
};
use store::BlobKind;
@@ -38,6 +42,174 @@ pub static DISABLE_UPLOAD_QUOTA: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(true);
impl JMAP {
pub async fn blob_upload_many(
&self,
request: BlobUploadRequest,
access_token: &AccessToken,
) -> Result<BlobUploadResponse, MethodError> {
let mut response = BlobUploadResponse {
account_id: request.account_id,
created: Default::default(),
not_created: Default::default(),
};
let account_id = request.account_id.document_id();
if request.create.len() > self.config.set_max_objects {
return Err(MethodError::RequestTooLarge);
}
'outer: for (create_id, upload_object) in request.create {
let mut data = Vec::new();
for data_source in upload_object.data {
let bytes = match data_source {
DataSourceObject::Id { id, length, offset } => {
let id = match id {
MaybeReference::Value(id) => id,
MaybeReference::Reference(reference) => {
if let Some(obj) = response.created.get(&reference) {
obj.id.clone()
} else {
response.not_created.append(
create_id,
SetError::not_found().with_description(format!(
"Id reference {reference:?} not found."
)),
);
continue 'outer;
}
}
};
if !self.has_access_blob(&id, access_token).await? {
response.not_created.append(
create_id,
SetError::forbidden().with_description(format!(
"You do not have access to blobId {id}."
)),
);
continue 'outer;
}
let offset = offset.unwrap_or(0) as u32;
let length = length
.map(|length| (length as u32).saturating_add(offset))
.unwrap_or(u32::MAX);
let bytes = if let Some(section) = &id.section {
self.get_blob_section(&id.kind, section)
.await?
.map(|bytes| {
if offset == 0 && length == u32::MAX {
bytes
} else {
bytes
.get(
offset as usize
..std::cmp::min(length as usize, bytes.len()),
)
.unwrap_or_default()
.to_vec()
}
})
} else {
self.get_blob(&id.kind, offset..length).await?
};
if let Some(bytes) = bytes {
bytes
} else {
response.not_created.append(
create_id,
SetError::blob_not_found()
.with_description(format!("BlobId {id} not found.")),
);
continue 'outer;
}
}
DataSourceObject::Value(bytes) => bytes,
};
if bytes.len() + data.len() < self.config.upload_max_size {
data.extend(bytes);
} else {
response.not_created.append(
create_id,
SetError::too_large().with_description(format!(
"Upload size exceeds maximum of {} bytes.",
self.config.upload_max_size
)),
);
continue 'outer;
}
}
if data.is_empty() {
response.not_created.append(
create_id,
SetError::invalid_properties()
.with_description("Must specify at least one valid DataSourceObject."),
);
continue 'outer;
}
// Enforce quota
let (total_files, total_bytes) = self
.store
.get_tmp_blob_usage(account_id, self.config.upload_tmp_ttl)
.await
.map_err(|err| {
tracing::error!(event = "error",
context = "blob_store",
account_id = account_id,
error = ?err,
"Failed to obtain blob quota");
MethodError::ServerPartialFail
})?;
if ((self.config.upload_tmp_quota_size > 0
&& total_bytes + data.len() > self.config.upload_tmp_quota_size)
|| (self.config.upload_tmp_quota_amount > 0
&& total_files + 1 > self.config.upload_tmp_quota_amount))
&& !access_token.is_super_user()
{
response.not_created.append(
create_id,
SetError::over_quota().with_description(format!(
"You have exceeded the blob upload quota of {} files or {} bytes.",
self.config.upload_tmp_quota_amount, self.config.upload_tmp_quota_size
)),
);
continue 'outer;
}
// Write blob
let blob_id = BlobId::temporary(account_id);
match self.store.put_blob(&blob_id.kind, &data).await {
Ok(_) => {
response.created.insert(
create_id,
BlobUploadResponseObject {
id: blob_id,
type_: upload_object.type_,
size: data.len(),
},
);
}
Err(err) => {
tracing::error!(event = "error",
context = "blob_store",
account_id = account_id,
blob_id = ?blob_id,
size = data.len(),
error = ?err,
"Failed to upload blob");
return Err(MethodError::ServerPartialFail);
}
}
}
Ok(response)
}
pub async fn blob_upload(
&self,
account_id: Id,