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.
220 lines
6.3 KiB
Rust
220 lines
6.3 KiB
Rust
/*
|
|
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
|
*
|
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
|
*/
|
|
|
|
use crate::{KV_QUOTA_BLOB, Server};
|
|
use mail_parser::{
|
|
Encoding,
|
|
decoders::{base64::base64_decode, quoted_printable::quoted_printable_decode},
|
|
};
|
|
use store::{
|
|
U32_LEN, U64_LEN,
|
|
dispatch::lookup::KeyValue,
|
|
write::{BatchBuilder, BlobLink, BlobOp, now},
|
|
};
|
|
use trc::AddContext;
|
|
use types::{
|
|
blob::{BlobClass, BlobId, BlobSection},
|
|
blob_hash::BlobHash,
|
|
};
|
|
|
|
const COUNT_BYTES: u32 = 20;
|
|
const COUNT_SHIFT: u32 = 64 - COUNT_BYTES;
|
|
const SIZE_MASK: u64 = (1u64 << COUNT_SHIFT) - 1;
|
|
|
|
pub struct BlobQuotaStatus {
|
|
pub allowed: bool,
|
|
pub expires_in: u64,
|
|
}
|
|
|
|
impl Server {
|
|
pub async fn blob_has_quota(
|
|
&self,
|
|
account_id: u32,
|
|
bytes: usize,
|
|
) -> trc::Result<BlobQuotaStatus> {
|
|
if self.core.jmap.upload_tmp_quota_size > 0 || self.core.jmap.upload_tmp_quota_amount > 0 {
|
|
let now = now();
|
|
let range_start = now / self.core.jmap.upload_tmp_ttl;
|
|
let range_end =
|
|
(range_start * self.core.jmap.upload_tmp_ttl) + self.core.jmap.upload_tmp_ttl;
|
|
let expires_in = range_end - now;
|
|
|
|
let mut bucket = Vec::with_capacity(U32_LEN + U64_LEN + 1);
|
|
bucket.push(KV_QUOTA_BLOB);
|
|
bucket.extend_from_slice(account_id.to_be_bytes().as_slice());
|
|
bucket.extend_from_slice(range_start.to_be_bytes().as_slice());
|
|
|
|
self.in_memory_store()
|
|
.counter_incr(
|
|
KeyValue::new(bucket, 1i64 << COUNT_SHIFT | bytes as i64).expires(expires_in),
|
|
true,
|
|
)
|
|
.await
|
|
.caused_by(trc::location!())
|
|
.map(|v| {
|
|
let v = v as u64;
|
|
let count = v >> COUNT_SHIFT;
|
|
let size = v & SIZE_MASK;
|
|
|
|
let allowed = (self.core.jmap.upload_tmp_quota_amount == 0
|
|
|| count <= self.core.jmap.upload_tmp_quota_amount as u64)
|
|
&& (self.core.jmap.upload_tmp_quota_size == 0
|
|
|| size <= self.core.jmap.upload_tmp_quota_size as u64);
|
|
|
|
BlobQuotaStatus {
|
|
allowed,
|
|
expires_in,
|
|
}
|
|
})
|
|
} else {
|
|
Ok(BlobQuotaStatus {
|
|
allowed: true,
|
|
expires_in: 0,
|
|
})
|
|
}
|
|
}
|
|
|
|
#[allow(clippy::blocks_in_conditions)]
|
|
pub async fn put_jmap_blob(&self, account_id: u32, data: &[u8]) -> trc::Result<BlobId> {
|
|
// First reserve the hash
|
|
let hash = BlobHash::generate(data);
|
|
let mut batch = BatchBuilder::new();
|
|
let until = now() + self.core.jmap.upload_tmp_ttl;
|
|
|
|
batch.with_account_id(account_id).set(
|
|
BlobOp::Link {
|
|
hash: hash.clone(),
|
|
to: BlobLink::Temporary { until },
|
|
},
|
|
vec![],
|
|
);
|
|
|
|
self.core
|
|
.storage
|
|
.data
|
|
.write(batch.build_all())
|
|
.await
|
|
.caused_by(trc::location!())?;
|
|
|
|
if !self
|
|
.core
|
|
.storage
|
|
.data
|
|
.blob_exists(&hash)
|
|
.await
|
|
.caused_by(trc::location!())?
|
|
{
|
|
// Upload blob to store
|
|
self.core
|
|
.storage
|
|
.blob
|
|
.put_blob(hash.as_ref(), data, self.core.email.compression)
|
|
.await
|
|
.caused_by(trc::location!())?;
|
|
|
|
// Commit blob
|
|
let mut batch = BatchBuilder::new();
|
|
batch.set(BlobOp::Commit { hash: hash.clone() }, Vec::new());
|
|
self.core
|
|
.storage
|
|
.data
|
|
.write(batch.build_all())
|
|
.await
|
|
.caused_by(trc::location!())?;
|
|
}
|
|
|
|
Ok(BlobId {
|
|
hash,
|
|
class: BlobClass::Reserved {
|
|
account_id,
|
|
expires: until,
|
|
},
|
|
section: None,
|
|
})
|
|
}
|
|
|
|
pub async fn put_temporary_blob(
|
|
&self,
|
|
account_id: u32,
|
|
data: &[u8],
|
|
hold_for: u64,
|
|
) -> trc::Result<(BlobHash, BlobOp)> {
|
|
// First reserve the hash
|
|
let hash = BlobHash::generate(data);
|
|
let mut batch = BatchBuilder::new();
|
|
let until = now() + hold_for;
|
|
|
|
batch.with_account_id(account_id).set(
|
|
BlobOp::Link {
|
|
hash: hash.clone(),
|
|
to: BlobLink::Temporary { until },
|
|
},
|
|
vec![],
|
|
);
|
|
|
|
self.core
|
|
.storage
|
|
.data
|
|
.write(batch.build_all())
|
|
.await
|
|
.caused_by(trc::location!())?;
|
|
|
|
if !self
|
|
.core
|
|
.storage
|
|
.data
|
|
.blob_exists(&hash)
|
|
.await
|
|
.caused_by(trc::location!())?
|
|
{
|
|
// Upload blob to store
|
|
self.core
|
|
.storage
|
|
.blob
|
|
.put_blob(hash.as_ref(), data, self.core.email.compression)
|
|
.await
|
|
.caused_by(trc::location!())?;
|
|
|
|
// Commit blob
|
|
let mut batch = BatchBuilder::new();
|
|
batch.set(BlobOp::Commit { hash: hash.clone() }, Vec::new());
|
|
self.core
|
|
.storage
|
|
.data
|
|
.write(batch.build_all())
|
|
.await
|
|
.caused_by(trc::location!())?;
|
|
}
|
|
|
|
Ok((
|
|
hash.clone(),
|
|
BlobOp::Link {
|
|
hash,
|
|
to: BlobLink::Temporary { until },
|
|
},
|
|
))
|
|
}
|
|
|
|
pub async fn get_blob_section(
|
|
&self,
|
|
hash: &BlobHash,
|
|
section: &BlobSection,
|
|
) -> trc::Result<Option<Vec<u8>>> {
|
|
Ok(self
|
|
.blob_store()
|
|
.get_blob(
|
|
hash.as_slice(),
|
|
(section.offset_start)..(section.offset_start.saturating_add(section.size)),
|
|
)
|
|
.await?
|
|
.and_then(|bytes| match Encoding::from(section.encoding) {
|
|
Encoding::None => Some(bytes),
|
|
Encoding::Base64 => base64_decode(&bytes),
|
|
Encoding::QuotedPrintable => quoted_printable_decode(&bytes),
|
|
}))
|
|
}
|
|
}
|