Import upstream v0.16.22, stripped

Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f
Enterprise-only files removed or emptied: 63
Enterprise-only snippets removed: 117 in 50 files
Dangling module declarations removed: 5
Cargo edits turning enterprise off: 14
Verification: clean
Enterprise feature gates left for rebuilt features: 19 in 18 files

Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
2026-09-18 10:21:56 -07:00
commit 7dae9b29fd
1650 changed files with 485521 additions and 0 deletions
+112
View File
@@ -0,0 +1,112 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::download::BlobDownload;
use common::{Server, auth::AccessToken};
use jmap_proto::{
error::set::{SetError, SetErrorType},
method::copy::{CopyBlobRequest, CopyBlobResponse},
request::MaybeInvalid,
};
use registry::schema::enums::Permission;
use std::future::Future;
use store::write::{BatchBuilder, BlobLink, BlobOp, now};
use trc::AddContext;
use types::blob::{BlobClass, BlobId};
use utils::map::vec_map::VecMap;
pub trait BlobCopy: Sync + Send {
fn blob_copy(
&self,
request: CopyBlobRequest,
access_token: &AccessToken,
) -> impl Future<Output = trc::Result<CopyBlobResponse>> + Send;
}
impl BlobCopy for Server {
async fn blob_copy(
&self,
request: CopyBlobRequest,
access_token: &AccessToken,
) -> trc::Result<CopyBlobResponse> {
let mut response = CopyBlobResponse {
from_account_id: request.from_account_id,
account_id: request.account_id,
copied: VecMap::with_capacity(request.blob_ids.len()),
not_copied: VecMap::new(),
};
let account_id = request.account_id.document_id();
for blob_id in request.blob_ids {
let blob_id = match blob_id {
MaybeInvalid::Value(blob_id) => blob_id,
invalid => {
response.not_copied.append(
invalid,
SetError::new(SetErrorType::BlobNotFound).with_description(
"blobId does not exist or not enough permissions to access it.",
),
);
continue;
}
};
if self.has_access_blob(&blob_id, access_token).await? {
// Enforce quota
if !access_token.has_permission(Permission::UnlimitedUploads)
&& !self
.blob_has_quota(account_id, 1)
.await
.caused_by(trc::location!())?
.allowed
{
response.not_copied.append(
blob_id,
SetError::over_quota().with_description(format!(
"You have exceeded the blob quota of {} files or {} bytes.",
self.core.jmap.upload_tmp_quota_amount,
self.core.jmap.upload_tmp_quota_size
)),
);
continue;
}
let mut batch = BatchBuilder::new();
let until = now() + self.core.jmap.upload_tmp_ttl;
batch.with_account_id(account_id).set(
BlobOp::Link {
hash: blob_id.hash.clone(),
to: BlobLink::Temporary { until },
},
vec![],
);
self.store()
.write(batch.build_all())
.await
.caused_by(trc::location!())?;
let dest_blob_id = BlobId {
hash: blob_id.hash.clone(),
class: BlobClass::Reserved {
account_id,
expires: until,
},
section: blob_id.section.clone(),
};
response.copied.append(blob_id, dest_blob_id);
} else {
response.not_copied.append(
blob_id,
SetError::new(SetErrorType::BlobNotFound).with_description(
"blobId does not exist or not enough permissions to access it.",
),
);
}
}
Ok(response)
}
}
+149
View File
@@ -0,0 +1,149 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use common::{Server, auth::AccessToken};
use email::cache::MessageCacheFetch;
use email::cache::email::MessageCacheAccess;
use email::message::metadata::MessageMetadata;
use groupware::cache::GroupwareCache;
use registry::schema::enums::Permission;
use std::future::Future;
use store::ValueKey;
use store::write::{AlignedBytes, Archive};
use trc::AddContext;
use types::acl::Acl;
use types::blob::{BlobClass, BlobId};
use types::collection::{Collection, SyncCollection};
use types::field::EmailField;
use utils::chained_bytes::ChainedBytes;
pub trait BlobDownload: Sync + Send {
fn blob_download(
&self,
blob_id: &BlobId,
access_token: &AccessToken,
) -> impl Future<Output = trc::Result<Option<Vec<u8>>>> + Send;
fn has_access_blob(
&self,
blob_id: &BlobId,
access_token: &AccessToken,
) -> impl Future<Output = trc::Result<bool>> + Send;
}
impl BlobDownload for Server {
#[allow(clippy::blocks_in_conditions)]
async fn blob_download(
&self,
blob_id: &BlobId,
access_token: &AccessToken,
) -> trc::Result<Option<Vec<u8>>> {
if self.has_access_blob(blob_id, access_token).await? {
if let Some(section) = &blob_id.section {
self.get_blob_section(&blob_id.hash, section)
.await
.caused_by(trc::location!())
} else {
let blob = self
.blob_store()
.get_blob(blob_id.hash.as_slice(), 0..usize::MAX)
.await
.caused_by(trc::location!());
match (&blob_id.class, blob) {
(
BlobClass::Linked {
account_id,
collection,
document_id,
},
Ok(Some(data)),
) if *collection == Collection::Email as u8 => {
let Some(archive) = self
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::property(
*account_id,
Collection::Email,
*document_id,
EmailField::Metadata,
))
.await
.caused_by(trc::location!())?
else {
return Ok(Some(data));
};
let metadata = archive
.to_unarchived::<MessageMetadata>()
.caused_by(trc::location!())?;
let body_offset = metadata.inner.blob_body_offset.to_native();
if metadata.inner.root_part().offset_body.to_native() != body_offset {
let raw_message = ChainedBytes::new(
metadata.inner.raw_headers.as_ref(),
)
.with_last(data.get(body_offset as usize..).unwrap_or_default());
Ok(Some(raw_message.to_bytes()))
} else {
Ok(Some(data))
}
}
(_, blob) => blob,
}
}
} else {
Ok(None)
}
}
async fn has_access_blob(
&self,
blob_id: &BlobId,
access_token: &AccessToken,
) -> trc::Result<bool> {
Ok(
(blob_id.class.is_superuser() && access_token.has_permission(Permission::FetchAnyBlob))
|| (self
.store()
.blob_has_access(&blob_id.hash, &blob_id.class)
.await
.caused_by(trc::location!())?
&& match &blob_id.class {
BlobClass::Linked {
account_id,
collection,
document_id,
} => {
if access_token.is_member(*account_id) {
true
} else {
match Collection::from(*collection) {
Collection::Email => self
.get_cached_messages(*account_id)
.await
.caused_by(trc::location!())?
.shared_messages(access_token, Acl::ReadItems)
.contains(*document_id),
collection @ (Collection::FileNode
| Collection::ContactCard
| Collection::CalendarEvent) => self
.fetch_dav_resources(
access_token.account_id(),
*account_id,
SyncCollection::from(collection),
)
.await
.caused_by(trc::location!())?
.shared_items(access_token, [Acl::ReadItems], true)
.contains(*document_id),
_ => false,
}
}
}
BlobClass::Reserved { account_id, .. } => {
access_token.is_member(*account_id)
}
}),
)
}
}
+274
View File
@@ -0,0 +1,274 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::download::BlobDownload;
use common::{Server, auth::AccessToken};
use email::message::metadata::MessageData;
use jmap_proto::{
method::{
get::{GetRequest, GetResponse},
lookup::{BlobInfo, BlobLookupRequest, BlobLookupResponse},
},
object::blob::{Blob, BlobProperty, BlobValue, DataProperty, DigestProperty},
request::{IntoValid, MaybeInvalid},
};
use jmap_tools::{Map, Value};
use mail_builder::encoders::Base64Encoder;
use sha1::{Digest, Sha1};
use sha2::{Sha256, Sha512};
use std::future::Future;
use store::{
ValueKey,
write::{AlignedBytes, Archive},
};
use trc::AddContext;
use types::{blob::BlobClass, collection::Collection, id::Id, type_state::DataType};
use utils::map::vec_map::VecMap;
pub trait BlobOperations: Sync + Send {
fn blob_get(
&self,
request: GetRequest<Blob>,
access_token: &AccessToken,
) -> impl Future<Output = trc::Result<GetResponse<Blob>>> + 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<Blob>,
access_token: &AccessToken,
) -> trc::Result<GetResponse<Blob>> {
let (ids, not_found_ids) = request.unwrap_ids(self.core.jmap.get_max_objects)?;
let ids = ids.unwrap_or_default();
let properties = request.unwrap_properties(&[
BlobProperty::Id,
BlobProperty::Data(DataProperty::Default),
BlobProperty::Size,
]);
let mut response = GetResponse {
account_id: request.account_id.into(),
state: None,
list: Vec::with_capacity(ids.len()),
not_found: not_found_ids,
};
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 = Map::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.insert_unchecked(BlobProperty::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<'static, BlobProperty, BlobValue> = match &property {
BlobProperty::Id => Value::Element(BlobValue::BlobId(blob_id.clone())),
BlobProperty::Size => bytes.len().into(),
BlobProperty::Digest(digest) => match digest {
DigestProperty::Sha => {
let mut hasher = Sha1::new();
hasher.update(bytes_range);
String::from_utf8(
Base64Encoder::new()
.encode(&hasher.finalize()[..])
.unwrap_or_default(),
)
.unwrap()
}
DigestProperty::Sha256 => {
let mut hasher = Sha256::new();
hasher.update(bytes_range);
String::from_utf8(
Base64Encoder::new()
.encode(&hasher.finalize()[..])
.unwrap_or_default(),
)
.unwrap()
}
DigestProperty::Sha512 => {
let mut hasher = Sha512::new();
hasher.update(bytes_range);
String::from_utf8(
Base64Encoder::new()
.encode(&hasher.finalize()[..])
.unwrap_or_default(),
)
.unwrap()
}
}
.into(),
BlobProperty::Data(data) => match data {
DataProperty::AsText => match std::str::from_utf8(bytes_range) {
Ok(text) => text.to_string().into(),
Err(_) => {
blob.insert_unchecked(BlobProperty::IsEncodingProblem, true);
Value::Null
}
},
DataProperty::AsBase64 => String::from_utf8(
Base64Encoder::new().encode(bytes_range).unwrap_or_default(),
)
.unwrap()
.into(),
DataProperty::Default => match std::str::from_utf8(bytes_range) {
Ok(text) => {
property = BlobProperty::Data(DataProperty::AsText);
text.to_string().into()
}
Err(_) => {
property = BlobProperty::Data(DataProperty::AsBase64);
blob.insert_unchecked(BlobProperty::IsEncodingProblem, true);
String::from_utf8(
Base64Encoder::new()
.encode(bytes_range)
.unwrap_or_default(),
)
.unwrap()
.into()
}
},
},
_ => Value::Null,
};
blob.insert_unchecked(property, value);
}
// Add result to response
response.list.push(blob.into());
} else {
response.push_not_found(blob_id);
}
}
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 {
MaybeInvalid::Value(value) => {
match &value {
DataType::Email => {
include_email = true;
}
DataType::Mailbox => {
include_mailbox = true;
}
DataType::Thread => {
include_thread = true;
}
_ => (),
}
Ok(value)
}
MaybeInvalid::Invalid(_) => 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.into_valid() {
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
.store()
.get_value::<Archive<AlignedBytes>>(ValueKey::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(id);
continue;
}
}
response.list.push(BlobInfo { id, matched_ids });
}
Ok(response)
}
}
+23
View File
@@ -0,0 +1,23 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use types::{blob::BlobId, id::Id};
pub mod copy;
pub mod download;
pub mod get;
pub mod upload;
#[derive(Debug, serde::Serialize)]
pub struct UploadResponse {
#[serde(rename(serialize = "accountId"))]
account_id: Id,
#[serde(rename(serialize = "blobId"))]
blob_id: BlobId,
#[serde(rename(serialize = "type"))]
c_type: String,
size: usize,
}
+252
View File
@@ -0,0 +1,252 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use super::{UploadResponse, download::BlobDownload};
use common::{Server, auth::AccessToken};
use jmap_proto::{
error::set::SetError,
method::upload::{
BlobUploadRequest, BlobUploadResponse, BlobUploadResponseObject, DataSourceObject,
},
request::reference::MaybeIdReference,
};
use registry::schema::enums::Permission;
use std::future::Future;
use trc::AddContext;
use types::id::Id;
#[cfg(feature = "test_mode")]
pub static DISABLE_UPLOAD_QUOTA: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(true);
pub trait BlobUpload: Sync + Send {
fn blob_upload_many(
&self,
request: BlobUploadRequest,
access_token: &AccessToken,
) -> impl Future<Output = trc::Result<BlobUploadResponse>> + Send;
fn blob_upload(
&self,
account_id: Id,
content_type: &str,
data: &[u8],
access_token: &AccessToken,
) -> impl Future<Output = trc::Result<UploadResponse>> + Send;
}
impl BlobUpload for Server {
async fn blob_upload_many(
&self,
request: BlobUploadRequest,
access_token: &AccessToken,
) -> trc::Result<BlobUploadResponse> {
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.core.jmap.set_max_objects {
return Err(trc::JmapEvent::RequestTooLarge.into_err());
}
'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 {
MaybeIdReference::Id(id) => id,
MaybeIdReference::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;
}
}
MaybeIdReference::Invalid(id) => {
response.not_created.append(
create_id,
SetError::invalid_properties()
.with_description(format!("Invalid blobId {id}.")),
);
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);
let length = length
.map(|length| length.saturating_add(offset))
.unwrap_or(usize::MAX);
let bytes = if let Some(section) = &id.section {
self.get_blob_section(&id.hash, section)
.await?
.map(|bytes| {
if offset == 0 && length == usize::MAX {
bytes
} else {
bytes
.get(offset..std::cmp::min(length, bytes.len()))
.unwrap_or_default()
.to_vec()
}
})
} else {
self.blob_store()
.get_blob(id.hash.as_slice(), 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,
DataSourceObject::Null => {
response.not_created.append(
create_id,
SetError::invalid_properties()
.with_description("Invalid DataSourceObject."),
);
continue 'outer;
}
};
if bytes.len() + data.len() < self.core.jmap.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.core.jmap.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
if !access_token.has_permission(Permission::UnlimitedUploads)
&& !self
.blob_has_quota(account_id, data.len())
.await
.caused_by(trc::location!())?
.allowed
{
response.not_created.append(
create_id,
SetError::over_quota().with_description(format!(
"You have exceeded the blob upload quota of {} files or {} bytes.",
self.core.jmap.upload_tmp_quota_amount,
self.core.jmap.upload_tmp_quota_size
)),
);
continue 'outer;
}
// Write blob
response.created.insert(
create_id,
BlobUploadResponseObject {
id: self.put_jmap_blob(account_id, &data).await?,
type_: upload_object.type_,
size: data.len(),
},
);
}
Ok(response)
}
async fn blob_upload(
&self,
account_id: Id,
content_type: &str,
data: &[u8],
access_token: &AccessToken,
) -> trc::Result<UploadResponse> {
// Limit concurrent uploads
let _in_flight = self
.is_upload_allowed(access_token)
.caused_by(trc::location!())?;
#[cfg(feature = "test_mode")]
{
// Used for concurrent upload tests
if data == b"sleep" {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
}
}
// Enforce quota
if !access_token.has_permission(Permission::UnlimitedUploads) {
let status = self
.blob_has_quota(account_id.document_id(), data.len())
.await
.caused_by(trc::location!())?;
if !status.allowed {
let err = Err(trc::LimitEvent::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)
.ctx(trc::Key::Expires, status.expires_in));
#[cfg(feature = "test_mode")]
if !DISABLE_UPLOAD_QUOTA.load(std::sync::atomic::Ordering::Relaxed) {
return err;
}
#[cfg(not(feature = "test_mode"))]
return err;
}
}
Ok(UploadResponse {
account_id,
blob_id: self
.put_jmap_blob(account_id.document_id(), data)
.await
.caused_by(trc::location!())?,
c_type: content_type.to_string(),
size: data.len(),
})
}
}