276 lines
11 KiB
Rust
276 lines
11 KiB
Rust
/*
|
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
|
|
*
|
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
|
*/
|
|
|
|
use common::{Server, auth::AccessToken};
|
|
use email::message::metadata::MessageData;
|
|
use jmap_proto::{
|
|
method::{
|
|
get::{GetRequest, GetResponse},
|
|
lookup::{BlobInfo, BlobLookupRequest, BlobLookupResponse},
|
|
},
|
|
object::blob::GetArguments,
|
|
types::{
|
|
MaybeUnparsable,
|
|
collection::Collection,
|
|
id::Id,
|
|
property::{DataProperty, DigestProperty, Property},
|
|
type_state::DataType,
|
|
value::{Object, Value},
|
|
},
|
|
};
|
|
use mail_builder::encoders::base64::base64_encode;
|
|
use sha1::{Digest, Sha1};
|
|
use sha2::{Sha256, Sha512};
|
|
use store::BlobClass;
|
|
use trc::AddContext;
|
|
use utils::map::vec_map::VecMap;
|
|
|
|
use std::future::Future;
|
|
|
|
use super::download::BlobDownload;
|
|
|
|
pub trait BlobOperations: Sync + Send {
|
|
fn blob_get(
|
|
&self,
|
|
request: GetRequest<GetArguments>,
|
|
access_token: &AccessToken,
|
|
) -> impl Future<Output = trc::Result<GetResponse>> + Send;
|
|
|
|
fn blob_lookup(
|
|
&self,
|
|
request: BlobLookupRequest,
|
|
) -> impl Future<Output = trc::Result<BlobLookupResponse>> + Send;
|
|
}
|
|
|
|
impl BlobOperations for Server {
|
|
async fn blob_get(
|
|
&self,
|
|
mut request: GetRequest<GetArguments>,
|
|
access_token: &AccessToken,
|
|
) -> trc::Result<GetResponse> {
|
|
let ids = request
|
|
.unwrap_blob_ids(self.core.jmap.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
|
|
};
|
|
bytes.get(range_from..range_to).unwrap_or_default()
|
|
};
|
|
|
|
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)
|
|
}
|
|
|
|
async fn blob_lookup(&self, request: BlobLookupRequest) -> trc::Result<BlobLookupResponse> {
|
|
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(trc::JmapEvent::UnknownDataType.into_err()),
|
|
})
|
|
.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.class {
|
|
BlobClass::Linked {
|
|
account_id,
|
|
collection,
|
|
document_id,
|
|
} if *account_id == req_account_id => {
|
|
let collection = Collection::from(*collection);
|
|
if collection == Collection::Email {
|
|
if let Some(data_) = self
|
|
.get_archive(req_account_id, Collection::Email, *document_id)
|
|
.await?
|
|
{
|
|
let data = data_
|
|
.unarchive::<MessageData>()
|
|
.caused_by(trc::location!())?;
|
|
if include_email {
|
|
matched_ids.append(
|
|
DataType::Email,
|
|
vec![Id::from_parts(
|
|
u32::from(data.thread_id),
|
|
*document_id,
|
|
)],
|
|
);
|
|
}
|
|
if include_thread {
|
|
matched_ids.append(
|
|
DataType::Thread,
|
|
vec![Id::from(u32::from(data.thread_id))],
|
|
);
|
|
}
|
|
if include_mailbox {
|
|
matched_ids.append(
|
|
DataType::Mailbox,
|
|
data.mailboxes
|
|
.iter()
|
|
.map(|m| {
|
|
debug_assert!(m.uid != 0);
|
|
Id::from(u32::from(m.mailbox_id))
|
|
})
|
|
.collect::<Vec<_>>(),
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
match DataType::try_from(collection) {
|
|
Ok(data_type) if type_names.contains(&data_type) => {
|
|
matched_ids.append(data_type, vec![Id::from(*document_id)]);
|
|
}
|
|
_ => (),
|
|
}
|
|
}
|
|
}
|
|
BlobClass::Reserved { 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)
|
|
}
|
|
}
|