Legacy vCard 2.1 and 3.0 serialization support

This commit is contained in:
mdecimus 2025-05-27 12:42:03 +02:00
parent 30907f6a00
commit 48cc823432
13 changed files with 104 additions and 53 deletions

56
Cargo.lock generated
View file

@ -989,16 +989,16 @@ dependencies = [
[[package]]
name = "calcard"
version = "0.1.0"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75eaecaebc21cf56381d401db71ed288d61f3fcbb880c777e08cab2f82dfc0ab"
checksum = "8e16f081c4da49b29187103679784b4dcf2fbda52e9b519388c397cb58414e5a"
dependencies = [
"ahash",
"chrono",
"chrono-tz",
"hashify",
"mail-builder",
"mail-parser 0.10.2",
"mail-parser",
"rkyv",
"serde",
]
@ -1289,7 +1289,7 @@ dependencies = [
"libc",
"mail-auth",
"mail-builder",
"mail-parser 0.11.0",
"mail-parser",
"mail-send",
"md5",
"nlp",
@ -1776,7 +1776,7 @@ dependencies = [
"compact_str",
"hashify",
"hyper 1.6.0",
"mail-parser 0.11.0",
"mail-parser",
"quick-xml 0.37.5",
"rkyv",
"serde",
@ -1974,7 +1974,7 @@ dependencies = [
"jmap_proto",
"ldap3",
"mail-builder",
"mail-parser 0.11.0",
"mail-parser",
"mail-send",
"md5",
"nlp",
@ -2252,7 +2252,7 @@ dependencies = [
"hashify",
"jmap_proto",
"mail-builder",
"mail-parser 0.11.0",
"mail-parser",
"nlp",
"rand 0.8.5",
"rasn",
@ -3174,7 +3174,7 @@ dependencies = [
"jmap_proto",
"mail-auth",
"mail-builder",
"mail-parser 0.11.0",
"mail-parser",
"mail-send",
"mime",
"pkcs8",
@ -3576,7 +3576,7 @@ dependencies = [
"imap_proto",
"indexmap 2.9.0",
"jmap_proto",
"mail-parser 0.11.0",
"mail-parser",
"mail-send",
"md5",
"nlp",
@ -3600,7 +3600,7 @@ dependencies = [
"compact_str",
"hashify",
"jmap_proto",
"mail-parser 0.11.0",
"mail-parser",
"store",
"tokio",
"trc",
@ -3860,7 +3860,7 @@ dependencies = [
"lz4_flex",
"mail-auth",
"mail-builder",
"mail-parser 0.11.0",
"mail-parser",
"mail-send",
"nlp",
"p256",
@ -3916,7 +3916,7 @@ dependencies = [
"compact_str",
"fast-float",
"hashify",
"mail-parser 0.11.0",
"mail-parser",
"rkyv",
"serde",
"serde_json",
@ -4332,7 +4332,7 @@ dependencies = [
"flate2",
"hickory-resolver",
"mail-builder",
"mail-parser 0.11.0",
"mail-parser",
"quick-xml 0.37.5",
"quick_cache",
"rand 0.8.5",
@ -4354,16 +4354,6 @@ dependencies = [
"gethostname",
]
[[package]]
name = "mail-parser"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "187a2b93c4c8c32f552ee06c2d99915e575de2fc7e04b07891c9edfee5b8edd6"
dependencies = [
"encoding_rs",
"hashify",
]
[[package]]
name = "mail-parser"
version = "0.11.0"
@ -4405,7 +4395,7 @@ dependencies = [
"imap",
"imap_proto",
"jmap_proto",
"mail-parser 0.11.0",
"mail-parser",
"mail-send",
"md5",
"parking_lot",
@ -4508,7 +4498,7 @@ dependencies = [
"jmap_proto",
"lz4_flex",
"mail-auth",
"mail-parser 0.11.0",
"mail-parser",
"nlp",
"rkyv",
"serde",
@ -5467,7 +5457,7 @@ dependencies = [
"email",
"imap",
"jmap_proto",
"mail-parser 0.11.0",
"mail-parser",
"mail-send",
"rustls 0.23.27",
"store",
@ -7296,7 +7286,7 @@ dependencies = [
"email",
"hkdf",
"jmap_proto",
"mail-parser 0.11.0",
"mail-parser",
"memory-stats",
"p256",
"reqwest 0.12.15",
@ -7421,7 +7411,7 @@ dependencies = [
"fancy-regex",
"hashify",
"mail-builder",
"mail-parser 0.11.0",
"mail-parser",
"rkyv",
"serde",
]
@ -7508,7 +7498,7 @@ dependencies = [
"lru-cache",
"mail-auth",
"mail-builder",
"mail-parser 0.11.0",
"mail-parser",
"mail-send",
"md5",
"nlp",
@ -7593,7 +7583,7 @@ dependencies = [
"infer 0.19.0",
"mail-auth",
"mail-builder",
"mail-parser 0.11.0",
"mail-parser",
"mail-send",
"nlp",
"psl",
@ -7684,7 +7674,7 @@ dependencies = [
"indicatif",
"jmap-client",
"mail-auth",
"mail-parser 0.11.0",
"mail-parser",
"num_cpus",
"prettytable-rs",
"pwhash",
@ -7946,7 +7936,7 @@ dependencies = [
"jmap-client",
"jmap_proto",
"mail-auth",
"mail-parser 0.11.0",
"mail-parser",
"mail-send",
"managesieve",
"migration",
@ -8467,7 +8457,7 @@ dependencies = [
"compact_str",
"event_macro",
"mail-auth",
"mail-parser 0.11.0",
"mail-parser",
"parking_lot",
"reqwest 0.12.15",
"rkyv",

View file

@ -19,7 +19,7 @@ mail-auth = { version = "0.7" }
mail-send = { version = "0.5", default-features = false, features = ["cram-md5", "ring", "tls12"] }
smtp-proto = { version = "0.1", features = ["rkyv"] }
dns-update = { version = "0.1" }
calcard = { version = "0.1", features = ["rkyv"] }
calcard = { version = "0.1.1", features = ["rkyv"] }
ahash = { version = "0.8.2", features = ["serde"] }
parking_lot = "0.12.1"
regex = "1.7.0"

View file

@ -7,7 +7,7 @@ edition = "2021"
trc = { path = "../trc" }
hashify = "0.2.6"
quick-xml = "0.37.2"
calcard = { version = "0.1", features = ["rkyv"] }
calcard = { version = "0.1.1", features = ["rkyv"] }
mail-parser = { version = "0.11", features = ["full_encoding", "rkyv"] }
hyper = "1.6.0"
rkyv = { version = "0.8.10", features = ["little_endian"] }
@ -15,7 +15,7 @@ chrono = { version = "0.4.40", features = ["serde"], optional = true }
compact_str = "0.9.0"
[dev-dependencies]
calcard = { version = "0.1", features = ["serde", "rkyv"] }
calcard = { version = "0.1.1", features = ["serde", "rkyv"] }
serde = { version = "1.0.217", features = ["derive"] }
serde_json = "1.0.138"
chrono = { version = "0.4.40", features = ["serde"] }

View file

@ -4,6 +4,7 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use calcard::vcard::VCardVersion;
use compact_str::{CompactString, ToCompactString};
use trc::Value;
@ -40,6 +41,7 @@ pub struct RequestHeaders<'x> {
pub content_type: Option<&'x str>,
pub destination: Option<&'x str>,
pub lock_token: Option<&'x str>,
pub max_vcard_version: Option<VCardVersion>,
pub overwrite_fail: bool,
pub no_timezones: bool,
pub ret: Return,

View file

@ -4,6 +4,8 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use calcard::vcard::VCardVersion;
use crate::{Condition, Depth, If, RequestHeaders, ResourceState, Return, Timeout};
impl<'x> RequestHeaders<'x> {
@ -82,6 +84,23 @@ impl<'x> RequestHeaders<'x> {
}
return true;
},
"Accept" => {
for value in value.split(',') {
if value.trim().starts_with("text/vcard") {
if let Some(version) = value.split_once("version=")
.and_then(|(_, version)| VCardVersion::try_parse(version.trim())) {
if let Some(max_vcard_version) = &mut self.max_vcard_version {
if version > *max_vcard_version {
*max_vcard_version = version;
}
} else {
self.max_vcard_version = Some(version);
}
}
}
}
return true;
},
_ => {}
);

View file

@ -14,7 +14,7 @@ directory = { path = "../directory" }
http_proto = { path = "../http-proto" }
jmap_proto = { path = "../jmap-proto" }
trc = { path = "../trc" }
calcard = { version = "0.1", features = ["rkyv"] }
calcard = { version = "0.1.1", features = ["rkyv"] }
hashify = { version = "0.2" }
hyper = { version = "1.0.1", features = ["server", "http1", "http2"] }
percent-encoding = "2.3.1"

View file

@ -105,7 +105,14 @@ impl CardGetRequestHandler for Server {
.with_etag(etag)
.with_last_modified(Rfc1123DateTime::new(i64::from(card.modified)).to_string());
let vcard = card.card.to_string();
let mut vcard = String::with_capacity(128);
let _ = card.card.write_to(
&mut vcard,
headers
.max_vcard_version
.or_else(|| card.card.version())
.unwrap_or_default(),
);
if !is_head {
Ok(response.with_binary_body(vcard))

View file

@ -13,7 +13,8 @@ use crate::{
},
};
use calcard::vcard::{
ArchivedVCard, ArchivedVCardEntry, ArchivedVCardParameter, VCardParameterName,
ArchivedVCard, ArchivedVCardEntry, ArchivedVCardParameter, VCardParameterName, VCardProperty,
VCardVersion,
};
use common::{Server, auth::AccessToken};
use dav_proto::{
@ -200,22 +201,30 @@ fn find_parameter<'x>(
pub(crate) fn serialize_vcard_with_props(
card: &ArchivedVCard,
props: &[CardDavPropertyName],
version: Option<VCardVersion>,
) -> String {
let mut vcard = String::with_capacity(128);
let version = version.or_else(|| card.version()).unwrap_or_default();
if !props.is_empty() {
let mut vcard = String::with_capacity(128);
let _ = write!(&mut vcard, "BEGIN:VCARD\r\n");
let is_v4 = matches!(version, VCardVersion::V4_0);
for entry in card.entries.iter() {
for item in props {
if entry.name == item.name && entry.group == item.group {
let _ = entry.write_to(&mut vcard, !item.no_value);
if item.name != VCardProperty::Version {
let _ = entry.write_to(&mut vcard, !item.no_value, is_v4);
} else {
let _ = write!(&mut vcard, "VERSION:{version}\r\n");
}
break;
}
}
}
let _ = write!(&mut vcard, "END:VCARD\r\n");
vcard
} else {
card.to_string()
let _ = card.write_to(&mut vcard, version);
}
vcard
}

View file

@ -6,7 +6,7 @@
use calcard::{
icalendar::{ICalendarComponentType, ICalendarParameterName, ICalendarProperty},
vcard::VCardParameterName,
vcard::{VCardParameterName, VCardVersion},
};
use dav_proto::{
Depth, RequestHeaders, Return,
@ -35,7 +35,7 @@ pub mod lock;
pub mod propfind;
pub mod uri;
#[derive(Default, Debug)]
#[derive(Debug)]
pub(crate) struct DavQuery<'x> {
pub uri: &'x str,
pub resource: DavQueryResource<'x>,
@ -43,6 +43,7 @@ pub(crate) struct DavQuery<'x> {
pub sync_type: SyncType,
pub depth: usize,
pub limit: Option<u32>,
pub max_vcard_version: Option<VCardVersion>,
pub ret: Return,
pub depth_no_root: bool,
pub expand: bool,
@ -158,7 +159,10 @@ impl<'x> DavQuery<'x> {
ret: headers.ret,
depth_no_root: headers.depth_no_root,
uri: headers.uri,
..Default::default()
max_vcard_version: headers.max_vcard_version,
sync_type: Default::default(),
limit: Default::default(),
expand: Default::default(),
}
}
@ -176,7 +180,11 @@ impl<'x> DavQuery<'x> {
ret: headers.ret,
depth_no_root: headers.depth_no_root,
uri: headers.uri,
..Default::default()
max_vcard_version: headers.max_vcard_version,
sync_type: Default::default(),
depth: Default::default(),
limit: Default::default(),
expand: Default::default(),
}
}
@ -196,7 +204,10 @@ impl<'x> DavQuery<'x> {
ret: headers.ret,
depth_no_root: headers.depth_no_root,
uri: headers.uri,
..Default::default()
max_vcard_version: headers.max_vcard_version,
sync_type: Default::default(),
depth: Default::default(),
expand: Default::default(),
}
}
@ -220,7 +231,11 @@ impl<'x> DavQuery<'x> {
ret: headers.ret,
depth_no_root: headers.depth_no_root,
uri: headers.uri,
..Default::default()
sync_type: Default::default(),
depth: Default::default(),
limit: Default::default(),
max_vcard_version: Default::default(),
expand: Default::default(),
}
}
@ -249,6 +264,7 @@ impl<'x> DavQuery<'x> {
depth_no_root: headers.depth_no_root,
expand: false,
uri: headers.uri,
max_vcard_version: headers.max_vcard_version,
}
}
@ -277,7 +293,9 @@ impl<'x> DavQuery<'x> {
depth_no_root: headers.depth_no_root,
expand: true,
uri: headers.uri,
..Default::default()
sync_type: Default::default(),
limit: Default::default(),
max_vcard_version: headers.max_vcard_version,
}
}

View file

@ -1191,6 +1191,9 @@ impl PropFindRequestHandler for Server {
DavValue::CData(serialize_vcard_with_props(
&card.inner.card,
items,
query
.max_vcard_version
.or_else(|| card.inner.card.version()),
)),
));
}

View file

@ -69,7 +69,10 @@ impl PrincipalMatching for Server {
ret: headers.ret,
depth_no_root: headers.depth_no_root,
uri: headers.uri,
..Default::default()
sync_type: Default::default(),
limit: Default::default(),
max_vcard_version: Default::default(),
expand: Default::default(),
},
)
.await;

View file

@ -12,7 +12,7 @@ jmap_proto = { path = "../jmap-proto" }
trc = { path = "../trc" }
directory = { path = "../directory" }
dav-proto = { path = "../dav-proto" }
calcard = { version = "0.1", features = ["rkyv"] }
calcard = { version = "0.1.1", features = ["rkyv"] }
hashify = "0.2"
tokio = { version = "1.45", features = ["net", "macros"] }
rkyv = { version = "0.8.10", features = ["little_endian"] }

View file

@ -29,7 +29,7 @@ imap = { path = "../crates/imap", features = ["test_mode"] }
imap_proto = { path = "../crates/imap-proto" }
dav = { path = "../crates/dav", features = ["test_mode"] }
dav-proto = { path = "../crates/dav-proto", features = ["test_mode"] }
calcard = { version = "0.1", features = ["rkyv"] }
calcard = { version = "0.1.1", features = ["rkyv"] }
groupware = { path = "../crates/groupware", features = ["test_mode"] }
http = { path = "../crates/http", features = ["test_mode", "enterprise"] }
http_proto = { path = "../crates/http-proto" }