Merge upstream v0.16.27 #204
No files matched your search
@@ -2,6 +2,22 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## [0.16.27] - 2026-10-18
|
||||
|
||||
If you are upgrading from v0.16.x, replace the binary (or run `docker pull`). If you are upgrading from v0.15.x and below, please read the [upgrading documentation](https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_16.md) for more information on how to upgrade from previous versions.
|
||||
|
||||
## Added
|
||||
|
||||
## Changed
|
||||
|
||||
## Fixed
|
||||
- IMAP:
|
||||
- `APPEND` and `COPY`/`MOVE` into another account's mailbox do not change the JMAP `EmailDelivery` state.
|
||||
- `SEARCH RETURN` with `MIN` or `MAX` alongside `ALL` returns only the minimum and maximum in `ALL`, and `SAVE` combined with `MIN` or `MAX` and `ALL` or `COUNT` saves only the minimum and maximum instead of every match.
|
||||
- iTIP: Merging a received update does not copy the `VTIMEZONE` definitions it carries, so an occurrence moved by an external organizer can reference a `TZID` (for example `W. Europe Standard Time`) the stored event no longer defines, and Apple Calendar shows both the original and the moved occurrence.
|
||||
- OAuth: Loopback redirect URIs without a path (for example `http://127.0.0.1:45678`) are not matched against registered loopback URIs on any port and are rejected by dynamic client registration.
|
||||
- WebDAV: `PROPFIND` `allprop` returns `DAV:current-user-principal`, so Windows Explorer shows every file and folder under the account name.
|
||||
|
||||
## [0.16.26] - 2026-10-11
|
||||
|
||||
If you are upgrading from v0.16.x, replace the binary (or run `docker pull`). If you are upgrading from v0.15.x and below, please read the [upgrading documentation](https://github.com/stalwartlabs/stalwart/blob/main/UPGRADING/v0_16.md) for more information on how to upgrade from previous versions.
|
||||
@@ -11,6 +27,7 @@ If you are upgrading from v0.16.x, replace the binary (or run `docker pull`). If
|
||||
- JMAP: `Mailbox/set` allows moving a mailbox under a parent that already has a child with the same name, and rejects renames that only change the case of a mailbox name with `alreadyExists`.
|
||||
|
||||
## Changed
|
||||
- Breaking: x86_64 binaries and Docker images (except musl builds) now require a CPU with the x86-64-v2 instruction set (which includes SSE3, SSSE3, SSE4.1, SSE4.2 and POPCNT), and the server refuses to start on older CPUs. Virtual machines using a generic CPU model such as QEMU's "Common KVM processor" are affected. To fix it, set the VM CPU type to `host` or `x86-64-v2-AES` (Proxmox), or switch to the musl build: the `-alpine` Docker tags (for example `stalwartlabs/stalwart:v0.16-alpine`) or the `x86_64-unknown-linux-musl` binary.
|
||||
|
||||
## Fixed
|
||||
- DNSSEC: Use `hickory-resolver` patched fork at https://github.com/stalwartlabs/hickory-dns.
|
||||
|
||||
Generated
+171
-147
File diff suppressed because it is too large.
Load diff
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "common"
|
||||
version = "0.16.26"
|
||||
version = "0.16.27"
|
||||
edition = "2024"
|
||||
build = "build.rs"
|
||||
|
||||
@@ -20,7 +20,7 @@ mail-parser = { version = "0.11", features = ["full_encoding"] }
|
||||
mail-builder = { version = "1.0" }
|
||||
mail-auth = { version = "0.13", features = ["generate", "arc"] }
|
||||
smtp-proto = { version = "0.2.3", features = ["rkyv"] }
|
||||
dns-update = { version = "0.5" }
|
||||
dns-update = { version = "0.6" }
|
||||
calcard = { version = "0.3", features = ["rkyv"] }
|
||||
ahash = { version = "0.8.12", features = ["serde"] }
|
||||
parking_lot = "0.12.5"
|
||||
|
||||
@@ -212,15 +212,23 @@ pub fn loopback_redirect_parts(uri: &str) -> Option<(&str, &str)> {
|
||||
|
||||
for host in ["127.0.0.1", "[::1]"] {
|
||||
if let Some(rest) = uri.strip_prefix(host) {
|
||||
if let Some(path) = rest.strip_prefix('/') {
|
||||
return Some((host, path));
|
||||
} else if let Some(after_colon) = rest.strip_prefix(':')
|
||||
&& let Some((port, path)) = after_colon.split_once('/')
|
||||
&& !port.is_empty()
|
||||
&& port.bytes().all(|b| b.is_ascii_digit())
|
||||
{
|
||||
return Some((host, path));
|
||||
}
|
||||
let rest = match rest.strip_prefix(':') {
|
||||
Some(after_colon) => {
|
||||
let rest = after_colon.trim_start_matches(|c: char| c.is_ascii_digit());
|
||||
if rest.len() == after_colon.len() {
|
||||
return None;
|
||||
}
|
||||
rest
|
||||
}
|
||||
None => rest,
|
||||
};
|
||||
return if let Some(path) = rest.strip_prefix('/') {
|
||||
Some((host, path))
|
||||
} else if rest.is_empty() || rest.starts_with('?') {
|
||||
Some((host, rest))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
}
|
||||
}
|
||||
None
|
||||
@@ -293,3 +301,55 @@ pub fn validate_grant_metadata(
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{loopback_redirect_parts, redirect_uri_matches};
|
||||
|
||||
#[test]
|
||||
fn loopback_matches_any_port() {
|
||||
// Loopback redirects match regardless of port and of an empty versus "/" path (RFC 8252 §7.3)
|
||||
for (registered, presented) in [
|
||||
("http://127.0.0.1", "http://127.0.0.1:45678"),
|
||||
("http://127.0.0.1/", "http://127.0.0.1:45678"),
|
||||
("http://127.0.0.1", "http://127.0.0.1:45678/"),
|
||||
("http://127.0.0.1/cb", "http://127.0.0.1:45678/cb"),
|
||||
("http://127.0.0.1/?a=1", "http://127.0.0.1:45678?a=1"),
|
||||
("http://[::1]", "http://[::1]:8080"),
|
||||
("http://[::1]/", "http://[::1]:8080/"),
|
||||
] {
|
||||
assert!(
|
||||
redirect_uri_matches(registered, presented),
|
||||
"{registered} should match {presented}"
|
||||
);
|
||||
}
|
||||
|
||||
// Different paths, hosts or schemes never match
|
||||
for (registered, presented) in [
|
||||
("http://127.0.0.1/cb", "http://127.0.0.1:45678"),
|
||||
("http://127.0.0.1", "http://127.0.0.1:45678/cb"),
|
||||
("http://127.0.0.1", "http://[::1]:45678"),
|
||||
("http://127.0.0.1", "https://127.0.0.1:45678"),
|
||||
("http://127.0.0.1", "http://127.0.0.1.evil.com"),
|
||||
("http://127.0.0.1", "http://127.0.0.1:[email protected]"),
|
||||
("http://127.0.0.1", "http://127.0.0.1:45678#frag"),
|
||||
] {
|
||||
assert!(
|
||||
!redirect_uri_matches(registered, presented),
|
||||
"{registered} should not match {presented}"
|
||||
);
|
||||
}
|
||||
|
||||
// Malformed loopback URIs are not recognised
|
||||
for uri in [
|
||||
"http://127.0.0.1:",
|
||||
"http://127.0.0.1:/cb",
|
||||
"http://127.0.0.1:abc/",
|
||||
"http://127.0.0.1.evil.com/",
|
||||
"http://[email protected]/",
|
||||
"http://localhost:8080/",
|
||||
] {
|
||||
assert_eq!(loopback_redirect_parts(uri), None, "{uri}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,13 +6,19 @@
|
||||
* Modified by Coffey Labs in 2026 for INBUXA.
|
||||
*/
|
||||
|
||||
use crate::{Server, manager::fetch_resource};
|
||||
use crate::{
|
||||
Server,
|
||||
manager::{
|
||||
defaults::{WEBUI_LATEST_URL, WEBUI_MAX_MAJOR_VERSION, WEBUI_PINNED_URL},
|
||||
fetch_resource,
|
||||
},
|
||||
};
|
||||
use ahash::AHashMap;
|
||||
use arc_swap::ArcSwap;
|
||||
use registry::schema::{enums::CompressionAlgo, structs::Application};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
io::{self, Cursor, Read},
|
||||
io::{self, Cursor, Read, Seek},
|
||||
path::{Path, PathBuf},
|
||||
sync::{
|
||||
Arc,
|
||||
@@ -199,7 +205,11 @@ impl WebApplicationManager {
|
||||
Self {
|
||||
base_path,
|
||||
blob_key: BlobHash::generate(format!("{}{}", APP_BLOB_PREFIX, app.id.id()).as_bytes()),
|
||||
url: app.object.resource_url,
|
||||
url: if app.object.resource_url == WEBUI_LATEST_URL {
|
||||
WEBUI_PINNED_URL.to_string()
|
||||
} else {
|
||||
app.object.resource_url
|
||||
},
|
||||
description: app.object.description,
|
||||
expiry: app.object.auto_update_frequency.as_secs(),
|
||||
oauth_client_id: app.object.oauth_client_id,
|
||||
@@ -263,6 +273,15 @@ impl WebApplicationManager {
|
||||
.ctx(Key::Url, url.clone())
|
||||
.details("Failed to decompress application bundle")
|
||||
})?;
|
||||
if let Some(version) = unsupported_bundle_version(&mut archive) {
|
||||
return Err(trc::ResourceEvent::Error
|
||||
.caused_by(trc::location!())
|
||||
.ctx(Key::Url, url.clone())
|
||||
.details(format!(
|
||||
"Application bundle version {version} is for a newer server, set the resource URL to {WEBUI_PINNED_URL}"
|
||||
)));
|
||||
}
|
||||
|
||||
let mut resources = AHashMap::with_capacity(archive.len());
|
||||
for i in 0..archive.len() {
|
||||
let mut file = archive.by_index(i).map_err(|err| {
|
||||
@@ -498,6 +517,19 @@ fn rewrite_index(html: &str, prefix: &str, oauth_client_id_meta: Option<&str>) -
|
||||
out.into_bytes()
|
||||
}
|
||||
|
||||
fn unsupported_bundle_version<R: Read + Seek>(archive: &mut zip::ZipArchive<R>) -> Option<String> {
|
||||
let file = archive.by_name("VERSION").ok()?;
|
||||
let mut version = String::new();
|
||||
let is_supported = file.take(32).read_to_string(&mut version).is_ok()
|
||||
&& version
|
||||
.trim()
|
||||
.split('.')
|
||||
.next()
|
||||
.and_then(|major| major.parse::<u32>().ok())
|
||||
.is_some_and(|major| major <= WEBUI_MAX_MAJOR_VERSION);
|
||||
(!is_supported).then(|| version.trim().to_string())
|
||||
}
|
||||
|
||||
fn oauth_client_id_meta(client_id: &str) -> String {
|
||||
let mut meta = String::with_capacity(OAUTH_CLIENT_ID.len() + client_id.len());
|
||||
meta.push_str("<meta name=\"oauth-client-id\" content=\"");
|
||||
@@ -752,4 +784,38 @@ mod tests {
|
||||
|
||||
assert_ne!(apps.next_generation(), apps.next_generation());
|
||||
}
|
||||
|
||||
fn bundle(version: Option<&str>) -> zip::ZipArchive<Cursor<Vec<u8>>> {
|
||||
let mut writer = zip::ZipWriter::new(Cursor::new(Vec::new()));
|
||||
let options = zip::write::SimpleFileOptions::default();
|
||||
writer.start_file("index.html", options).unwrap();
|
||||
io::Write::write_all(&mut writer, INDEX.as_bytes()).unwrap();
|
||||
if let Some(version) = version {
|
||||
writer.start_file("VERSION", options).unwrap();
|
||||
io::Write::write_all(&mut writer, version.as_bytes()).unwrap();
|
||||
}
|
||||
zip::ZipArchive::new(writer.finish().unwrap()).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bundles_newer_than_the_supported_major_version_are_rejected() {
|
||||
// Bundles without VERSION or with a 1.x VERSION are accepted, anything else is rejected
|
||||
assert_eq!(unsupported_bundle_version(&mut bundle(None)), None);
|
||||
assert_eq!(
|
||||
unsupported_bundle_version(&mut bundle(Some("1.0.12\n"))),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
unsupported_bundle_version(&mut bundle(Some("2.0.0\n"))),
|
||||
Some("2.0.0".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
unsupported_bundle_version(&mut bundle(Some("10.1.0"))),
|
||||
Some("10.1.0".to_string())
|
||||
);
|
||||
assert_eq!(
|
||||
unsupported_bundle_version(&mut bundle(Some("garbage"))),
|
||||
Some("garbage".to_string())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,12 @@ use store::{
|
||||
},
|
||||
};
|
||||
|
||||
pub const WEBUI_LATEST_URL: &str =
|
||||
"https://github.com/stalwartlabs/webui/releases/latest/download/webui.zip";
|
||||
pub const WEBUI_PINNED_URL: &str =
|
||||
"https://github.com/stalwartlabs/webui/releases/download/v1.0.11/webui.zip";
|
||||
pub const WEBUI_MAX_MAJOR_VERSION: u32 = 1;
|
||||
|
||||
pub const ASN_IPV4: &str =
|
||||
"https://github.com/sapics/ip-location-db/releases/download/latest/origin-asn-ipv4.csv";
|
||||
pub const ASN_IPV6: &str =
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "coordinator"
|
||||
version = "0.16.26"
|
||||
version = "0.16.27"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "dav-proto"
|
||||
version = "0.16.26"
|
||||
version = "0.16.27"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -366,7 +366,7 @@ impl Rfc1123DateTime {
|
||||
}
|
||||
|
||||
impl DavProperty {
|
||||
pub const ALL_PROPS: [DavProperty; 11] = [
|
||||
pub const ALL_PROPS: [DavProperty; 10] = [
|
||||
DavProperty::WebDav(WebDavProperty::CreationDate),
|
||||
DavProperty::WebDav(WebDavProperty::DisplayName),
|
||||
DavProperty::WebDav(WebDavProperty::GetETag),
|
||||
@@ -374,7 +374,6 @@ impl DavProperty {
|
||||
DavProperty::WebDav(WebDavProperty::ResourceType),
|
||||
DavProperty::WebDav(WebDavProperty::LockDiscovery),
|
||||
DavProperty::WebDav(WebDavProperty::SupportedLock),
|
||||
DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal),
|
||||
DavProperty::WebDav(WebDavProperty::GetContentLanguage),
|
||||
DavProperty::WebDav(WebDavProperty::GetContentLength),
|
||||
DavProperty::WebDav(WebDavProperty::GetContentType),
|
||||
@@ -390,7 +389,6 @@ impl DavProperty {
|
||||
| DavProperty::WebDav(WebDavProperty::ResourceType)
|
||||
| DavProperty::WebDav(WebDavProperty::LockDiscovery)
|
||||
| DavProperty::WebDav(WebDavProperty::SupportedLock)
|
||||
| DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal)
|
||||
| DavProperty::WebDav(WebDavProperty::GetContentLanguage)
|
||||
| DavProperty::WebDav(WebDavProperty::GetContentLength)
|
||||
| DavProperty::WebDav(WebDavProperty::GetContentType)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "dav"
|
||||
version = "0.16.26"
|
||||
version = "0.16.27"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -57,7 +57,7 @@ use groupware::{
|
||||
use http_proto::HttpResponse;
|
||||
use hyper::StatusCode;
|
||||
use registry::schema::{enums::Permission, prelude::ObjectType};
|
||||
use std::sync::Arc;
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
use store::{
|
||||
ValueKey,
|
||||
registry::RegistryQuery,
|
||||
@@ -1725,13 +1725,20 @@ async fn add_base_collection_response(
|
||||
));
|
||||
return Ok(());
|
||||
}
|
||||
PropFind::AllProp(_) => [
|
||||
DavProperty::WebDav(WebDavProperty::ResourceType),
|
||||
DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal),
|
||||
DavProperty::WebDav(WebDavProperty::SupportedReportSet),
|
||||
]
|
||||
.as_slice(),
|
||||
PropFind::Prop(items) => items,
|
||||
PropFind::AllProp(include) => {
|
||||
let mut props = Vec::with_capacity(include.len() + 2);
|
||||
props.extend([
|
||||
DavProperty::WebDav(WebDavProperty::ResourceType),
|
||||
DavProperty::WebDav(WebDavProperty::SupportedReportSet),
|
||||
]);
|
||||
for prop in include {
|
||||
if !props.contains(prop) {
|
||||
props.push(prop.clone());
|
||||
}
|
||||
}
|
||||
Cow::Owned(props)
|
||||
}
|
||||
PropFind::Prop(items) => Cow::Borrowed(items.as_slice()),
|
||||
};
|
||||
|
||||
let mut fields = Vec::with_capacity(properties.len());
|
||||
@@ -1741,9 +1748,9 @@ async fn add_base_collection_response(
|
||||
.await
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
for prop in properties {
|
||||
for prop in properties.iter() {
|
||||
response.set_namespace(prop.namespace());
|
||||
match &prop {
|
||||
match prop {
|
||||
DavProperty::WebDav(WebDavProperty::ResourceType) => {
|
||||
fields.push(DavPropertyValue::new(
|
||||
prop.clone(),
|
||||
|
||||
@@ -448,17 +448,23 @@ fn all_props(collection: Collection, all_props: Option<&[DavProperty]>) -> Vec<D
|
||||
DavProperty::WebDav(WebDavProperty::DisplayName),
|
||||
DavProperty::WebDav(WebDavProperty::ResourceType),
|
||||
DavProperty::WebDav(WebDavProperty::SupportedReportSet),
|
||||
DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal),
|
||||
DavProperty::WebDav(WebDavProperty::SyncToken),
|
||||
DavProperty::WebDav(WebDavProperty::Owner),
|
||||
DavProperty::WebDav(WebDavProperty::PrincipalCollectionSet),
|
||||
];
|
||||
|
||||
if let Some(all_props) = all_props {
|
||||
props.extend(all_props.iter().filter(|p| !p.is_all_prop()).cloned());
|
||||
props
|
||||
if let Some(include) = all_props {
|
||||
for prop in include {
|
||||
if !prop.is_all_prop() && !props.contains(prop) {
|
||||
props.push(prop.clone());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
props
|
||||
props.extend([
|
||||
DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal),
|
||||
DavProperty::WebDav(WebDavProperty::SyncToken),
|
||||
DavProperty::WebDav(WebDavProperty::Owner),
|
||||
DavProperty::WebDav(WebDavProperty::PrincipalCollectionSet),
|
||||
]);
|
||||
}
|
||||
|
||||
props
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "directory"
|
||||
version = "0.16.26"
|
||||
version = "0.16.27"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "email"
|
||||
version = "0.16.26"
|
||||
version = "0.16.27"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "groupware"
|
||||
version = "0.16.26"
|
||||
version = "0.16.27"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -761,7 +761,7 @@ async fn commit_itip_merge(
|
||||
.caused_by(trc::location!())?;
|
||||
|
||||
// Merge changes
|
||||
itip_merge_changes(&mut event.data.event, changes);
|
||||
itip_merge_changes(&mut event.data.event, &itip, changes);
|
||||
|
||||
// Calculate the new ical size
|
||||
event.size = ical_size(&event.data.event) as u32;
|
||||
|
||||
@@ -542,7 +542,7 @@ fn handle_reply(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn itip_merge_changes(ical: &mut ICalendar, changes: Vec<MergeAction>) {
|
||||
pub fn itip_merge_changes(ical: &mut ICalendar, itip: &ICalendar, changes: Vec<MergeAction>) {
|
||||
let mut remove_component_ids: Vec<u32> = Vec::new();
|
||||
for action in changes {
|
||||
match action {
|
||||
@@ -600,6 +600,85 @@ pub fn itip_merge_changes(ical: &mut ICalendar, changes: Vec<MergeAction>) {
|
||||
if !remove_component_ids.is_empty() {
|
||||
ical.remove_component_ids(&remove_component_ids);
|
||||
}
|
||||
|
||||
merge_timezones(ical, itip);
|
||||
}
|
||||
|
||||
fn merge_timezones(ical: &mut ICalendar, itip: &ICalendar) {
|
||||
let Some(root) = ical
|
||||
.components
|
||||
.first()
|
||||
.filter(|root| root.component_type == ICalendarComponentType::VCalendar)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let mut insert_at = root
|
||||
.component_ids
|
||||
.iter()
|
||||
.position(|component_id| {
|
||||
ical.component_by_id(*component_id)
|
||||
.is_none_or(|comp| comp.component_type != ICalendarComponentType::VTimezone)
|
||||
})
|
||||
.unwrap_or(root.component_ids.len());
|
||||
|
||||
for tz in itip.timezones() {
|
||||
let Some(tz_id) = timezone_id(tz) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut is_referenced = false;
|
||||
for component in &ical.components {
|
||||
if component.component_type == ICalendarComponentType::VTimezone {
|
||||
if timezone_id(component) == Some(tz_id) {
|
||||
is_referenced = false;
|
||||
break;
|
||||
}
|
||||
} else if !is_referenced {
|
||||
is_referenced = component
|
||||
.entries
|
||||
.iter()
|
||||
.any(|entry| entry.tz_id() == Some(tz_id));
|
||||
}
|
||||
}
|
||||
if !is_referenced {
|
||||
continue;
|
||||
}
|
||||
|
||||
let tz_component_id = ical.components.len() as u32;
|
||||
ical.components.reserve(tz.component_ids.len() + 1);
|
||||
ical.components.push(ICalendarComponent {
|
||||
component_type: ICalendarComponentType::VTimezone,
|
||||
entries: tz.entries.clone(),
|
||||
component_ids: Vec::new(),
|
||||
});
|
||||
ical.components.extend(
|
||||
tz.component_ids
|
||||
.iter()
|
||||
.filter_map(|component_id| itip.component_by_id(*component_id))
|
||||
.map(|item| ICalendarComponent {
|
||||
component_type: item.component_type.clone(),
|
||||
entries: item.entries.clone(),
|
||||
component_ids: Vec::new(),
|
||||
}),
|
||||
);
|
||||
let last_item_id = ical.components.len() as u32;
|
||||
if let Some(tz_component) = ical.components.get_mut(tz_component_id as usize) {
|
||||
tz_component.component_ids = (tz_component_id + 1..last_item_id).collect();
|
||||
}
|
||||
if let Some(root) = ical.components.first_mut() {
|
||||
root.component_ids.insert(insert_at, tz_component_id);
|
||||
insert_at += 1;
|
||||
}
|
||||
}
|
||||
|
||||
ical.add_missing_timezones();
|
||||
}
|
||||
|
||||
fn timezone_id(component: &ICalendarComponent) -> Option<&str> {
|
||||
component
|
||||
.property(&ICalendarProperty::Tzid)
|
||||
.and_then(|entry| entry.values.first())
|
||||
.and_then(|value| value.as_text())
|
||||
}
|
||||
|
||||
pub fn itip_method(ical: &ICalendar) -> Result<&ICalendarMethod, ItipError> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "http_proto"
|
||||
version = "0.16.26"
|
||||
version = "0.16.27"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "http"
|
||||
version = "0.16.26"
|
||||
version = "0.16.27"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "imap_proto"
|
||||
version = "0.16.26"
|
||||
version = "0.16.27"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "imap"
|
||||
version = "0.16.26"
|
||||
version = "0.16.27"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -169,6 +169,7 @@ impl<T: SessionStream> SessionData<T> {
|
||||
.broadcast_push_notification(PushNotification::StateChange(
|
||||
StateChange::new(account_id)
|
||||
.with_change_id(change_id)
|
||||
.with_change(DataType::EmailDelivery)
|
||||
.with_change(DataType::Email)
|
||||
.with_change(DataType::Mailbox)
|
||||
.with_change(DataType::Thread),
|
||||
|
||||
@@ -648,6 +648,7 @@ impl<T: SessionStream> SessionData<T> {
|
||||
.broadcast_push_notification(PushNotification::StateChange(
|
||||
StateChange::new(dest_account_id)
|
||||
.with_change_id(change_id)
|
||||
.with_change(DataType::EmailDelivery)
|
||||
.with_change(DataType::Email)
|
||||
.with_change(DataType::Thread)
|
||||
.with_change(DataType::Mailbox),
|
||||
|
||||
@@ -175,9 +175,14 @@ impl<T: SessionStream> SessionData<T> {
|
||||
None
|
||||
};
|
||||
let mut imap_ids = Vec::with_capacity(results_len);
|
||||
let return_all = arguments.result_options.is_empty()
|
||||
|| arguments.result_options.contains(&ResultOption::All);
|
||||
mailbox.map_search_results(
|
||||
result_set.into_iter(),
|
||||
is_uid,
|
||||
return_all
|
||||
|| (saved_results.is_some()
|
||||
&& arguments.result_options.contains(&ResultOption::Count)),
|
||||
arguments.result_options.contains(&ResultOption::Min),
|
||||
arguments.result_options.contains(&ResultOption::Max),
|
||||
&mut min,
|
||||
@@ -247,13 +252,7 @@ impl<T: SessionStream> SessionData<T> {
|
||||
} else {
|
||||
None
|
||||
},
|
||||
ids: if arguments.result_options.is_empty()
|
||||
|| arguments.result_options.contains(&ResultOption::All)
|
||||
{
|
||||
imap_ids
|
||||
} else {
|
||||
vec![]
|
||||
},
|
||||
ids: if return_all { imap_ids } else { vec![] },
|
||||
is_sort,
|
||||
is_esearch: arguments.is_esearch,
|
||||
highest_modseq,
|
||||
@@ -747,6 +746,7 @@ impl SelectedMailbox {
|
||||
&self,
|
||||
ids: impl Iterator<Item = u32>,
|
||||
is_uid: bool,
|
||||
collect_all: bool,
|
||||
find_min: bool,
|
||||
find_max: bool,
|
||||
min: &mut Option<(u32, ImapId)>,
|
||||
@@ -759,26 +759,25 @@ impl SelectedMailbox {
|
||||
let find_min_or_max = find_min || find_max;
|
||||
for document_id in ids {
|
||||
if let Some((id, imap_id)) = state.map_result_id(document_id, is_uid) {
|
||||
if find_min_or_max {
|
||||
if find_min {
|
||||
if let Some((prev_min, _)) = min {
|
||||
if id < *prev_min {
|
||||
*min = Some((id, imap_id));
|
||||
}
|
||||
} else {
|
||||
if find_min {
|
||||
if let Some((prev_min, _)) = min {
|
||||
if id < *prev_min {
|
||||
*min = Some((id, imap_id));
|
||||
}
|
||||
} else {
|
||||
*min = Some((id, imap_id));
|
||||
}
|
||||
if find_max {
|
||||
if let Some((prev_max, _)) = max {
|
||||
if id > *prev_max {
|
||||
*max = Some((id, imap_id));
|
||||
}
|
||||
} else {
|
||||
}
|
||||
if find_max {
|
||||
if let Some((prev_max, _)) = max {
|
||||
if id > *prev_max {
|
||||
*max = Some((id, imap_id));
|
||||
}
|
||||
} else {
|
||||
*max = Some((id, imap_id));
|
||||
}
|
||||
} else {
|
||||
}
|
||||
if collect_all || !find_min_or_max {
|
||||
imap_ids.push(id);
|
||||
if let Some(r) = saved_results.as_mut() {
|
||||
r.push(imap_id)
|
||||
@@ -787,11 +786,12 @@ impl SelectedMailbox {
|
||||
*total += 1;
|
||||
}
|
||||
}
|
||||
if find_min || find_max {
|
||||
for (id, imap_id) in [min, max].into_iter().flatten() {
|
||||
imap_ids.push(*id);
|
||||
if find_min_or_max && !collect_all {
|
||||
let max = max.filter(|(max_id, _)| min.is_none_or(|(min_id, _)| min_id != *max_id));
|
||||
for (id, imap_id) in [*min, max].into_iter().flatten() {
|
||||
imap_ids.push(id);
|
||||
if let Some(r) = saved_results.as_mut() {
|
||||
r.push(*imap_id)
|
||||
r.push(imap_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "jmap_proto"
|
||||
version = "0.16.26"
|
||||
version = "0.16.27"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "jmap"
|
||||
version = "0.16.26"
|
||||
version = "0.16.27"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::{
|
||||
changes::state::JmapCacheState,
|
||||
email::{PatchResult, handle_email_patch, ingested_into_object},
|
||||
};
|
||||
use common::{Server, auth::AccessToken};
|
||||
use common::{Server, auth::AccessToken, ipc::PushNotification};
|
||||
use email::{
|
||||
cache::{MessageCacheFetch, email::MessageCacheAccess, mailbox::MailboxCacheAccess},
|
||||
mailbox::JUNK_ID,
|
||||
@@ -37,7 +37,10 @@ use jmap_tools::{Key, Value};
|
||||
use std::future::Future;
|
||||
use store::write::BatchBuilder;
|
||||
use trc::AddContext;
|
||||
use types::acl::Acl;
|
||||
use types::{
|
||||
acl::Acl,
|
||||
type_state::{DataType, StateChange},
|
||||
};
|
||||
use utils::map::vec_map::VecMap;
|
||||
|
||||
pub trait JmapEmailCopy: Sync + Send {
|
||||
@@ -96,6 +99,7 @@ impl JmapEmailCopy for Server {
|
||||
let mut destroy_ids = Vec::new();
|
||||
let mut train_batch = BatchBuilder::new();
|
||||
let mut did_train = false;
|
||||
let mut last_change_id = None;
|
||||
train_batch.with_account_id(from_account_id);
|
||||
|
||||
'create: for (id, create) in request.create.into_valid() {
|
||||
@@ -232,6 +236,7 @@ impl JmapEmailCopy for Server {
|
||||
.await?
|
||||
{
|
||||
Ok(email) => {
|
||||
last_change_id = Some(email.change_id);
|
||||
if train_spam {
|
||||
self.add_account_spam_sample(
|
||||
&mut train_batch,
|
||||
@@ -277,7 +282,16 @@ impl JmapEmailCopy for Server {
|
||||
}
|
||||
|
||||
// Update state
|
||||
if !response.created.is_empty() {
|
||||
if let Some(change_id) = last_change_id {
|
||||
self.broadcast_push_notification(PushNotification::StateChange(
|
||||
StateChange::new(account_id)
|
||||
.with_change_id(change_id)
|
||||
.with_change(DataType::EmailDelivery)
|
||||
.with_change(DataType::Email)
|
||||
.with_change(DataType::Mailbox)
|
||||
.with_change(DataType::Thread),
|
||||
))
|
||||
.await;
|
||||
response.new_state = self.get_cached_messages(account_id).await?.get_state(false);
|
||||
}
|
||||
|
||||
|
||||
@@ -223,6 +223,7 @@ impl EmailImport for Server {
|
||||
self.broadcast_push_notification(PushNotification::StateChange(
|
||||
StateChange::new(account_id)
|
||||
.with_change_id(change_id)
|
||||
.with_change(DataType::EmailDelivery)
|
||||
.with_change(DataType::Email)
|
||||
.with_change(DataType::Mailbox)
|
||||
.with_change(DataType::Thread),
|
||||
|
||||
@@ -1233,12 +1233,21 @@ impl EmailSet for Server {
|
||||
if let Some(change_id) = last_change_id {
|
||||
if response.updated.is_empty() && response.destroyed.is_empty() {
|
||||
// Message ingest does not broadcast state changes
|
||||
let mut state_change = StateChange::new(account_id)
|
||||
.with_change_id(change_id)
|
||||
.with_change(DataType::Email)
|
||||
.with_change(DataType::Mailbox)
|
||||
.with_change(DataType::Thread);
|
||||
if !response.created.is_empty() {
|
||||
state_change.set_change(DataType::EmailDelivery);
|
||||
}
|
||||
self.broadcast_push_notification(PushNotification::StateChange(state_change))
|
||||
.await;
|
||||
} else if !response.created.is_empty() {
|
||||
self.broadcast_push_notification(PushNotification::StateChange(
|
||||
StateChange::new(account_id)
|
||||
.with_change_id(change_id)
|
||||
.with_change(DataType::Email)
|
||||
.with_change(DataType::Mailbox)
|
||||
.with_change(DataType::Thread),
|
||||
.with_change(DataType::EmailDelivery),
|
||||
))
|
||||
.await;
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ keywords = ["imap", "jmap", "smtp", "email", "mail", "webdav", "server"]
|
||||
categories = ["email"]
|
||||
# Upstream offers AGPL-3.0-only OR LicenseRef-SEL; inbuxa takes the AGPL only.
|
||||
license = "AGPL-3.0-only"
|
||||
version = "0.16.26"
|
||||
version = "0.16.27"
|
||||
edition = "2024"
|
||||
|
||||
[[bin]]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "managesieve"
|
||||
version = "0.16.26"
|
||||
version = "0.16.27"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "migration"
|
||||
version = "0.16.26"
|
||||
version = "0.16.27"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nlp"
|
||||
version = "0.16.26"
|
||||
version = "0.16.27"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "pop3"
|
||||
version = "0.16.26"
|
||||
version = "0.16.27"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "registry"
|
||||
version = "0.16.26"
|
||||
version = "0.16.27"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*
|
||||
* Modified by Coffey Labs in 2026 for INBUXA.
|
||||
*/
|
||||
pub use crate::jmap::IntoValue;
|
||||
pub use crate::jmap::JmapValue;
|
||||
@@ -31,6 +33,7 @@ pub use crate::types::string::StringValidator;
|
||||
pub use serde::{Deserialize, Serialize};
|
||||
pub use std::borrow::Cow;
|
||||
pub use std::str::FromStr;
|
||||
use std::sync::LazyLock;
|
||||
pub use types::blob::BlobId;
|
||||
pub use types::id::Id;
|
||||
pub use utils::map::vec_map::VecMap;
|
||||
@@ -56,3 +59,17 @@ pub const OBJ_FILTER_ACCOUNT: u64 = 1 << 2;
|
||||
pub const OBJ_FILTER_TENANT: u64 = 1 << 3;
|
||||
|
||||
pub const MASKED_PASSWORD: &str = "****";
|
||||
|
||||
// Temporary migration aid, will be removed by v1.
|
||||
pub static INCLUDE_SECRETS: LazyLock<bool> = LazyLock::new(|| {
|
||||
std::env::var("INBUXA_INCLUDE_SECRETS")
|
||||
.is_ok_and(|v| v == "1" || v.eq_ignore_ascii_case("true"))
|
||||
});
|
||||
|
||||
pub fn secret_value(value: impl IntoValue) -> JmapValue<'static> {
|
||||
if *INCLUDE_SECRETS {
|
||||
value.into_value()
|
||||
} else {
|
||||
JmapValue::Str(MASKED_PASSWORD.into())
|
||||
}
|
||||
}
|
||||
@@ -169,13 +169,10 @@ impl IntoValue for AccountPassword {
|
||||
fn into_value(self) -> JmapValue<'static> {
|
||||
let mut map = jmap_tools::Map::with_capacity(5);
|
||||
if self.secret.is_some() {
|
||||
map.insert_unchecked(Property::Secret, JmapValue::Str(MASKED_PASSWORD.into()));
|
||||
map.insert_unchecked(Property::Secret, secret_value(self.secret));
|
||||
}
|
||||
if self.current_secret.is_some() {
|
||||
map.insert_unchecked(
|
||||
Property::CurrentSecret,
|
||||
JmapValue::Str(MASKED_PASSWORD.into()),
|
||||
);
|
||||
map.insert_unchecked(Property::CurrentSecret, secret_value(self.current_secret));
|
||||
}
|
||||
map.insert_unchecked(Property::OtpAuth, self.otp_auth.into_value());
|
||||
JmapValue::Object(map)
|
||||
@@ -399,7 +396,7 @@ impl IntoValue for AcmeProvider {
|
||||
map.insert_unchecked(Property::ChallengeType, self.challenge_type.into_value());
|
||||
map.insert_unchecked(Property::Contact, self.contact.into_value());
|
||||
map.insert_unchecked(Property::Directory, self.directory.into_value());
|
||||
map.insert_unchecked(Property::AccountKey, JmapValue::Str(MASKED_PASSWORD.into()));
|
||||
map.insert_unchecked(Property::AccountKey, secret_value(self.account_key));
|
||||
map.insert_unchecked(Property::AccountUri, self.account_uri.into_value());
|
||||
map.insert_unchecked(Property::RenewBefore, self.renew_before.into_value());
|
||||
map.insert_unchecked(Property::MaxRetries, self.max_retries.into_value());
|
||||
@@ -1525,7 +1522,7 @@ impl IntoValue for ApiKey {
|
||||
fn into_value(self) -> JmapValue<'static> {
|
||||
let mut map = jmap_tools::Map::with_capacity(8);
|
||||
map.insert_unchecked(Property::Description, self.description.into_value());
|
||||
map.insert_unchecked(Property::Secret, JmapValue::Str(MASKED_PASSWORD.into()));
|
||||
map.insert_unchecked(Property::Secret, secret_value(self.secret));
|
||||
map.insert_unchecked(Property::CreatedAt, self.created_at.into_value());
|
||||
map.insert_unchecked(Property::ExpiresAt, self.expires_at.into_value());
|
||||
map.insert_unchecked(Property::Permissions, self.permissions.into_value());
|
||||
@@ -1633,7 +1630,7 @@ impl IntoValue for AppPassword {
|
||||
fn into_value(self) -> JmapValue<'static> {
|
||||
let mut map = jmap_tools::Map::with_capacity(8);
|
||||
map.insert_unchecked(Property::Description, self.description.into_value());
|
||||
map.insert_unchecked(Property::Secret, JmapValue::Str(MASKED_PASSWORD.into()));
|
||||
map.insert_unchecked(Property::Secret, secret_value(self.secret));
|
||||
map.insert_unchecked(Property::CreatedAt, self.created_at.into_value());
|
||||
map.insert_unchecked(Property::ExpiresAt, self.expires_at.into_value());
|
||||
map.insert_unchecked(Property::Permissions, self.permissions.into_value());
|
||||
@@ -30554,7 +30551,7 @@ impl IntoValue for OAuthClient {
|
||||
map.insert_unchecked(Property::Description, self.description.into_value());
|
||||
map.insert_unchecked(Property::Contacts, self.contacts.into_value());
|
||||
if self.secret.is_some() {
|
||||
map.insert_unchecked(Property::Secret, JmapValue::Str(MASKED_PASSWORD.into()));
|
||||
map.insert_unchecked(Property::Secret, secret_value(self.secret));
|
||||
}
|
||||
map.insert_unchecked(Property::CreatedAt, self.created_at.into_value());
|
||||
map.insert_unchecked(Property::ExpiresAt, self.expires_at.into_value());
|
||||
@@ -30956,10 +30953,10 @@ impl IntoValue for OtpAuth {
|
||||
fn into_value(self) -> JmapValue<'static> {
|
||||
let mut map = jmap_tools::Map::with_capacity(4);
|
||||
if self.otp_code.is_some() {
|
||||
map.insert_unchecked(Property::OtpCode, JmapValue::Str(MASKED_PASSWORD.into()));
|
||||
map.insert_unchecked(Property::OtpCode, secret_value(self.otp_code));
|
||||
}
|
||||
if self.otp_url.is_some() {
|
||||
map.insert_unchecked(Property::OtpUrl, JmapValue::Str(MASKED_PASSWORD.into()));
|
||||
map.insert_unchecked(Property::OtpUrl, secret_value(self.otp_url));
|
||||
}
|
||||
JmapValue::Object(map)
|
||||
}
|
||||
@@ -31050,9 +31047,9 @@ impl IntoValue for PasswordCredential {
|
||||
fn into_value(self) -> JmapValue<'static> {
|
||||
let mut map = jmap_tools::Map::with_capacity(7);
|
||||
map.insert_unchecked(Property::CredentialId, self.credential_id.into_value());
|
||||
map.insert_unchecked(Property::Secret, JmapValue::Str(MASKED_PASSWORD.into()));
|
||||
map.insert_unchecked(Property::Secret, secret_value(self.secret));
|
||||
if self.otp_auth.is_some() {
|
||||
map.insert_unchecked(Property::OtpAuth, JmapValue::Str(MASKED_PASSWORD.into()));
|
||||
map.insert_unchecked(Property::OtpAuth, secret_value(self.otp_auth));
|
||||
}
|
||||
map.insert_unchecked(Property::ExpiresAt, self.expires_at.into_value());
|
||||
map.insert_unchecked(Property::AllowedIps, self.allowed_ips.into_value());
|
||||
@@ -34815,7 +34812,7 @@ impl IntoValue for SecondaryCredential {
|
||||
let mut map = jmap_tools::Map::with_capacity(9);
|
||||
map.insert_unchecked(Property::CredentialId, self.credential_id.into_value());
|
||||
map.insert_unchecked(Property::Description, self.description.into_value());
|
||||
map.insert_unchecked(Property::Secret, JmapValue::Str(MASKED_PASSWORD.into()));
|
||||
map.insert_unchecked(Property::Secret, secret_value(self.secret));
|
||||
map.insert_unchecked(Property::CreatedAt, self.created_at.into_value());
|
||||
map.insert_unchecked(Property::ExpiresAt, self.expires_at.into_value());
|
||||
map.insert_unchecked(Property::Permissions, self.permissions.into_value());
|
||||
@@ -35218,7 +35215,7 @@ impl Default for SecretKeyValue {
|
||||
impl IntoValue for SecretKeyValue {
|
||||
fn into_value(self) -> JmapValue<'static> {
|
||||
let mut map = jmap_tools::Map::with_capacity(3);
|
||||
map.insert_unchecked(Property::Secret, JmapValue::Str(MASKED_PASSWORD.into()));
|
||||
map.insert_unchecked(Property::Secret, secret_value(self.secret));
|
||||
JmapValue::Object(map)
|
||||
}
|
||||
}
|
||||
@@ -35503,7 +35500,7 @@ impl Default for SecretTextValue {
|
||||
impl IntoValue for SecretTextValue {
|
||||
fn into_value(self) -> JmapValue<'static> {
|
||||
let mut map = jmap_tools::Map::with_capacity(3);
|
||||
map.insert_unchecked(Property::Secret, JmapValue::Str(MASKED_PASSWORD.into()));
|
||||
map.insert_unchecked(Property::Secret, secret_value(self.secret));
|
||||
JmapValue::Object(map)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "scim-proto"
|
||||
version = "0.16.26"
|
||||
version = "0.16.27"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "scim"
|
||||
version = "0.16.26"
|
||||
version = "0.16.27"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "services"
|
||||
version = "0.16.26"
|
||||
version = "0.16.27"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
@@ -32,7 +32,7 @@ hkdf = "0.13"
|
||||
sha2 = "0.11"
|
||||
reqwest = { version = "0.13", default-features = false, features = ["rustls", "http2"]}
|
||||
base64 = "0.23"
|
||||
dns-update = { version = "0.5" }
|
||||
dns-update = { version = "0.6" }
|
||||
psl = "2"
|
||||
# inbuxa: the deliverability check
|
||||
mail-auth = { version = "0.13" }
|
||||
|
||||
@@ -6,7 +6,7 @@ homepage = "https://inbuxa.org"
|
||||
keywords = ["smtp", "email", "mail", "server"]
|
||||
categories = ["email"]
|
||||
license = "AGPL-3.0-only OR LicenseRef-SEL"
|
||||
version = "0.16.26"
|
||||
version = "0.16.27"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "spam-filter"
|
||||
version = "0.16.26"
|
||||
version = "0.16.27"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "store"
|
||||
version = "0.16.26"
|
||||
version = "0.16.27"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "trc"
|
||||
version = "0.16.26"
|
||||
version = "0.16.27"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "event_macro"
|
||||
version = "0.16.26"
|
||||
version = "0.16.27"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "types"
|
||||
version = "0.16.26"
|
||||
version = "0.16.27"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "utils"
|
||||
version = "0.16.26"
|
||||
version = "0.16.27"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "proc_macros"
|
||||
version = "0.16.26"
|
||||
version = "0.16.27"
|
||||
edition = "2024"
|
||||
|
||||
[lib]
|
||||
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tests"
|
||||
version = "0.16.26"
|
||||
version = "0.16.27"
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
@@ -82,7 +82,7 @@ form_urlencoded = "1.2.2"
|
||||
rkyv = { version = "0.8.18", features = ["little_endian"] }
|
||||
quick-xml = "0.41"
|
||||
jmap-tools = { version = "0.1" }
|
||||
dns-update = { version = "0.5", features = ["test_provider"] }
|
||||
dns-update = { version = "0.6", features = ["test_provider"] }
|
||||
x509-parser = "0.18"
|
||||
rcgen = "0.14"
|
||||
sha2 = "0.11"
|
||||
|
||||
@@ -29,7 +29,7 @@ pub async fn test(imap: &mut ImapConnection, imap_check: &mut ImapConnection, te
|
||||
imap.send("SEARCH RETURN (MIN MAX COUNT ALL) ALL").await;
|
||||
imap.assert_read(Type::Tagged, ResponseType::Ok)
|
||||
.await
|
||||
.assert_contains("COUNT 10 MIN 1 MAX 10 ALL 1,10");
|
||||
.assert_contains("COUNT 10 MIN 1 MAX 10 ALL 1:10");
|
||||
imap_check.send("UID SEARCH ALL").await;
|
||||
imap_check
|
||||
.assert_read(Type::Tagged, ResponseType::Ok)
|
||||
@@ -118,6 +118,65 @@ pub async fn test(imap: &mut ImapConnection, imap_check: &mut ImapConnection, te
|
||||
.await
|
||||
.assert_contains("MIN 2 MAX 9");
|
||||
|
||||
// Saved search with MIN/MAX and ALL saves every match
|
||||
imap_check.send(
|
||||
"UID SEARCH RETURN (SAVE MIN MAX ALL) OR OR FROM nathaniel FROM vandelay OR SUBJECT rfc FROM gore",
|
||||
)
|
||||
.await;
|
||||
imap_check
|
||||
.assert_read(Type::Tagged, ResponseType::Ok)
|
||||
.await
|
||||
.assert_contains("MIN 1 MAX 10 ALL 1,3:4,6,8,10");
|
||||
imap_check.send("UID SEARCH $").await;
|
||||
imap_check
|
||||
.assert_read(Type::Tagged, ResponseType::Ok)
|
||||
.await
|
||||
.assert_equals("* SEARCH 1 3 4 6 8 10");
|
||||
|
||||
// Saved search with MIN and COUNT saves every match
|
||||
imap_check.send(
|
||||
"UID SEARCH RETURN (SAVE MIN COUNT) OR OR FROM nathaniel FROM vandelay OR SUBJECT rfc FROM gore",
|
||||
)
|
||||
.await;
|
||||
imap_check
|
||||
.assert_read(Type::Tagged, ResponseType::Ok)
|
||||
.await
|
||||
.assert_contains("COUNT 6 MIN 1");
|
||||
imap_check.send("UID SEARCH $").await;
|
||||
imap_check
|
||||
.assert_read(Type::Tagged, ResponseType::Ok)
|
||||
.await
|
||||
.assert_equals("* SEARCH 1 3 4 6 8 10");
|
||||
|
||||
// Saved search with only MIN and MAX saves the extremes
|
||||
imap_check.send(
|
||||
"UID SEARCH RETURN (SAVE MIN MAX) OR OR FROM nathaniel FROM vandelay OR SUBJECT rfc FROM gore",
|
||||
)
|
||||
.await;
|
||||
imap_check
|
||||
.assert_read(Type::Tagged, ResponseType::Ok)
|
||||
.await
|
||||
.assert_contains("MIN 1 MAX 10");
|
||||
imap_check.send("UID SEARCH $").await;
|
||||
imap_check
|
||||
.assert_read(Type::Tagged, ResponseType::Ok)
|
||||
.await
|
||||
.assert_equals("* SEARCH 1 10");
|
||||
|
||||
// Saved search with MIN and MAX on a single match saves it once
|
||||
imap_check
|
||||
.send("UID SEARCH RETURN (SAVE MIN MAX) UID 8")
|
||||
.await;
|
||||
imap_check
|
||||
.assert_read(Type::Tagged, ResponseType::Ok)
|
||||
.await
|
||||
.assert_contains("MIN 8 MAX 8");
|
||||
imap_check.send("UID SEARCH $").await;
|
||||
imap_check
|
||||
.assert_read(Type::Tagged, ResponseType::Ok)
|
||||
.await
|
||||
.assert_equals("* SEARCH 8");
|
||||
|
||||
// Sort
|
||||
imap_check
|
||||
.send("UID SORT (REVERSE SUBJECT REVERSE DATE) UTF-8 FROM Nathaniel")
|
||||
|
||||
@@ -97,6 +97,37 @@ pub async fn test(test: &TestServer) {
|
||||
.await;
|
||||
assert_ping(&mut event_rx).await;
|
||||
|
||||
// Import email and expect EmailDelivery state change
|
||||
client
|
||||
.email_import(
|
||||
concat!(
|
||||
"From: [email protected]\r\n",
|
||||
"To: [email protected]\r\n",
|
||||
"Subject: TPS Report (imported)\r\n",
|
||||
"\r\n",
|
||||
"Did you get the memo?"
|
||||
)
|
||||
.as_bytes()
|
||||
.to_vec(),
|
||||
[Id::from(INBOX_ID).to_string()],
|
||||
None::<Vec<&str>>,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_state(
|
||||
&mut event_rx,
|
||||
account.id_string(),
|
||||
&[
|
||||
DataType::EmailDelivery,
|
||||
DataType::Email,
|
||||
DataType::Thread,
|
||||
DataType::Mailbox,
|
||||
],
|
||||
)
|
||||
.await;
|
||||
assert_ping(&mut event_rx).await;
|
||||
|
||||
// Destroy mailbox
|
||||
client.mailbox_destroy(&mailbox_id, true).await.unwrap();
|
||||
assert_state(&mut event_rx, account.id_string(), &[DataType::Mailbox]).await;
|
||||
|
||||
@@ -250,6 +250,9 @@ pub async fn test(test: &mut TestServer) {
|
||||
"http://127.0.0.1/cb",
|
||||
"http://127.0.0.1:54321/cb",
|
||||
"http://[::1]:8080/cb",
|
||||
"http://127.0.0.1",
|
||||
"http://127.0.0.1:54321",
|
||||
"http://[::1]:8080",
|
||||
] {
|
||||
let (status, body) = post_json_raw(
|
||||
&metadata.registration_endpoint,
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*
|
||||
* Modified by Coffey Labs in 2026 for INBUXA.
|
||||
*/
|
||||
|
||||
use ahash::AHashMap;
|
||||
@@ -277,7 +279,7 @@ pub fn test() {
|
||||
) {
|
||||
Ok(result) => match result {
|
||||
MergeResult::Actions(changes) => {
|
||||
itip_merge_changes(ical, changes);
|
||||
itip_merge_changes(ical, &message.message, changes);
|
||||
Ok(None)
|
||||
}
|
||||
MergeResult::Message(message) => {
|
||||
|
||||
@@ -170,6 +170,66 @@ pub async fn test(test: &TestServer, assisted_discovery: bool) {
|
||||
.with_status(StatusCode::MULTI_STATUS)
|
||||
.with_hrefs([test_path.as_str()]);
|
||||
|
||||
// Allprop must not return href-valued properties outside RFC 4918 unless included
|
||||
let user_base_href = format!("{user_base_path}/");
|
||||
let href_props = [
|
||||
DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal),
|
||||
DavProperty::WebDav(WebDavProperty::Owner),
|
||||
DavProperty::WebDav(WebDavProperty::PrincipalCollectionSet),
|
||||
];
|
||||
for (path, hrefs) in [
|
||||
(
|
||||
resource_type.base_path(),
|
||||
[resource_type.collection_path(), user_base_href.as_str()],
|
||||
),
|
||||
(
|
||||
user_base_path.as_str(),
|
||||
[user_base_href.as_str(), test_base_path.as_str()],
|
||||
),
|
||||
(
|
||||
test_base_path.as_str(),
|
||||
[test_base_path.as_str(), test_path.as_str()],
|
||||
),
|
||||
] {
|
||||
let response = client
|
||||
.request_with_headers("PROPFIND", path, [("depth", "1")], "")
|
||||
.await
|
||||
.with_status(StatusCode::MULTI_STATUS)
|
||||
.into_propfind_response(None);
|
||||
for href in hrefs {
|
||||
let properties = response.properties(href);
|
||||
properties.is_defined(DavProperty::WebDav(WebDavProperty::ResourceType));
|
||||
for prop in &href_props {
|
||||
properties.is_undefined(prop);
|
||||
}
|
||||
}
|
||||
|
||||
let response = client
|
||||
.request_with_headers(
|
||||
"PROPFIND",
|
||||
path,
|
||||
[("depth", "1")],
|
||||
concat!(
|
||||
r#"<D:propfind xmlns:D="DAV:"><D:allprop/>"#,
|
||||
r#"<D:include><D:current-user-principal/></D:include>"#,
|
||||
r#"</D:propfind>"#
|
||||
),
|
||||
)
|
||||
.await
|
||||
.with_status(StatusCode::MULTI_STATUS)
|
||||
.into_propfind_response(None);
|
||||
for href in hrefs {
|
||||
response
|
||||
.properties(href)
|
||||
.get(DavProperty::WebDav(WebDavProperty::CurrentUserPrincipal))
|
||||
.with_values([format!(
|
||||
"D:href:{}/jane%40example.com/",
|
||||
DavResourceName::Principal.base_path()
|
||||
)
|
||||
.as_str()]);
|
||||
}
|
||||
}
|
||||
|
||||
// Test 8 PROPFIND with prefer return=minimal
|
||||
let response = client
|
||||
.propfind_with_headers(&test_base_path, ALL_DAV_PROPERTIES, [])
|
||||
|
||||
@@ -26,3 +26,10 @@ crates/types/src/branding.rs "STALWART"
|
||||
# Upstream's old default rules source, read only to treat it as unset: the
|
||||
# server uses the rules bundled with it (resources/spam-filter/).
|
||||
crates/common/src/manager/spam_rules.rs "https://github.com/stalwartlabs/spam-filter/releases/latest/download/spam-filter-rules.json.gz"
|
||||
|
||||
# Upstream's web interface downloads, used only by an install upgraded from
|
||||
# Stalwart that still has its web application: inbuxa installs none
|
||||
# (docs/spec/SPEC.md §5.3), and the pin keeps an upgraded one on the 1.x
|
||||
# bundles this server line can serve.
|
||||
crates/common/src/manager/defaults.rs "https://github.com/stalwartlabs/webui/releases/download/v1.0.11/webui.zip"
|
||||
crates/common/src/manager/defaults.rs "https://github.com/stalwartlabs/webui/releases/latest/download/webui.zip"
|
||||
@@ -41,6 +41,7 @@ TEXT_RENAMES = [
|
||||
('(vnd.stalwart)', '(vnd.inbuxa)', None),
|
||||
('stalwart-webui', 'inbuxa-webui', None),
|
||||
('STALWART_SPAM_', 'INBUXA_SPAM_', None),
|
||||
('STALWART_INCLUDE_SECRETS', 'INBUXA_INCLUDE_SECRETS', ('crates',)),
|
||||
# Configuration defaults, as upstream's generated registry code spells
|
||||
# them. Server code only: the tests use the same spelling for fixtures
|
||||
# that must match their containers and identity provider (database users,
|
||||
|
||||
Reference in new issue
Block a user