Compare commits

..

No commits in common. "main" and "caldav" have entirely different histories.
main ... caldav

10 changed files with 727 additions and 1126 deletions

1532
Cargo.nix vendored

File diff suppressed because it is too large Load diff

View file

@ -18,12 +18,19 @@ A resilient & standards-compliant open-source IMAP server with built-in encrypti
## Roadmap ## Roadmap
- ✅ 0.1 Better emails parsing. - ✅ 0.1 Better emails parsing (july '23, see [eml-codec](https://git.deuxfleurs.fr/Deuxfleurs/eml-codec)).
- ✅ 0.2 IMAP4 support. - ✅ 0.2 Support of IMAP4. (~january '24).
- ✅ 0.3 CalDAV support. - ⌛0.3 CalDAV support. (~february '24).
- ⌛0.4 CardDAV support. - ⌛0.4 CardDAV support.
- ⌛0.5 Internals rework. - ⌛0.5 Public beta.
- ⌛0.6 Public beta.
## A note about cargo2nix
Currently, you must edit Cargo.nix by hand after running `cargo2nix`.
Find the `tokio` dependency declaration.
Look at tokio's dependencies, the `tracing` is disable through a `if false` logic.
Activate it by replacing the condition with `if true`.
## Sponsors and funding ## Sponsors and funding

View file

@ -177,10 +177,6 @@ impl CalendarInternal {
.iter() .iter()
.filter_map(|t: &Token| davstate.change.get(t)) .filter_map(|t: &Token| davstate.change.get(t))
.map(|s| s.clone()) .map(|s| s.clone())
.filter(|s| match s {
SyncChange::Ok((filename, _)) => davstate.idx_by_filename.get(filename).is_some(),
SyncChange::NotFound(filename) => davstate.idx_by_filename.get(filename).is_none(),
})
.collect(); .collect();
let token = self.current_token().await?; let token = self.current_token().await?;

View file

@ -128,6 +128,7 @@ mod tests {
src.qwrite(&mut writer).await.expect("xml serialization"); src.qwrite(&mut writer).await.expect("xml serialization");
tokio_buffer.flush().await.expect("tokio buffer flush"); tokio_buffer.flush().await.expect("tokio buffer flush");
let got = std::str::from_utf8(buffer.as_slice()).unwrap(); let got = std::str::from_utf8(buffer.as_slice()).unwrap();
println!("{:?}", got);
// deserialize // deserialize
let mut rdr = Reader::new(quick_xml::NsReader::from_reader(got.as_bytes())) let mut rdr = Reader::new(quick_xml::NsReader::from_reader(got.as_bytes()))

View file

@ -61,19 +61,18 @@ impl Controller {
} }
}; };
let dav_hdrs = node.dav_header();
let ctrl = Self { node, user, req }; let ctrl = Self { node, user, req };
match method.as_str() { match method.as_str() {
"OPTIONS" => Ok(Response::builder() "OPTIONS" => Ok(Response::builder()
.status(200) .status(200)
.header("DAV", dav_hdrs) .header("DAV", "1")
.header("Allow", "HEAD,GET,PUT,OPTIONS,DELETE,PROPFIND,PROPPATCH,MKCOL,COPY,MOVE,LOCK,UNLOCK,MKCALENDAR,REPORT") .header("Allow", "HEAD,GET,PUT,OPTIONS,DELETE,PROPFIND,PROPPATCH,MKCOL,COPY,MOVE,LOCK,UNLOCK,MKCALENDAR,REPORT")
.body(codec::text_body(""))?), .body(codec::text_body(""))?),
"HEAD" => { "HEAD" => {
tracing::warn!("HEAD might not correctly implemented: should return ETags & co"); tracing::warn!("HEAD not correctly implemented");
Ok(Response::builder() Ok(Response::builder()
.status(200) .status(404)
.body(codec::text_body(""))?) .body(codec::text_body(""))?)
}, },
"GET" => ctrl.get().await, "GET" => ctrl.get().await,
@ -185,7 +184,7 @@ impl Controller {
}, },
}; };
extension = Some(realization::Multistatus::Sync(sync::Multistatus { extension = Some(realization::Multistatus::Sync(sync::Multistatus {
sync_token: sync::SyncToken(format!("{}{}", BASE_TOKEN_URI, new_token)), sync_token: sync::SyncToken(new_token.to_string()),
})); }));
} }
_ => { _ => {
@ -349,14 +348,11 @@ impl Controller {
} }
// Build response // Build response
let multistatus = dav::Multistatus::<All> { dav::Multistatus::<All> {
responses, responses,
responsedescription: None, responsedescription: None,
extension, extension,
}; }
tracing::debug!(multistatus=?multistatus, "multistatus response");
multistatus
} }
} }

View file

@ -98,7 +98,7 @@ impl Server {
let conn = tokio::spawn(async move { let conn = tokio::spawn(async move {
//@FIXME should create a generic "public web" server on which "routers" could be //@FIXME should create a generic "public web" server on which "routers" could be
//abitrarily bound //abitrarily bound
//@FIXME replace with a handler supporting http2 //@FIXME replace with a handler supporting http2 and TLS
match http::Builder::new() match http::Builder::new()
.serve_connection( .serve_connection(
@ -106,9 +106,8 @@ impl Server {
service_fn(|req: Request<hyper::body::Incoming>| { service_fn(|req: Request<hyper::body::Incoming>| {
let login = login.clone(); let login = login.clone();
tracing::info!("{:?} {:?}", req.method(), req.uri()); tracing::info!("{:?} {:?}", req.method(), req.uri());
tracing::debug!(req=?req, "full request");
async { async {
let response = match middleware::auth(login, req, |user, request| { match middleware::auth(login, req, |user, request| {
async { Controller::route(user, request).await }.boxed() async { Controller::route(user, request).await }.boxed()
}) })
.await .await
@ -120,9 +119,7 @@ impl Server {
.status(500) .status(500)
.body(codec::text_body("Internal error")) .body(codec::text_body("Internal error"))
} }
}; }
tracing::debug!(resp=?response, "full response");
response
} }
}), }),
) )

View file

@ -40,9 +40,7 @@ pub(crate) trait DavNode: Send {
fn supported_properties(&self, user: &ArcUser) -> dav::PropName<All>; fn supported_properties(&self, user: &ArcUser) -> dav::PropName<All>;
/// Get the values for the given properties /// Get the values for the given properties
fn properties(&self, user: &ArcUser, prop: dav::PropName<All>) -> PropertyStream<'static>; fn properties(&self, user: &ArcUser, prop: dav::PropName<All>) -> PropertyStream<'static>;
/// Get the value of the DAV header to return //fn properties(&self, user: &ArcUser, prop: dav::PropName<All>) -> Vec<dav::AnyProperty<All>>;
fn dav_header(&self) -> String;
/// Put an element (create or update) /// Put an element (create or update)
fn put<'a>( fn put<'a>(
&'a self, &'a self,

View file

@ -21,16 +21,6 @@ use aero_dav::versioningtypes as vers;
use super::node::PropertyStream; use super::node::PropertyStream;
use crate::dav::node::{Content, DavNode, PutPolicy}; use crate::dav::node::{Content, DavNode, PutPolicy};
/// Why "https://aerogramme.0"?
/// Because tokens must be valid URI.
/// And numeric TLD are ~mostly valid in URI (check the .42 TLD experience)
/// and at the same time, they are not used sold by the ICANN and there is no plan to use them.
/// So I am sure that the URL remains invalid, avoiding leaking requests to an hardcoded URL in the
/// future.
/// The best option would be to make it configurable ofc, so someone can put a domain name
/// that they control, it would probably improve compatibility (maybe some WebDAV spec tells us
/// how to handle/resolve this URI but I am not aware of that...). But that's not the plan for
/// now. So here we are: https://aerogramme.0.
pub const BASE_TOKEN_URI: &str = "https://aerogramme.0/sync/"; pub const BASE_TOKEN_URI: &str = "https://aerogramme.0/sync/";
#[derive(Clone)] #[derive(Clone)]
@ -139,10 +129,6 @@ impl DavNode for RootNode {
> { > {
async { Err(std::io::Error::from(std::io::ErrorKind::Unsupported)) }.boxed() async { Err(std::io::Error::from(std::io::ErrorKind::Unsupported)) }.boxed()
} }
fn dav_header(&self) -> String {
"1".into()
}
} }
#[derive(Clone)] #[derive(Clone)]
@ -264,10 +250,6 @@ impl DavNode for HomeNode {
> { > {
async { Err(std::io::Error::from(std::io::ErrorKind::Unsupported)) }.boxed() async { Err(std::io::Error::from(std::io::ErrorKind::Unsupported)) }.boxed()
} }
fn dav_header(&self) -> String {
"1, access-control, calendar-access".into()
}
} }
#[derive(Clone)] #[derive(Clone)]
@ -401,10 +383,6 @@ impl DavNode for CalendarListNode {
> { > {
async { Err(std::io::Error::from(std::io::ErrorKind::Unsupported)) }.boxed() async { Err(std::io::Error::from(std::io::ErrorKind::Unsupported)) }.boxed()
} }
fn dav_header(&self) -> String {
"1, access-control, calendar-access".into()
}
} }
#[derive(Clone)] #[derive(Clone)]
@ -597,31 +575,7 @@ impl DavNode for CalendarNode {
let col = self.col.clone(); let col = self.col.clone();
let calname = self.calname.clone(); let calname = self.calname.clone();
async move { async move {
let sync_token = match sync_token { let sync_token = sync_token.unwrap();
Some(v) => v,
None => {
let token = col
.token()
.await
.or(Err(std::io::Error::from(std::io::ErrorKind::Interrupted)))?;
let ok_nodes = col
.dag()
.await
.idx_by_filename
.iter()
.map(|(filename, blob_id)| {
Box::new(EventNode {
col: col.clone(),
calname: calname.clone(),
filename: filename.to_string(),
blob_id: *blob_id,
}) as Box<dyn DavNode>
})
.collect();
return Ok((token, ok_nodes, vec![]));
}
};
let (new_token, listed_changes) = match col.diff(sync_token).await { let (new_token, listed_changes) = match col.diff(sync_token).await {
Ok(v) => v, Ok(v) => v,
Err(e) => { Err(e) => {
@ -653,9 +607,6 @@ impl DavNode for CalendarNode {
} }
.boxed() .boxed()
} }
fn dav_header(&self) -> String {
"1, access-control, calendar-access".into()
}
} }
#[derive(Clone)] #[derive(Clone)]
@ -887,10 +838,6 @@ impl DavNode for EventNode {
> { > {
async { Err(std::io::Error::from(std::io::ErrorKind::Unsupported)) }.boxed() async { Err(std::io::Error::from(std::io::ErrorKind::Unsupported)) }.boxed()
} }
fn dav_header(&self) -> String {
"1, access-control".into()
}
} }
#[derive(Clone)] #[derive(Clone)]
@ -992,8 +939,4 @@ impl DavNode for CreateEventNode {
> { > {
async { Err(std::io::Error::from(std::io::ErrorKind::Unsupported)) }.boxed() async { Err(std::io::Error::from(std::io::ErrorKind::Unsupported)) }.boxed()
} }
fn dav_header(&self) -> String {
"1, access-control".into()
}
} }

View file

@ -6,7 +6,7 @@ use crate::common::fragments::*;
fn main() { fn main() {
// IMAP // IMAP
rfc3501_imap4rev1_base(); /*rfc3501_imap4rev1_base();
rfc6851_imapext_move(); rfc6851_imapext_move();
rfc4551_imapext_condstore(); rfc4551_imapext_condstore();
rfc2177_imapext_idle(); rfc2177_imapext_idle();
@ -14,7 +14,7 @@ fn main() {
rfc3691_imapext_unselect(); rfc3691_imapext_unselect();
rfc7888_imapext_literal(); rfc7888_imapext_literal();
rfc4315_imapext_uidplus(); rfc4315_imapext_uidplus();
rfc5819_imapext_liststatus(); rfc5819_imapext_liststatus();*/
// WebDAV // WebDAV
rfc4918_webdav_core(); rfc4918_webdav_core();
@ -370,7 +370,7 @@ use aero_dav::synctypes as sync;
use aero_dav::types as dav; use aero_dav::types as dav;
use aero_dav::versioningtypes as vers; use aero_dav::versioningtypes as vers;
use crate::common::{dav_deserialize, dav_serialize}; use crate::common::dav_deserialize;
fn rfc4918_webdav_core() { fn rfc4918_webdav_core() {
println!("🧪 rfc4918_webdav_core"); println!("🧪 rfc4918_webdav_core");
@ -435,7 +435,6 @@ fn rfc4918_webdav_core() {
assert!(root_success.prop.0.iter().find(|p| matches!(p, dav::AnyProperty::Value(dav::Property::GetContentType(_)))).is_none()); assert!(root_success.prop.0.iter().find(|p| matches!(p, dav::AnyProperty::Value(dav::Property::GetContentType(_)))).is_none());
assert!(root_not_found.prop.0.iter().find(|p| matches!(p, dav::AnyProperty::Request(dav::PropertyRequest::GetContentLength))).is_some()); assert!(root_not_found.prop.0.iter().find(|p| matches!(p, dav::AnyProperty::Request(dav::PropertyRequest::GetContentLength))).is_some());
// -- HIERARCHY EXPLORATION WITH THE DEPTH: X HEADER FIELD --
// depth 1 / -> /alice/ // depth 1 / -> /alice/
let body = http.request(reqwest::Method::from_bytes(b"PROPFIND")?, "http://localhost:8087").header("Depth", "1").send()?.text()?; let body = http.request(reqwest::Method::from_bytes(b"PROPFIND")?, "http://localhost:8087").header("Depth", "1").send()?.text()?;
let multistatus = dav_deserialize::<dav::Multistatus<All>>(&body); let multistatus = dav_deserialize::<dav::Multistatus<All>>(&body);
@ -471,7 +470,7 @@ fn rfc4918_webdav_core() {
let multistatus = dav_deserialize::<dav::Multistatus<All>>(&body); let multistatus = dav_deserialize::<dav::Multistatus<All>>(&body);
assert_eq!(multistatus.responses.len(), 1); assert_eq!(multistatus.responses.len(), 1);
// --- PUT (add objets) --- // --- PUT ---
// first object // first object
let resp = http.put("http://localhost:8087/alice/calendar/Personal/rfc2.ics").header("If-None-Match", "*").body(ICAL_RFC2).send()?; let resp = http.put("http://localhost:8087/alice/calendar/Personal/rfc2.ics").header("If-None-Match", "*").body(ICAL_RFC2).send()?;
let obj1_etag = resp.headers().get("etag").expect("etag must be set"); let obj1_etag = resp.headers().get("etag").expect("etag must be set");
@ -497,14 +496,14 @@ fn rfc4918_webdav_core() {
let resp = http.put("http://localhost:8087/alice/calendar/Personal/rfc2.ics").header("If-Match", obj1_etag).body(ICAL_RFC1).send()?; let resp = http.put("http://localhost:8087/alice/calendar/Personal/rfc2.ics").header("If-Match", obj1_etag).body(ICAL_RFC1).send()?;
assert_eq!(resp.status(), 201); assert_eq!(resp.status(), 201);
// --- GET (fetch objects) --- // --- GET ---
let body = http.get("http://localhost:8087/alice/calendar/Personal/rfc2.ics").send()?.text()?; let body = http.get("http://localhost:8087/alice/calendar/Personal/rfc2.ics").send()?.text()?;
assert_eq!(body.as_bytes(), ICAL_RFC1); assert_eq!(body.as_bytes(), ICAL_RFC1);
let body = http.get("http://localhost:8087/alice/calendar/Personal/rfc3.ics").send()?.text()?; let body = http.get("http://localhost:8087/alice/calendar/Personal/rfc3.ics").send()?.text()?;
assert_eq!(body.as_bytes(), ICAL_RFC3); assert_eq!(body.as_bytes(), ICAL_RFC3);
// --- DELETE (delete objects) --- // --- DELETE ---
// delete 1st object // delete 1st object
let resp = http.delete("http://localhost:8087/alice/calendar/Personal/rfc2.ics").send()?; let resp = http.delete("http://localhost:8087/alice/calendar/Personal/rfc2.ics").send()?;
assert_eq!(resp.status(), 204); assert_eq!(resp.status(), 204);
@ -529,7 +528,7 @@ fn rfc4918_webdav_core() {
fn rfc5397_webdav_principal() { fn rfc5397_webdav_principal() {
println!("🧪 rfc5397_webdav_principal"); println!("🧪 rfc5397_webdav_principal");
common::aerogramme_provider_daemon_dev(|_imap, _lmtp, http| { common::aerogramme_provider_daemon_dev(|_imap, _lmtp, http| {
// -- AUTODISCOVERY: FIND "PRINCIPAL" AS DEFINED IN WEBDAV ACL (~USER'S HOME) -- // Find principal
let propfind_req = r#"<?xml version="1.0" encoding="utf-8" ?><propfind xmlns="DAV:"><prop><current-user-principal/></prop></propfind>"#; let propfind_req = r#"<?xml version="1.0" encoding="utf-8" ?><propfind xmlns="DAV:"><prop><current-user-principal/></prop></propfind>"#;
let body = http.request(reqwest::Method::from_bytes(b"PROPFIND")?, "http://localhost:8087").body(propfind_req).send()?.text()?; let body = http.request(reqwest::Method::from_bytes(b"PROPFIND")?, "http://localhost:8087").body(propfind_req).send()?.text()?;
let multistatus = dav_deserialize::<dav::Multistatus<All>>(&body); let multistatus = dav_deserialize::<dav::Multistatus<All>>(&body);
@ -1018,8 +1017,7 @@ fn rfc4791_webdav_caldav() {
fn rfc6578_webdav_sync() { fn rfc6578_webdav_sync() {
println!("🧪 rfc6578_webdav_sync"); println!("🧪 rfc6578_webdav_sync");
common::aerogramme_provider_daemon_dev(|_imap, _lmtp, http| { common::aerogramme_provider_daemon_dev(|_imap, _lmtp, http| {
// -- PROPFIND -- // propname on a calendar node must return <sync-token/> + <supported-report-set/> (2nd element is theoretically from versioning)
// propname must return sync-token & supported-report-set (from webdav versioning)
let propfind_req = r#"<?xml version="1.0" encoding="utf-8" ?><propfind xmlns="DAV:"><propname/></propfind>"#; let propfind_req = r#"<?xml version="1.0" encoding="utf-8" ?><propfind xmlns="DAV:"><propname/></propfind>"#;
let body = http.request(reqwest::Method::from_bytes(b"PROPFIND")?, "http://localhost:8087/alice/calendar/Personal/").body(propfind_req).send()?.text()?; let body = http.request(reqwest::Method::from_bytes(b"PROPFIND")?, "http://localhost:8087/alice/calendar/Personal/").body(propfind_req).send()?.text()?;
let multistatus = dav_deserialize::<dav::Multistatus<All>>(&body); let multistatus = dav_deserialize::<dav::Multistatus<All>>(&body);
@ -1112,180 +1110,6 @@ fn rfc6578_webdav_sync() {
assert!(init_sync_token != del_sync_token); assert!(init_sync_token != del_sync_token);
assert!(rfc1_sync_token != del_sync_token); assert!(rfc1_sync_token != del_sync_token);
// -- TEST SYNC CUSTOM REPORT: SYNC-COLLECTION --
// 3.8. Example: Initial DAV:sync-collection Report
// Part 1: check the empty case
let sync_query = r#"<?xml version="1.0" encoding="utf-8" ?>
<D:sync-collection xmlns:D="DAV:">
<D:sync-token/>
<D:sync-level>1</D:sync-level>
<D:prop>
<D:getetag/>
</D:prop>
</D:sync-collection>
"#;
let resp = http
.request(
reqwest::Method::from_bytes(b"REPORT")?,
"http://localhost:8087/alice/calendar/Personal/",
)
.body(sync_query)
.send()?;
assert_eq!(resp.status(), 207);
let multistatus = dav_deserialize::<dav::Multistatus<All>>(&resp.text()?);
assert_eq!(multistatus.responses.len(), 0);
let empty_token = match &multistatus.extension {
Some(realization::Multistatus::Sync(sync::Multistatus { sync_token: sync::SyncToken(x) } )) => x,
_ => anyhow::bail!("wrong content"),
};
// Part 2: check with one file
let resp = http
.put("http://localhost:8087/alice/calendar/Personal/rfc1.ics")
.header("If-None-Match", "*")
.body(ICAL_RFC1)
.send()?;
assert_eq!(resp.status(), 201);
let resp = http
.request(
reqwest::Method::from_bytes(b"REPORT")?,
"http://localhost:8087/alice/calendar/Personal/",
)
.body(sync_query)
.send()?;
assert_eq!(resp.status(), 207);
let multistatus = dav_deserialize::<dav::Multistatus<All>>(&resp.text()?);
assert_eq!(multistatus.responses.len(), 1);
let initial_one_file_token = match &multistatus.extension {
Some(realization::Multistatus::Sync(sync::Multistatus { sync_token: sync::SyncToken(x) } )) => x,
_ => anyhow::bail!("wrong content"),
};
assert!(empty_token != initial_one_file_token);
// 3.9. Example: DAV:sync-collection Report with Token
// Part 1: nothing changed, response must be empty
let sync_query = |token: &str| vers::Report::<realization::All>::Extension(realization::ReportType::Sync(sync::SyncCollection {
sync_token: sync::SyncTokenRequest::IncrementalSync(token.into()),
sync_level: sync::SyncLevel::One,
limit: None,
prop: dav::PropName(vec![
dav::PropertyRequest::GetEtag,
]),
}));
let resp = http
.request(
reqwest::Method::from_bytes(b"REPORT")?,
"http://localhost:8087/alice/calendar/Personal/",
)
.body(dav_serialize(&sync_query(initial_one_file_token)))
.send()?;
assert_eq!(resp.status(), 207);
let multistatus = dav_deserialize::<dav::Multistatus<All>>(&resp.text()?);
assert_eq!(multistatus.responses.len(), 0);
let no_change = match &multistatus.extension {
Some(realization::Multistatus::Sync(sync::Multistatus { sync_token: sync::SyncToken(x) } )) => x,
_ => anyhow::bail!("wrong content"),
};
assert_eq!(initial_one_file_token, no_change);
// Part 2: add a new node (rfc2) + remove a node (rfc1)
// add rfc2
let resp = http
.put("http://localhost:8087/alice/calendar/Personal/rfc2.ics")
.header("If-None-Match", "*")
.body(ICAL_RFC2)
.send()?;
assert_eq!(resp.status(), 201);
// delete rfc1
let resp = http.delete("http://localhost:8087/alice/calendar/Personal/rfc1.ics").send()?;
assert_eq!(resp.status(), 204);
// call REPORT <sync-collection>
let resp = http
.request(
reqwest::Method::from_bytes(b"REPORT")?,
"http://localhost:8087/alice/calendar/Personal/",
)
.body(dav_serialize(&sync_query(initial_one_file_token)))
.send()?;
assert_eq!(resp.status(), 207);
let multistatus = dav_deserialize::<dav::Multistatus<All>>(&resp.text()?);
assert_eq!(multistatus.responses.len(), 2);
let token_addrm = match &multistatus.extension {
Some(realization::Multistatus::Sync(sync::Multistatus { sync_token: sync::SyncToken(x) } )) => x,
_ => anyhow::bail!("wrong content"),
};
assert!(initial_one_file_token != token_addrm);
// Part 3: remove a node (rfc2) and add it again with new content
// delete rfc2
let resp = http.delete("http://localhost:8087/alice/calendar/Personal/rfc2.ics").send()?;
assert_eq!(resp.status(), 204);
// add rfc2 with ICAL_RFC3 content
let resp = http
.put("http://localhost:8087/alice/calendar/Personal/rfc2.ics")
.header("If-None-Match", "*")
.body(ICAL_RFC3)
.send()?;
let rfc2_etag = resp.headers().get("etag").expect("etag must be set");
assert_eq!(resp.status(), 201);
// call REPORT <sync-collection>
let resp = http
.request(
reqwest::Method::from_bytes(b"REPORT")?,
"http://localhost:8087/alice/calendar/Personal/",
)
.body(dav_serialize(&sync_query(token_addrm)))
.send()?;
assert_eq!(resp.status(), 207);
let multistatus = dav_deserialize::<dav::Multistatus<All>>(&resp.text()?);
assert_eq!(multistatus.responses.len(), 1);
let token_addrm_same = match &multistatus.extension {
Some(realization::Multistatus::Sync(sync::Multistatus { sync_token: sync::SyncToken(x) } )) => x,
_ => anyhow::bail!("wrong content"),
};
assert!(token_addrm_same != token_addrm);
// Part 4: overwrite an event (rfc1) with new content
let resp = http
.put("http://localhost:8087/alice/calendar/Personal/rfc1.ics")
.header("If-Match", rfc2_etag)
.body(ICAL_RFC4)
.send()?;
assert_eq!(resp.status(), 201);
// call REPORT <sync-collection>
let resp = http
.request(
reqwest::Method::from_bytes(b"REPORT")?,
"http://localhost:8087/alice/calendar/Personal/",
)
.body(dav_serialize(&sync_query(token_addrm_same)))
.send()?;
assert_eq!(resp.status(), 207);
let multistatus = dav_deserialize::<dav::Multistatus<All>>(&resp.text()?);
assert_eq!(multistatus.responses.len(), 1);
let token_addrm_same = match &multistatus.extension {
Some(realization::Multistatus::Sync(sync::Multistatus { sync_token: sync::SyncToken(x) } )) => x,
_ => anyhow::bail!("wrong content"),
};
assert!(token_addrm_same != token_addrm);
// Unknown token must return 410 GONE.
// Token can be forgotten as we garbage collect the DAG.
let resp = http
.request(
reqwest::Method::from_bytes(b"REPORT")?,
"http://localhost:8087/alice/calendar/Personal/",
)
.body(dav_serialize(&sync_query("https://aerogramme.0/sync/000000000000000000000000000000000000000000000000")))
.send()?;
assert_eq!(resp.status(), 410);
Ok(()) Ok(())
}) })
.expect("test fully run") .expect("test fully run")

View file

@ -108,8 +108,7 @@ pub fn read_first_u32(inp: &str) -> Result<u32> {
.parse::<u32>()?) .parse::<u32>()?)
} }
use aero_dav::xml::{Node, Reader, Writer}; use aero_dav::xml::{Node, Reader};
use tokio::io::AsyncWriteExt;
pub fn dav_deserialize<T: Node<T>>(src: &str) -> T { pub fn dav_deserialize<T: Node<T>>(src: &str) -> T {
futures::executor::block_on(async { futures::executor::block_on(async {
let mut rdr = Reader::new(quick_xml::NsReader::from_reader(src.as_bytes())) let mut rdr = Reader::new(quick_xml::NsReader::from_reader(src.as_bytes()))
@ -118,19 +117,3 @@ pub fn dav_deserialize<T: Node<T>>(src: &str) -> T {
rdr.find().await.expect("parse XML") rdr.find().await.expect("parse XML")
}) })
} }
pub fn dav_serialize<T: Node<T>>(src: &T) -> String {
futures::executor::block_on(async {
let mut buffer = Vec::new();
let mut tokio_buffer = tokio::io::BufWriter::new(&mut buffer);
let q = quick_xml::writer::Writer::new_with_indent(&mut tokio_buffer, b' ', 4);
let ns_to_apply = vec![
("xmlns:D".into(), "DAV:".into()),
("xmlns:C".into(), "urn:ietf:params:xml:ns:caldav".into()),
];
let mut writer = Writer { q, ns_to_apply };
src.qwrite(&mut writer).await.expect("xml serialization");
tokio_buffer.flush().await.expect("tokio buffer flush");
std::str::from_utf8(buffer.as_slice()).unwrap().into()
})
}