Refactored local delivery to avoid mpsc channel

This commit is contained in:
mdecimus
2025-01-17 15:29:55 +01:00
parent 00ad5a5c44
commit 2eb388674d
117 changed files with 2611 additions and 2628 deletions
Generated
+45 -25
View File
@@ -2058,6 +2058,37 @@ dependencies = [
"zeroize",
]
[[package]]
name = "email"
version = "0.11.2"
dependencies = [
"aes",
"aes-gcm",
"aes-gcm-siv",
"bincode",
"cbc",
"common",
"directory",
"jmap_proto",
"mail-builder",
"mail-parser",
"nlp",
"rasn",
"rasn-cms",
"rasn-pkix",
"rsa",
"sequoia-openpgp",
"serde",
"serde_json",
"sieve-rs",
"smtp-proto",
"spam-filter",
"store",
"tokio",
"trc",
"utils",
]
[[package]]
name = "ena"
version = "0.14.3"
@@ -3225,6 +3256,7 @@ dependencies = [
"common",
"dashmap",
"directory",
"email",
"imap_proto",
"jmap",
"jmap_proto",
@@ -3471,18 +3503,16 @@ dependencies = [
name = "jmap"
version = "0.11.2"
dependencies = [
"aes",
"aes-gcm",
"aes-gcm-siv",
"async-stream",
"async-trait",
"base64 0.22.1",
"bincode",
"cbc",
"chrono",
"common",
"dashmap",
"directory",
"email",
"form-data",
"form_urlencoded",
"futures-util",
@@ -3501,15 +3531,11 @@ dependencies = [
"nlp",
"p256",
"pkcs8",
"quick-xml 0.36.2",
"quick-xml 0.37.1",
"rand 0.8.5",
"rasn",
"rasn-cms",
"rasn-pkix",
"reqwest 0.12.9",
"rev_lines",
"rsa",
"sequoia-openpgp",
"serde",
"serde_json",
"sha1",
@@ -3520,9 +3546,9 @@ dependencies = [
"spam-filter",
"store",
"tokio",
"tokio-tungstenite 0.24.0",
"tokio-tungstenite 0.26.1",
"trc",
"tungstenite 0.24.0",
"tungstenite 0.26.1",
"utils",
"x509-parser 0.16.0",
]
@@ -4786,6 +4812,7 @@ version = "0.11.2"
dependencies = [
"common",
"directory",
"email",
"imap",
"jmap",
"jmap_proto",
@@ -5070,15 +5097,6 @@ dependencies = [
"serde",
]
[[package]]
name = "quick-xml"
version = "0.36.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe"
dependencies = [
"memchr",
]
[[package]]
name = "quick-xml"
version = "0.37.1"
@@ -6443,6 +6461,7 @@ dependencies = [
"common",
"dashmap",
"directory",
"email",
"form_urlencoded",
"http-body-util",
"hyper 1.5.2",
@@ -6830,6 +6849,7 @@ dependencies = [
"dashmap",
"directory",
"ece",
"email",
"flate2",
"form_urlencoded",
"futures",
@@ -7091,14 +7111,14 @@ dependencies = [
[[package]]
name = "tokio-tungstenite"
version = "0.24.0"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edc5f74e248dc973e0dbb7b74c7e0d6fcc301c694ff50049504004ef4d0cdcd9"
checksum = "be4bf6fecd69fcdede0ec680aaf474cdab988f9de6bc73d3758f0160e3b7025a"
dependencies = [
"futures-util",
"log",
"tokio",
"tungstenite 0.24.0",
"tungstenite 0.26.1",
]
[[package]]
@@ -7312,9 +7332,9 @@ dependencies = [
[[package]]
name = "tungstenite"
version = "0.24.0"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18e5b8366ee7a95b16d32197d0b2604b43a0be89dc5fac9f8e96ccafbaedda8a"
checksum = "413083a99c579593656008130e29255e54dcaae495be556cc26888f211648c24"
dependencies = [
"byteorder",
"bytes",
@@ -7324,7 +7344,7 @@ dependencies = [
"log",
"rand 0.8.5",
"sha1",
"thiserror 1.0.69",
"thiserror 2.0.9",
"utf-8",
]
+1
View File
@@ -4,6 +4,7 @@ members = [
"crates/main",
"crates/jmap",
"crates/jmap-proto",
"crates/email",
"crates/imap",
"crates/imap-proto",
"crates/smtp",
-6
View File
@@ -85,7 +85,6 @@ pub struct QueueOutboundTimeout {
#[derive(Debug, Clone)]
pub struct QueueThrottle {
pub outbound_concurrency: usize,
pub local_concurrency: usize,
pub sender: Vec<Throttle>,
pub rcpt: Vec<Throttle>,
pub host: Vec<Throttle>,
@@ -204,7 +203,6 @@ impl Default for QueueConfig {
},
throttle: QueueThrottle {
outbound_concurrency: 25,
local_concurrency: 10,
sender: Default::default(),
rcpt: Default::default(),
host: Default::default(),
@@ -392,10 +390,6 @@ fn parse_queue_throttle(config: &mut Config) -> QueueThrottle {
.property_or_default::<usize>("queue.threads.remote", "25")
.unwrap_or(25)
.max(1),
local_concurrency: config
.property_or_default::<usize>("queue.threads.local", "10")
.unwrap_or(10)
.max(1),
};
let all_throttles = parse_throttle(
+419 -4
View File
@@ -4,21 +4,33 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::sync::Arc;
use std::{sync::Arc, time::Duration};
use directory::{backend::internal::manage::ManageDirectory, Directory, Type};
use directory::{backend::internal::manage::ManageDirectory, Directory, QueryBy, Type};
use jmap_proto::types::{
blob::BlobId, collection::Collection, property::Property, state::StateChange,
};
use sieve::Sieve;
use store::{
write::{QueueClass, ValueClass},
BlobStore, FtsStore, InMemoryStore, IterateParams, Store, ValueKey,
dispatch::DocumentSet,
roaring::RoaringBitmap,
write::{
key::DeserializeBigEndian, log::ChangeLogBuilder, now, BatchBuilder, BitmapClass, BlobOp,
DirectoryClass, QueueClass, TagValue, ValueClass,
},
BitmapKey, BlobClass, BlobStore, Deserialize, FtsStore, InMemoryStore, IterateParams, LogKey,
Serialize, Store, ValueKey, U32_LEN,
};
use trc::AddContext;
use utils::BlobHash;
use crate::{
auth::{AccessToken, ResourceToken, TenantInfo},
config::smtp::{
auth::{ArcSealer, DkimSigner},
queue::RelayHost,
},
ipc::StateEvent,
ImapId, Inner, MailboxState, Server,
};
@@ -170,6 +182,249 @@ impl Server {
})
}
pub async fn get_used_quota(&self, account_id: u32) -> trc::Result<i64> {
self.core
.storage
.data
.get_counter(DirectoryClass::UsedQuota(account_id))
.await
.add_context(|err| err.caused_by(trc::location!()).account_id(account_id))
}
pub async fn has_available_quota(
&self,
quotas: &ResourceToken,
item_size: u64,
) -> trc::Result<()> {
if quotas.quota != 0 {
let used_quota = self.get_used_quota(quotas.account_id).await? as u64;
if used_quota + item_size > quotas.quota {
return Err(trc::LimitEvent::Quota
.into_err()
.ctx(trc::Key::Limit, quotas.quota)
.ctx(trc::Key::Size, used_quota));
}
}
// SPDX-SnippetBegin
// SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
// SPDX-License-Identifier: LicenseRef-SEL
#[cfg(feature = "enterprise")]
if self.core.is_enterprise_edition() {
if let Some(tenant) = quotas.tenant.filter(|tenant| tenant.quota != 0) {
let used_quota = self.get_used_quota(tenant.id).await? as u64;
if used_quota + item_size > tenant.quota {
return Err(trc::LimitEvent::TenantQuota
.into_err()
.ctx(trc::Key::Limit, tenant.quota)
.ctx(trc::Key::Size, used_quota));
}
}
}
// SPDX-SnippetEnd
Ok(())
}
pub async fn get_resource_token(
&self,
access_token: &AccessToken,
account_id: u32,
) -> trc::Result<ResourceToken> {
Ok(if access_token.primary_id == account_id {
ResourceToken {
account_id,
quota: access_token.quota,
tenant: access_token.tenant,
}
} else {
let mut quotas = ResourceToken {
account_id,
..Default::default()
};
if let Some(principal) = self
.core
.storage
.directory
.query(QueryBy::Id(account_id), false)
.await
.add_context(|err| err.caused_by(trc::location!()).account_id(account_id))?
{
quotas.quota = principal.quota();
// SPDX-SnippetBegin
// SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
// SPDX-License-Identifier: LicenseRef-SEL
#[cfg(feature = "enterprise")]
if self.core.is_enterprise_edition() {
if let Some(tenant_id) = principal.tenant() {
quotas.tenant = TenantInfo {
id: tenant_id,
quota: self
.core
.storage
.directory
.query(QueryBy::Id(tenant_id), false)
.await
.add_context(|err| {
err.caused_by(trc::location!()).account_id(tenant_id)
})?
.map(|tenant| tenant.quota())
.unwrap_or_default(),
}
.into();
}
}
// SPDX-SnippetEnd
}
quotas
})
}
pub async fn get_property<U>(
&self,
account_id: u32,
collection: Collection,
document_id: u32,
property: impl AsRef<Property> + Sync + Send,
) -> trc::Result<Option<U>>
where
U: Deserialize + 'static,
{
let property = property.as_ref();
self.core
.storage
.data
.get_value::<U>(ValueKey {
account_id,
collection: collection.into(),
document_id,
class: ValueClass::Property(property.into()),
})
.await
.add_context(|err| {
err.caused_by(trc::location!())
.account_id(account_id)
.collection(collection)
.document_id(document_id)
.id(property.to_string())
})
}
pub async fn get_properties<U, I, P>(
&self,
account_id: u32,
collection: Collection,
iterate: &I,
property: P,
) -> trc::Result<Vec<(u32, U)>>
where
I: DocumentSet + Send + Sync,
P: AsRef<Property> + Sync + Send,
U: Deserialize + 'static,
{
let property: u8 = property.as_ref().into();
let collection: u8 = collection.into();
let expected_results = iterate.len();
let mut results = Vec::with_capacity(expected_results);
self.core
.storage
.data
.iterate(
IterateParams::new(
ValueKey {
account_id,
collection,
document_id: iterate.min(),
class: ValueClass::Property(property),
},
ValueKey {
account_id,
collection,
document_id: iterate.max(),
class: ValueClass::Property(property),
},
),
|key, value| {
let document_id = key.deserialize_be_u32(key.len() - U32_LEN)?;
if iterate.contains(document_id) {
results.push((document_id, U::deserialize(value)?));
Ok(expected_results == 0 || results.len() < expected_results)
} else {
Ok(true)
}
},
)
.await
.add_context(|err| {
err.caused_by(trc::location!())
.account_id(account_id)
.collection(collection)
.id(property.to_string())
})
.map(|_| results)
}
pub async fn get_document_ids(
&self,
account_id: u32,
collection: Collection,
) -> trc::Result<Option<RoaringBitmap>> {
self.core
.storage
.data
.get_bitmap(BitmapKey::document_ids(account_id, collection))
.await
.add_context(|err| {
err.caused_by(trc::location!())
.account_id(account_id)
.collection(collection)
})
}
pub async fn get_tag(
&self,
account_id: u32,
collection: Collection,
property: impl AsRef<Property> + Sync + Send,
value: impl Into<TagValue<u32>> + Sync + Send,
) -> trc::Result<Option<RoaringBitmap>> {
let property = property.as_ref();
self.core
.storage
.data
.get_bitmap(BitmapKey {
account_id,
collection: collection.into(),
class: BitmapClass::Tag {
field: property.into(),
value: value.into(),
},
document_id: 0,
})
.await
.add_context(|err| {
err.caused_by(trc::location!())
.account_id(account_id)
.collection(collection)
.id(property.to_string())
})
}
pub fn notify_task_queue(&self) {
self.inner.ipc.index_tx.notify_one();
}
pub async fn total_queued_messages(&self) -> trc::Result<u64> {
let mut total = 0;
self.store()
@@ -190,6 +445,166 @@ impl Server {
.map(|_| total)
}
pub fn begin_changes(&self, account_id: u32) -> trc::Result<ChangeLogBuilder> {
self.assign_change_id(account_id)
.map(ChangeLogBuilder::with_change_id)
}
#[inline(always)]
pub fn assign_change_id(&self, _: u32) -> trc::Result<u64> {
self.generate_snowflake_id()
}
pub fn generate_snowflake_id(&self) -> trc::Result<u64> {
self.inner.data.jmap_id_gen.generate().ok_or_else(|| {
trc::StoreEvent::UnexpectedError
.into_err()
.caused_by(trc::location!())
.ctx(trc::Key::Reason, "Failed to generate snowflake id.")
})
}
pub async fn commit_changes(
&self,
account_id: u32,
mut changes: ChangeLogBuilder,
) -> trc::Result<u64> {
if changes.change_id == u64::MAX || changes.change_id == 0 {
changes.change_id = self.assign_change_id(account_id)?;
}
let state = changes.change_id;
let mut builder = BatchBuilder::new();
builder.with_account_id(account_id).custom(changes);
self.core
.storage
.data
.write(builder.build())
.await
.caused_by(trc::location!())
.map(|_| state)
}
pub async fn delete_changes(&self, account_id: u32, before: Duration) -> trc::Result<()> {
let reference_cid = self.inner.data.jmap_id_gen.past_id(before).ok_or_else(|| {
trc::StoreEvent::UnexpectedError
.caused_by(trc::location!())
.ctx(trc::Key::Reason, "Failed to generate reference change id.")
})?;
for collection in [
Collection::Email,
Collection::Mailbox,
Collection::Thread,
Collection::Identity,
Collection::EmailSubmission,
] {
self.core
.storage
.data
.delete_range(
LogKey {
account_id,
collection: collection.into(),
change_id: 0,
},
LogKey {
account_id,
collection: collection.into(),
change_id: reference_cid,
},
)
.await?;
}
Ok(())
}
pub async fn broadcast_state_change(&self, state_change: StateChange) -> bool {
match self
.inner
.ipc
.state_tx
.clone()
.send(StateEvent::Publish { state_change })
.await
{
Ok(_) => true,
Err(_) => {
trc::event!(
Server(trc::ServerEvent::ThreadError),
Details = "Error sending state change.",
CausedBy = trc::location!()
);
false
}
}
}
#[allow(clippy::blocks_in_conditions)]
pub async fn put_blob(
&self,
account_id: u32,
data: &[u8],
set_quota: bool,
) -> trc::Result<BlobId> {
// First reserve the hash
let hash = BlobHash::from(data);
let mut batch = BatchBuilder::new();
let until = now() + self.core.jmap.upload_tmp_ttl;
batch.with_account_id(account_id).set(
BlobOp::Reserve {
hash: hash.clone(),
until,
},
(if set_quota { data.len() as u32 } else { 0u32 }).serialize(),
);
self.core
.storage
.data
.write(batch.build())
.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)
.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())
.await
.caused_by(trc::location!())?;
}
Ok(BlobId {
hash,
class: BlobClass::Reserved {
account_id,
expires: until,
},
section: None,
})
}
pub async fn total_accounts(&self) -> trc::Result<u64> {
self.store()
.count_principals(None, Type::Individual.into(), None)
+3 -33
View File
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::{borrow::Cow, sync::Arc, time::Instant};
use std::{sync::Arc, time::Instant};
use ahash::RandomState;
use jmap_proto::types::{state::StateChange, type_state::DataType};
@@ -14,8 +14,8 @@ use mail_auth::{
report::{tlsrpt::FailureDetails, Record},
};
use store::{BlobStore, InMemoryStore, Store};
use tokio::sync::{mpsc, oneshot};
use utils::{map::bitmap::Bitmap, BlobHash};
use tokio::sync::mpsc;
use utils::map::bitmap::Bitmap;
use crate::{
config::smtp::{
@@ -25,36 +25,6 @@ use crate::{
listener::limiter::ConcurrencyLimiter,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DeliveryResult {
Success,
TemporaryFailure {
reason: Cow<'static, str>,
},
PermanentFailure {
code: [u8; 3],
reason: Cow<'static, str>,
},
}
#[derive(Debug)]
pub enum DeliveryEvent {
Ingest {
message: IngestMessage,
result_tx: oneshot::Sender<Vec<DeliveryResult>>,
},
Stop,
}
#[derive(Debug)]
pub struct IngestMessage {
pub sender_address: String,
pub recipients: Vec<String>,
pub message_blob: BlobHash,
pub message_size: usize,
pub session_id: u64,
}
pub enum HousekeeperEvent {
AcmeReschedule {
provider_id: String,
+4 -4
View File
@@ -33,7 +33,7 @@ use config::{
use dashmap::DashMap;
use imap_proto::protocol::list::Attribute;
use ipc::{DeliveryEvent, HousekeeperEvent, QueueEvent, ReportingEvent, StateEvent};
use ipc::{HousekeeperEvent, QueueEvent, ReportingEvent, StateEvent};
use listener::{
asn::AsnGeoLookupData, blocked::Security, limiter::ConcurrencyLimiter, tls::AcmeProviders,
};
@@ -43,7 +43,7 @@ use manager::webadmin::{Resource, WebAdminManager};
use nlp::bayes::{TokenHash, Weights};
use parking_lot::{Mutex, RwLock};
use rustls::sign::CertifiedKey;
use tokio::sync::{mpsc, Notify};
use tokio::sync::{mpsc, Notify, Semaphore};
use tokio_rustls::TlsConnector;
use utils::{
cache::{Cache, CacheItemWeight, CacheWithTtl},
@@ -167,10 +167,10 @@ pub struct HttpAuthCache {
pub struct Ipc {
pub state_tx: mpsc::Sender<StateEvent>,
pub housekeeper_tx: mpsc::Sender<HousekeeperEvent>,
pub delivery_tx: mpsc::Sender<DeliveryEvent>,
pub index_tx: Arc<Notify>,
pub queue_tx: mpsc::Sender<QueueEvent>,
pub report_tx: mpsc::Sender<ReportingEvent>,
pub local_delivery_sm: Arc<Semaphore>,
}
pub struct TlsConnectors {
@@ -442,10 +442,10 @@ impl Default for Ipc {
Self {
state_tx: mpsc::channel(IPC_CHANNEL_BUFFER).0,
housekeeper_tx: mpsc::channel(IPC_CHANNEL_BUFFER).0,
delivery_tx: mpsc::channel(IPC_CHANNEL_BUFFER).0,
index_tx: Default::default(),
queue_tx: mpsc::channel(IPC_CHANNEL_BUFFER).0,
report_tx: mpsc::channel(IPC_CHANNEL_BUFFER).0,
local_delivery_sm: Arc::new(Semaphore::new(10)),
}
}
}
+10 -8
View File
@@ -16,7 +16,7 @@ use store::{
rand::{distributions::Alphanumeric, thread_rng, Rng},
Stores,
};
use tokio::sync::{mpsc, Notify};
use tokio::sync::{mpsc, Notify, Semaphore};
use utils::{
config::{Config, ConfigKey},
failed, Semver, UnwrapFailure,
@@ -25,7 +25,7 @@ use utils::{
use crate::{
config::{network::AsnGeoLookupConfig, server::Listeners, telemetry::Telemetry},
core::BuildServer,
ipc::{DeliveryEvent, HousekeeperEvent, QueueEvent, ReportingEvent, StateEvent},
ipc::{HousekeeperEvent, QueueEvent, ReportingEvent, StateEvent},
Caches, Core, Data, Inner, Ipc, IPC_CHANNEL_BUFFER,
};
@@ -46,7 +46,6 @@ pub struct BootManager {
pub struct IpcReceivers {
pub state_rx: Option<mpsc::Receiver<StateEvent>>,
pub housekeeper_rx: Option<mpsc::Receiver<HousekeeperEvent>>,
pub delivery_rx: Option<mpsc::Receiver<DeliveryEvent>>,
pub queue_rx: Option<mpsc::Receiver<QueueEvent>>,
pub report_rx: Option<mpsc::Receiver<ReportingEvent>>,
}
@@ -427,7 +426,7 @@ impl BootManager {
core.network.asn_geo_lookup,
AsnGeoLookupConfig::Resource { .. }
);
let (ipc, ipc_rxs) = build_ipc();
let (ipc, ipc_rxs) = build_ipc(&mut config);
let inner = Arc::new(Inner {
shared_core: ArcSwap::from_pointee(core),
data,
@@ -484,9 +483,8 @@ impl BootManager {
}
}
pub fn build_ipc() -> (Ipc, IpcReceivers) {
pub fn build_ipc(config: &mut Config) -> (Ipc, IpcReceivers) {
// Build ipc receivers
let (delivery_tx, delivery_rx) = mpsc::channel(IPC_CHANNEL_BUFFER);
let (state_tx, state_rx) = mpsc::channel(IPC_CHANNEL_BUFFER);
let (housekeeper_tx, housekeeper_rx) = mpsc::channel(IPC_CHANNEL_BUFFER);
let (queue_tx, queue_rx) = mpsc::channel(IPC_CHANNEL_BUFFER);
@@ -495,15 +493,19 @@ pub fn build_ipc() -> (Ipc, IpcReceivers) {
Ipc {
state_tx,
housekeeper_tx,
delivery_tx,
queue_tx,
report_tx,
index_tx: Arc::new(Notify::new()),
local_delivery_sm: Arc::new(Semaphore::new(
config
.property_or_default::<usize>("queue.threads.local", "10")
.unwrap_or(10)
.max(1),
)),
},
IpcReceivers {
state_rx: Some(state_rx),
housekeeper_rx: Some(housekeeper_rx),
delivery_rx: Some(delivery_rx),
queue_rx: Some(queue_rx),
report_rx: Some(report_rx),
},
+39
View File
@@ -0,0 +1,39 @@
[package]
name = "email"
version = "0.11.2"
edition = "2021"
resolver = "2"
[dependencies]
utils = { path = "../utils" }
nlp = { path = "../nlp" }
store = { path = "../store" }
trc = { path = "../trc" }
jmap_proto = { path = "../jmap-proto" }
common = { path = "../common" }
directory = { path = "../directory" }
spam-filter = { path = "../spam-filter" }
smtp-proto = { version = "0.1", features = ["serde_support"] }
mail-parser = { version = "0.9", features = ["full_encoding", "ludicrous_mode"] }
mail-builder = { version = "0.3", features = ["ludicrous_mode"] }
sieve-rs = { version = "0.5" }
tokio = { version = "1.23", features = ["net", "macros"] }
serde = { version = "1.0", features = ["derive"]}
serde_json = "1.0"
bincode = "1.3.3"
aes = "0.8.3"
aes-gcm = "0.10.1"
aes-gcm-siv = "0.11.1"
cbc = { version = "0.1.2", features = ["alloc"] }
rasn = "0.10"
rasn-cms = "0.10"
rasn-pkix = "0.10"
rsa = "0.9.2"
sequoia-openpgp = { version = "1.16", default-features = false, features = ["crypto-rust", "allow-experimental-crypto", "allow-variable-time-crypto"] }
[features]
test_mode = []
enterprise = []
[dev-dependencies]
tokio = { version = "1.23", features = ["full"] }
@@ -11,8 +11,6 @@ use jmap_proto::types::{collection::Collection, property::Property};
use std::future::Future;
use trc::AddContext;
use crate::JmapMethods;
pub trait ThreadCache: Sync + Send {
fn get_cached_thread_ids(
&self,
+670
View File
@@ -0,0 +1,670 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::{borrow::Cow, collections::BTreeSet, fmt::Display, io::Cursor};
use aes::cipher::{block_padding::Pkcs7, BlockEncryptMut, KeyIvInit};
use mail_builder::{encoders::base64::base64_encode_mime, mime::make_boundary};
use mail_parser::{decoders::base64::base64_decode, Message, MimeHeaders, PartType};
use openpgp::{
parse::Parse,
serialize::stream,
types::{KeyFlags, SymmetricAlgorithm},
};
use rasn::types::{ObjectIdentifier, OctetString};
use rasn_cms::{
algorithms::{AES128_CBC, AES256_CBC, RSA},
pkcs7_compat::EncapsulatedContentInfo,
AlgorithmIdentifier, EncryptedContent, EncryptedContentInfo, EncryptedKey, EnvelopedData,
IssuerAndSerialNumber, KeyTransRecipientInfo, RecipientIdentifier, RecipientInfo, CONTENT_DATA,
CONTENT_ENVELOPED_DATA,
};
use rsa::{pkcs1::DecodeRsaPublicKey, Pkcs1v15Encrypt, RsaPublicKey};
use sequoia_openpgp as openpgp;
use store::rand::{rngs::StdRng, RngCore, SeedableRng};
use store::{
write::{Bincode, ToBitmaps},
Deserialize, Serialize,
};
const P: openpgp::policy::StandardPolicy<'static> = openpgp::policy::StandardPolicy::new();
#[derive(Debug)]
pub enum EncryptMessageError {
AlreadyEncrypted,
Error(String),
}
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
pub enum Algorithm {
Aes128,
Aes256,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum EncryptionMethod {
PGP,
SMIME,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct EncryptionParams {
pub method: EncryptionMethod,
pub algo: Algorithm,
pub certs: Vec<Vec<u8>>,
}
#[derive(Debug, serde::Serialize, serde::Deserialize, Default)]
#[serde(tag = "type")]
#[serde(rename_all = "camelCase")]
pub enum EncryptionType {
PGP {
algo: Algorithm,
certs: String,
},
SMIME {
algo: Algorithm,
certs: String,
},
#[default]
Disabled,
}
#[allow(async_fn_in_trait)]
pub trait EncryptMessage {
async fn encrypt(&self, params: &EncryptionParams) -> Result<Vec<u8>, EncryptMessageError>;
fn is_encrypted(&self) -> bool;
}
impl EncryptMessage for Message<'_> {
async fn encrypt(&self, params: &EncryptionParams) -> Result<Vec<u8>, EncryptMessageError> {
let root = self.root_part();
let raw_message = self.raw_message();
let mut outer_message = Vec::with_capacity((raw_message.len() as f64 * 1.5) as usize);
let mut inner_message = Vec::with_capacity(raw_message.len());
// Move MIME headers and body to inner message
for header in root.headers() {
(if header.name.is_mime_header() {
&mut inner_message
} else {
&mut outer_message
})
.extend_from_slice(&raw_message[header.offset_field()..header.offset_end()]);
}
inner_message.extend_from_slice(b"\r\n");
inner_message.extend_from_slice(&raw_message[root.raw_body_offset()..]);
// Encrypt inner message
match params.method {
EncryptionMethod::PGP => {
// Prepare encrypted message
let boundary = make_boundary("_");
outer_message.extend_from_slice(
concat!(
"Content-Type: multipart/encrypted;\r\n\t",
"protocol=\"application/pgp-encrypted\";\r\n\t",
"boundary=\""
)
.as_bytes(),
);
outer_message.extend_from_slice(boundary.as_bytes());
outer_message.extend_from_slice(
concat!(
"\"\r\n\r\n",
"OpenPGP/MIME message (Automatically encrypted by Stalwart)\r\n\r\n",
"--"
)
.as_bytes(),
);
outer_message.extend_from_slice(boundary.as_bytes());
outer_message.extend_from_slice(
concat!(
"\r\nContent-Type: application/pgp-encrypted\r\n\r\n",
"Version: 1\r\n\r\n--"
)
.as_bytes(),
);
outer_message.extend_from_slice(boundary.as_bytes());
outer_message.extend_from_slice(
concat!(
"\r\nContent-Type: application/octet-stream; name=\"encrypted.asc\"\r\n",
"Content-Disposition: inline; filename=\"encrypted.asc\"\r\n\r\n"
)
.as_bytes(),
);
let certs = params
.certs
.iter()
.map(openpgp::Cert::from_bytes)
.collect::<Result<Vec<_>, _>>()
.map_err(|err| {
EncryptMessageError::Error(format!(
"Failed to parse OpenPGP public key: {}",
err
))
})?;
// Encrypt contents (TODO: use rayon)
let algo = params.algo;
let encrypted_contents = tokio::task::spawn_blocking(move || {
// Parse public key
let mut keys = Vec::with_capacity(certs.len());
let policy = openpgp::policy::StandardPolicy::new();
for cert in &certs {
for key in cert
.keys()
.with_policy(&policy, None)
.supported()
.alive()
.revoked(false)
.key_flags(KeyFlags::empty().set_transport_encryption())
{
keys.push(key);
}
}
// Compose a writer stack corresponding to the output format and
// packet structure we want.
let mut sink = Vec::with_capacity(inner_message.len());
// Stream an OpenPGP message.
let message = stream::Armorer::new(stream::Message::new(&mut sink))
.build()
.map_err(|err| {
EncryptMessageError::Error(format!("Failed to create armorer: {}", err))
})?;
let message = stream::Encryptor2::for_recipients(message, keys)
.symmetric_algo(match algo {
Algorithm::Aes128 => SymmetricAlgorithm::AES128,
Algorithm::Aes256 => SymmetricAlgorithm::AES256,
})
.build()
.map_err(|err| {
EncryptMessageError::Error(format!(
"Failed to build encryptor: {}",
err
))
})?;
let mut message =
stream::LiteralWriter::new(message).build().map_err(|err| {
EncryptMessageError::Error(format!(
"Failed to create literal writer: {}",
err
))
})?;
std::io::copy(&mut Cursor::new(inner_message), &mut message).map_err(
|err| {
EncryptMessageError::Error(format!(
"Failed to encrypt message: {}",
err
))
},
)?;
message.finalize().map_err(|err| {
EncryptMessageError::Error(format!("Failed to finalize message: {}", err))
})?;
String::from_utf8(sink).map_err(|err| {
EncryptMessageError::Error(format!(
"Failed to convert encrypted message to UTF-8: {}",
err
))
})
})
.await
.map_err(|err| {
EncryptMessageError::Error(format!("Failed to encrypt message: {}", err))
})??;
outer_message.extend_from_slice(encrypted_contents.as_bytes());
outer_message.extend_from_slice(b"\r\n--");
outer_message.extend_from_slice(boundary.as_bytes());
outer_message.extend_from_slice(b"--\r\n");
}
EncryptionMethod::SMIME => {
// Generate random IV
let mut rng = StdRng::from_entropy();
let mut iv = vec![0u8; 16];
rng.fill_bytes(&mut iv);
// Generate random key
let mut key = vec![0u8; params.algo.key_size()];
rng.fill_bytes(&mut key);
// Encrypt contents (TODO: use rayon)
let algo = params.algo;
let (encrypted_contents, key, iv) = tokio::task::spawn_blocking(move || {
(algo.encrypt(&key, &iv, &inner_message), key, iv)
})
.await
.map_err(|err| {
EncryptMessageError::Error(format!("Failed to encrypt message: {}", err))
})?;
// Encrypt key using public keys
#[allow(clippy::mutable_key_type)]
let mut recipient_infos = BTreeSet::new();
for cert in &params.certs {
let cert =
rasn::der::decode::<rasn_pkix::Certificate>(cert).map_err(|err| {
EncryptMessageError::Error(format!(
"Failed to parse certificate: {}",
err
))
})?;
let public_key = RsaPublicKey::from_pkcs1_der(
cert.tbs_certificate
.subject_public_key_info
.subject_public_key
.as_raw_slice(),
)
.map_err(|err| {
EncryptMessageError::Error(format!("Failed to parse public key: {}", err))
})?;
let encrypted_key = public_key
.encrypt(&mut rng, Pkcs1v15Encrypt, &key[..])
.map_err(|err| {
EncryptMessageError::Error(format!("Failed to encrypt key: {}", err))
})
.unwrap();
recipient_infos.insert(RecipientInfo::KeyTransRecipientInfo(
KeyTransRecipientInfo {
version: 0.into(),
rid: RecipientIdentifier::IssuerAndSerialNumber(
IssuerAndSerialNumber {
issuer: cert.tbs_certificate.issuer,
serial_number: cert.tbs_certificate.serial_number,
},
),
key_encryption_algorithm: AlgorithmIdentifier {
algorithm: RSA.into(),
parameters: Some(
rasn::der::encode(&())
.map_err(|err| {
EncryptMessageError::Error(format!(
"Failed to encode RSA algorithm identifier: {}",
err
))
})?
.into(),
),
},
encrypted_key: EncryptedKey::from(encrypted_key),
},
));
}
let pkcs7 = rasn::der::encode(&EncapsulatedContentInfo {
content_type: CONTENT_ENVELOPED_DATA.into(),
content: Some(
rasn::der::encode(&EnvelopedData {
version: 0.into(),
originator_info: None,
recipient_infos,
encrypted_content_info: EncryptedContentInfo {
content_type: CONTENT_DATA.into(),
content_encryption_algorithm: AlgorithmIdentifier {
algorithm: params.algo.to_algorithm_identifier(),
parameters: Some(
rasn::der::encode(&OctetString::from(iv))
.map_err(|err| {
EncryptMessageError::Error(format!(
"Failed to encode IV: {}",
err
))
})?
.into(),
),
},
encrypted_content: Some(EncryptedContent::from(encrypted_contents)),
},
unprotected_attrs: None,
})
.map_err(|err| {
EncryptMessageError::Error(format!(
"Failed to encode EnvelopedData: {}",
err
))
})?
.into(),
),
})
.map_err(|err| {
EncryptMessageError::Error(format!("Failed to encode ContentInfo: {}", err))
})?;
// Generate message
outer_message.extend_from_slice(
concat!(
"Content-Type: application/pkcs7-mime;\r\n",
"\tname=\"smime.p7m\";\r\n",
"\tsmime-type=enveloped-data\r\n",
"Content-Disposition: attachment;\r\n",
"\tfilename=\"smime.p7m\"\r\n",
"Content-Transfer-Encoding: base64\r\n\r\n"
)
.as_bytes(),
);
base64_encode_mime(&pkcs7, &mut outer_message, false).map_err(|err| {
EncryptMessageError::Error(format!("Failed to base64 encode PKCS7: {}", err))
})?;
}
}
Ok(outer_message)
}
fn is_encrypted(&self) -> bool {
if self.content_type().is_some_and(|ct| {
let main_type = ct.c_type.as_ref();
let sub_type = ct
.c_subtype
.as_ref()
.map(|s| s.as_ref())
.unwrap_or_default();
(main_type.eq_ignore_ascii_case("application")
&& (sub_type.eq_ignore_ascii_case("pkcs7-mime")
|| sub_type.eq_ignore_ascii_case("pkcs7-signature")
|| (sub_type.eq_ignore_ascii_case("octet-stream")
&& self.attachment_name().is_some_and(|name| {
name.rsplit_once('.')
.is_some_and(|(_, ext)| ["p7m", "p7s", "p7c", "p7z"].contains(&ext))
}))))
|| (main_type.eq_ignore_ascii_case("multipart")
&& sub_type.eq_ignore_ascii_case("encrypted"))
}) {
return true;
}
if self.parts.len() <= 2 {
let mut text_part = None;
let mut is_multipart = false;
for part in &self.parts {
match &part.body {
PartType::Text(text) => {
text_part = Some(text.as_ref());
}
PartType::Multipart(_) => {
is_multipart = true;
}
_ => (),
}
}
match text_part {
Some(text) if self.parts.len() == 1 || is_multipart => {
if text.trim_start().starts_with("-----BEGIN PGP MESSAGE-----") {
return true;
}
}
_ => (),
}
}
false
}
}
impl Algorithm {
fn key_size(&self) -> usize {
match self {
Algorithm::Aes128 => 16,
Algorithm::Aes256 => 32,
}
}
fn to_algorithm_identifier(self) -> ObjectIdentifier {
match self {
Algorithm::Aes128 => AES128_CBC.into(),
Algorithm::Aes256 => AES256_CBC.into(),
}
}
fn encrypt(&self, key: &[u8], iv: &[u8], contents: &[u8]) -> Vec<u8> {
match self {
Algorithm::Aes128 => cbc::Encryptor::<aes::Aes128>::new(key.into(), iv.into())
.encrypt_padded_vec_mut::<Pkcs7>(contents),
Algorithm::Aes256 => cbc::Encryptor::<aes::Aes256>::new(key.into(), iv.into())
.encrypt_padded_vec_mut::<Pkcs7>(contents),
}
}
}
pub fn try_parse_certs(
expected_method: EncryptionMethod,
cert: Vec<u8>,
) -> Result<Vec<Vec<u8>>, Cow<'static, str>> {
// Check if it's a PEM file
let (method, certs) = if let Some(result) = try_parse_pem(&cert)? {
result
} else if rasn::der::decode::<rasn_pkix::Certificate>(&cert[..]).is_ok() {
(EncryptionMethod::SMIME, vec![cert])
} else if let Ok(cert_) = openpgp::Cert::from_bytes(&cert[..]) {
if !has_pgp_keys(cert_) {
(EncryptionMethod::PGP, vec![cert])
} else {
return Err("Could not find any suitable keys in certificate".into());
}
} else {
return Err("Could not find any valid certificates".into());
};
if method == expected_method {
Ok(certs)
} else {
Err("No valid certificates found for the selected encryption".into())
}
}
fn has_pgp_keys(cert: openpgp::Cert) -> bool {
cert.keys()
.with_policy(&P, None)
.supported()
.alive()
.revoked(false)
.key_flags(KeyFlags::empty().set_transport_encryption())
.next()
.is_some()
}
#[allow(clippy::type_complexity)]
fn try_parse_pem(
bytes_: &[u8],
) -> Result<Option<(EncryptionMethod, Vec<Vec<u8>>)>, Cow<'static, str>> {
if let Some(internal) = std::str::from_utf8(bytes_)
.ok()
.and_then(|cert| cert.strip_prefix("-----STALWART CERTIFICATE-----"))
{
return base64_decode(internal.as_bytes())
.ok_or(Cow::from("Failed to decode base64"))
.and_then(|bytes| {
Bincode::<EncryptionParams>::deserialize(&bytes)
.map_err(|_| Cow::from("Failed to deserialize internal certificate"))
})
.map(|params| Some((params.inner.method, params.inner.certs)));
}
let mut bytes = bytes_.iter().enumerate();
let mut buf = vec![];
let mut method = None;
let mut certs = vec![];
loop {
// Find start of PEM block
let mut start_pos = 0;
for (pos, &ch) in bytes.by_ref() {
if ch.is_ascii_whitespace() {
continue;
} else if ch == b'-' {
start_pos = pos;
break;
} else {
return Ok(None);
}
}
// Find block type
for (_, &ch) in bytes.by_ref() {
match ch {
b'-' => (),
b'\n' => break,
_ => {
if ch.is_ascii() {
buf.push(ch.to_ascii_uppercase());
} else {
return Ok(None);
}
}
}
}
if buf.is_empty() {
break;
}
// Find type
let tag = std::str::from_utf8(&buf).unwrap();
if tag.contains("CERTIFICATE") {
if method.is_some_and(|m| m == EncryptionMethod::PGP) {
return Err("Cannot mix OpenPGP and S/MIME certificates".into());
} else {
method = Some(EncryptionMethod::SMIME);
}
} else if tag.contains("PGP") {
if method.is_some_and(|m| m == EncryptionMethod::SMIME) {
return Err("Cannot mix OpenPGP and S/MIME certificates".into());
} else {
method = Some(EncryptionMethod::PGP);
}
} else {
// Ignore block
let mut found_end = false;
for (_, &ch) in bytes.by_ref() {
if ch == b'-' {
found_end = true;
} else if ch == b'\n' && found_end {
break;
}
}
buf.clear();
continue;
}
// Collect base64
buf.clear();
let mut found_end = false;
let mut end_pos = 0;
for (pos, &ch) in bytes.by_ref() {
match ch {
b'-' => {
found_end = true;
}
b'\n' => {
if found_end {
end_pos = pos;
break;
}
}
_ => {
if !ch.is_ascii_whitespace() {
buf.push(ch);
}
}
}
}
// Decode base64
let cert =
base64_decode(&buf).ok_or_else(|| Cow::from("Failed to decode base64 certificate."))?;
match method.unwrap() {
EncryptionMethod::PGP => match openpgp::Cert::from_bytes(bytes_) {
Ok(cert) => {
if !has_pgp_keys(cert) {
return Err("Could not find any suitable keys in OpenPGP public key".into());
}
certs.push(
bytes_
.get(start_pos..end_pos + 1)
.unwrap_or_default()
.to_vec(),
);
}
Err(err) => {
return Err(format!("Failed to decode OpenPGP public key: {err}").into())
}
},
EncryptionMethod::SMIME => {
if let Err(err) = rasn::der::decode::<rasn_pkix::Certificate>(&cert) {
return Err(format!("Failed to decode X509 certificate: {err}").into());
}
certs.push(cert);
}
}
buf.clear();
}
Ok(method.map(|method| (method, certs)))
}
impl Serialize for &EncryptionParams {
fn serialize(self) -> Vec<u8> {
let len = bincode::serialized_size(&self).unwrap_or_default();
let mut buf = Vec::with_capacity(len as usize + 1);
buf.push(1);
let _ = bincode::serialize_into(&mut buf, &self);
buf
}
}
impl Deserialize for EncryptionParams {
fn deserialize(bytes: &[u8]) -> trc::Result<Self> {
let version = *bytes
.first()
.ok_or_else(|| trc::StoreEvent::DataCorruption.caused_by(trc::location!()))?;
match version {
1 if bytes.len() > 1 => bincode::deserialize(&bytes[1..]).map_err(|err| {
trc::EventType::Store(trc::StoreEvent::DeserializeError)
.from_bincode_error(err)
.caused_by(trc::location!())
}),
_ => Err(trc::StoreEvent::DeserializeError
.into_err()
.caused_by(trc::location!())
.ctx(trc::Key::Value, version as u64)),
}
}
}
impl ToBitmaps for &EncryptionParams {
fn to_bitmaps(&self, _: &mut Vec<store::write::Operation>, _: u8, _: bool) {
unreachable!()
}
}
impl Display for EncryptionMethod {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EncryptionMethod::PGP => write!(f, "OpenPGP"),
EncryptionMethod::SMIME => write!(f, "S/MIME"),
}
}
}
impl Display for Algorithm {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Algorithm::Aes128 => write!(f, "AES-128"),
Algorithm::Aes256 => write!(f, "AES-256"),
}
}
}
@@ -4,36 +4,128 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use common::{
ipc::{DeliveryResult, IngestMessage},
Server,
};
use common::Server;
use directory::Permission;
use jmap_proto::types::{state::StateChange, type_state::DataType};
use mail_parser::MessageParser;
use std::future::Future;
use std::{borrow::Cow, future::Future};
use store::ahash::AHashMap;
use utils::BlobHash;
use crate::{
email::{
bayes::EmailBayesTrain,
ingest::{EmailIngest, IngestEmail, IngestSource},
},
ingest::{EmailIngest, IngestEmail, IngestSource},
mailbox::INBOX_ID,
sieve::{get::SieveScriptGet, ingest::SieveScriptIngest},
sieve::SieveScriptIngest,
};
use super::state::StateManager;
#[derive(Debug)]
pub struct IngestMessage {
pub sender_address: String,
pub recipients: Vec<String>,
pub message_blob: BlobHash,
pub message_size: usize,
pub session_id: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LocalDeliveryStatus {
Success,
TemporaryFailure {
reason: Cow<'static, str>,
},
PermanentFailure {
code: [u8; 3],
reason: Cow<'static, str>,
},
}
pub struct LocalDeliveryResult {
pub status: Vec<LocalDeliveryStatus>,
pub autogenerated: Vec<AutogeneratedMessage>,
}
pub struct AutogeneratedMessage {
pub sender_address: String,
pub recipients: Vec<String>,
pub message: Vec<u8>,
}
pub trait MailDelivery: Sync + Send {
fn deliver_message(
&self,
message: IngestMessage,
) -> impl Future<Output = Vec<DeliveryResult>> + Send;
) -> impl Future<Output = LocalDeliveryResult> + Send;
}
/*
let semaphore = Arc::new(Semaphore::new(
inner
.shared_core
.load()
.smtp
.queue
.throttle
.local_concurrency,
));
loop {
let permit = match semaphore.clone().acquire_owned().await {
Ok(permit) => permit,
Err(_) => {
trc::error!(trc::StoreEvent::UnexpectedError
.into_err()
.details("Semaphore error")
.caused_by(trc::location!()));
break;
}
};
match delivery_rx.recv().await {
Some(event) => match event {
DeliveryEvent::Ingest { message, result_tx } => {
let server = inner.build_server();
tokio::spawn(async move {
result_tx.send(server.deliver_message(message).await).ok();
drop(permit);
});
}
DeliveryEvent::Stop => break,
},
None => {
break;
}
}
}
*/
impl MailDelivery for Server {
async fn deliver_message(&self, message: IngestMessage) -> Vec<DeliveryResult> {
async fn deliver_message(&self, message: IngestMessage) -> LocalDeliveryResult {
// Obtain permit
let _permit = match self.inner.ipc.local_delivery_sm.acquire().await {
Ok(permit) => permit,
Err(_) => {
trc::error!(
trc::Error::new(trc::EventType::Server(trc::ServerEvent::ThreadError))
.details("Failed to obtain semaphore permit.")
.span_id(message.session_id)
.caused_by(trc::location!())
);
return LocalDeliveryResult {
status: (0..message.recipients.len())
.map(|_| LocalDeliveryStatus::TemporaryFailure {
reason: "Temporary I/O error.".into(),
})
.collect::<Vec<_>>(),
autogenerated: vec![],
};
}
};
// Read message
let raw_message = match self
.core
@@ -51,11 +143,14 @@ impl MailDelivery for Server {
CausedBy = trc::location!()
);
return (0..message.recipients.len())
.map(|_| DeliveryResult::TemporaryFailure {
reason: "Blob not found.".into(),
})
.collect::<Vec<_>>();
return LocalDeliveryResult {
status: (0..message.recipients.len())
.map(|_| LocalDeliveryStatus::TemporaryFailure {
reason: "Blob not found.".into(),
})
.collect::<Vec<_>>(),
autogenerated: vec![],
};
}
Err(err) => {
trc::error!(err
@@ -63,17 +158,24 @@ impl MailDelivery for Server {
.span_id(message.session_id)
.caused_by(trc::location!()));
return (0..message.recipients.len())
.map(|_| DeliveryResult::TemporaryFailure {
reason: "Temporary I/O error.".into(),
})
.collect::<Vec<_>>();
return LocalDeliveryResult {
status: (0..message.recipients.len())
.map(|_| LocalDeliveryStatus::TemporaryFailure {
reason: "Temporary I/O error.".into(),
})
.collect::<Vec<_>>(),
autogenerated: vec![],
};
}
};
// Obtain the UIDs for each recipient
let mut uids: AHashMap<u32, usize> = AHashMap::with_capacity(message.recipients.len());
let mut results = Vec::with_capacity(message.recipients.len());
let mut result = LocalDeliveryResult {
status: Vec::with_capacity(message.recipients.len()),
autogenerated: Vec::new(),
};
for rcpt in message.recipients {
let uid = match self
.email_to_id(&self.core.storage.directory, &rcpt, message.session_id)
@@ -82,7 +184,7 @@ impl MailDelivery for Server {
Ok(Some(uid)) => uid,
Ok(None) => {
// Something went wrong
results.push(DeliveryResult::PermanentFailure {
result.status.push(LocalDeliveryStatus::PermanentFailure {
code: [5, 5, 0],
reason: "Mailbox not found.".into(),
});
@@ -94,19 +196,19 @@ impl MailDelivery for Server {
.ctx(trc::Key::To, rcpt)
.span_id(message.session_id)
.caused_by(trc::location!()));
results.push(DeliveryResult::TemporaryFailure {
result.status.push(LocalDeliveryStatus::TemporaryFailure {
reason: "Address lookup failed.".into(),
});
continue;
}
};
if let Some(result) = uids.get(&uid).and_then(|pos| results.get(*pos)) {
results.push(result.clone());
if let Some(status) = uids.get(&uid).and_then(|pos| result.status.get(*pos)) {
result.status.push(status.clone());
continue;
}
// Obtain access token
let result = match self.get_access_token(uid).await.and_then(|token| {
let status = match self.get_access_token(uid).await.and_then(|token| {
token
.assert_has_permission(Permission::EmailReceive)
.map(|_| token)
@@ -114,17 +216,6 @@ impl MailDelivery for Server {
Ok(access_token) => {
// Check if there is an active sieve script
match self.sieve_script_get_active(uid).await {
Ok(Some(active_script)) => {
self.sieve_script_ingest(
&access_token,
&raw_message,
&message.sender_address,
&rcpt,
message.session_id,
active_script,
)
.await
}
Ok(None) => {
// Ingest message
self.email_ingest(IngestEmail {
@@ -142,6 +233,18 @@ impl MailDelivery for Server {
})
.await
}
Ok(Some(active_script)) => {
self.sieve_script_ingest(
&access_token,
&raw_message,
&message.sender_address,
&rcpt,
message.session_id,
active_script,
&mut result.autogenerated,
)
.await
}
Err(err) => Err(err),
}
}
@@ -149,7 +252,7 @@ impl MailDelivery for Server {
Err(err) => Err(err),
};
let result = match result {
let status = match status {
Ok(ingested_message) => {
// Notify state change
if ingested_message.change_id != u64::MAX {
@@ -163,28 +266,28 @@ impl MailDelivery for Server {
.await;
}
DeliveryResult::Success
LocalDeliveryStatus::Success
}
Err(err) => {
let result = match err.as_ref() {
let status = match err.as_ref() {
trc::EventType::Limit(trc::LimitEvent::Quota) => {
DeliveryResult::TemporaryFailure {
LocalDeliveryStatus::TemporaryFailure {
reason: "Mailbox over quota.".into(),
}
}
trc::EventType::Limit(trc::LimitEvent::TenantQuota) => {
DeliveryResult::TemporaryFailure {
LocalDeliveryStatus::TemporaryFailure {
reason: "Organization over quota.".into(),
}
}
trc::EventType::Security(trc::SecurityEvent::Unauthorized) => {
DeliveryResult::PermanentFailure {
LocalDeliveryStatus::PermanentFailure {
code: [5, 5, 0],
reason: "This account is not authorized to receive email.".into(),
}
}
trc::EventType::MessageIngest(trc::MessageIngestEvent::Error) => {
DeliveryResult::PermanentFailure {
LocalDeliveryStatus::PermanentFailure {
code: err
.value(trc::Key::Code)
.and_then(|v| v.to_uint())
@@ -199,7 +302,7 @@ impl MailDelivery for Server {
.into(),
}
}
_ => DeliveryResult::TemporaryFailure {
_ => LocalDeliveryStatus::TemporaryFailure {
reason: "Transient server failure.".into(),
},
};
@@ -208,16 +311,16 @@ impl MailDelivery for Server {
.ctx(trc::Key::To, rcpt.to_string())
.span_id(message.session_id));
result
status
}
};
// Cache response for UID to avoid duplicate deliveries
uids.insert(uid, results.len());
uids.insert(uid, result.status.len());
results.push(result);
result.status.push(status);
}
results
result
}
}
@@ -10,7 +10,11 @@ use std::{
time::{Duration, Instant},
};
use common::{auth::ResourceToken, Server};
use common::{
auth::{AccessToken, ResourceToken},
Server,
};
use directory::Permission;
use jmap_proto::{
object::Object,
types::{
@@ -23,11 +27,11 @@ use mail_parser::{
PartType,
};
use rand::Rng;
use spam_filter::{
analysis::init::SpamFilterInit, modules::bayes::BayesClassifier, SpamFilterInput,
};
use std::future::Future;
use store::rand::Rng;
use store::{
ahash::AHashSet,
query::Filter,
@@ -42,12 +46,8 @@ use trc::{AddContext, MessageIngestEvent};
use utils::map::vec_map::VecMap;
use crate::{
blob::upload::BlobUpload,
changes::write::ChangeLog,
email::index::{IndexMessage, VisitValues, MAX_ID_LENGTH},
index::{IndexMessage, VisitValues, MAX_ID_LENGTH},
mailbox::{UidMailbox, INBOX_ID, JUNK_ID},
services::index::Indexer,
JmapMethods,
};
use super::{
@@ -104,6 +104,7 @@ pub trait EmailIngest: Sync + Send {
account_id: u32,
mailbox_id: u32,
) -> impl Future<Output = trc::Result<u32>> + Send;
fn email_bayes_can_train(&self, access_token: &AccessToken) -> bool;
}
impl EmailIngest for Server {
@@ -437,7 +438,6 @@ impl EmailIngest for Server {
// Obtain a documentId and changeId
let change_id = self
.assign_change_id(account_id)
.await
.caused_by(trc::location!())?;
// Store blob
@@ -646,7 +646,6 @@ impl EmailIngest for Server {
let mut batch = BatchBuilder::new();
let change_id = self
.assign_change_id(account_id)
.await
.caused_by(trc::location!())?;
let mut changes = ChangeLogBuilder::with_change_id(change_id);
batch
@@ -702,7 +701,7 @@ impl EmailIngest for Server {
match self.core.storage.data.write(batch.build()).await {
Ok(_) => return Ok(Some(thread_id)),
Err(err) if err.is_assertion_failure() && try_count < MAX_RETRIES => {
let backoff = rand::thread_rng().gen_range(50..=300);
let backoff = store::rand::thread_rng().gen_range(50..=300);
tokio::time::sleep(Duration::from_millis(backoff)).await;
try_count += 1;
}
@@ -728,6 +727,12 @@ impl EmailIngest for Server {
.await
.and_then(|v| v.last_counter_id().map(|id| id as u32))
}
fn email_bayes_can_train(&self, access_token: &AccessToken) -> bool {
self.core.spam.bayes.as_ref().is_some_and(|bayes| {
bayes.account_classify && access_token.has_permission(Permission::SpamFilterTrain)
})
}
}
pub struct LogEmailInsert(Option<u32>);
+14
View File
@@ -0,0 +1,14 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
pub mod cache;
pub mod crypto;
pub mod delivery;
pub mod index;
pub mod ingest;
pub mod mailbox;
pub mod metadata;
pub mod sieve;
+495
View File
@@ -0,0 +1,495 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::{future::Future, slice::Iter};
use common::{config::jmap::settings::SpecialUse, Server};
use jmap_proto::{
object::{
index::{IndexAs, IndexProperty, ObjectIndexBuilder},
Object,
},
types::{collection::Collection, id::Id, keyword::Keyword, property::Property, value::Value},
};
use store::{
ahash::AHashSet,
query::Filter,
rand,
roaring::RoaringBitmap,
write::{
BatchBuilder, BitmapClass, DeserializeFrom, MaybeDynamicId, Operation, SerializeInto,
TagValue, ToBitmaps,
},
Serialize, U32_LEN,
};
use trc::AddContext;
use utils::codec::leb128::{Leb128Iterator, Leb128Vec};
use crate::cache::ThreadCache;
pub const INBOX_ID: u32 = 0;
pub const TRASH_ID: u32 = 1;
pub const JUNK_ID: u32 = 2;
pub const DRAFTS_ID: u32 = 3;
pub const SENT_ID: u32 = 4;
pub const ARCHIVE_ID: u32 = 5;
pub const TOMBSTONE_ID: u32 = u32::MAX - 1;
#[derive(Debug)]
pub struct ExpandPath<'x> {
pub path: Vec<&'x str>,
pub found_names: Vec<(String, u32, u32)>,
}
pub static SCHEMA: &[IndexProperty] = &[
IndexProperty::new(Property::Name)
.index_as(IndexAs::Text {
tokenize: true,
index: true,
})
.required(),
IndexProperty::new(Property::Role).index_as(IndexAs::Text {
tokenize: false,
index: true,
}),
IndexProperty::new(Property::Role).index_as(IndexAs::HasProperty),
IndexProperty::new(Property::ParentId).index_as(IndexAs::Integer),
IndexProperty::new(Property::SortOrder).index_as(IndexAs::Integer),
IndexProperty::new(Property::IsSubscribed).index_as(IndexAs::IntegerList),
IndexProperty::new(Property::Acl).index_as(IndexAs::Acl),
];
#[derive(Debug, Clone, Copy)]
pub struct UidMailbox {
pub mailbox_id: u32,
pub uid: u32,
}
pub trait MailboxFnc: Sync + Send {
fn mailbox_get_or_create(
&self,
account_id: u32,
) -> impl Future<Output = trc::Result<RoaringBitmap>> + Send;
fn mailbox_create_path(
&self,
account_id: u32,
path: &str,
) -> impl Future<Output = trc::Result<Option<(u32, Option<u64>)>>> + Send;
fn mailbox_count_threads(
&self,
account_id: u32,
document_ids: Option<RoaringBitmap>,
) -> impl Future<Output = trc::Result<usize>> + Send;
fn mailbox_unread_tags(
&self,
account_id: u32,
document_id: u32,
message_ids: &Option<RoaringBitmap>,
) -> impl Future<Output = trc::Result<Option<RoaringBitmap>>> + Send;
fn mailbox_expand_path<'x>(
&self,
account_id: u32,
path: &'x str,
exact_match: bool,
) -> impl Future<Output = trc::Result<Option<ExpandPath<'x>>>> + Send;
fn mailbox_get_by_name(
&self,
account_id: u32,
path: &str,
) -> impl Future<Output = trc::Result<Option<u32>>> + Send;
fn mailbox_get_by_role(
&self,
account_id: u32,
role: &str,
) -> impl Future<Output = trc::Result<Option<u32>>> + Send;
}
impl MailboxFnc for Server {
async fn mailbox_get_or_create(&self, account_id: u32) -> trc::Result<RoaringBitmap> {
let mut mailbox_ids = self
.get_document_ids(account_id, Collection::Mailbox)
.await?
.unwrap_or_default();
if !mailbox_ids.is_empty() {
return Ok(mailbox_ids);
}
#[cfg(feature = "test_mode")]
if mailbox_ids.is_empty() && account_id == 0 {
return Ok(mailbox_ids);
}
let mut batch = BatchBuilder::new();
batch
.with_account_id(account_id)
.with_collection(Collection::Mailbox);
// Create mailboxes
let mut last_document_id = ARCHIVE_ID;
for folder in &self.core.jmap.default_folders {
let (role, document_id) = match folder.special_use {
SpecialUse::Inbox => ("inbox", INBOX_ID),
SpecialUse::Trash => ("trash", TRASH_ID),
SpecialUse::Junk => ("junk", JUNK_ID),
SpecialUse::Drafts => ("drafts", DRAFTS_ID),
SpecialUse::Sent => ("sent", SENT_ID),
SpecialUse::Archive => ("archive", ARCHIVE_ID),
SpecialUse::None => {
last_document_id += 1;
("", last_document_id)
}
SpecialUse::Shared => unreachable!(),
};
let mut object = Object::with_capacity(4)
.with_property(Property::Name, folder.name.clone())
.with_property(Property::ParentId, Value::Id(0u64.into()))
.with_property(
Property::Cid,
Value::UnsignedInt(rand::random::<u32>() as u64),
);
if !role.is_empty() {
object.set(Property::Role, role);
}
if folder.subscribe {
object.set(
Property::IsSubscribed,
Value::List(vec![Value::Id(account_id.into())]),
);
}
batch
.create_document_with_id(document_id)
.custom(ObjectIndexBuilder::new(SCHEMA).with_changes(object));
mailbox_ids.insert(document_id);
}
self.core
.storage
.data
.write(batch.build())
.await
.caused_by(trc::location!())
.map(|_| mailbox_ids)
}
async fn mailbox_create_path(
&self,
account_id: u32,
path: &str,
) -> trc::Result<Option<(u32, Option<u64>)>> {
let expanded_path =
if let Some(expand_path) = self.mailbox_expand_path(account_id, path, false).await? {
expand_path
} else {
return Ok(None);
};
let mut next_parent_id = 0;
let mut path = expanded_path.path.into_iter().enumerate().peekable();
'outer: while let Some((pos, name)) = path.peek() {
let is_inbox = *pos == 0 && name.eq_ignore_ascii_case("inbox");
for (part, parent_id, document_id) in &expanded_path.found_names {
if (part.eq(name) || (is_inbox && part.eq_ignore_ascii_case("inbox")))
&& *parent_id == next_parent_id
{
next_parent_id = *document_id;
path.next();
continue 'outer;
}
}
break;
}
// Create missing folders
if path.peek().is_some() {
let mut changes = self.begin_changes(account_id)?;
for (_, name) in path {
if name.len() > self.core.jmap.mailbox_name_max_len {
return Ok(None);
}
let mut batch = BatchBuilder::new();
batch
.with_account_id(account_id)
.with_collection(Collection::Mailbox)
.create_document()
.custom(
ObjectIndexBuilder::new(SCHEMA).with_changes(
Object::with_capacity(3)
.with_property(Property::Name, name)
.with_property(
Property::ParentId,
Value::Id(Id::from(next_parent_id)),
)
.with_property(
Property::Cid,
Value::UnsignedInt(rand::random::<u32>() as u64),
),
),
);
let document_id = self
.store()
.write_expect_id(batch)
.await
.caused_by(trc::location!())?;
changes.log_insert(Collection::Mailbox, document_id);
next_parent_id = document_id + 1;
}
let change_id = changes.change_id;
let mut batch = BatchBuilder::new();
batch
.with_account_id(account_id)
.with_collection(Collection::Mailbox)
.custom(changes);
self.store()
.write(batch.build())
.await
.caused_by(trc::location!())?;
Ok(Some((next_parent_id - 1, Some(change_id))))
} else {
Ok(Some((next_parent_id - 1, None)))
}
}
async fn mailbox_count_threads(
&self,
account_id: u32,
document_ids: Option<RoaringBitmap>,
) -> trc::Result<usize> {
if let Some(document_ids) = document_ids {
let mut thread_ids = AHashSet::default();
self.get_cached_thread_ids(account_id, document_ids.into_iter())
.await
.caused_by(trc::location!())?
.into_iter()
.for_each(|(_, thread_id)| {
thread_ids.insert(thread_id);
});
Ok(thread_ids.len())
} else {
Ok(0)
}
}
async fn mailbox_unread_tags(
&self,
account_id: u32,
document_id: u32,
message_ids: &Option<RoaringBitmap>,
) -> trc::Result<Option<RoaringBitmap>> {
if let (Some(message_ids), Some(mailbox_message_ids)) = (
message_ids,
self.get_tag(
account_id,
Collection::Email,
Property::MailboxIds,
document_id,
)
.await?,
) {
if let Some(mut seen) = self
.get_tag(
account_id,
Collection::Email,
Property::Keywords,
Keyword::Seen,
)
.await?
{
seen ^= message_ids;
seen &= &mailbox_message_ids;
if !seen.is_empty() {
Ok(Some(seen))
} else {
Ok(None)
}
} else {
Ok(mailbox_message_ids.into())
}
} else {
Ok(None)
}
}
async fn mailbox_expand_path<'x>(
&self,
account_id: u32,
path: &'x str,
exact_match: bool,
) -> trc::Result<Option<ExpandPath<'x>>> {
let path = path
.split('/')
.filter_map(|p| {
let p = p.trim();
if !p.is_empty() {
p.into()
} else {
None
}
})
.collect::<Vec<_>>();
if path.is_empty() || path.len() > self.core.jmap.mailbox_max_depth {
return Ok(None);
}
let mut filter = Vec::with_capacity(path.len() + 2);
let mut has_inbox = false;
filter.push(Filter::Or);
for (pos, item) in path.iter().enumerate() {
if pos == 0 && item.eq_ignore_ascii_case("inbox") {
has_inbox = true;
} else {
filter.push(Filter::eq(Property::Name, *item));
}
}
filter.push(Filter::End);
let mut document_ids = if filter.len() > 2 {
self.store()
.filter(account_id, Collection::Mailbox, filter)
.await
.caused_by(trc::location!())?
.results
} else {
RoaringBitmap::new()
};
if has_inbox {
document_ids.insert(INBOX_ID);
}
if exact_match && (document_ids.len() as usize) < path.len() {
return Ok(None);
}
let mut found_names = Vec::new();
for document_id in document_ids {
if let Some(mut obj) = self
.get_property::<Object<Value>>(
account_id,
Collection::Mailbox,
document_id,
Property::Value,
)
.await?
{
if let Some(Value::Text(value)) = obj.properties.remove(&Property::Name) {
found_names.push((
value,
if let Some(Value::Id(value)) = obj.properties.remove(&Property::ParentId) {
value.document_id()
} else {
0
},
document_id + 1,
));
} else {
return Ok(None);
}
} else {
return Ok(None);
}
}
Ok(Some(ExpandPath { path, found_names }))
}
async fn mailbox_get_by_name(&self, account_id: u32, path: &str) -> trc::Result<Option<u32>> {
Ok(self
.mailbox_expand_path(account_id, path, true)
.await?
.and_then(|ep| {
let mut next_parent_id = 0;
'outer: for (pos, name) in ep.path.iter().enumerate() {
let is_inbox = pos == 0 && name.eq_ignore_ascii_case("inbox");
for (part, parent_id, document_id) in &ep.found_names {
if (part.eq(name) || (is_inbox && part.eq_ignore_ascii_case("inbox")))
&& *parent_id == next_parent_id
{
next_parent_id = *document_id;
continue 'outer;
}
}
return None;
}
Some(next_parent_id - 1)
}))
}
async fn mailbox_get_by_role(&self, account_id: u32, role: &str) -> trc::Result<Option<u32>> {
self.store()
.filter(
account_id,
Collection::Mailbox,
vec![Filter::eq(Property::Role, role)],
)
.await
.caused_by(trc::location!())
.map(|r| r.results.min())
}
}
impl PartialEq for UidMailbox {
fn eq(&self, other: &Self) -> bool {
self.mailbox_id == other.mailbox_id
}
}
impl Eq for UidMailbox {}
impl ToBitmaps for UidMailbox {
fn to_bitmaps(&self, ops: &mut Vec<Operation>, field: u8, set: bool) {
ops.push(Operation::Bitmap {
class: BitmapClass::Tag {
field,
value: TagValue::Id(MaybeDynamicId::Static(self.mailbox_id)),
},
set,
});
}
}
impl SerializeInto for UidMailbox {
fn serialize_into(&self, buf: &mut Vec<u8>) {
buf.push_leb128(self.mailbox_id);
buf.push_leb128(self.uid);
}
}
impl DeserializeFrom for UidMailbox {
fn deserialize_from(bytes: &mut Iter<'_, u8>) -> Option<Self> {
Some(UidMailbox {
mailbox_id: bytes.next_leb128()?,
uid: bytes.next_leb128()?,
})
}
}
impl Serialize for UidMailbox {
fn serialize(self) -> Vec<u8> {
let mut buf = Vec::with_capacity(U32_LEN * 2);
self.serialize_into(&mut buf);
buf
}
}
impl UidMailbox {
pub fn new(mailbox_id: u32, uid: u32) -> Self {
UidMailbox { mailbox_id, uid }
}
pub fn new_unassigned(mailbox_id: u32) -> Self {
UidMailbox { mailbox_id, uid: 0 }
}
}
@@ -4,33 +4,31 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::borrow::Cow;
use std::{borrow::Cow, sync::Arc};
use common::{
auth::AccessToken, listener::stream::NullIo, scripts::plugins::PluginContext, Server,
use crate::{
delivery::AutogeneratedMessage,
ingest::{EmailIngest, IngestEmail, IngestSource, IngestedEmail},
mailbox::{MailboxFnc, INBOX_ID, TRASH_ID},
};
use common::{auth::AccessToken, scripts::plugins::PluginContext, Server};
use directory::{backend::internal::PrincipalField, Permission, QueryBy};
use jmap_proto::types::{collection::Collection, id::Id, keyword::Keyword, property::Property};
use jmap_proto::{
object::Object,
types::{collection::Collection, id::Id, keyword::Keyword, property::Property, value::Value},
};
use mail_parser::MessageParser;
use sieve::{Envelope, Event, Input, Mailbox, Recipient};
use smtp::core::{Session, SessionAddress};
use serde::ser::SerializeSeq;
use sieve::{Envelope, Event, Input, Mailbox, Recipient, Sieve};
use store::{
ahash::AHashSet,
write::{now, BatchBuilder, Bincode, F_VALUE},
blake3,
query::Filter,
write::{assert::HashedValue, now, BatchBuilder, Bincode, BlobOp, F_VALUE},
Deserialize, Serialize,
};
use trc::{AddContext, SieveEvent};
use crate::{
email::{
bayes::EmailBayesTrain,
ingest::{EmailIngest, IngestEmail, IngestSource, IngestedEmail},
},
mailbox::{get::MailboxGet, set::MailboxSet, INBOX_ID, TRASH_ID},
sieve::SeenIdHash,
JmapMethods,
};
use super::{get::SieveScriptGet, ActiveScript};
use std::future::Future;
struct SieveMessage<'x> {
@@ -39,7 +37,27 @@ struct SieveMessage<'x> {
pub flags: Vec<Keyword>,
}
pub struct ActiveScript {
pub document_id: u32,
pub script_name: String,
pub script: Arc<Sieve>,
pub seen_ids: SeenIds,
}
#[derive(Debug, Clone)]
pub struct SeenIdHash {
hash: [u8; 32],
expiry: u64,
}
#[derive(Debug, Clone, Default)]
pub struct SeenIds {
pub ids: AHashSet<SeenIdHash>,
pub has_changes: bool,
}
pub trait SieveScriptIngest: Sync + Send {
#[allow(clippy::too_many_arguments)]
fn sieve_script_ingest(
&self,
access_token: &AccessToken,
@@ -48,7 +66,25 @@ pub trait SieveScriptIngest: Sync + Send {
envelope_to: &str,
session_id: u64,
active_script: ActiveScript,
autogenerated: &mut Vec<AutogeneratedMessage>,
) -> impl Future<Output = trc::Result<IngestedEmail>> + Send;
fn sieve_script_get_active(
&self,
account_id: u32,
) -> impl Future<Output = trc::Result<Option<ActiveScript>>> + Send;
fn sieve_script_get_by_name(
&self,
account_id: u32,
name: &str,
) -> impl Future<Output = trc::Result<Option<Sieve>>> + Send;
fn sieve_script_compile(
&self,
account_id: u32,
document_id: u32,
) -> impl Future<Output = trc::Result<(Sieve, Object<Value>)>> + Send;
}
impl SieveScriptIngest for Server {
@@ -61,6 +97,7 @@ impl SieveScriptIngest for Server {
envelope_to: &str,
session_id: u64,
mut active_script: ActiveScript,
autogenerated: &mut Vec<AutogeneratedMessage>,
) -> trc::Result<IngestedEmail> {
// Parse message
let message = if let Some(message) = MessageParser::new().parse(raw_message) {
@@ -347,10 +384,8 @@ impl SieveScriptIngest for Server {
input = true.into();
if let Some(message) = messages.get(message_id) {
let recipients = match recipient {
Recipient::Address(rcpt) => vec![SessionAddress::new(rcpt)],
Recipient::Group(rcpts) => {
rcpts.into_iter().map(SessionAddress::new).collect()
}
Recipient::Address(rcpt) => vec![rcpt],
Recipient::Group(rcpts) => rcpts,
Recipient::List(_) => {
// Not yet implemented
continue;
@@ -363,28 +398,24 @@ impl SieveScriptIngest for Server {
From = mail_from.clone(),
To = recipients
.iter()
.map(|r| trc::Value::String(r.address_lcase.clone()))
.map(|r| trc::Value::String(r.clone()))
.collect::<Vec<_>>(),
Size = message.raw_message.len(),
SpanId = session_id
);
Session::<NullIo>::sieve(
self.clone(),
SessionAddress::new(mail_from.clone()),
autogenerated.push(AutogeneratedMessage {
sender_address: mail_from.clone(),
recipients,
message.raw_message.to_vec(),
0,
)
.queue_message()
.await;
message: message.raw_message.to_vec(),
});
} else {
trc::event!(
Sieve(SieveEvent::MessageTooLarge),
From = mail_from.clone(),
To = recipients
.iter()
.map(|r| trc::Value::String(r.address_lcase.clone()))
.map(|r| trc::Value::String(r.clone()))
.collect::<Vec<_>>(),
Size = message.raw_message.len(),
Limit = self.core.jmap.mail_max_size,
@@ -521,7 +552,9 @@ impl SieveScriptIngest for Server {
Bincode::new(active_script.seen_ids),
F_VALUE,
);
let _ = self.write_batch(batch).await;
if let Err(err) = self.store().write(batch).await.caused_by(trc::location!()) {
trc::error!(err.details("Failed to save Sieve seen ids changes."));
}
}
if let Some(reject_reason) = reject_reason {
@@ -538,6 +571,189 @@ impl SieveScriptIngest for Server {
Err(last_temp_error.unwrap())
}
}
async fn sieve_script_get_active(&self, account_id: u32) -> trc::Result<Option<ActiveScript>> {
// Find the currently active script
if let Some(document_id) = self
.store()
.filter(
account_id,
Collection::SieveScript,
vec![Filter::eq(Property::IsActive, 1u32)],
)
.await
.caused_by(trc::location!())?
.results
.min()
{
let (script, mut script_object) =
self.sieve_script_compile(account_id, document_id).await?;
Ok(Some(ActiveScript {
document_id,
script: Arc::new(script),
script_name: script_object
.properties
.remove(&Property::Name)
.and_then(|name| name.try_unwrap_string())
.unwrap_or_else(|| account_id.to_string()),
seen_ids: self
.get_property::<Bincode<SeenIds>>(
account_id,
Collection::SieveScript,
document_id,
Property::EmailIds,
)
.await?
.map(|seen_ids| seen_ids.inner)
.unwrap_or_default(),
}))
} else {
Ok(None)
}
}
async fn sieve_script_get_by_name(
&self,
account_id: u32,
name: &str,
) -> trc::Result<Option<Sieve>> {
// Find the script by name
if let Some(document_id) = self
.store()
.filter(
account_id,
Collection::SieveScript,
vec![Filter::eq(Property::Name, name)],
)
.await
.caused_by(trc::location!())?
.results
.min()
{
self.sieve_script_compile(account_id, document_id)
.await
.map(|(sieve, _)| Some(sieve))
} else {
Ok(None)
}
}
#[allow(clippy::blocks_in_conditions)]
async fn sieve_script_compile(
&self,
account_id: u32,
document_id: u32,
) -> trc::Result<(Sieve, Object<Value>)> {
// Obtain script object
let script_object = self
.get_property::<HashedValue<Object<Value>>>(
account_id,
Collection::SieveScript,
document_id,
Property::Value,
)
.await?
.ok_or_else(|| {
trc::StoreEvent::NotFound
.into_err()
.caused_by(trc::location!())
.document_id(document_id)
})?;
// Obtain the sieve script length
let (script_offset, blob_id) = script_object
.inner
.properties
.get(&Property::BlobId)
.and_then(|v| v.as_blob_id())
.and_then(|v| (v.section.as_ref()?.size, v).into())
.ok_or_else(|| {
trc::StoreEvent::NotFound
.into_err()
.caused_by(trc::location!())
.document_id(document_id)
})?;
// Obtain the sieve script blob
let script_bytes = self
.core
.storage
.blob
.get_blob(blob_id.hash.as_ref(), 0..usize::MAX)
.await
.caused_by(trc::location!())?
.ok_or_else(|| {
trc::StoreEvent::NotFound
.into_err()
.caused_by(trc::location!())
.document_id(document_id)
})?;
// Obtain the precompiled script
if let Some(sieve) = script_bytes
.get(script_offset..)
.and_then(|bytes| Bincode::<Sieve>::deserialize(bytes).ok())
{
Ok((sieve.inner, script_object.inner))
} else {
// Deserialization failed, probably because the script compiler version changed
match self.core.sieve.untrusted_compiler.compile(
script_bytes.get(0..script_offset).ok_or_else(|| {
trc::StoreEvent::NotFound
.into_err()
.caused_by(trc::location!())
.document_id(document_id)
})?,
) {
Ok(sieve) => {
// Store updated compiled sieve script
let sieve = Bincode::new(sieve);
let compiled_bytes = (&sieve).serialize();
let mut updated_sieve_bytes =
Vec::with_capacity(script_offset + compiled_bytes.len());
updated_sieve_bytes.extend_from_slice(&script_bytes[0..script_offset]);
updated_sieve_bytes.extend_from_slice(&compiled_bytes);
// Store updated blob
let mut new_blob_id = blob_id.clone();
new_blob_id.hash = self
.put_blob(account_id, &updated_sieve_bytes, false)
.await?
.hash;
let mut new_script_object = script_object.inner.clone();
new_script_object.set(Property::BlobId, new_blob_id.clone());
// Update script object
let mut batch = BatchBuilder::new();
batch
.with_account_id(account_id)
.with_collection(Collection::SieveScript)
.update_document(document_id)
.assert_value(Property::Value, &script_object)
.set(Property::Value, (&new_script_object).serialize())
.clear(BlobOp::Link {
hash: blob_id.hash.clone(),
})
.set(
BlobOp::Link {
hash: new_blob_id.hash,
},
Vec::new(),
);
self.store()
.write(batch.build())
.await
.caused_by(trc::location!())?;
Ok((sieve.inner, new_script_object))
}
Err(error) => Err(trc::StoreEvent::UnexpectedError
.caused_by(trc::location!())
.reason(error)
.details("Failed to compile Sieve script")),
}
}
}
}
#[inline(always)]
@@ -554,3 +770,107 @@ pub fn is_valid_role(role: &str) -> bool {
]
.contains(&role)
}
impl SeenIdHash {
pub fn new(id: &str, expiry: u64) -> Self {
let mut hasher = blake3::Hasher::new();
hasher.update(id.as_bytes());
SeenIdHash {
hash: hasher.finalize().into(),
expiry,
}
}
}
impl PartialOrd for SeenIdHash {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for SeenIdHash {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.expiry.cmp(&other.expiry)
}
}
impl std::hash::Hash for SeenIdHash {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.hash.hash(state);
}
}
impl PartialEq for SeenIdHash {
fn eq(&self, other: &Self) -> bool {
self.hash == other.hash
}
}
impl Eq for SeenIdHash {}
// SeenIds serializer
impl serde::Serialize for SeenIds {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut seq = serializer.serialize_seq((self.ids.len() * 2).into())?;
for id in &self.ids {
seq.serialize_element(&id.expiry)?;
seq.serialize_element(&id.hash)?;
}
seq.end()
}
}
impl<'de> serde::Deserialize<'de> for SeenIds {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_seq(SeenIdsVisitor)
}
}
struct SeenIdsVisitor;
impl<'de> serde::de::Visitor<'de> for SeenIdsVisitor {
type Value = SeenIds;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("invalid SeenIds")
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
let num_entries = seq.size_hint().unwrap_or(0) / 2;
let mut seen_ids = SeenIds {
ids: AHashSet::with_capacity(num_entries),
has_changes: false,
};
let now = now();
for _ in 0..num_entries {
let expiry = seq
.next_element::<u64>()?
.ok_or_else(|| serde::de::Error::custom("Expected expiry."))?;
if expiry > now {
seen_ids.ids.insert(SeenIdHash {
hash: seq
.next_element()?
.ok_or_else(|| serde::de::Error::custom("Expected hash."))?,
expiry,
});
} else {
seq.next_element::<[u8; 32]>()?
.ok_or_else(|| serde::de::Error::custom("Expected hash."))?;
seen_ids.has_changes = true;
}
}
Ok(seen_ids)
}
}
+1
View File
@@ -12,6 +12,7 @@ directory = { path = "../directory" }
trc = { path = "../trc" }
store = { path = "../store" }
common = { path = "../common" }
email = { path = "../email" }
nlp = { path = "../nlp" }
utils = { path = "../utils" }
mail-parser = { version = "0.9", features = ["full_encoding", "ludicrous_mode"] }
+1 -2
View File
@@ -11,12 +11,11 @@ use common::{
AccountId, Mailbox,
};
use directory::{backend::internal::PrincipalField, QueryBy};
use email::mailbox::{MailboxFnc, INBOX_ID};
use imap_proto::protocol::list::Attribute;
use jmap::{
auth::acl::{AclMethods, EffectiveAcl},
changes::get::ChangesLookup,
mailbox::{get::MailboxGet, set::MailboxSet, INBOX_ID},
JmapMethods,
};
use jmap_proto::{
object::Object,
+1 -1
View File
@@ -8,8 +8,8 @@ use std::{collections::BTreeMap, sync::Arc};
use ahash::AHashMap;
use common::{listener::SessionStream, NextMailboxState};
use email::mailbox::UidMailbox;
use imap_proto::protocol::{expunge, select::Exists, Sequence};
use jmap::{mailbox::UidMailbox, JmapMethods};
use jmap_proto::{
object::Object,
types::{collection::Collection, property::Property, value::Value},
+4 -5
View File
@@ -11,6 +11,7 @@ use directory::{
backend::internal::{manage::ChangedPrincipals, PrincipalField},
Permission, QueryBy, Type,
};
use email::mailbox::SCHEMA;
use imap_proto::{
protocol::acl::{
Arguments, GetAclResponse, ListRightsResponse, ModRightsOp, MyRightsResponse, Rights,
@@ -19,10 +20,7 @@ use imap_proto::{
Command, ResponseCode, StatusResponse,
};
use jmap::{
auth::acl::EffectiveAcl, changes::write::ChangeLog, mailbox::set::SCHEMA,
services::state::StateManager, JmapMethods,
};
use jmap::auth::acl::EffectiveAcl;
use jmap_proto::{
object::{index::ObjectIndexBuilder, Object},
types::{
@@ -352,7 +350,8 @@ impl<T: SessionStream> Session<T> {
);
if !batch.is_empty() {
data.server
.write_batch(batch)
.store()
.write(batch)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
let mut changes = ChangeLogBuilder::new();
+1 -7
View File
@@ -7,6 +7,7 @@
use std::{sync::Arc, time::Instant};
use directory::Permission;
use email::ingest::{EmailIngest, IngestEmail, IngestSource};
use imap_proto::{
protocol::{append::Arguments, select::HighestModSeq},
receiver::Request,
@@ -18,13 +19,6 @@ use crate::{
spawn_op,
};
use common::{listener::SessionStream, MailboxId};
use jmap::{
email::{
bayes::EmailBayesTrain,
ingest::{EmailIngest, IngestEmail, IngestSource},
},
services::state::StateManager,
};
use jmap_proto::types::{acl::Acl, keyword::Keyword, state::StateChange, type_state::DataType};
use mail_parser::MessageParser;
+7 -9
View File
@@ -7,6 +7,10 @@
use std::{sync::Arc, time::Instant};
use directory::Permission;
use email::{
ingest::EmailIngest,
mailbox::{UidMailbox, JUNK_ID},
};
use imap_proto::{
protocol::copy_move::Arguments, receiver::Request, Command, ResponseCode, ResponseType,
StatusResponse,
@@ -17,13 +21,7 @@ use crate::{
spawn_op,
};
use common::{listener::SessionStream, MailboxId};
use jmap::{
changes::write::ChangeLog,
email::{bayes::EmailBayesTrain, copy::EmailCopy, ingest::EmailIngest, set::TagManager},
mailbox::{UidMailbox, JUNK_ID},
services::{index::Indexer, state::StateManager},
JmapMethods,
};
use jmap::email::{bayes::EmailBayesTrain, copy::EmailCopy, set::TagManager};
use jmap_proto::{
error::set::SetErrorType,
types::{
@@ -236,7 +234,6 @@ impl<T: SessionStream> SessionData<T> {
changelog.change_id = self
.server
.assign_change_id(account_id)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
}
batch.value(Property::Cid, changelog.change_id, F_VALUE);
@@ -270,7 +267,8 @@ impl<T: SessionStream> SessionData<T> {
// Write changes
self.server
.write_batch(batch)
.store()
.write(batch)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
+6 -6
View File
@@ -13,14 +13,13 @@ use crate::{
};
use common::{listener::SessionStream, Account, Mailbox};
use directory::Permission;
use email::mailbox::SCHEMA;
use imap_proto::{
protocol::{create::Arguments, list::Attribute},
receiver::Request,
Command, ResponseCode, StatusResponse,
};
use jmap::{
changes::write::ChangeLog, mailbox::set::SCHEMA, services::state::StateManager, JmapMethods,
};
use jmap::JmapMethods;
use jmap_proto::{
object::{index::ObjectIndexBuilder, Object},
types::{
@@ -79,7 +78,6 @@ impl<T: SessionStream> SessionData<T> {
let mut changes = self
.server
.begin_changes(params.account_id)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
let mut parent_id = params.parent_mailbox_id.map(|id| id + 1).unwrap_or(0);
@@ -105,7 +103,8 @@ impl<T: SessionStream> SessionData<T> {
.custom(ObjectIndexBuilder::new(SCHEMA).with_changes(mailbox));
let mailbox_id = self
.server
.write_batch_expect_id(batch)
.store()
.write_expect_id(batch)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
changes.log_insert(Collection::Mailbox, mailbox_id);
@@ -121,7 +120,8 @@ impl<T: SessionStream> SessionData<T> {
.with_collection(Collection::Mailbox)
.custom(changes);
self.server
.write_batch(batch)
.store()
.write(batch)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
+1 -1
View File
@@ -15,7 +15,7 @@ use directory::Permission;
use imap_proto::{
protocol::delete::Arguments, receiver::Request, Command, ResponseCode, StatusResponse,
};
use jmap::{changes::write::ChangeLog, mailbox::set::MailboxSet, services::state::StateManager};
use jmap::mailbox::set::MailboxSet;
use jmap_proto::types::{state::StateChange, type_state::DataType};
use store::write::log::ChangeLogBuilder;
+10 -9
View File
@@ -8,6 +8,7 @@ use std::{sync::Arc, time::Instant};
use ahash::AHashMap;
use directory::Permission;
use email::mailbox::UidMailbox;
use imap_proto::{
parser::parse_sequence_set,
receiver::{Request, Token},
@@ -17,13 +18,7 @@ use trc::AddContext;
use crate::core::{SavedSearch, SelectedMailbox, Session, SessionData};
use common::{listener::SessionStream, ImapId};
use jmap::{
changes::write::ChangeLog,
email::{delete::EmailDeletion, set::TagManager},
mailbox::UidMailbox,
services::state::StateManager,
JmapMethods,
};
use jmap::email::{delete::EmailDeletion, set::TagManager};
use jmap_proto::types::{
acl::Acl, collection::Collection, id::Id, keyword::Keyword, property::Property,
state::StateChange, type_state::DataType,
@@ -251,10 +246,16 @@ impl<T: SessionStream> SessionData<T> {
mailboxes.update_batch(&mut batch, Property::MailboxIds);
keywords.update_batch(&mut batch, Property::Keywords);
if changelog.change_id == u64::MAX {
changelog.change_id = self.server.assign_change_id(account_id).await?
changelog.change_id = self.server.assign_change_id(account_id)?
}
batch.value(Property::Cid, changelog.change_id, F_VALUE);
match self.server.write_batch(batch).await {
match self
.server
.store()
.write(batch)
.await
.caused_by(trc::location!())
{
Ok(_) => {
changelog.log_update(Collection::Email, Id::from_parts(thread_id, id));
changelog.log_child_update(Collection::Mailbox, mailbox_id.mailbox_id);
+10 -9
View File
@@ -13,6 +13,7 @@ use crate::{
use ahash::AHashMap;
use common::listener::SessionStream;
use directory::Permission;
use email::metadata::MessageMetadata;
use imap_proto::{
parser::PushUnique,
protocol::{
@@ -26,13 +27,7 @@ use imap_proto::{
receiver::Request,
Command, ResponseCode, ResponseType, StatusResponse,
};
use jmap::{
blob::download::BlobDownload,
changes::{get::ChangesLookup, write::ChangeLog},
email::metadata::MessageMetadata,
services::state::StateManager,
JmapMethods,
};
use jmap::{blob::download::BlobDownload, changes::get::ChangesLookup};
use jmap_proto::types::{
acl::Acl, collection::Collection, id::Id, keyword::Keyword, property::Property,
state::StateChange, type_state::DataType,
@@ -42,6 +37,7 @@ use store::{
query::log::{Change, Query},
write::{assert::HashedValue, BatchBuilder, Bincode, F_BITMAP, F_VALUE},
};
use trc::AddContext;
use super::{FromModSeq, ImapContext};
@@ -532,7 +528,6 @@ impl<T: SessionStream> SessionData<T> {
let mut changelog = self
.server
.begin_changes(account_id)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
for (id, mut keywords) in set_seen_ids {
keywords.inner.push(Keyword::Seen);
@@ -545,7 +540,13 @@ impl<T: SessionStream> SessionData<T> {
.value(Property::Keywords, keywords.inner, F_VALUE)
.value(Property::Keywords, Keyword::Seen, F_BITMAP)
.value(Property::Cid, changelog.change_id, F_VALUE);
match self.server.write_batch(batch).await {
match self
.server
.store()
.write(batch)
.await
.caused_by(trc::location!())
{
Ok(_) => {
changelog.log_update(Collection::Email, id);
}
+6 -7
View File
@@ -12,13 +12,11 @@ use crate::{
};
use common::listener::SessionStream;
use directory::Permission;
use email::mailbox::SCHEMA;
use imap_proto::{
protocol::rename::Arguments, receiver::Request, Command, ResponseCode, StatusResponse,
};
use jmap::{
auth::acl::EffectiveAcl, changes::write::ChangeLog, mailbox::set::SCHEMA,
services::state::StateManager, JmapMethods,
};
use jmap::auth::acl::EffectiveAcl;
use jmap_proto::{
object::{index::ObjectIndexBuilder, Object},
types::{
@@ -138,7 +136,6 @@ impl<T: SessionStream> SessionData<T> {
let mut changes = self
.server
.begin_changes(params.account_id)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
let mut parent_id = params.parent_mailbox_id.map(|id| id + 1).unwrap_or(0);
@@ -163,7 +160,8 @@ impl<T: SessionStream> SessionData<T> {
let mailbox_id = self
.server
.write_batch_expect_id(batch)
.store()
.write_expect_id(batch)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
@@ -195,7 +193,8 @@ impl<T: SessionStream> SessionData<T> {
let change_id = changes.change_id;
batch.custom(changes);
self.server
.write_batch(batch)
.store()
.write(batch)
.await
.imap_ctx(&arguments.tag, trc::location!())?;
-1
View File
@@ -19,7 +19,6 @@ use imap_proto::{
receiver::Request,
Command, ResponseCode, StatusResponse,
};
use jmap::JmapMethods;
use jmap_proto::{
object::Object,
types::{collection::Collection, id::Id, keyword::Keyword, property::Property, value::Value},
+10 -6
View File
@@ -13,6 +13,7 @@ use crate::{
use ahash::AHashSet;
use common::listener::SessionStream;
use directory::Permission;
use email::{ingest::EmailIngest, mailbox::UidMailbox};
use imap_proto::{
protocol::{
fetch::{DataItem, FetchItem},
@@ -23,11 +24,8 @@ use imap_proto::{
Command, ResponseCode, ResponseType, StatusResponse,
};
use jmap::{
changes::{get::ChangesLookup, write::ChangeLog},
changes::get::ChangesLookup,
email::{bayes::EmailBayesTrain, set::TagManager},
mailbox::UidMailbox,
services::{index::Indexer, state::StateManager},
JmapMethods,
};
use jmap_proto::types::{
acl::Acl, collection::Collection, id::Id, keyword::Keyword, property::Property,
@@ -37,6 +35,7 @@ use store::{
query::log::{Change, Query},
write::{assert::HashedValue, log::ChangeLogBuilder, BatchBuilder, ValueClass, F_VALUE},
};
use trc::AddContext;
use super::{FromModSeq, ImapContext};
@@ -291,7 +290,6 @@ impl<T: SessionStream> SessionData<T> {
changelog.change_id = self
.server
.assign_change_id(account_id)
.await
.imap_ctx(response.tag.as_ref().unwrap(), trc::location!())?
}
batch.value(Property::Cid, changelog.change_id, F_VALUE);
@@ -310,7 +308,13 @@ impl<T: SessionStream> SessionData<T> {
has_spam_train_tasks = true;
}
match self.server.write_batch(batch).await {
match self
.server
.store()
.write(batch)
.await
.caused_by(trc::location!())
{
Ok(_) => {
// Set all current mailboxes as changed if the Seen tag changed
if seen_changed {
+4 -8
View File
@@ -12,13 +12,9 @@ use crate::{
};
use common::listener::SessionStream;
use directory::Permission;
use email::mailbox::SCHEMA;
use imap_proto::{receiver::Request, Command, ResponseCode, StatusResponse};
use jmap::{
changes::write::ChangeLog,
mailbox::set::{MailboxSubscribe, SCHEMA},
services::state::StateManager,
JmapMethods,
};
use jmap::mailbox::set::MailboxSubscribe;
use jmap_proto::{
object::{index::ObjectIndexBuilder, Object},
types::{
@@ -129,7 +125,6 @@ impl<T: SessionStream> SessionData<T> {
let mut changes = self
.server
.begin_changes(account_id)
.await
.imap_ctx(&tag, trc::location!())?;
let mut batch = BatchBuilder::new();
batch
@@ -148,7 +143,8 @@ impl<T: SessionStream> SessionData<T> {
let change_id = changes.change_id;
batch.custom(changes);
self.server
.write_batch(batch)
.store()
.write(batch)
.await
.imap_ctx(&tag, trc::location!())?;
+1 -1
View File
@@ -13,6 +13,7 @@ use crate::{
use ahash::AHashMap;
use common::listener::SessionStream;
use directory::Permission;
use email::cache::ThreadCache;
use imap_proto::{
protocol::{
thread::{Arguments, Response},
@@ -21,7 +22,6 @@ use imap_proto::{
receiver::Request,
Command, StatusResponse,
};
use jmap::email::cache::ThreadCache;
use trc::AddContext;
impl<T: SessionStream> Session<T> {
+7 -13
View File
@@ -14,6 +14,7 @@ common = { path = "../common" }
directory = { path = "../directory" }
trc = { path = "../trc" }
spam-filter = { path = "../spam-filter" }
email = { path = "../email" }
smtp-proto = { version = "0.1" }
mail-parser = { version = "0.9", features = ["full_encoding", "serde_support", "ludicrous_mode"] }
mail-builder = { version = "0.3", features = ["ludicrous_mode"] }
@@ -27,8 +28,6 @@ hyper-util = { version = "0.1.1", features = ["tokio"] }
http-body-util = "0.1.0"
form_urlencoded = "1.1.0"
tokio = { version = "1.23", features = ["rt"] }
aes-gcm = "0.10.1"
aes-gcm-siv = "0.11.1"
bincode = "1.3.3"
form-data = { version = "0.5.0", features = ["sync"], default-features = false }
mime = "0.3.17"
@@ -40,25 +39,20 @@ hkdf = "0.12.3"
sha1 = "0.10"
sha2 = "0.10"
reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-webpki-roots", "http2"]}
tokio-tungstenite = "0.24"
tungstenite = "0.24"
tokio-tungstenite = "0.26"
tungstenite = "0.26"
chrono = "0.4"
dashmap = "6.0"
aes = "0.8.3"
cbc = { version = "0.1.2", features = ["alloc"] }
sequoia-openpgp = { version = "1.16", default-features = false, features = ["crypto-rust", "allow-experimental-crypto", "allow-variable-time-crypto"] }
rand = "0.8.5"
pkcs8 = { version = "0.10.2", features = ["alloc", "std"] }
rasn = "0.10"
rasn-cms = "0.10"
rasn-pkix = "0.10"
rsa = "0.9.2"
async-trait = "0.1.68"
lz4_flex = { version = "0.11", default-features = false }
rev_lines = "0.3.0"
x509-parser = "0.16.0"
quick-xml = "0.36"
quick-xml = "0.37"
memory-stats = "1.2.0"
aes-gcm = "0.10.1"
aes-gcm-siv = "0.11.1"
rsa = "0.9.2"
[features]
test_mode = []
+9 -7
View File
@@ -9,10 +9,9 @@ use std::{borrow::Cow, fmt::Write, future::Future};
use chrono::Utc;
use common::{
config::network::{ContactForm, FieldOrDefault},
ip_to_bytes,
ipc::{DeliveryResult, IngestMessage},
psl, Server, KV_RATE_LIMIT_CONTACT,
ip_to_bytes, psl, Server, KV_RATE_LIMIT_CONTACT,
};
use email::delivery::{IngestMessage, LocalDeliveryStatus, MailDelivery};
use hyper::StatusCode;
use mail_auth::common::cache::NoCache;
use mail_builder::{
@@ -32,7 +31,7 @@ use trc::AddContext;
use utils::BlobHash;
use x509_parser::nom::AsBytes;
use crate::{auth::oauth::FormData, services::ingest::MailDelivery};
use crate::auth::oauth::FormData;
use super::{
http::{HttpSessionData, ToHttpResponse},
@@ -210,13 +209,16 @@ impl FormHandler for Server {
session_id: session.session_id,
})
.await
.status
{
match result {
DeliveryResult::Success => {
LocalDeliveryStatus::Success => {
has_success = true;
}
DeliveryResult::TemporaryFailure { reason }
| DeliveryResult::PermanentFailure { reason, .. } => failure = Some(reason),
LocalDeliveryStatus::TemporaryFailure { reason }
| LocalDeliveryStatus::PermanentFailure { reason, .. } => {
failure = Some(reason)
}
}
}
@@ -13,6 +13,10 @@ use std::str::FromStr;
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use common::{auth::AccessToken, enterprise::undelete::DeletedBlob, Server};
use directory::backend::internal::manage::ManageDirectory;
use email::{
ingest::{EmailIngest, IngestEmail, IngestSource},
mailbox::INBOX_ID,
};
use hyper::Method;
use jmap_proto::types::collection::Collection;
use mail_parser::{DateTime, MessageParser};
@@ -29,9 +33,6 @@ use crate::{
HttpRequest, HttpResponse, JsonResponse,
},
blob::download::BlobDownload,
email::ingest::{EmailIngest, IngestEmail, IngestSource},
mailbox::INBOX_ID,
JmapMethods,
};
#[derive(serde::Deserialize, serde::Serialize)]
+8 -5
View File
@@ -15,6 +15,10 @@ use directory::{
backend::internal::manage::{self, ManageDirectory},
Permission,
};
use email::{
ingest::EmailIngest,
mailbox::{UidMailbox, SCHEMA},
};
use hyper::Method;
use jmap_proto::{
object::{index::ObjectIndexBuilder, Object},
@@ -30,10 +34,7 @@ use crate::{
http::{HttpSessionData, ToHttpResponse},
HttpRequest, HttpResponse, JsonResponse,
},
email::ingest::EmailIngest,
mailbox::{set::SCHEMA, UidMailbox},
services::index::Indexer,
JmapMethods,
};
use super::decode_path_element;
@@ -344,7 +345,8 @@ pub async fn reset_imap_uids(server: &Server, account_id: u32) -> trc::Result<(u
)
.clear(Property::EmailIds);
server
.write_batch(batch)
.store()
.write(batch)
.await
.caused_by(trc::location!())?;
mailbox_count += 1;
@@ -388,7 +390,8 @@ pub async fn reset_imap_uids(server: &Server, account_id: u32) -> trc::Result<(u
.assert_value(ValueClass::Property(Property::MailboxIds.into()), &uids)
.value(Property::MailboxIds, uids.inner, F_VALUE);
server
.write_batch(batch)
.store()
.write(batch)
.await
.caused_by(trc::location!())?;
email_count += 1;
-1
View File
@@ -30,7 +30,6 @@ use crate::{
principal::{get::PrincipalGet, query::PrincipalQuery},
push::{get::PushSubscriptionFetch, set::PushSubscriptionSet},
quota::{get::QuotaGet, query::QuotaQuery},
services::state::StateManager,
sieve::{
get::SieveScriptGet, query::SieveScriptQuery, set::SieveScriptSet,
validate::SieveScriptValidate,
-2
View File
@@ -30,8 +30,6 @@ use store::{
use trc::AddContext;
use utils::map::bitmap::Bitmap;
use crate::JmapMethods;
pub trait AclMethods: Sync + Send {
fn shared_documents(
&self,
+5 -3
View File
@@ -10,6 +10,7 @@ use jmap_proto::{
method::copy::{CopyBlobRequest, CopyBlobResponse},
types::blob::BlobId,
};
use trc::AddContext;
use std::future::Future;
use store::{
@@ -18,8 +19,6 @@ use store::{
};
use utils::map::vec_map::VecMap;
use crate::JmapMethods;
use super::download::BlobDownload;
pub trait BlobCopy: Sync + Send {
@@ -55,7 +54,10 @@ impl BlobCopy for Server {
},
0u32.serialize(),
);
self.write_batch(batch).await?;
self.store()
.write(batch)
.await
.caused_by(trc::location!())?;
let dest_blob_id = BlobId {
hash: blob_id.hash.clone(),
class: BlobClass::Reserved {
+1
View File
@@ -129,6 +129,7 @@ impl BlobDownload for Server {
}))
}
#[inline(always)]
async fn get_blob(&self, hash: &BlobHash, range: Range<usize>) -> trc::Result<Option<Vec<u8>>> {
self.core
.storage
+1 -1
View File
@@ -5,6 +5,7 @@
*/
use common::{auth::AccessToken, Server};
use email::mailbox::UidMailbox;
use jmap_proto::{
method::{
get::{GetRequest, GetResponse},
@@ -26,7 +27,6 @@ use sha2::{Sha256, Sha512};
use store::BlobClass;
use utils::map::vec_map::VecMap;
use crate::{mailbox::UidMailbox, JmapMethods};
use std::future::Future;
use super::download::BlobDownload;
+4 -63
View File
@@ -14,16 +14,12 @@ use jmap_proto::{
BlobUploadRequest, BlobUploadResponse, BlobUploadResponseObject, DataSourceObject,
},
request::reference::MaybeReference,
types::{blob::BlobId, id::Id},
types::id::Id,
};
use store::{
write::{now, BatchBuilder, BlobOp},
BlobClass, Serialize,
};
use trc::AddContext;
use utils::BlobHash;
use crate::{auth::rate_limit::RateLimiter, JmapMethods};
use trc::AddContext;
use crate::auth::rate_limit::RateLimiter;
use super::{download::BlobDownload, UploadResponse};
use std::future::Future;
@@ -46,13 +42,6 @@ pub trait BlobUpload: Sync + Send {
data: &[u8],
access_token: Arc<AccessToken>,
) -> impl Future<Output = trc::Result<UploadResponse>> + Send;
fn put_blob(
&self,
account_id: u32,
data: &[u8],
set_quota: bool,
) -> impl Future<Output = trc::Result<BlobId>> + Send;
}
impl BlobUpload for Server {
@@ -261,52 +250,4 @@ impl BlobUpload for Server {
size: data.len(),
})
}
#[allow(clippy::blocks_in_conditions)]
async fn put_blob(&self, account_id: u32, data: &[u8], set_quota: bool) -> trc::Result<BlobId> {
// First reserve the hash
let hash = BlobHash::from(data);
let mut batch = BatchBuilder::new();
let until = now() + self.core.jmap.upload_tmp_ttl;
batch.with_account_id(account_id).set(
BlobOp::Reserve {
hash: hash.clone(),
until,
},
(if set_quota { data.len() as u32 } else { 0u32 }).serialize(),
);
self.write_batch(batch).await?;
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)
.await
.caused_by(trc::location!())?;
// Commit blob
let mut batch = BatchBuilder::new();
batch.set(BlobOp::Commit { hash: hash.clone() }, Vec::new());
self.write_batch(batch).await?;
}
Ok(BlobId {
hash,
class: BlobClass::Reserved {
account_id,
expires: until,
},
section: None,
})
}
}
-1
View File
@@ -7,4 +7,3 @@
pub mod get;
pub mod query;
pub mod state;
pub mod write;
-112
View File
@@ -1,112 +0,0 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::time::Duration;
use common::Server;
use jmap_proto::types::collection::Collection;
use std::future::Future;
use store::{
write::{log::ChangeLogBuilder, BatchBuilder},
LogKey,
};
use trc::AddContext;
pub trait ChangeLog: Sync + Send {
fn begin_changes(
&self,
account_id: u32,
) -> impl Future<Output = trc::Result<ChangeLogBuilder>> + Send;
fn assign_change_id(&self, account_id: u32) -> impl Future<Output = trc::Result<u64>> + Send;
fn generate_snowflake_id(&self) -> trc::Result<u64>;
fn commit_changes(
&self,
account_id: u32,
changes: ChangeLogBuilder,
) -> impl Future<Output = trc::Result<u64>> + Send;
fn delete_changes(
&self,
account_id: u32,
before: Duration,
) -> impl Future<Output = trc::Result<()>> + Send;
}
impl ChangeLog for Server {
async fn begin_changes(&self, account_id: u32) -> trc::Result<ChangeLogBuilder> {
self.assign_change_id(account_id)
.await
.map(ChangeLogBuilder::with_change_id)
}
async fn assign_change_id(&self, _: u32) -> trc::Result<u64> {
self.generate_snowflake_id()
}
fn generate_snowflake_id(&self) -> trc::Result<u64> {
self.inner.data.jmap_id_gen.generate().ok_or_else(|| {
trc::StoreEvent::UnexpectedError
.into_err()
.caused_by(trc::location!())
.ctx(trc::Key::Reason, "Failed to generate snowflake id.")
})
}
async fn commit_changes(
&self,
account_id: u32,
mut changes: ChangeLogBuilder,
) -> trc::Result<u64> {
if changes.change_id == u64::MAX || changes.change_id == 0 {
changes.change_id = self.assign_change_id(account_id).await?;
}
let state = changes.change_id;
let mut builder = BatchBuilder::new();
builder.with_account_id(account_id).custom(changes);
self.core
.storage
.data
.write(builder.build())
.await
.caused_by(trc::location!())
.map(|_| state)
}
async fn delete_changes(&self, account_id: u32, before: Duration) -> trc::Result<()> {
let reference_cid = self.inner.data.jmap_id_gen.past_id(before).ok_or_else(|| {
trc::StoreEvent::UnexpectedError
.caused_by(trc::location!())
.ctx(trc::Key::Reason, "Failed to generate reference change id.")
})?;
for collection in [
Collection::Email,
Collection::Mailbox,
Collection::Thread,
Collection::Identity,
Collection::EmailSubmission,
] {
self.core
.storage
.data
.delete_range(
LogKey {
account_id,
collection: collection.into(),
change_id: 0,
},
LogKey {
account_id,
collection: collection.into(),
change_id: reference_cid,
},
)
.await?;
}
Ok(())
}
}
+2 -14
View File
@@ -6,8 +6,8 @@
use std::future::Future;
use common::{auth::AccessToken, Server};
use directory::Permission;
use common::Server;
use email::metadata::MessageMetadata;
use jmap_proto::types::{collection::Collection, property::Property};
use mail_parser::Message;
use spam_filter::{
@@ -16,10 +16,6 @@ use spam_filter::{
use store::write::{Bincode, TaskQueueClass};
use trc::StoreEvent;
use crate::{changes::write::ChangeLog, JmapMethods};
use super::metadata::MessageMetadata;
pub trait EmailBayesTrain: Sync + Send {
fn email_bayes_train(
&self,
@@ -35,8 +31,6 @@ pub trait EmailBayesTrain: Sync + Send {
document_id: u32,
learn_spam: bool,
) -> impl Future<Output = trc::Result<TaskQueueClass>> + Send;
fn email_bayes_can_train(&self, access_token: &AccessToken) -> bool;
}
impl EmailBayesTrain for Server {
@@ -83,10 +77,4 @@ impl EmailBayesTrain for Server {
learn_spam,
})
}
fn email_bayes_can_train(&self, access_token: &AccessToken) -> bool {
self.core.spam.bayes.as_ref().is_some_and(|bayes| {
bayes.account_classify && access_token.has_permission(Permission::SpamFilterTrain)
})
}
}
+2 -4
View File
@@ -4,16 +4,14 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use email::metadata::{MessageMetadataContents, MetadataPartType};
use jmap_proto::{
object::Object,
types::{blob::BlobId, property::Property, value::Value},
};
use mail_parser::{HeaderValue, MessagePart, MimeHeaders, PartType};
use super::{
headers::HeaderToValue,
metadata::{MessageMetadataContents, MetadataPartType},
};
use super::headers::HeaderToValue;
pub trait ToBodyPart {
fn to_body_part(
+8 -15
View File
@@ -8,6 +8,12 @@ use common::{
auth::{AccessToken, ResourceToken},
Server,
};
use email::{
index::{EmailIndexBuilder, TrimTextValue, VisitValues, MAX_ID_LENGTH, MAX_SORT_FIELD_LENGTH},
ingest::{EmailIngest, IngestedEmail, LogEmailInsert},
mailbox::{MailboxFnc, UidMailbox},
metadata::MessageMetadata,
};
use jmap_proto::{
error::set::SetError,
method::{
@@ -45,22 +51,9 @@ use store::{
use trc::AddContext;
use utils::map::vec_map::VecMap;
use crate::{
api::http::HttpSessionData,
auth::acl::AclMethods,
changes::{state::StateManager, write::ChangeLog},
mailbox::{set::MailboxSet, UidMailbox},
services::index::Indexer,
JmapMethods,
};
use crate::{api::http::HttpSessionData, auth::acl::AclMethods, changes::state::StateManager};
use std::future::Future;
use super::{
index::{EmailIndexBuilder, TrimTextValue, VisitValues, MAX_ID_LENGTH, MAX_SORT_FIELD_LENGTH},
ingest::{EmailIngest, IngestedEmail, LogEmailInsert},
metadata::MessageMetadata,
};
pub trait EmailCopy: Sync + Send {
fn email_copy(
&self,
@@ -413,7 +406,7 @@ impl EmailCopy for Server {
}
// Prepare batch
let change_id = self.assign_change_id(account_id).await?;
let change_id = self.assign_change_id(account_id)?;
let mut batch = BatchBuilder::new();
batch
.with_account_id(account_id)
+9 -668
View File
@@ -4,661 +4,20 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::{
borrow::Cow, collections::BTreeSet, fmt::Display, future::Future, io::Cursor, sync::Arc,
};
use std::{future::Future, sync::Arc};
use crate::{
api::{http::ToHttpResponse, HttpResponse, JsonResponse},
JmapMethods,
};
use aes::cipher::{block_padding::Pkcs7, BlockEncryptMut, KeyIvInit};
use crate::api::{http::ToHttpResponse, HttpResponse, JsonResponse};
use common::{auth::AccessToken, Server};
use directory::backend::internal::manage;
use email::crypto::{
try_parse_certs, EncryptMessage, EncryptMessageError, EncryptionMethod, EncryptionParams,
EncryptionType,
};
use jmap_proto::types::{collection::Collection, property::Property};
use mail_builder::{encoders::base64::base64_encode_mime, mime::make_boundary};
use mail_parser::{decoders::base64::base64_decode, Message, MessageParser, MimeHeaders, PartType};
use openpgp::{
parse::Parse,
serialize::stream,
types::{KeyFlags, SymmetricAlgorithm},
};
use rand::{rngs::StdRng, RngCore, SeedableRng};
use rasn::types::{ObjectIdentifier, OctetString};
use rasn_cms::{
algorithms::{AES128_CBC, AES256_CBC, RSA},
pkcs7_compat::EncapsulatedContentInfo,
AlgorithmIdentifier, EncryptedContent, EncryptedContentInfo, EncryptedKey, EnvelopedData,
IssuerAndSerialNumber, KeyTransRecipientInfo, RecipientIdentifier, RecipientInfo, CONTENT_DATA,
CONTENT_ENVELOPED_DATA,
};
use rsa::{pkcs1::DecodeRsaPublicKey, Pkcs1v15Encrypt, RsaPublicKey};
use sequoia_openpgp as openpgp;
use mail_builder::encoders::base64::base64_encode_mime;
use mail_parser::MessageParser;
use serde_json::json;
use store::{
write::{BatchBuilder, Bincode, ToBitmaps, F_CLEAR, F_VALUE},
Deserialize, Serialize,
};
const P: openpgp::policy::StandardPolicy<'static> = openpgp::policy::StandardPolicy::new();
#[derive(Debug)]
pub enum EncryptMessageError {
AlreadyEncrypted,
Error(String),
}
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
pub enum Algorithm {
Aes128,
Aes256,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum EncryptionMethod {
PGP,
SMIME,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct EncryptionParams {
pub method: EncryptionMethod,
pub algo: Algorithm,
pub certs: Vec<Vec<u8>>,
}
#[derive(Debug, serde::Serialize, serde::Deserialize, Default)]
#[serde(tag = "type")]
#[serde(rename_all = "camelCase")]
pub enum EncryptionType {
PGP {
algo: Algorithm,
certs: String,
},
SMIME {
algo: Algorithm,
certs: String,
},
#[default]
Disabled,
}
#[allow(async_fn_in_trait)]
pub trait EncryptMessage {
async fn encrypt(&self, params: &EncryptionParams) -> Result<Vec<u8>, EncryptMessageError>;
fn is_encrypted(&self) -> bool;
}
impl EncryptMessage for Message<'_> {
async fn encrypt(&self, params: &EncryptionParams) -> Result<Vec<u8>, EncryptMessageError> {
let root = self.root_part();
let raw_message = self.raw_message();
let mut outer_message = Vec::with_capacity((raw_message.len() as f64 * 1.5) as usize);
let mut inner_message = Vec::with_capacity(raw_message.len());
// Move MIME headers and body to inner message
for header in root.headers() {
(if header.name.is_mime_header() {
&mut inner_message
} else {
&mut outer_message
})
.extend_from_slice(&raw_message[header.offset_field()..header.offset_end()]);
}
inner_message.extend_from_slice(b"\r\n");
inner_message.extend_from_slice(&raw_message[root.raw_body_offset()..]);
// Encrypt inner message
match params.method {
EncryptionMethod::PGP => {
// Prepare encrypted message
let boundary = make_boundary("_");
outer_message.extend_from_slice(
concat!(
"Content-Type: multipart/encrypted;\r\n\t",
"protocol=\"application/pgp-encrypted\";\r\n\t",
"boundary=\""
)
.as_bytes(),
);
outer_message.extend_from_slice(boundary.as_bytes());
outer_message.extend_from_slice(
concat!(
"\"\r\n\r\n",
"OpenPGP/MIME message (Automatically encrypted by Stalwart)\r\n\r\n",
"--"
)
.as_bytes(),
);
outer_message.extend_from_slice(boundary.as_bytes());
outer_message.extend_from_slice(
concat!(
"\r\nContent-Type: application/pgp-encrypted\r\n\r\n",
"Version: 1\r\n\r\n--"
)
.as_bytes(),
);
outer_message.extend_from_slice(boundary.as_bytes());
outer_message.extend_from_slice(
concat!(
"\r\nContent-Type: application/octet-stream; name=\"encrypted.asc\"\r\n",
"Content-Disposition: inline; filename=\"encrypted.asc\"\r\n\r\n"
)
.as_bytes(),
);
let certs = params
.certs
.iter()
.map(openpgp::Cert::from_bytes)
.collect::<Result<Vec<_>, _>>()
.map_err(|err| {
EncryptMessageError::Error(format!(
"Failed to parse OpenPGP public key: {}",
err
))
})?;
// Encrypt contents (TODO: use rayon)
let algo = params.algo;
let encrypted_contents = tokio::task::spawn_blocking(move || {
// Parse public key
let mut keys = Vec::with_capacity(certs.len());
let policy = openpgp::policy::StandardPolicy::new();
for cert in &certs {
for key in cert
.keys()
.with_policy(&policy, None)
.supported()
.alive()
.revoked(false)
.key_flags(KeyFlags::empty().set_transport_encryption())
{
keys.push(key);
}
}
// Compose a writer stack corresponding to the output format and
// packet structure we want.
let mut sink = Vec::with_capacity(inner_message.len());
// Stream an OpenPGP message.
let message = stream::Armorer::new(stream::Message::new(&mut sink))
.build()
.map_err(|err| {
EncryptMessageError::Error(format!("Failed to create armorer: {}", err))
})?;
let message = stream::Encryptor2::for_recipients(message, keys)
.symmetric_algo(match algo {
Algorithm::Aes128 => SymmetricAlgorithm::AES128,
Algorithm::Aes256 => SymmetricAlgorithm::AES256,
})
.build()
.map_err(|err| {
EncryptMessageError::Error(format!(
"Failed to build encryptor: {}",
err
))
})?;
let mut message =
stream::LiteralWriter::new(message).build().map_err(|err| {
EncryptMessageError::Error(format!(
"Failed to create literal writer: {}",
err
))
})?;
std::io::copy(&mut Cursor::new(inner_message), &mut message).map_err(
|err| {
EncryptMessageError::Error(format!(
"Failed to encrypt message: {}",
err
))
},
)?;
message.finalize().map_err(|err| {
EncryptMessageError::Error(format!("Failed to finalize message: {}", err))
})?;
String::from_utf8(sink).map_err(|err| {
EncryptMessageError::Error(format!(
"Failed to convert encrypted message to UTF-8: {}",
err
))
})
})
.await
.map_err(|err| {
EncryptMessageError::Error(format!("Failed to encrypt message: {}", err))
})??;
outer_message.extend_from_slice(encrypted_contents.as_bytes());
outer_message.extend_from_slice(b"\r\n--");
outer_message.extend_from_slice(boundary.as_bytes());
outer_message.extend_from_slice(b"--\r\n");
}
EncryptionMethod::SMIME => {
// Generate random IV
let mut rng = StdRng::from_entropy();
let mut iv = vec![0u8; 16];
rng.fill_bytes(&mut iv);
// Generate random key
let mut key = vec![0u8; params.algo.key_size()];
rng.fill_bytes(&mut key);
// Encrypt contents (TODO: use rayon)
let algo = params.algo;
let (encrypted_contents, key, iv) = tokio::task::spawn_blocking(move || {
(algo.encrypt(&key, &iv, &inner_message), key, iv)
})
.await
.map_err(|err| {
EncryptMessageError::Error(format!("Failed to encrypt message: {}", err))
})?;
// Encrypt key using public keys
#[allow(clippy::mutable_key_type)]
let mut recipient_infos = BTreeSet::new();
for cert in &params.certs {
let cert =
rasn::der::decode::<rasn_pkix::Certificate>(cert).map_err(|err| {
EncryptMessageError::Error(format!(
"Failed to parse certificate: {}",
err
))
})?;
let public_key = RsaPublicKey::from_pkcs1_der(
cert.tbs_certificate
.subject_public_key_info
.subject_public_key
.as_raw_slice(),
)
.map_err(|err| {
EncryptMessageError::Error(format!("Failed to parse public key: {}", err))
})?;
let encrypted_key = public_key
.encrypt(&mut rng, Pkcs1v15Encrypt, &key[..])
.map_err(|err| {
EncryptMessageError::Error(format!("Failed to encrypt key: {}", err))
})
.unwrap();
recipient_infos.insert(RecipientInfo::KeyTransRecipientInfo(
KeyTransRecipientInfo {
version: 0.into(),
rid: RecipientIdentifier::IssuerAndSerialNumber(
IssuerAndSerialNumber {
issuer: cert.tbs_certificate.issuer,
serial_number: cert.tbs_certificate.serial_number,
},
),
key_encryption_algorithm: AlgorithmIdentifier {
algorithm: RSA.into(),
parameters: Some(
rasn::der::encode(&())
.map_err(|err| {
EncryptMessageError::Error(format!(
"Failed to encode RSA algorithm identifier: {}",
err
))
})?
.into(),
),
},
encrypted_key: EncryptedKey::from(encrypted_key),
},
));
}
let pkcs7 = rasn::der::encode(&EncapsulatedContentInfo {
content_type: CONTENT_ENVELOPED_DATA.into(),
content: Some(
rasn::der::encode(&EnvelopedData {
version: 0.into(),
originator_info: None,
recipient_infos,
encrypted_content_info: EncryptedContentInfo {
content_type: CONTENT_DATA.into(),
content_encryption_algorithm: AlgorithmIdentifier {
algorithm: params.algo.to_algorithm_identifier(),
parameters: Some(
rasn::der::encode(&OctetString::from(iv))
.map_err(|err| {
EncryptMessageError::Error(format!(
"Failed to encode IV: {}",
err
))
})?
.into(),
),
},
encrypted_content: Some(EncryptedContent::from(encrypted_contents)),
},
unprotected_attrs: None,
})
.map_err(|err| {
EncryptMessageError::Error(format!(
"Failed to encode EnvelopedData: {}",
err
))
})?
.into(),
),
})
.map_err(|err| {
EncryptMessageError::Error(format!("Failed to encode ContentInfo: {}", err))
})?;
// Generate message
outer_message.extend_from_slice(
concat!(
"Content-Type: application/pkcs7-mime;\r\n",
"\tname=\"smime.p7m\";\r\n",
"\tsmime-type=enveloped-data\r\n",
"Content-Disposition: attachment;\r\n",
"\tfilename=\"smime.p7m\"\r\n",
"Content-Transfer-Encoding: base64\r\n\r\n"
)
.as_bytes(),
);
base64_encode_mime(&pkcs7, &mut outer_message, false).map_err(|err| {
EncryptMessageError::Error(format!("Failed to base64 encode PKCS7: {}", err))
})?;
}
}
Ok(outer_message)
}
fn is_encrypted(&self) -> bool {
if self.content_type().is_some_and(|ct| {
let main_type = ct.c_type.as_ref();
let sub_type = ct
.c_subtype
.as_ref()
.map(|s| s.as_ref())
.unwrap_or_default();
(main_type.eq_ignore_ascii_case("application")
&& (sub_type.eq_ignore_ascii_case("pkcs7-mime")
|| sub_type.eq_ignore_ascii_case("pkcs7-signature")
|| (sub_type.eq_ignore_ascii_case("octet-stream")
&& self.attachment_name().is_some_and(|name| {
name.rsplit_once('.')
.is_some_and(|(_, ext)| ["p7m", "p7s", "p7c", "p7z"].contains(&ext))
}))))
|| (main_type.eq_ignore_ascii_case("multipart")
&& sub_type.eq_ignore_ascii_case("encrypted"))
}) {
return true;
}
if self.parts.len() <= 2 {
let mut text_part = None;
let mut is_multipart = false;
for part in &self.parts {
match &part.body {
PartType::Text(text) => {
text_part = Some(text.as_ref());
}
PartType::Multipart(_) => {
is_multipart = true;
}
_ => (),
}
}
match text_part {
Some(text) if self.parts.len() == 1 || is_multipart => {
if text.trim_start().starts_with("-----BEGIN PGP MESSAGE-----") {
return true;
}
}
_ => (),
}
}
false
}
}
impl Algorithm {
fn key_size(&self) -> usize {
match self {
Algorithm::Aes128 => 16,
Algorithm::Aes256 => 32,
}
}
fn to_algorithm_identifier(self) -> ObjectIdentifier {
match self {
Algorithm::Aes128 => AES128_CBC.into(),
Algorithm::Aes256 => AES256_CBC.into(),
}
}
fn encrypt(&self, key: &[u8], iv: &[u8], contents: &[u8]) -> Vec<u8> {
match self {
Algorithm::Aes128 => cbc::Encryptor::<aes::Aes128>::new(key.into(), iv.into())
.encrypt_padded_vec_mut::<Pkcs7>(contents),
Algorithm::Aes256 => cbc::Encryptor::<aes::Aes256>::new(key.into(), iv.into())
.encrypt_padded_vec_mut::<Pkcs7>(contents),
}
}
}
pub fn try_parse_certs(
expected_method: EncryptionMethod,
cert: Vec<u8>,
) -> Result<Vec<Vec<u8>>, Cow<'static, str>> {
// Check if it's a PEM file
let (method, certs) = if let Some(result) = try_parse_pem(&cert)? {
result
} else if rasn::der::decode::<rasn_pkix::Certificate>(&cert[..]).is_ok() {
(EncryptionMethod::SMIME, vec![cert])
} else if let Ok(cert_) = openpgp::Cert::from_bytes(&cert[..]) {
if !has_pgp_keys(cert_) {
(EncryptionMethod::PGP, vec![cert])
} else {
return Err("Could not find any suitable keys in certificate".into());
}
} else {
return Err("Could not find any valid certificates".into());
};
if method == expected_method {
Ok(certs)
} else {
Err("No valid certificates found for the selected encryption".into())
}
}
fn has_pgp_keys(cert: openpgp::Cert) -> bool {
cert.keys()
.with_policy(&P, None)
.supported()
.alive()
.revoked(false)
.key_flags(KeyFlags::empty().set_transport_encryption())
.next()
.is_some()
}
#[allow(clippy::type_complexity)]
fn try_parse_pem(
bytes_: &[u8],
) -> Result<Option<(EncryptionMethod, Vec<Vec<u8>>)>, Cow<'static, str>> {
if let Some(internal) = std::str::from_utf8(bytes_)
.ok()
.and_then(|cert| cert.strip_prefix("-----STALWART CERTIFICATE-----"))
{
return base64_decode(internal.as_bytes())
.ok_or(Cow::from("Failed to decode base64"))
.and_then(|bytes| {
Bincode::<EncryptionParams>::deserialize(&bytes)
.map_err(|_| Cow::from("Failed to deserialize internal certificate"))
})
.map(|params| Some((params.inner.method, params.inner.certs)));
}
let mut bytes = bytes_.iter().enumerate();
let mut buf = vec![];
let mut method = None;
let mut certs = vec![];
loop {
// Find start of PEM block
let mut start_pos = 0;
for (pos, &ch) in bytes.by_ref() {
if ch.is_ascii_whitespace() {
continue;
} else if ch == b'-' {
start_pos = pos;
break;
} else {
return Ok(None);
}
}
// Find block type
for (_, &ch) in bytes.by_ref() {
match ch {
b'-' => (),
b'\n' => break,
_ => {
if ch.is_ascii() {
buf.push(ch.to_ascii_uppercase());
} else {
return Ok(None);
}
}
}
}
if buf.is_empty() {
break;
}
// Find type
let tag = std::str::from_utf8(&buf).unwrap();
if tag.contains("CERTIFICATE") {
if method.is_some_and(|m| m == EncryptionMethod::PGP) {
return Err("Cannot mix OpenPGP and S/MIME certificates".into());
} else {
method = Some(EncryptionMethod::SMIME);
}
} else if tag.contains("PGP") {
if method.is_some_and(|m| m == EncryptionMethod::SMIME) {
return Err("Cannot mix OpenPGP and S/MIME certificates".into());
} else {
method = Some(EncryptionMethod::PGP);
}
} else {
// Ignore block
let mut found_end = false;
for (_, &ch) in bytes.by_ref() {
if ch == b'-' {
found_end = true;
} else if ch == b'\n' && found_end {
break;
}
}
buf.clear();
continue;
}
// Collect base64
buf.clear();
let mut found_end = false;
let mut end_pos = 0;
for (pos, &ch) in bytes.by_ref() {
match ch {
b'-' => {
found_end = true;
}
b'\n' => {
if found_end {
end_pos = pos;
break;
}
}
_ => {
if !ch.is_ascii_whitespace() {
buf.push(ch);
}
}
}
}
// Decode base64
let cert =
base64_decode(&buf).ok_or_else(|| Cow::from("Failed to decode base64 certificate."))?;
match method.unwrap() {
EncryptionMethod::PGP => match openpgp::Cert::from_bytes(bytes_) {
Ok(cert) => {
if !has_pgp_keys(cert) {
return Err("Could not find any suitable keys in OpenPGP public key".into());
}
certs.push(
bytes_
.get(start_pos..end_pos + 1)
.unwrap_or_default()
.to_vec(),
);
}
Err(err) => {
return Err(format!("Failed to decode OpenPGP public key: {err}").into())
}
},
EncryptionMethod::SMIME => {
if let Err(err) = rasn::der::decode::<rasn_pkix::Certificate>(&cert) {
return Err(format!("Failed to decode X509 certificate: {err}").into());
}
certs.push(cert);
}
}
buf.clear();
}
Ok(method.map(|method| (method, certs)))
}
impl Serialize for &EncryptionParams {
fn serialize(self) -> Vec<u8> {
let len = bincode::serialized_size(&self).unwrap_or_default();
let mut buf = Vec::with_capacity(len as usize + 1);
buf.push(1);
let _ = bincode::serialize_into(&mut buf, &self);
buf
}
}
impl Deserialize for EncryptionParams {
fn deserialize(bytes: &[u8]) -> trc::Result<Self> {
let version = *bytes
.first()
.ok_or_else(|| trc::StoreEvent::DataCorruption.caused_by(trc::location!()))?;
match version {
1 if bytes.len() > 1 => bincode::deserialize(&bytes[1..]).map_err(|err| {
trc::EventType::Store(trc::StoreEvent::DeserializeError)
.from_bincode_error(err)
.caused_by(trc::location!())
}),
_ => Err(trc::StoreEvent::DeserializeError
.into_err()
.caused_by(trc::location!())
.ctx(trc::Key::Value, version as u64)),
}
}
}
impl ToBitmaps for &EncryptionParams {
fn to_bitmaps(&self, _: &mut Vec<store::write::Operation>, _: u8, _: bool) {
unreachable!()
}
}
use store::{write::{BatchBuilder, Bincode, F_CLEAR, F_VALUE}, Serialize};
pub trait CryptoHandler: Sync + Send {
fn handle_crypto_get(
@@ -777,21 +136,3 @@ impl CryptoHandler for Server {
.into_http_response())
}
}
impl Display for EncryptionMethod {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EncryptionMethod::PGP => write!(f, "OpenPGP"),
EncryptionMethod::SMIME => write!(f, "S/MIME"),
}
}
}
impl Display for Algorithm {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Algorithm::Aes128 => write!(f, "AES-128"),
Algorithm::Aes256 => write!(f, "AES-256"),
}
}
}
+5 -8
View File
@@ -7,6 +7,11 @@
use std::time::Duration;
use common::{Server, KV_LOCK_PURGE_ACCOUNT};
use email::{
index::EmailIndexBuilder,
mailbox::{UidMailbox, JUNK_ID, TOMBSTONE_ID, TRASH_ID},
metadata::MessageMetadata,
};
use jmap_proto::types::{
collection::Collection, id::Id, keyword::Keyword, property::Property, state::StateChange,
type_state::DataType,
@@ -23,14 +28,6 @@ use store::{
use trc::{AddContext, StoreEvent};
use utils::codec::leb128::Leb128Reader;
use crate::{
changes::write::ChangeLog,
mailbox::{UidMailbox, JUNK_ID, TOMBSTONE_ID, TRASH_ID},
services::state::StateManager,
JmapMethods,
};
use super::{index::EmailIndexBuilder, metadata::MessageMetadata};
use rand::prelude::SliceRandom;
use std::future::Future;
+6 -3
View File
@@ -5,6 +5,11 @@
*/
use common::{auth::AccessToken, Server};
use email::{
cache::ThreadCache,
mailbox::UidMailbox,
metadata::{MessageMetadata, MetadataPartType},
};
use jmap_proto::{
method::get::{GetRequest, GetResponse},
object::{email::GetArguments, Object},
@@ -25,15 +30,13 @@ use trc::{AddContext, StoreEvent};
use crate::{
auth::acl::AclMethods, blob::download::BlobDownload, changes::state::StateManager,
email::headers::HeaderToValue, mailbox::UidMailbox, JmapMethods,
email::headers::HeaderToValue,
};
use std::future::Future;
use super::{
body::{ToBodyPart, TruncateBody},
cache::ThreadCache,
headers::IntoForm,
metadata::{MessageMetadata, MetadataPartType},
};
pub trait EmailGet: Sync + Send {
+5 -5
View File
@@ -5,6 +5,10 @@
*/
use common::{auth::AccessToken, Server};
use email::{
ingest::{EmailIngest, IngestEmail, IngestSource},
mailbox::MailboxFnc,
};
use jmap_proto::{
error::set::{SetError, SetErrorType},
method::import::{ImportEmailRequest, ImportEmailResponse},
@@ -22,13 +26,9 @@ use utils::map::vec_map::VecMap;
use crate::{
api::http::HttpSessionData, auth::acl::AclMethods, blob::download::BlobDownload,
changes::state::StateManager, mailbox::set::MailboxSet, JmapMethods,
changes::state::StateManager,
};
use super::{
bayes::EmailBayesTrain,
ingest::{EmailIngest, IngestEmail, IngestSource},
};
use std::future::Future;
pub trait EmailImport: Sync + Send {
-4
View File
@@ -6,16 +6,12 @@
pub mod bayes;
pub mod body;
pub mod cache;
pub mod copy;
pub mod crypto;
pub mod delete;
pub mod get;
pub mod headers;
pub mod import;
pub mod index;
pub mod ingest;
pub mod metadata;
pub mod parse;
pub mod query;
pub mod set;
+1 -1
View File
@@ -5,6 +5,7 @@
*/
use common::{auth::AccessToken, Server};
use email::index::PREVIEW_LENGTH;
use jmap_proto::{
method::parse::{ParseEmailRequest, ParseEmailResponse},
object::Object,
@@ -21,7 +22,6 @@ use crate::blob::download::BlobDownload;
use super::{
body::{ToBodyPart, TruncateBody},
headers::HeaderToValue,
index::PREVIEW_LENGTH,
};
pub trait EmailParse: Sync + Send {
+1 -2
View File
@@ -5,6 +5,7 @@
*/
use common::{auth::AccessToken, Server};
use email::cache::ThreadCache;
use jmap_proto::{
method::query::{Comparator, Filter, QueryRequest, QueryResponse, SortProperty},
object::email::QueryArguments,
@@ -23,8 +24,6 @@ use store::{
use crate::{auth::acl::AclMethods, JmapMethods};
use super::cache::ThreadCache;
pub trait EmailQuery: Sync + Send {
fn email_query(
&self,
+7 -9
View File
@@ -7,6 +7,10 @@
use std::{borrow::Cow, collections::HashMap, slice::IterMut};
use common::{auth::AccessToken, Server};
use email::{
ingest::{EmailIngest, IngestEmail, IngestSource},
mailbox::{MailboxFnc, UidMailbox},
};
use jmap_proto::{
error::set::{SetError, SetErrorType},
method::set::{RequestArguments, SetRequest, SetResponse},
@@ -42,20 +46,14 @@ use store::{
use trc::AddContext;
use crate::{
api::http::HttpSessionData,
auth::acl::AclMethods,
blob::download::BlobDownload,
changes::{state::StateManager, write::ChangeLog},
mailbox::{set::MailboxSet, UidMailbox},
JmapMethods,
api::http::HttpSessionData, auth::acl::AclMethods, blob::download::BlobDownload,
changes::state::StateManager, JmapMethods,
};
use std::future::Future;
use super::{
bayes::EmailBayesTrain,
delete::EmailDeletion,
headers::{BuildHeader, ValueToHeader},
ingest::{EmailIngest, IngestEmail, IngestSource},
};
pub trait EmailSet: Sync + Send {
@@ -892,7 +890,7 @@ impl EmailSet for Server {
// Update last change id
if changes.change_id == u64::MAX {
changes.change_id = self.assign_change_id(account_id).await?;
changes.change_id = self.assign_change_id(account_id)?;
}
batch.value(Property::Cid, changes.change_id, F_VALUE);
}
+2 -2
View File
@@ -5,6 +5,7 @@
*/
use common::{auth::AccessToken, Server};
use email::metadata::{MessageMetadata, MetadataPartType};
use jmap_proto::{
method::{
query::Filter,
@@ -16,9 +17,8 @@ use mail_parser::{decoders::html::html_to_text, GetHeader, HeaderName, PartType}
use nlp::language::{search_snippet::generate_snippet, stemmer::Stemmer, Language};
use store::{backend::MAX_TOKEN_LENGTH, write::Bincode};
use crate::{auth::acl::AclMethods, blob::download::BlobDownload, JmapMethods};
use crate::{auth::acl::AclMethods, blob::download::BlobDownload};
use super::metadata::{MessageMetadata, MetadataPartType};
use std::future::Future;
pub trait EmailSearchSnippet: Sync + Send {
+1 -1
View File
@@ -18,7 +18,7 @@ use store::{
use trc::AddContext;
use utils::sanitize_email;
use crate::{changes::state::StateManager, JmapMethods};
use crate::changes::state::StateManager;
use std::future::Future;
+14 -5
View File
@@ -19,10 +19,9 @@ use jmap_proto::{
};
use std::future::Future;
use store::write::{log::ChangeLogBuilder, BatchBuilder, F_CLEAR, F_VALUE};
use trc::AddContext;
use utils::sanitize_email;
use crate::{changes::write::ChangeLog, JmapMethods};
pub trait IdentitySet: Sync + Send {
fn identity_set(
&self,
@@ -102,7 +101,11 @@ impl IdentitySet for Server {
.with_collection(Collection::Identity)
.create_document()
.value(Property::Value, identity, F_VALUE);
let document_id = self.write_batch_expect_id(batch).await?;
let document_id = self
.store()
.write_expect_id(batch)
.await
.caused_by(trc::location!())?;
identity_ids.insert(document_id);
changes.log_insert(Collection::Identity, document_id);
response.created(id, document_id);
@@ -158,7 +161,10 @@ impl IdentitySet for Server {
.with_collection(Collection::Identity)
.update_document(document_id)
.value(Property::Value, identity, F_VALUE);
self.write_batch(batch).await?;
self.store()
.write(batch)
.await
.caused_by(trc::location!())?;
changes.log_update(Collection::Identity, document_id);
response.updated.append(id, None);
}
@@ -174,7 +180,10 @@ impl IdentitySet for Server {
.with_collection(Collection::Identity)
.delete_document(document_id)
.value(Property::Value, (), F_VALUE | F_CLEAR);
self.write_batch(batch).await?;
self.store()
.write(batch)
.await
.caused_by(trc::location!())?;
changes.log_delete(Collection::Identity, document_id);
response.destroyed.push(id);
} else {
+2 -324
View File
@@ -8,33 +8,24 @@ use std::{fmt::Display, future::Future, sync::Arc, time::Duration};
use changes::state::StateManager;
use common::{
auth::{AccessToken, ResourceToken, TenantInfo},
manager::boot::{BootManager, IpcReceivers},
Inner, Server,
};
use directory::QueryBy;
use jmap_proto::{
method::{
query::{QueryRequest, QueryResponse},
set::{SetRequest, SetResponse},
},
types::{collection::Collection, property::Property},
types::collection::Collection,
};
use services::{
delivery::spawn_delivery_manager, housekeeper::spawn_housekeeper,
index::spawn_email_queue_task, state::spawn_state_manager,
housekeeper::spawn_housekeeper, index::spawn_email_queue_task, state::spawn_state_manager,
};
use store::{
dispatch::DocumentSet,
fts::FtsFilter,
query::{sort::Pagination, Comparator, Filter, ResultSet, SortedResultSet},
roaring::RoaringBitmap,
write::{
key::DeserializeBigEndian, AssignedIds, BatchBuilder, BitmapClass, DirectoryClass,
TagValue, ValueClass,
},
BitmapKey, Deserialize, IterateParams, ValueKey, U32_LEN,
};
use trc::AddContext;
@@ -88,9 +79,6 @@ impl StartServices for BootManager {
impl SpawnServices for IpcReceivers {
fn spawn_services(&mut self, inner: Arc<Inner>) {
// Spawn delivery manager
spawn_delivery_manager(inner.clone(), self.delivery_rx.take().unwrap());
// Spawn state manager
spawn_state_manager(inner.clone(), self.state_rx.take().unwrap());
@@ -103,138 +91,6 @@ impl SpawnServices for IpcReceivers {
}
impl JmapMethods for Server {
async fn get_property<U>(
&self,
account_id: u32,
collection: Collection,
document_id: u32,
property: impl AsRef<Property> + Sync + Send,
) -> trc::Result<Option<U>>
where
U: Deserialize + 'static,
{
let property = property.as_ref();
self.core
.storage
.data
.get_value::<U>(ValueKey {
account_id,
collection: collection.into(),
document_id,
class: ValueClass::Property(property.into()),
})
.await
.add_context(|err| {
err.caused_by(trc::location!())
.account_id(account_id)
.collection(collection)
.document_id(document_id)
.id(property.to_string())
})
}
async fn get_properties<U, I, P>(
&self,
account_id: u32,
collection: Collection,
iterate: &I,
property: P,
) -> trc::Result<Vec<(u32, U)>>
where
I: DocumentSet + Send + Sync,
P: AsRef<Property> + Sync + Send,
U: Deserialize + 'static,
{
let property: u8 = property.as_ref().into();
let collection: u8 = collection.into();
let expected_results = iterate.len();
let mut results = Vec::with_capacity(expected_results);
self.core
.storage
.data
.iterate(
IterateParams::new(
ValueKey {
account_id,
collection,
document_id: iterate.min(),
class: ValueClass::Property(property),
},
ValueKey {
account_id,
collection,
document_id: iterate.max(),
class: ValueClass::Property(property),
},
),
|key, value| {
let document_id = key.deserialize_be_u32(key.len() - U32_LEN)?;
if iterate.contains(document_id) {
results.push((document_id, U::deserialize(value)?));
Ok(expected_results == 0 || results.len() < expected_results)
} else {
Ok(true)
}
},
)
.await
.add_context(|err| {
err.caused_by(trc::location!())
.account_id(account_id)
.collection(collection)
.id(property.to_string())
})
.map(|_| results)
}
async fn get_document_ids(
&self,
account_id: u32,
collection: Collection,
) -> trc::Result<Option<RoaringBitmap>> {
self.core
.storage
.data
.get_bitmap(BitmapKey::document_ids(account_id, collection))
.await
.add_context(|err| {
err.caused_by(trc::location!())
.account_id(account_id)
.collection(collection)
})
}
async fn get_tag(
&self,
account_id: u32,
collection: Collection,
property: impl AsRef<Property> + Sync + Send,
value: impl Into<TagValue<u32>> + Sync + Send,
) -> trc::Result<Option<RoaringBitmap>> {
let property = property.as_ref();
self.core
.storage
.data
.get_bitmap(BitmapKey {
account_id,
collection: collection.into(),
class: BitmapClass::Tag {
field: property.into(),
value: value.into(),
},
document_id: 0,
})
.await
.add_context(|err| {
err.caused_by(trc::location!())
.account_id(account_id)
.collection(collection)
.id(property.to_string())
})
}
async fn prepare_set_response<T: Sync + Send>(
&self,
request: &SetRequest<T>,
@@ -252,109 +108,6 @@ impl JmapMethods for Server {
)
}
async fn get_resource_token(
&self,
access_token: &AccessToken,
account_id: u32,
) -> trc::Result<ResourceToken> {
Ok(if access_token.primary_id == account_id {
ResourceToken {
account_id,
quota: access_token.quota,
tenant: access_token.tenant,
}
} else {
let mut quotas = ResourceToken {
account_id,
..Default::default()
};
if let Some(principal) = self
.core
.storage
.directory
.query(QueryBy::Id(account_id), false)
.await
.add_context(|err| err.caused_by(trc::location!()).account_id(account_id))?
{
quotas.quota = principal.quota();
// SPDX-SnippetBegin
// SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
// SPDX-License-Identifier: LicenseRef-SEL
#[cfg(feature = "enterprise")]
if self.core.is_enterprise_edition() {
if let Some(tenant_id) = principal.tenant() {
quotas.tenant = TenantInfo {
id: tenant_id,
quota: self
.core
.storage
.directory
.query(QueryBy::Id(tenant_id), false)
.await
.add_context(|err| {
err.caused_by(trc::location!()).account_id(tenant_id)
})?
.map(|tenant| tenant.quota())
.unwrap_or_default(),
}
.into();
}
}
// SPDX-SnippetEnd
}
quotas
})
}
async fn get_used_quota(&self, account_id: u32) -> trc::Result<i64> {
self.core
.storage
.data
.get_counter(DirectoryClass::UsedQuota(account_id))
.await
.add_context(|err| err.caused_by(trc::location!()).account_id(account_id))
}
async fn has_available_quota(&self, quotas: &ResourceToken, item_size: u64) -> trc::Result<()> {
if quotas.quota != 0 {
let used_quota = self.get_used_quota(quotas.account_id).await? as u64;
if used_quota + item_size > quotas.quota {
return Err(trc::LimitEvent::Quota
.into_err()
.ctx(trc::Key::Limit, quotas.quota)
.ctx(trc::Key::Size, used_quota));
}
}
// SPDX-SnippetBegin
// SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
// SPDX-License-Identifier: LicenseRef-SEL
#[cfg(feature = "enterprise")]
if self.core.is_enterprise_edition() {
if let Some(tenant) = quotas.tenant.filter(|tenant| tenant.quota != 0) {
let used_quota = self.get_used_quota(tenant.id).await? as u64;
if used_quota + item_size > tenant.quota {
return Err(trc::LimitEvent::TenantQuota
.into_err()
.ctx(trc::Key::Limit, tenant.quota)
.ctx(trc::Key::Size, used_quota));
}
}
}
// SPDX-SnippetEnd
Ok(())
}
async fn filter(
&self,
account_id: u32,
@@ -466,21 +219,6 @@ impl JmapMethods for Server {
Ok(response)
}
async fn write_batch(&self, batch: BatchBuilder) -> trc::Result<AssignedIds> {
self.core
.storage
.data
.write(batch.build())
.await
.caused_by(trc::location!())
}
async fn write_batch_expect_id(&self, batch: BatchBuilder) -> trc::Result<u32> {
self.write_batch(batch)
.await
.and_then(|ids| ids.last_document_id().caused_by(trc::location!()))
}
fn increment_config_version(&self) {
self.inner
.data
@@ -490,62 +228,12 @@ impl JmapMethods for Server {
}
pub trait JmapMethods: Sync + Send {
fn get_property<U>(
&self,
account_id: u32,
collection: Collection,
document_id: u32,
property: impl AsRef<Property> + Sync + Send,
) -> impl Future<Output = trc::Result<Option<U>>> + Send
where
U: Deserialize + 'static;
fn get_properties<U, I, P>(
&self,
account_id: u32,
collection: Collection,
iterate: &I,
property: P,
) -> impl Future<Output = trc::Result<Vec<(u32, U)>>> + Send
where
I: DocumentSet + Send + Sync,
P: AsRef<Property> + Sync + Send,
U: Deserialize + 'static;
fn get_document_ids(
&self,
account_id: u32,
collection: Collection,
) -> impl Future<Output = trc::Result<Option<RoaringBitmap>>> + Send;
fn get_tag(
&self,
account_id: u32,
collection: Collection,
property: impl AsRef<Property> + Sync + Send,
value: impl Into<TagValue<u32>> + Sync + Send,
) -> impl Future<Output = trc::Result<Option<RoaringBitmap>>> + Send;
fn prepare_set_response<T: Sync + Send>(
&self,
request: &SetRequest<T>,
collection: Collection,
) -> impl Future<Output = trc::Result<SetResponse>> + Send;
fn get_resource_token(
&self,
access_token: &AccessToken,
account_id: u32,
) -> impl Future<Output = trc::Result<ResourceToken>> + Send;
fn get_used_quota(&self, account_id: u32) -> impl Future<Output = trc::Result<i64>> + Send;
fn has_available_quota(
&self,
quotas: &ResourceToken,
item_size: u64,
) -> impl Future<Output = trc::Result<()>> + Send;
fn filter(
&self,
account_id: u32,
@@ -574,16 +262,6 @@ pub trait JmapMethods: Sync + Send {
response: QueryResponse,
) -> impl Future<Output = trc::Result<QueryResponse>> + Send;
fn write_batch(
&self,
batch: BatchBuilder,
) -> impl Future<Output = trc::Result<AssignedIds>> + Send;
fn write_batch_expect_id(
&self,
batch: BatchBuilder,
) -> impl Future<Output = trc::Result<u32>> + Send;
fn increment_config_version(&self);
}
+2 -216
View File
@@ -5,22 +5,18 @@
*/
use common::{auth::AccessToken, Server};
use email::mailbox::MailboxFnc;
use jmap_proto::{
method::get::{GetRequest, GetResponse, RequestArguments},
object::Object,
types::{acl::Acl, collection::Collection, keyword::Keyword, property::Property, value::Value},
types::{acl::Acl, collection::Collection, property::Property, value::Value},
};
use store::{ahash::AHashSet, query::Filter, roaring::RoaringBitmap};
use trc::AddContext;
use crate::{
auth::acl::{AclMethods, EffectiveAcl},
changes::state::StateManager,
email::cache::ThreadCache,
JmapMethods,
};
use super::{set::MailboxSet, INBOX_ID};
use std::future::Future;
pub trait MailboxGet: Sync + Send {
@@ -29,38 +25,6 @@ pub trait MailboxGet: Sync + Send {
request: GetRequest<RequestArguments>,
access_token: &AccessToken,
) -> impl Future<Output = trc::Result<GetResponse>> + Send;
fn mailbox_count_threads(
&self,
account_id: u32,
document_ids: Option<RoaringBitmap>,
) -> impl Future<Output = trc::Result<usize>> + Send;
fn mailbox_unread_tags(
&self,
account_id: u32,
document_id: u32,
message_ids: &Option<RoaringBitmap>,
) -> impl Future<Output = trc::Result<Option<RoaringBitmap>>> + Send;
fn mailbox_expand_path<'x>(
&self,
account_id: u32,
path: &'x str,
exact_match: bool,
) -> impl Future<Output = trc::Result<Option<ExpandPath<'x>>>> + Send;
fn mailbox_get_by_name(
&self,
account_id: u32,
path: &str,
) -> impl Future<Output = trc::Result<Option<u32>>> + Send;
fn mailbox_get_by_role(
&self,
account_id: u32,
role: &str,
) -> impl Future<Output = trc::Result<Option<u32>>> + Send;
}
impl MailboxGet for Server {
@@ -282,182 +246,4 @@ impl MailboxGet for Server {
}
Ok(response)
}
async fn mailbox_count_threads(
&self,
account_id: u32,
document_ids: Option<RoaringBitmap>,
) -> trc::Result<usize> {
if let Some(document_ids) = document_ids {
let mut thread_ids = AHashSet::default();
self.get_cached_thread_ids(account_id, document_ids.into_iter())
.await
.caused_by(trc::location!())?
.into_iter()
.for_each(|(_, thread_id)| {
thread_ids.insert(thread_id);
});
Ok(thread_ids.len())
} else {
Ok(0)
}
}
async fn mailbox_unread_tags(
&self,
account_id: u32,
document_id: u32,
message_ids: &Option<RoaringBitmap>,
) -> trc::Result<Option<RoaringBitmap>> {
if let (Some(message_ids), Some(mailbox_message_ids)) = (
message_ids,
self.get_tag(
account_id,
Collection::Email,
Property::MailboxIds,
document_id,
)
.await?,
) {
if let Some(mut seen) = self
.get_tag(
account_id,
Collection::Email,
Property::Keywords,
Keyword::Seen,
)
.await?
{
seen ^= message_ids;
seen &= &mailbox_message_ids;
if !seen.is_empty() {
Ok(Some(seen))
} else {
Ok(None)
}
} else {
Ok(mailbox_message_ids.into())
}
} else {
Ok(None)
}
}
async fn mailbox_expand_path<'x>(
&self,
account_id: u32,
path: &'x str,
exact_match: bool,
) -> trc::Result<Option<ExpandPath<'x>>> {
let path = path
.split('/')
.filter_map(|p| {
let p = p.trim();
if !p.is_empty() {
p.into()
} else {
None
}
})
.collect::<Vec<_>>();
if path.is_empty() || path.len() > self.core.jmap.mailbox_max_depth {
return Ok(None);
}
let mut filter = Vec::with_capacity(path.len() + 2);
let mut has_inbox = false;
filter.push(Filter::Or);
for (pos, item) in path.iter().enumerate() {
if pos == 0 && item.eq_ignore_ascii_case("inbox") {
has_inbox = true;
} else {
filter.push(Filter::eq(Property::Name, *item));
}
}
filter.push(Filter::End);
let mut document_ids = if filter.len() > 2 {
self.filter(account_id, Collection::Mailbox, filter)
.await?
.results
} else {
RoaringBitmap::new()
};
if has_inbox {
document_ids.insert(INBOX_ID);
}
if exact_match && (document_ids.len() as usize) < path.len() {
return Ok(None);
}
let mut found_names = Vec::new();
for document_id in document_ids {
if let Some(mut obj) = self
.get_property::<Object<Value>>(
account_id,
Collection::Mailbox,
document_id,
Property::Value,
)
.await?
{
if let Some(Value::Text(value)) = obj.properties.remove(&Property::Name) {
found_names.push((
value,
if let Some(Value::Id(value)) = obj.properties.remove(&Property::ParentId) {
value.document_id()
} else {
0
},
document_id + 1,
));
} else {
return Ok(None);
}
} else {
return Ok(None);
}
}
Ok(Some(ExpandPath { path, found_names }))
}
async fn mailbox_get_by_name(&self, account_id: u32, path: &str) -> trc::Result<Option<u32>> {
Ok(self
.mailbox_expand_path(account_id, path, true)
.await?
.and_then(|ep| {
let mut next_parent_id = 0;
'outer: for (pos, name) in ep.path.iter().enumerate() {
let is_inbox = pos == 0 && name.eq_ignore_ascii_case("inbox");
for (part, parent_id, document_id) in &ep.found_names {
if (part.eq(name) || (is_inbox && part.eq_ignore_ascii_case("inbox")))
&& *parent_id == next_parent_id
{
next_parent_id = *document_id;
continue 'outer;
}
}
return None;
}
Some(next_parent_id - 1)
}))
}
async fn mailbox_get_by_role(&self, account_id: u32, role: &str) -> trc::Result<Option<u32>> {
self.filter(
account_id,
Collection::Mailbox,
vec![Filter::eq(Property::Role, role)],
)
.await
.map(|r| r.results.min())
}
}
#[derive(Debug)]
pub struct ExpandPath<'x> {
pub path: Vec<&'x str>,
pub found_names: Vec<(String, u32, u32)>,
}
-78
View File
@@ -4,84 +4,6 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::slice::Iter;
use store::{
write::{
BitmapClass, DeserializeFrom, MaybeDynamicId, Operation, SerializeInto, TagValue, ToBitmaps,
},
Serialize, U32_LEN,
};
use utils::codec::leb128::{Leb128Iterator, Leb128Vec};
pub mod get;
pub mod query;
pub mod set;
pub const INBOX_ID: u32 = 0;
pub const TRASH_ID: u32 = 1;
pub const JUNK_ID: u32 = 2;
pub const DRAFTS_ID: u32 = 3;
pub const SENT_ID: u32 = 4;
pub const ARCHIVE_ID: u32 = 5;
pub const TOMBSTONE_ID: u32 = u32::MAX - 1;
#[derive(Debug, Clone, Copy)]
pub struct UidMailbox {
pub mailbox_id: u32,
pub uid: u32,
}
impl PartialEq for UidMailbox {
fn eq(&self, other: &Self) -> bool {
self.mailbox_id == other.mailbox_id
}
}
impl Eq for UidMailbox {}
impl ToBitmaps for UidMailbox {
fn to_bitmaps(&self, ops: &mut Vec<Operation>, field: u8, set: bool) {
ops.push(Operation::Bitmap {
class: BitmapClass::Tag {
field,
value: TagValue::Id(MaybeDynamicId::Static(self.mailbox_id)),
},
set,
});
}
}
impl SerializeInto for UidMailbox {
fn serialize_into(&self, buf: &mut Vec<u8>) {
buf.push_leb128(self.mailbox_id);
buf.push_leb128(self.uid);
}
}
impl DeserializeFrom for UidMailbox {
fn deserialize_from(bytes: &mut Iter<'_, u8>) -> Option<Self> {
Some(UidMailbox {
mailbox_id: bytes.next_leb128()?,
uid: bytes.next_leb128()?,
})
}
}
impl Serialize for UidMailbox {
fn serialize(self) -> Vec<u8> {
let mut buf = Vec::with_capacity(U32_LEN * 2);
self.serialize_into(&mut buf);
buf
}
}
impl UidMailbox {
pub fn new(mailbox_id: u32, uid: u32) -> Self {
UidMailbox { mailbox_id, uid }
}
pub fn new_unassigned(mailbox_id: u32) -> Self {
UidMailbox { mailbox_id, uid: 0 }
}
}
+1 -2
View File
@@ -5,6 +5,7 @@
*/
use common::{auth::AccessToken, Server};
use email::mailbox::MailboxFnc;
use jmap_proto::{
method::query::{Comparator, Filter, QueryRequest, QueryResponse, SortProperty},
object::{mailbox::QueryArguments, Object},
@@ -19,8 +20,6 @@ use store::{
use crate::{auth::acl::AclMethods, JmapMethods, UpdateResults};
use std::future::Future;
use super::set::MailboxSet;
pub trait MailboxQuery: Sync + Send {
fn mailbox_query(
&self,
+4 -181
View File
@@ -4,16 +4,13 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use common::{auth::AccessToken, config::jmap::settings::SpecialUse, Server};
use common::{auth::AccessToken, Server};
use directory::Permission;
use email::mailbox::{MailboxFnc, SCHEMA};
use jmap_proto::{
error::set::{SetError, SetErrorType},
method::set::{SetRequest, SetResponse},
object::{
index::{IndexAs, IndexProperty, ObjectIndexBuilder},
mailbox::SetArguments,
Object,
},
object::{index::ObjectIndexBuilder, mailbox::SetArguments, Object},
response::references::EvalObjectReferences,
types::{
acl::Acl,
@@ -34,18 +31,15 @@ use store::{
BatchBuilder, F_BITMAP, F_CLEAR, F_VALUE,
},
};
use trc::AddContext;
use crate::{
auth::acl::{AclMethods, EffectiveAcl},
changes::write::ChangeLog,
email::delete::EmailDeletion,
JmapMethods,
};
use super::{get::MailboxGet, ARCHIVE_ID, DRAFTS_ID, SENT_ID};
#[allow(unused_imports)]
use super::{UidMailbox, INBOX_ID, JUNK_ID, TRASH_ID};
use email::mailbox::{UidMailbox, INBOX_ID, JUNK_ID, TRASH_ID};
use std::future::Future;
pub struct SetContext<'x> {
@@ -57,24 +51,6 @@ pub struct SetContext<'x> {
will_destroy: Vec<Id>,
}
pub static SCHEMA: &[IndexProperty] = &[
IndexProperty::new(Property::Name)
.index_as(IndexAs::Text {
tokenize: true,
index: true,
})
.required(),
IndexProperty::new(Property::Role).index_as(IndexAs::Text {
tokenize: false,
index: true,
}),
IndexProperty::new(Property::Role).index_as(IndexAs::HasProperty),
IndexProperty::new(Property::ParentId).index_as(IndexAs::Integer),
IndexProperty::new(Property::SortOrder).index_as(IndexAs::Integer),
IndexProperty::new(Property::IsSubscribed).index_as(IndexAs::IntegerList),
IndexProperty::new(Property::Acl).index_as(IndexAs::Acl),
];
pub trait MailboxSet: Sync + Send {
fn mailbox_set(
&self,
@@ -97,17 +73,6 @@ pub trait MailboxSet: Sync + Send {
update: Option<(u32, HashedValue<Object<Value>>)>,
ctx: &SetContext,
) -> impl Future<Output = trc::Result<Result<ObjectIndexBuilder, SetError>>> + Send;
fn mailbox_get_or_create(
&self,
account_id: u32,
) -> impl Future<Output = trc::Result<RoaringBitmap>> + Send;
fn mailbox_create_path(
&self,
account_id: u32,
path: &str,
) -> impl Future<Output = trc::Result<Option<(u32, Option<u64>)>>> + Send;
}
impl MailboxSet for Server {
@@ -801,148 +766,6 @@ impl MailboxSet for Server {
.with_current_opt(current)
.validate())
}
async fn mailbox_get_or_create(&self, account_id: u32) -> trc::Result<RoaringBitmap> {
let mut mailbox_ids = self
.get_document_ids(account_id, Collection::Mailbox)
.await?
.unwrap_or_default();
if !mailbox_ids.is_empty() {
return Ok(mailbox_ids);
}
#[cfg(feature = "test_mode")]
if mailbox_ids.is_empty() && account_id == 0 {
return Ok(mailbox_ids);
}
let mut batch = BatchBuilder::new();
batch
.with_account_id(account_id)
.with_collection(Collection::Mailbox);
// Create mailboxes
let mut last_document_id = ARCHIVE_ID;
for folder in &self.core.jmap.default_folders {
let (role, document_id) = match folder.special_use {
SpecialUse::Inbox => ("inbox", INBOX_ID),
SpecialUse::Trash => ("trash", TRASH_ID),
SpecialUse::Junk => ("junk", JUNK_ID),
SpecialUse::Drafts => ("drafts", DRAFTS_ID),
SpecialUse::Sent => ("sent", SENT_ID),
SpecialUse::Archive => ("archive", ARCHIVE_ID),
SpecialUse::None => {
last_document_id += 1;
("", last_document_id)
}
SpecialUse::Shared => unreachable!(),
};
let mut object = Object::with_capacity(4)
.with_property(Property::Name, folder.name.clone())
.with_property(Property::ParentId, Value::Id(0u64.into()))
.with_property(
Property::Cid,
Value::UnsignedInt(rand::random::<u32>() as u64),
);
if !role.is_empty() {
object.set(Property::Role, role);
}
if folder.subscribe {
object.set(
Property::IsSubscribed,
Value::List(vec![Value::Id(account_id.into())]),
);
}
batch
.create_document_with_id(document_id)
.custom(ObjectIndexBuilder::new(SCHEMA).with_changes(object));
mailbox_ids.insert(document_id);
}
self.core
.storage
.data
.write(batch.build())
.await
.caused_by(trc::location!())
.map(|_| mailbox_ids)
}
async fn mailbox_create_path(
&self,
account_id: u32,
path: &str,
) -> trc::Result<Option<(u32, Option<u64>)>> {
let expanded_path =
if let Some(expand_path) = self.mailbox_expand_path(account_id, path, false).await? {
expand_path
} else {
return Ok(None);
};
let mut next_parent_id = 0;
let mut path = expanded_path.path.into_iter().enumerate().peekable();
'outer: while let Some((pos, name)) = path.peek() {
let is_inbox = *pos == 0 && name.eq_ignore_ascii_case("inbox");
for (part, parent_id, document_id) in &expanded_path.found_names {
if (part.eq(name) || (is_inbox && part.eq_ignore_ascii_case("inbox")))
&& *parent_id == next_parent_id
{
next_parent_id = *document_id;
path.next();
continue 'outer;
}
}
break;
}
// Create missing folders
if path.peek().is_some() {
let mut changes = self.begin_changes(account_id).await?;
for (_, name) in path {
if name.len() > self.core.jmap.mailbox_name_max_len {
return Ok(None);
}
let mut batch = BatchBuilder::new();
batch
.with_account_id(account_id)
.with_collection(Collection::Mailbox)
.create_document()
.custom(
ObjectIndexBuilder::new(SCHEMA).with_changes(
Object::with_capacity(3)
.with_property(Property::Name, name)
.with_property(
Property::ParentId,
Value::Id(Id::from(next_parent_id)),
)
.with_property(
Property::Cid,
Value::UnsignedInt(rand::random::<u32>() as u64),
),
),
);
let document_id = self.write_batch_expect_id(batch).await?;
changes.log_insert(Collection::Mailbox, document_id);
next_parent_id = document_id + 1;
}
let change_id = changes.change_id;
let mut batch = BatchBuilder::new();
batch
.with_account_id(account_id)
.with_collection(Collection::Mailbox)
.custom(changes);
self.write_batch(batch).await?;
Ok(Some((next_parent_id - 1, Some(change_id))))
} else {
Ok(Some((next_parent_id - 1, None)))
}
}
}
pub trait MailboxSubscribe {
-2
View File
@@ -13,8 +13,6 @@ use jmap_proto::{
};
use std::future::Future;
use crate::JmapMethods;
pub trait PrincipalGet: Sync + Send {
fn principal_get(
&self,
-2
View File
@@ -21,8 +21,6 @@ use store::{
};
use utils::map::bitmap::Bitmap;
use crate::JmapMethods;
use super::{EncryptionKeys, PushSubscription};
use std::future::Future;
+15 -4
View File
@@ -24,8 +24,9 @@ use store::{
rand::{distributions::Alphanumeric, thread_rng, Rng},
write::{now, BatchBuilder, F_CLEAR, F_VALUE},
};
use trc::AddContext;
use crate::{services::state::StateManager, JmapMethods};
use crate::services::state::StateManager;
const EXPIRES_MAX: i64 = 7 * 24 * 3600; // 7 days
const VERIFICATION_CODE_LEN: usize = 32;
@@ -119,7 +120,11 @@ impl PushSubscriptionSet for Server {
.with_collection(Collection::PushSubscription)
.create_document()
.value(Property::Value, push, F_VALUE);
let document_id = self.write_batch_expect_id(batch).await?;
let document_id = self
.store()
.write_expect_id(batch)
.await
.caused_by(trc::location!())?;
push_ids.insert(document_id);
response.created.insert(
id,
@@ -180,7 +185,10 @@ impl PushSubscriptionSet for Server {
.with_collection(Collection::PushSubscription)
.update_document(document_id)
.value(Property::Value, push, F_VALUE);
self.write_batch(batch).await?;
self.store()
.write(batch)
.await
.caused_by(trc::location!())?;
response.updated.append(id, None);
}
@@ -195,7 +203,10 @@ impl PushSubscriptionSet for Server {
.with_collection(Collection::PushSubscription)
.delete_document(document_id)
.value(Property::Value, (), F_VALUE | F_CLEAR);
self.write_batch(batch).await?;
self.store()
.write(batch)
.await
.caused_by(trc::location!())?;
response.destroyed.push(id);
} else {
response.not_destroyed.append(id, SetError::not_found());
-2
View File
@@ -12,8 +12,6 @@ use jmap_proto::{
};
use std::future::Future;
use crate::JmapMethods;
pub trait QuotaGet: Sync + Send {
fn quota_get(
&self,
-57
View File
@@ -1,57 +0,0 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::sync::Arc;
use common::{core::BuildServer, ipc::DeliveryEvent, Inner};
use tokio::sync::{mpsc, Semaphore};
use super::ingest::MailDelivery;
pub fn spawn_delivery_manager(inner: Arc<Inner>, mut delivery_rx: mpsc::Receiver<DeliveryEvent>) {
tokio::spawn(async move {
let semaphore = Arc::new(Semaphore::new(
inner
.shared_core
.load()
.smtp
.queue
.throttle
.local_concurrency,
));
loop {
let permit = match semaphore.clone().acquire_owned().await {
Ok(permit) => permit,
Err(_) => {
trc::error!(trc::StoreEvent::UnexpectedError
.into_err()
.details("Semaphore error")
.caused_by(trc::location!()));
break;
}
};
match delivery_rx.recv().await {
Some(event) => match event {
DeliveryEvent::Ingest { message, result_tx } => {
let server = inner.build_server();
tokio::spawn(async move {
result_tx.send(server.deliver_message(message).await).ok();
drop(permit);
});
}
DeliveryEvent::Stop => break,
},
None => {
break;
}
}
}
});
}
-2
View File
@@ -10,8 +10,6 @@ use common::{
};
use trc::ClusterEvent;
use crate::services::index::Indexer;
use super::{request::Request, Gossiper, PeerStatus};
impl Gossiper {
+2 -11
View File
@@ -11,6 +11,7 @@ use directory::{
backend::internal::{manage::ManageDirectory, PrincipalField},
Type,
};
use email::{index::IndexMessageText, metadata::MessageMetadata};
use jmap_proto::types::{collection::Collection, property::Property};
use store::{
ahash::AHashMap,
@@ -27,12 +28,7 @@ use std::future::Future;
use trc::{AddContext, TaskQueueEvent};
use utils::{BlobHash, BLOB_HASH_LEN};
use crate::{
blob::download::BlobDownload,
changes::write::ChangeLog,
email::{bayes::EmailBayesTrain, index::IndexMessageText, metadata::MessageMetadata},
JmapMethods,
};
use crate::{blob::download::BlobDownload, email::bayes::EmailBayesTrain};
#[derive(Debug, Clone)]
pub struct EmailTask {
@@ -81,7 +77,6 @@ pub trait Indexer: Sync + Send {
account_id: Option<u32>,
tenant_id: Option<u32>,
) -> impl Future<Output = trc::Result<()>> + Send;
fn notify_task_queue(&self);
}
impl Indexer for Server {
@@ -313,10 +308,6 @@ impl Indexer for Server {
}
}
fn notify_task_queue(&self) {
self.inner.ipc.index_tx.notify_one();
}
async fn reindex(&self, account_id: Option<u32>, tenant_id: Option<u32>) -> trc::Result<()> {
let accounts = if let Some(account_id) = account_id {
RoaringBitmap::from_sorted_iter([account_id]).unwrap()
-2
View File
@@ -4,9 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
pub mod delivery;
pub mod gossip;
pub mod housekeeper;
pub mod index;
pub mod ingest;
pub mod state;
-27
View File
@@ -372,11 +372,6 @@ pub trait StateManager: Sync + Send {
types: Bitmap<DataType>,
) -> impl Future<Output = trc::Result<mpsc::Receiver<StateChange>>> + Send;
fn broadcast_state_change(
&self,
state_change: StateChange,
) -> impl Future<Output = bool> + Send;
fn update_push_subscriptions(&self, account_id: u32) -> impl Future<Output = bool> + Send;
}
@@ -407,28 +402,6 @@ impl StateManager for Server {
Ok(change_rx)
}
async fn broadcast_state_change(&self, state_change: StateChange) -> bool {
match self
.inner
.ipc
.state_tx
.clone()
.send(StateEvent::Publish { state_change })
.await
{
Ok(_) => true,
Err(_) => {
trc::event!(
Server(ServerEvent::ThreadError),
Details = "Error sending state change.",
CausedBy = trc::location!()
);
false
}
}
}
async fn update_push_subscriptions(&self, account_id: u32) -> bool {
let push_subs = match self.fetch_push_subscriptions(account_id).await {
Ok(push_subs) => push_subs,
+2 -204
View File
@@ -4,29 +4,16 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::sync::Arc;
use common::Server;
use jmap_proto::{
method::get::{GetRequest, GetResponse, RequestArguments},
object::Object,
types::{collection::Collection, property::Property, value::Value},
};
use sieve::Sieve;
use store::{
query::Filter,
write::{assert::HashedValue, BatchBuilder, Bincode, BlobOp},
BlobClass, Deserialize, Serialize,
};
use store::BlobClass;
use crate::{
blob::{download::BlobDownload, upload::BlobUpload},
changes::state::StateManager,
sieve::SeenIds,
JmapMethods,
};
use crate::changes::state::StateManager;
use super::ActiveScript;
use std::future::Future;
pub trait SieveScriptGet: Sync + Send {
@@ -34,23 +21,6 @@ pub trait SieveScriptGet: Sync + Send {
&self,
request: GetRequest<RequestArguments>,
) -> impl Future<Output = trc::Result<GetResponse>> + Send;
fn sieve_script_get_active(
&self,
account_id: u32,
) -> impl Future<Output = trc::Result<Option<ActiveScript>>> + Send;
fn sieve_script_get_by_name(
&self,
account_id: u32,
name: &str,
) -> impl Future<Output = trc::Result<Option<Sieve>>> + Send;
fn sieve_script_compile(
&self,
account_id: u32,
document_id: u32,
) -> impl Future<Output = trc::Result<(Sieve, Object<Value>)>> + Send;
}
impl SieveScriptGet for Server {
@@ -145,176 +115,4 @@ impl SieveScriptGet for Server {
Ok(response)
}
async fn sieve_script_get_active(&self, account_id: u32) -> trc::Result<Option<ActiveScript>> {
// Find the currently active script
if let Some(document_id) = self
.filter(
account_id,
Collection::SieveScript,
vec![Filter::eq(Property::IsActive, 1u32)],
)
.await?
.results
.min()
{
let (script, mut script_object) =
self.sieve_script_compile(account_id, document_id).await?;
Ok(Some(ActiveScript {
document_id,
script: Arc::new(script),
script_name: script_object
.properties
.remove(&Property::Name)
.and_then(|name| name.try_unwrap_string())
.unwrap_or_else(|| account_id.to_string()),
seen_ids: self
.get_property::<Bincode<SeenIds>>(
account_id,
Collection::SieveScript,
document_id,
Property::EmailIds,
)
.await?
.map(|seen_ids| seen_ids.inner)
.unwrap_or_default(),
}))
} else {
Ok(None)
}
}
async fn sieve_script_get_by_name(
&self,
account_id: u32,
name: &str,
) -> trc::Result<Option<Sieve>> {
// Find the script by name
if let Some(document_id) = self
.filter(
account_id,
Collection::SieveScript,
vec![Filter::eq(Property::Name, name)],
)
.await?
.results
.min()
{
self.sieve_script_compile(account_id, document_id)
.await
.map(|(sieve, _)| Some(sieve))
} else {
Ok(None)
}
}
#[allow(clippy::blocks_in_conditions)]
async fn sieve_script_compile(
&self,
account_id: u32,
document_id: u32,
) -> trc::Result<(Sieve, Object<Value>)> {
// Obtain script object
let script_object = self
.get_property::<HashedValue<Object<Value>>>(
account_id,
Collection::SieveScript,
document_id,
Property::Value,
)
.await?
.ok_or_else(|| {
trc::StoreEvent::NotFound
.into_err()
.caused_by(trc::location!())
.document_id(document_id)
})?;
// Obtain the sieve script length
let (script_offset, blob_id) = script_object
.inner
.properties
.get(&Property::BlobId)
.and_then(|v| v.as_blob_id())
.and_then(|v| (v.section.as_ref()?.size, v).into())
.ok_or_else(|| {
trc::StoreEvent::NotFound
.into_err()
.caused_by(trc::location!())
.document_id(document_id)
})?;
// Obtain the sieve script blob
let script_bytes = self
.get_blob(&blob_id.hash, 0..usize::MAX)
.await?
.ok_or_else(|| {
trc::StoreEvent::NotFound
.into_err()
.caused_by(trc::location!())
.document_id(document_id)
})?;
// Obtain the precompiled script
if let Some(sieve) = script_bytes
.get(script_offset..)
.and_then(|bytes| Bincode::<Sieve>::deserialize(bytes).ok())
{
Ok((sieve.inner, script_object.inner))
} else {
// Deserialization failed, probably because the script compiler version changed
match self.core.sieve.untrusted_compiler.compile(
script_bytes.get(0..script_offset).ok_or_else(|| {
trc::StoreEvent::NotFound
.into_err()
.caused_by(trc::location!())
.document_id(document_id)
})?,
) {
Ok(sieve) => {
// Store updated compiled sieve script
let sieve = Bincode::new(sieve);
let compiled_bytes = (&sieve).serialize();
let mut updated_sieve_bytes =
Vec::with_capacity(script_offset + compiled_bytes.len());
updated_sieve_bytes.extend_from_slice(&script_bytes[0..script_offset]);
updated_sieve_bytes.extend_from_slice(&compiled_bytes);
// Store updated blob
let mut new_blob_id = blob_id.clone();
new_blob_id.hash = self
.put_blob(account_id, &updated_sieve_bytes, false)
.await?
.hash;
let mut new_script_object = script_object.inner.clone();
new_script_object.set(Property::BlobId, new_blob_id.clone());
// Update script object
let mut batch = BatchBuilder::new();
batch
.with_account_id(account_id)
.with_collection(Collection::SieveScript)
.update_document(document_id)
.assert_value(Property::Value, &script_object)
.set(Property::Value, (&new_script_object).serialize())
.clear(BlobOp::Link {
hash: blob_id.hash.clone(),
})
.set(
BlobOp::Link {
hash: new_blob_id.hash,
},
Vec::new(),
);
self.write_batch(batch).await?;
Ok((sieve.inner, new_script_object))
}
Err(error) => Err(trc::StoreEvent::UnexpectedError
.caused_by(trc::location!())
.reason(error)
.details("Failed to compile Sieve script")),
}
}
}
}
-130
View File
@@ -4,137 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use std::sync::Arc;
use serde::ser::SerializeSeq;
use sieve::Sieve;
use store::{ahash::AHashSet, blake3, write::now};
pub mod get;
pub mod ingest;
pub mod query;
pub mod set;
pub mod validate;
pub struct ActiveScript {
pub document_id: u32,
pub script_name: String,
pub script: Arc<Sieve>,
pub seen_ids: SeenIds,
}
#[derive(Debug, Clone)]
pub struct SeenIdHash {
hash: [u8; 32],
expiry: u64,
}
#[derive(Debug, Clone, Default)]
pub struct SeenIds {
pub ids: AHashSet<SeenIdHash>,
pub has_changes: bool,
}
impl SeenIdHash {
pub fn new(id: &str, expiry: u64) -> Self {
let mut hasher = blake3::Hasher::new();
hasher.update(id.as_bytes());
SeenIdHash {
hash: hasher.finalize().into(),
expiry,
}
}
}
impl PartialOrd for SeenIdHash {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for SeenIdHash {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.expiry.cmp(&other.expiry)
}
}
impl std::hash::Hash for SeenIdHash {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.hash.hash(state);
}
}
impl PartialEq for SeenIdHash {
fn eq(&self, other: &Self) -> bool {
self.hash == other.hash
}
}
impl Eq for SeenIdHash {}
// SeenIds serializer
impl serde::Serialize for SeenIds {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut seq = serializer.serialize_seq((self.ids.len() * 2).into())?;
for id in &self.ids {
seq.serialize_element(&id.expiry)?;
seq.serialize_element(&id.hash)?;
}
seq.end()
}
}
impl<'de> serde::Deserialize<'de> for SeenIds {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_seq(SeenIdsVisitor)
}
}
struct SeenIdsVisitor;
impl<'de> serde::de::Visitor<'de> for SeenIdsVisitor {
type Value = SeenIds;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("invalid SeenIds")
}
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
let num_entries = seq.size_hint().unwrap_or(0) / 2;
let mut seen_ids = SeenIds {
ids: AHashSet::with_capacity(num_entries),
has_changes: false,
};
let now = now();
for _ in 0..num_entries {
let expiry = seq
.next_element::<u64>()?
.ok_or_else(|| serde::de::Error::custom("Expected expiry."))?;
if expiry > now {
seen_ids.ids.insert(SeenIdHash {
hash: seq
.next_element()?
.ok_or_else(|| serde::de::Error::custom("Expected hash."))?,
expiry,
});
} else {
seq.next_element::<[u8; 32]>()?
.ok_or_else(|| serde::de::Error::custom("Expected hash."))?;
seen_ids.has_changes = true;
}
}
Ok(seen_ids)
}
}
+11 -8
View File
@@ -36,13 +36,9 @@ use store::{
},
BlobClass,
};
use trc::AddContext;
use crate::{
api::http::HttpSessionData,
blob::{download::BlobDownload, upload::BlobUpload},
changes::write::ChangeLog,
JmapMethods,
};
use crate::{api::http::HttpSessionData, blob::download::BlobDownload, JmapMethods};
use std::future::Future;
pub struct SetContext<'x> {
@@ -152,7 +148,11 @@ impl SieveScriptSet for Server {
}
}
let document_id = self.write_batch_expect_id(batch).await?;
let document_id = self
.store()
.write_expect_id(batch)
.await
.caused_by(trc::location!())?;
sieve_ids.insert(document_id);
changes.log_insert(Collection::SieveScript, document_id);
@@ -444,7 +444,10 @@ impl SieveScriptSet for Server {
}
}
self.write_batch(batch).await?;
self.store()
.write(batch)
.await
.caused_by(trc::location!())?;
Ok(true)
}
+1 -1
View File
@@ -13,7 +13,7 @@ use jmap_proto::{
use smtp::queue::{self, spool::SmtpSpool};
use std::future::Future;
use crate::{changes::state::StateManager, JmapMethods};
use crate::changes::state::StateManager;
pub trait EmailSubmissionGet: Sync + Send {
fn email_submission_get(
+16 -7
View File
@@ -10,6 +10,7 @@ use common::{
listener::{stream::NullIo, ServerInstance},
Server,
};
use email::metadata::MessageMetadata;
use jmap_proto::{
error::set::{SetError, SetErrorType},
method::set::{self, SetRequest, SetResponse},
@@ -38,12 +39,10 @@ use smtp::{
};
use smtp_proto::{request::parser::Rfc5321Parser, MailFrom, RcptTo};
use store::write::{assert::HashedValue, log::ChangeLogBuilder, now, BatchBuilder, Bincode};
use trc::AddContext;
use utils::{map::vec_map::VecMap, sanitize_email};
use crate::{
blob::download::BlobDownload, changes::write::ChangeLog, email::metadata::MessageMetadata,
JmapMethods,
};
use crate::blob::download::BlobDownload;
use std::future::Future;
pub static SCHEMA: &[IndexProperty] = &[
@@ -107,7 +106,11 @@ impl EmailSubmissionSet for Server {
.with_collection(Collection::EmailSubmission)
.create_document()
.custom(ObjectIndexBuilder::new(SCHEMA).with_changes(submission));
let document_id = self.write_batch_expect_id(batch).await?;
let document_id = self
.store()
.write_expect_id(batch)
.await
.caused_by(trc::location!())?;
changes.log_insert(Collection::EmailSubmission, document_id);
response.created(id, document_id);
}
@@ -193,7 +196,10 @@ impl EmailSubmissionSet for Server {
.with_property(Property::UndoStatus, undo_status),
),
);
self.write_batch(batch).await?;
self.store()
.write(batch)
.await
.caused_by(trc::location!())?;
changes.log_update(Collection::EmailSubmission, document_id);
response.updated.append(id, None);
} else {
@@ -242,7 +248,10 @@ impl EmailSubmissionSet for Server {
.with_collection(Collection::EmailSubmission)
.delete_document(document_id)
.custom(ObjectIndexBuilder::new(SCHEMA).with_current(submission));
self.write_batch(batch).await?;
self.store()
.write(batch)
.await
.caused_by(trc::location!())?;
changes.log_delete(Collection::EmailSubmission, document_id);
response.destroyed.push(id);
} else {
+1 -1
View File
@@ -14,7 +14,7 @@ use std::future::Future;
use store::query::{sort::Pagination, Comparator, ResultSet};
use trc::AddContext;
use crate::{changes::state::StateManager, JmapMethods};
use crate::changes::state::StateManager;
pub trait ThreadGet: Sync + Send {
fn thread_get(
+11 -5
View File
@@ -28,10 +28,9 @@ use store::write::{
log::{Changes, LogInsert},
BatchBuilder, BlobOp, DirectoryClass, F_CLEAR, F_VALUE,
};
use trc::AddContext;
use crate::{
blob::upload::BlobUpload,
changes::write::ChangeLog,
sieve::set::{ObjectBlobId, SieveScriptSet, SCHEMA},
JmapMethods,
};
@@ -119,7 +118,7 @@ impl VacationResponseSet for Server {
// Prepare write batch
let mut batch = BatchBuilder::new();
let change_id = self.assign_change_id(account_id).await?;
let change_id = self.assign_change_id(account_id)?;
batch
.with_change_id(change_id)
.with_account_id(account_id)
@@ -307,7 +306,11 @@ impl VacationResponseSet for Server {
// Write changes
batch.custom(obj);
let document_id = if !batch.is_empty() {
let ids = self.write_batch(batch).await?;
let ids = self
.store()
.write(batch)
.await
.caused_by(trc::location!())?;
response.new_state = Some(change_id.into());
match document_id {
Some(document_id) => document_id,
@@ -350,7 +353,10 @@ impl VacationResponseSet for Server {
// Write changes
if !batch.is_empty() {
self.write_batch(batch).await?;
self.store()
.write(batch)
.await
.caused_by(trc::location!())?;
response.new_state = Some(change_id.into());
}
}
+5 -4
View File
@@ -76,7 +76,8 @@ impl WebSocketHandler for Server {
let _ = stream
.send(Message::Text(
WebSocketRequestError::from(RequestError::internal_server_error())
.to_json(),
.to_json()
.into(),
))
.await;
return;
@@ -128,7 +129,7 @@ impl WebSocketHandler for Server {
response
},
};
if let Err(err) = stream.send(Message::Text(response)).await {
if let Err(err) = stream.send(Message::Text(response.into())).await {
trc::event!(Jmap(JmapEvent::WebsocketError),
Details = "Failed to send text message",
SpanId = session.session_id,
@@ -208,7 +209,7 @@ impl WebSocketHandler for Server {
// Send any queued changes
let elapsed = last_changes_sent.elapsed();
if elapsed >= throttle {
if let Err(err) = stream.send(Message::Text(changes.to_json())).await {
if let Err(err) = stream.send(Message::Text(changes.to_json().into())).await {
trc::event!(
Jmap(JmapEvent::WebsocketError),
Details = "Failed to send state change message.",
@@ -224,7 +225,7 @@ impl WebSocketHandler for Server {
next_event = throttle - elapsed;
}
} else if last_heartbeat.elapsed() > heartbeat {
if let Err(err) = stream.send(Message::Ping(vec![])).await {
if let Err(err) = stream.send(Message::Ping(Vec::<u8>::new().into())).await {
trc::event!(
Jmap(JmapEvent::WebsocketError),
Details = "Failed to send ping message.",
+1 -1
View File
@@ -9,7 +9,7 @@ use std::time::Instant;
use common::listener::SessionStream;
use directory::Permission;
use imap_proto::receiver::Request;
use jmap::{changes::write::ChangeLog, sieve::set::SieveScriptSet};
use jmap::sieve::set::SieveScriptSet;
use jmap_proto::types::collection::Collection;
use store::write::log::ChangeLogBuilder;
use trc::AddContext;
+1 -1
View File
@@ -9,7 +9,7 @@ use std::time::Instant;
use common::listener::SessionStream;
use directory::Permission;
use imap_proto::receiver::Request;
use jmap::{blob::download::BlobDownload, sieve::set::ObjectBlobId, JmapMethods};
use jmap::{blob::download::BlobDownload, sieve::set::ObjectBlobId};
use jmap_proto::{
object::Object,
types::{collection::Collection, property::Property, value::Value},
-1
View File
@@ -9,7 +9,6 @@ use std::time::Instant;
use common::listener::SessionStream;
use directory::Permission;
use imap_proto::receiver::Request;
use jmap::JmapMethods;
use trc::AddContext;
use crate::core::{Command, ResponseCode, Session, StatusResponse};
-1
View File
@@ -8,7 +8,6 @@ use std::time::Instant;
use common::listener::SessionStream;
use directory::Permission;
use jmap::JmapMethods;
use jmap_proto::{
object::Object,
types::{collection::Collection, property::Property, value::Value},
+4 -3
View File
@@ -10,7 +10,6 @@ use common::listener::SessionStream;
use directory::Permission;
use imap_proto::receiver::Request;
use jmap::{
blob::upload::BlobUpload,
sieve::set::{ObjectBlobId, SCHEMA},
JmapMethods,
};
@@ -188,7 +187,8 @@ impl<T: SessionStream> Session<T> {
),
);
self.server
.write_batch(batch)
.store()
.write(batch)
.await
.caused_by(trc::location!())?;
@@ -248,7 +248,8 @@ impl<T: SessionStream> Session<T> {
let assigned_ids = self
.server
.write_batch(batch)
.store()
.write(batch)
.await
.caused_by(trc::location!())?;
+3 -2
View File
@@ -9,7 +9,7 @@ use std::time::Instant;
use common::listener::SessionStream;
use directory::Permission;
use imap_proto::receiver::Request;
use jmap::{changes::write::ChangeLog, sieve::set::SCHEMA, JmapMethods};
use jmap::sieve::set::SCHEMA;
use jmap_proto::{
object::{index::ObjectIndexBuilder, Object},
types::{collection::Collection, property::Property, value::Value},
@@ -93,7 +93,8 @@ impl<T: SessionStream> Session<T> {
);
if !batch.is_empty() {
self.server
.write_batch(batch)
.store()
.write(batch)
.await
.caused_by(trc::location!())?;
let mut changelog = ChangeLogBuilder::new();
+1 -1
View File
@@ -9,7 +9,7 @@ use std::time::Instant;
use common::listener::SessionStream;
use directory::Permission;
use imap_proto::receiver::Request;
use jmap::{changes::write::ChangeLog, sieve::set::SieveScriptSet};
use jmap::sieve::set::SieveScriptSet;
use jmap_proto::types::collection::Collection;
use store::write::log::ChangeLogBuilder;
use trc::AddContext;
+1
View File
@@ -13,6 +13,7 @@ imap = { path = "../imap" }
utils = { path = "../utils" }
trc = { path = "../trc" }
jmap_proto = { path = "../jmap-proto" }
email = { path = "../email" }
mail-parser = { version = "0.9", features = ["full_encoding", "ludicrous_mode"] }
mail-send = { version = "0.4", default-features = false, features = ["cram-md5", "ring", "tls12"] }
rustls = { version = "0.23.5", default-features = false, features = ["std", "ring", "tls12"] }
+1 -4
View File
@@ -7,10 +7,7 @@
use std::collections::BTreeMap;
use common::listener::SessionStream;
use jmap::{
mailbox::{set::MailboxSet, UidMailbox, INBOX_ID},
JmapMethods,
};
use email::mailbox::{MailboxFnc, UidMailbox, INBOX_ID};
use jmap_proto::{
object::Object,
types::{collection::Collection, property::Property, value::Value},
+1 -3
View File
@@ -8,9 +8,7 @@ use std::time::Instant;
use common::listener::SessionStream;
use directory::Permission;
use jmap::{
changes::write::ChangeLog, email::delete::EmailDeletion, services::state::StateManager,
};
use jmap::email::delete::EmailDeletion;
use jmap_proto::types::{state::StateChange, type_state::DataType};
use store::roaring::RoaringBitmap;
use trc::AddContext;
+2 -1
View File
@@ -8,7 +8,8 @@ use std::time::Instant;
use common::listener::SessionStream;
use directory::Permission;
use jmap::{blob::download::BlobDownload, email::metadata::MessageMetadata, JmapMethods};
use email::metadata::MessageMetadata;
use jmap::blob::download::BlobDownload;
use jmap_proto::types::{collection::Collection, property::Property};
use store::write::Bincode;
use trc::AddContext;
+1
View File
@@ -17,6 +17,7 @@ utils = { path = "../utils" }
nlp = { path = "../nlp" }
directory = { path = "../directory" }
common = { path = "../common" }
email = { path = "../email" }
spam-filter = { path = "../spam-filter" }
trc = { path = "../trc" }
mail-auth = { version = "0.6" }
+2 -33
View File
@@ -7,18 +7,14 @@
use std::{
hash::Hash,
net::IpAddr,
sync::{Arc, LazyLock},
sync::Arc,
time::{Duration, Instant},
};
use common::{
auth::AccessToken,
config::smtp::auth::VerifyStrategy,
listener::{
asn::AsnGeoLookupResult,
limiter::{ConcurrencyLimiter, InFlight},
ServerInstance,
},
listener::{asn::AsnGeoLookupResult, limiter::InFlight, ServerInstance},
Inner, Server,
};
use directory::Directory;
@@ -27,7 +23,6 @@ use smtp_proto::request::receiver::{
BdatReceiver, DataReceiver, DummyDataReceiver, DummyLineReceiver, LineReceiver, RequestReceiver,
};
use tokio::io::{AsyncRead, AsyncWrite};
use utils::snowflake::SnowflakeIdGenerator;
use crate::{
inbound::auth::SaslToken,
@@ -219,18 +214,6 @@ impl PartialOrd for SessionAddress {
}
}
static SIEVE: LazyLock<Arc<ServerInstance>> = LazyLock::new(|| {
Arc::new(ServerInstance {
id: "sieve".to_string(),
protocol: common::config::server::ServerProtocol::Lmtp,
acceptor: common::listener::TcpAcceptor::Plain,
limiter: ConcurrencyLimiter::new(0),
shutdown_rx: tokio::sync::watch::channel(false).1,
proxy_networks: vec![],
span_id_gen: Arc::new(SnowflakeIdGenerator::new()),
})
});
impl Session<common::listener::stream::NullIo> {
pub fn local(
server: Server,
@@ -268,20 +251,6 @@ impl Session<common::listener::stream::NullIo> {
}
}
pub fn sieve(
server: Server,
mail_from: SessionAddress,
rcpt_to: Vec<SessionAddress>,
message: Vec<u8>,
session_id: u64,
) -> Self {
Self::local(
server,
SIEVE.clone(),
SessionData::local(mail_from.into(), rcpt_to, message, session_id),
)
}
pub fn has_failed(&mut self) -> Option<String> {
if self.stream.tx_buf.first().map_or(true, |&c| c == b'2') {
self.stream.tx_buf.clear();
-6
View File
@@ -68,12 +68,6 @@ impl SessionManager for SmtpSessionManager {
.report_tx
.send(common::ipc::ReportingEvent::Stop)
.await;
let _ = self
.inner
.ipc
.delivery_tx
.send(common::ipc::DeliveryEvent::Stop)
.await;
}
}
}
+1 -1
View File
@@ -271,7 +271,7 @@ impl DeliveryAttempt {
let delivery_result = message
.deliver_local(
recipients.iter_mut().filter(|r| r.domain_idx == domain_idx),
&server.inner.ipc.delivery_tx,
&server,
)
.await;
+73 -51
View File
@@ -4,20 +4,24 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use common::ipc::{DeliveryEvent, DeliveryResult, IngestMessage};
use common::Server;
use email::delivery::{IngestMessage, LocalDeliveryStatus, MailDelivery};
use smtp_proto::Response;
use tokio::sync::{mpsc, oneshot};
use trc::ServerEvent;
use trc::SieveEvent;
use crate::queue::{
Error, ErrorDetails, HostResponse, Message, Recipient, Status, RCPT_STATUS_CHANGED,
use crate::{
queue::{
quota::HasQueueQuota, spool::SmtpSpool, DomainPart, Error, ErrorDetails, HostResponse,
Message, MessageSource, Recipient, Status, RCPT_STATUS_CHANGED,
},
reporting::SmtpReporting,
};
impl Message {
pub async fn deliver_local(
&self,
recipients: impl Iterator<Item = &mut Recipient>,
delivery_tx: &mpsc::Sender<DeliveryEvent>,
server: &Server,
) -> Status<(), Error> {
// Prepare recipients list
let mut total_rcpt = 0;
@@ -37,54 +41,22 @@ impl Message {
pending_recipients.push(rcpt);
}
// Create oneshot channel
let (result_tx, result_rx) = oneshot::channel();
// Deliver message to JMAP server
let delivery_result = match delivery_tx
.send(DeliveryEvent::Ingest {
message: IngestMessage {
sender_address: self.return_path_lcase.clone(),
recipients: recipient_addresses,
message_blob: self.blob_hash.clone(),
message_size: self.size,
session_id: self.span_id,
},
result_tx,
// Deliver message
let delivery_result = server
.deliver_message(IngestMessage {
sender_address: self.return_path_lcase.clone(),
recipients: recipient_addresses,
message_blob: self.blob_hash.clone(),
message_size: self.size,
session_id: self.span_id,
})
.await
{
Ok(_) => {
// Wait for result
match result_rx.await {
Ok(delivery_result) => delivery_result,
Err(_) => {
trc::event!(
Server(ServerEvent::ThreadError),
CausedBy = trc::location!(),
SpanId = self.span_id,
Reason = "Result channel closed",
);
return Status::local_error();
}
}
}
Err(_) => {
trc::event!(
Server(ServerEvent::ThreadError),
CausedBy = trc::location!(),
SpanId = self.span_id,
Reason = "TX channel closed",
);
return Status::local_error();
}
};
.await;
// Process delivery results
for (rcpt, result) in pending_recipients.into_iter().zip(delivery_result) {
for (rcpt, result) in pending_recipients.into_iter().zip(delivery_result.status) {
rcpt.flags |= RCPT_STATUS_CHANGED;
match result {
DeliveryResult::Success => {
LocalDeliveryStatus::Success => {
rcpt.status = Status::Completed(HostResponse {
hostname: "localhost".to_string(),
response: Response {
@@ -95,7 +67,7 @@ impl Message {
});
total_completed += 1;
}
DeliveryResult::TemporaryFailure { reason } => {
LocalDeliveryStatus::TemporaryFailure { reason } => {
rcpt.status = Status::TemporaryFailure(HostResponse {
hostname: ErrorDetails {
entity: "localhost".to_string(),
@@ -108,7 +80,7 @@ impl Message {
},
});
}
DeliveryResult::PermanentFailure { code, reason } => {
LocalDeliveryStatus::PermanentFailure { code, reason } => {
total_completed += 1;
rcpt.status = Status::PermanentFailure(HostResponse {
hostname: ErrorDetails {
@@ -125,6 +97,56 @@ impl Message {
}
}
// Process autogenerated messages
for autogenerated in delivery_result.autogenerated {
let from_addr_lcase = autogenerated.sender_address.to_lowercase();
let from_addr_domain = from_addr_lcase.domain_part().to_string();
let mut message = server.new_message(
autogenerated.sender_address,
from_addr_lcase,
from_addr_domain,
self.span_id,
);
for rcpt in autogenerated.recipients {
message.add_recipient(rcpt, server).await;
}
// Sign message
let signature = server
.sign_message(
&mut message,
&server.core.sieve.sign,
&autogenerated.message,
)
.await;
// Queue Message
message.size = autogenerated.message.len() + signature.as_ref().map_or(0, |s| s.len());
if server.has_quota(&mut message).await {
message
.queue(
signature.as_deref(),
&autogenerated.message,
self.span_id,
server,
MessageSource::Autogenerated,
)
.await;
} else {
trc::event!(
Sieve(SieveEvent::QuotaExceeded),
SpanId = self.span_id,
From = message.return_path_lcase,
To = message
.recipients
.into_iter()
.map(|r| trc::Value::from(r.address_lcase))
.collect::<Vec<_>>(),
);
}
}
if total_completed == total_rcpt {
Status::Completed(())
} else {
+15 -1
View File
@@ -182,7 +182,8 @@ impl Store {
result.caused_by(trc::location!())
}
pub async fn write(&self, batch: Batch) -> trc::Result<AssignedIds> {
pub async fn write(&self, batch: impl Into<Batch>) -> trc::Result<AssignedIds> {
let batch = batch.into();
#[cfg(feature = "test_mode")]
if std::env::var("PARANOID_WRITE").is_ok_and(|v| v == "1") {
let mut account_id = u32::MAX;
@@ -301,6 +302,13 @@ impl Store {
result
}
#[inline]
pub async fn write_expect_id(&self, batch: impl Into<Batch>) -> trc::Result<u32> {
self.write(batch)
.await
.and_then(|ids| ids.last_document_id())
}
pub async fn purge_store(&self) -> trc::Result<()> {
// Delete expired reports
let now = now();
@@ -925,3 +933,9 @@ impl Store {
}
}
}
impl From<BatchBuilder> for Batch {
fn from(builder: BatchBuilder) -> Self {
builder.build()
}
}
+1
View File
@@ -29,6 +29,7 @@ imap_proto = { path = "../crates/imap-proto" }
pop3 = { path = "../crates/pop3", features = ["test_mode"] }
smtp = { path = "../crates/smtp", features = ["test_mode", "enterprise"] }
common = { path = "../crates/common", features = ["test_mode", "enterprise"] }
email = { path = "../crates/email", features = ["test_mode"] }
spam-filter = { path = "../crates/spam-filter", features = ["test_mode", "enterprise"] }
trc = { path = "../crates/trc" }
managesieve = { path = "../crates/managesieve", features = ["test_mode", "enterprise"] }
+1 -1
View File
@@ -334,7 +334,7 @@ async fn init_imap_tests(store_id: &str, delete_if_exists: bool) -> IMAPTest {
let cache = Caches::parse(&mut config);
let store = core.storage.data.clone();
let (ipc, mut ipc_rxs) = build_ipc();
let (ipc, mut ipc_rxs) = build_ipc(&mut config);
let inner = Arc::new(Inner {
shared_core: core.into_shared(),
data,
+1 -1
View File
@@ -4,7 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use jmap::mailbox::{INBOX_ID, TRASH_ID};
use ::email::mailbox::{INBOX_ID, TRASH_ID};
use jmap_client::{
core::{
error::{MethodError, MethodErrorType},

Some files were not shown because too many files have changed in this diff Show More