Skip to main content

io_webdav/
client.rs

1//! # Standard, blocking WebDAV client
2//!
3//! Holds a single boxed stream (any blocking `Read + Write` impl) plus the
4//! [`WebdavAuth`] credential, the user-facing pub options ([`base_url`],
5//! [`user_agent`]) and the discovery caches ([`principal_url`],
6//! [`calendar_home_set`], [`addressbook_home_set`]).
7//!
8//! The bare [`new`] constructor takes a pre-connected stream; callers handle
9//! TCP and TLS themselves. With one of the TLS feature flags enabled
10//! (`rustls-ring`, `rustls-aws`, `native-tls`), [`connect`] is also available
11//! and handles `https://` URLs end-to-end via
12//! [`pimalaya_stream::std::stream::StreamStd`].
13//!
14//! Discovery flows top-down from the configured [`base_url`] (the DAV
15//! context root, resolved by pimconf's RFC 6764 discovery upstream):
16//! [`current_user_principal`] resolves the principal URL;
17//! [`calendar_home_set`] / [`addressbook_home_set`] resolve the per-RFC
18//! home-set URL. Each step caches its result; higher-level methods return
19//! [`MissingPrincipal`] / [`MissingCalendarHomeSet`] /
20//! [`MissingAddressbookHomeSet`] when the cache is empty (mirrors io-jmap's
21//! `MissingSession`).
22//!
23//! [`base_url`]: WebdavClientStd::base_url
24//! [`user_agent`]: WebdavClientStd::user_agent
25//! [`principal_url`]: WebdavClientStd::principal_url
26//! [`calendar_home_set`]: WebdavClientStd::calendar_home_set
27//! [`addressbook_home_set`]: WebdavClientStd::addressbook_home_set
28//! [`new`]: WebdavClientStd::new
29//! [`connect`]: WebdavClientStd::connect
30//! [`current_user_principal`]: WebdavClientStd::current_user_principal
31//! [`MissingPrincipal`]: WebdavClientStdError::MissingPrincipal
32//! [`MissingCalendarHomeSet`]: WebdavClientStdError::MissingCalendarHomeSet
33//! [`MissingAddressbookHomeSet`]: WebdavClientStdError::MissingAddressbookHomeSet
34
35use core::fmt;
36
37use alloc::{
38    boxed::Box,
39    collections::BTreeSet,
40    format,
41    string::{String, ToString},
42    vec::Vec,
43};
44
45use std::io::{self, Read, Write};
46
47#[cfg(any(
48    feature = "rustls-aws",
49    feature = "rustls-ring",
50    feature = "native-tls"
51))]
52use pimalaya_stream::{std::stream::StreamStd, tls::Tls};
53use thiserror::Error;
54use url::Url;
55
56use crate::{
57    coroutine::*,
58    rfc4791::{
59        calendar::{
60            Calendar, create::CreateCalendar, delete::DeleteCalendar, home_set::CalendarHomeSet,
61            list::ListCalendars, update::UpdateCalendar,
62        },
63        item::{
64            create::{CreateItem, CreateItemOk},
65            delete::DeleteItem,
66            list::{ItemEntry, ListItems},
67            read::{ItemBody, ReadItem},
68            update::{UpdateItem, UpdateItemOk},
69        },
70    },
71    rfc4918::{
72        GETETAG, WebdavAuth, coroutine::WebdavRedirectYield,
73        follow_redirects::FollowRedirectsError, send::SendError,
74    },
75    rfc5397::current_user_principal::CurrentUserPrincipal,
76    rfc6352::{
77        addressbook::{
78            Addressbook, create::CreateAddressbook, delete::DeleteAddressbook,
79            home_set::AddressbookHomeSet, list::ListAddressbooks, update::UpdateAddressbook,
80        },
81        card::{
82            CardEntry, CardRef,
83            create::{CreateCard, CreateCardOk},
84            delete::DeleteCard,
85            enumerate::EnumCards,
86            list::ListCards,
87            multiget::MultigetCards,
88            read::{CardBody, ReadCard},
89            update::{UpdateCard, UpdateCardOk},
90        },
91    },
92    rfc6578::sync_collection::{SyncCollection, SyncCollectionError, SyncDelta},
93};
94
95const READ_BUFFER_SIZE: usize = 16 * 1024;
96
97const DEFAULT_USER_AGENT: &str = concat!("io-webdav/", env!("CARGO_PKG_VERSION"));
98
99/// Errors returned by [`WebdavClientStd`].
100#[derive(Debug, Error)]
101pub enum WebdavClientStdError {
102    /// A WebDAV request failed while being sent.
103    #[error(transparent)]
104    Send(#[from] SendError),
105    /// A redirect-aware WebDAV send failed.
106    #[error(transparent)]
107    FollowRedirects(#[from] FollowRedirectsError),
108    /// A `sync-collection` REPORT failed.
109    #[error(transparent)]
110    SyncCollection(#[from] SyncCollectionError),
111
112    /// An underlying I/O operation failed.
113    #[error(transparent)]
114    Io(#[from] io::Error),
115
116    /// TLS negotiation or the underlying TLS stack failed.
117    #[cfg(any(
118        feature = "rustls-aws",
119        feature = "rustls-ring",
120        feature = "native-tls"
121    ))]
122    #[error(transparent)]
123    Tls(#[from] anyhow::Error),
124    /// The target WebDAV URL carries no host.
125    #[cfg(any(
126        feature = "rustls-aws",
127        feature = "rustls-ring",
128        feature = "native-tls"
129    ))]
130    #[error("WebDAV URL `{0}` has no host")]
131    UrlMissingHost(String),
132    /// The target WebDAV URL uses a scheme other than `http` or `https`.
133    #[cfg(any(
134        feature = "rustls-aws",
135        feature = "rustls-ring",
136        feature = "native-tls"
137    ))]
138    #[error("WebDAV URL `{0}` has unsupported scheme `{1}` (expected `http` or `https`)")]
139    UrlUnsupportedScheme(String, String),
140
141    /// The server redirected during an operation that must not follow
142    /// redirects.
143    #[error("WebDAV server redirected to `{0}` during a non-redirectable operation")]
144    UnexpectedRedirect(Url),
145
146    /// The client has no principal URL yet; `current_user_principal`
147    /// must run first.
148    #[error("WebDAV client missing principal URL; call `current_user_principal` first")]
149    MissingPrincipal,
150    /// The client has no calendar home-set yet; `calendar_home_set` must
151    /// run first.
152    #[error("WebDAV client missing calendar home-set; call `calendar_home_set` first")]
153    MissingCalendarHomeSet,
154    /// The client has no addressbook home-set yet; `addressbook_home_set`
155    /// must run first.
156    #[error("WebDAV client missing addressbook home-set; call `addressbook_home_set` first")]
157    MissingAddressbookHomeSet,
158}
159
160/// Std-blocking WebDAV client wrapping a single blocking stream.
161pub struct WebdavClientStd {
162    /// The active blocking stream. Public so higher-level crates can pump their
163    /// own [`WebdavCoroutine`]s through it (as io-jmap exposes its stream),
164    /// reusing this client's discovery cache.
165    pub stream: Box<dyn WebdavStream>,
166
167    auth: WebdavAuth,
168
169    /// Base URL prepended to every request path.
170    pub base_url: Url,
171
172    /// `User-Agent` header value.
173    pub user_agent: String,
174
175    /// Cached principal URL (RFC 5397).
176    pub principal_url: Option<Url>,
177
178    /// Cached CalDAV home-set URL (RFC 4791 §6.2.1).
179    pub calendar_home_set: Option<Url>,
180
181    /// Cached CardDAV home-set URL (RFC 6352 §7.1.1).
182    pub addressbook_home_set: Option<Url>,
183}
184
185impl fmt::Debug for WebdavClientStd {
186    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
187        f.debug_struct("WebdavClientStd")
188            .field("base_url", &self.base_url.as_str())
189            .field("user_agent", &self.user_agent)
190            .field(
191                "principal_url",
192                &self.principal_url.as_ref().map(Url::as_str),
193            )
194            .field(
195                "calendar_home_set",
196                &self.calendar_home_set.as_ref().map(Url::as_str),
197            )
198            .field(
199                "addressbook_home_set",
200                &self.addressbook_home_set.as_ref().map(Url::as_str),
201            )
202            .finish_non_exhaustive()
203    }
204}
205
206impl WebdavClientStd {
207    /// Builds a client around `stream`. The caller is responsible for
208    /// opening the connection (TCP, TLS handshake if needed).
209    pub fn new<S: Read + Write + Send + 'static>(
210        stream: S,
211        auth: WebdavAuth,
212        base_url: Url,
213    ) -> Self {
214        Self {
215            stream: Box::new(stream),
216            auth,
217            base_url,
218            user_agent: DEFAULT_USER_AGENT.to_string(),
219            principal_url: None,
220            calendar_home_set: None,
221            addressbook_home_set: None,
222        }
223    }
224
225    /// Builds a client from a pre-connected stream and the full
226    /// discovery state already in hand. Skips every discovery step.
227    pub fn from_parts<S: Read + Write + Send + 'static>(
228        stream: S,
229        auth: WebdavAuth,
230        base_url: Url,
231        principal_url: Option<Url>,
232        calendar_home_set: Option<Url>,
233        addressbook_home_set: Option<Url>,
234    ) -> Self {
235        Self {
236            stream: Box::new(stream),
237            auth,
238            base_url,
239            user_agent: DEFAULT_USER_AGENT.to_string(),
240            principal_url,
241            calendar_home_set,
242            addressbook_home_set,
243        }
244    }
245
246    /// Connects to `url`'s host and runs the TLS handshake when the
247    /// scheme is `https`. `http` goes through plain TCP. ALPN is set
248    /// to `http/1.1`.
249    #[cfg(any(
250        feature = "rustls-aws",
251        feature = "rustls-ring",
252        feature = "native-tls"
253    ))]
254    pub fn connect(url: &Url, tls: &Tls, auth: WebdavAuth) -> Result<Self, WebdavClientStdError> {
255        let host = url
256            .host_str()
257            .ok_or_else(|| WebdavClientStdError::UrlMissingHost(url.to_string()))?;
258
259        let stream = match url.scheme() {
260            "http" => StreamStd::connect_tcp(host, url.port().unwrap_or(80))?,
261            "https" => StreamStd::connect_tls(host, url.port().unwrap_or(443), tls)?,
262            scheme => {
263                return Err(WebdavClientStdError::UrlUnsupportedScheme(
264                    url.to_string(),
265                    scheme.to_string(),
266                ));
267            }
268        };
269
270        Ok(Self::new(stream, auth, url.clone()))
271    }
272
273    /// Replaces the underlying stream; useful when discovery surfaces a
274    /// new authority and the caller has to reconnect.
275    pub fn set_stream<S: Read + Write + Send + 'static>(&mut self, stream: S) {
276        self.stream = Box::new(stream);
277    }
278
279    /// Returns the active authentication scheme.
280    pub fn auth(&self) -> &WebdavAuth {
281        &self.auth
282    }
283
284    /// Runs any standard-shape coroutine (`Yield = WebdavYield`)
285    /// against the client stream until completion. Redirect-aware
286    /// discovery uses [`run_redirect`](Self::run_redirect) instead.
287    fn run<C, T, E>(&mut self, mut coroutine: C) -> Result<T, WebdavClientStdError>
288    where
289        C: WebdavCoroutine<Yield = WebdavYield, Return = Result<T, E>>,
290        E: Into<WebdavClientStdError>,
291    {
292        let mut buf = [0u8; READ_BUFFER_SIZE];
293        let mut arg: Option<&[u8]> = None;
294
295        let ret = loop {
296            match coroutine.resume(arg.take()) {
297                WebdavCoroutineState::Complete(ret) => break ret,
298                WebdavCoroutineState::Yielded(yielded) => {
299                    let n = pump(&mut *self.stream, &mut buf, yielded)?;
300                    arg = n.map(|n| &buf[..n]);
301                }
302            }
303        };
304
305        ret.map_err(Into::into)
306    }
307
308    /// Runs a redirect-aware discovery coroutine (`Yield =
309    /// WebdavRedirectYield`, `Return = Option<Url>`). A 3xx is surfaced
310    /// as [`UnexpectedRedirect`] rather than followed: this client owns
311    /// a single connected stream, so only the caller (who owns
312    /// connection creation) can reconnect to the target and retry, e.g.
313    /// via [`set_stream`] (mirrors io-http's `HttpClientStd`).
314    ///
315    /// [`UnexpectedRedirect`]: WebdavClientStdError::UnexpectedRedirect
316    /// [`set_stream`]: WebdavClientStd::set_stream
317    fn run_redirect(
318        &mut self,
319        coroutine: &mut dyn WebdavCoroutine<
320            Yield = WebdavRedirectYield,
321            Return = Result<Option<Url>, FollowRedirectsError>,
322        >,
323    ) -> Result<Option<Url>, WebdavClientStdError> {
324        let mut buf = [0u8; READ_BUFFER_SIZE];
325        let mut arg: Option<&[u8]> = None;
326
327        loop {
328            match coroutine.resume(arg.take()) {
329                WebdavCoroutineState::Complete(Ok(url)) => return Ok(url),
330                WebdavCoroutineState::Complete(Err(err)) => return Err(err.into()),
331                WebdavCoroutineState::Yielded(WebdavRedirectYield::WantsRead) => {
332                    let n = self.stream.read(&mut buf)?;
333                    arg = Some(&buf[..n]);
334                }
335                WebdavCoroutineState::Yielded(WebdavRedirectYield::WantsWrite(bytes)) => {
336                    self.stream.write_all(&bytes)?;
337                    arg = None;
338                }
339                WebdavCoroutineState::Yielded(WebdavRedirectYield::WantsRedirect {
340                    url, ..
341                }) => {
342                    return Err(WebdavClientStdError::UnexpectedRedirect(url));
343                }
344            }
345        }
346    }
347
348    // ---- Discovery (RFC 5397 + per-RFC home-set) ------------------------
349
350    /// Discovers the current user principal URL (RFC 5397) and caches
351    /// it in [`principal_url`]. Subsequent calls return the cached
352    /// value without hitting the network.
353    ///
354    /// [`principal_url`]: WebdavClientStd::principal_url
355    pub fn current_user_principal(&mut self) -> Result<Url, WebdavClientStdError> {
356        if let Some(url) = &self.principal_url {
357            return Ok(url.clone());
358        }
359
360        let mut coroutine = CurrentUserPrincipal::new(&self.base_url, &self.auth, &self.user_agent);
361        let url = self.run_redirect(&mut coroutine)?;
362        let url = url.ok_or(WebdavClientStdError::MissingPrincipal)?;
363
364        self.principal_url = Some(url.clone());
365        Ok(url)
366    }
367
368    // ---- CalDAV (RFC 4791) ----------------------------------------------
369
370    /// Discovers the CalDAV home-set URL (RFC 4791 §6.2.1) and caches
371    /// it in [`calendar_home_set`]. Resolves [`principal_url`] first
372    /// when it is not cached.
373    ///
374    /// [`calendar_home_set`]: WebdavClientStd::calendar_home_set
375    /// [`principal_url`]: WebdavClientStd::principal_url
376    pub fn calendar_home_set(&mut self) -> Result<Url, WebdavClientStdError> {
377        if let Some(url) = &self.calendar_home_set {
378            return Ok(url.clone());
379        }
380
381        let principal = self.current_user_principal()?;
382        let path = principal.path().to_string();
383
384        let mut coroutine =
385            CalendarHomeSet::new(&self.base_url, &self.auth, &self.user_agent, &path);
386        let url = self.run_redirect(&mut coroutine)?;
387        let url = url.ok_or(WebdavClientStdError::MissingCalendarHomeSet)?;
388
389        self.calendar_home_set = Some(url.clone());
390        Ok(url)
391    }
392
393    /// Lists every calendar under the cached
394    /// [`calendar_home_set`].
395    ///
396    /// [`calendar_home_set`]: WebdavClientStd::calendar_home_set
397    pub fn list_calendars(&mut self) -> Result<BTreeSet<Calendar>, WebdavClientStdError> {
398        let home = self
399            .calendar_home_set
400            .as_ref()
401            .ok_or(WebdavClientStdError::MissingCalendarHomeSet)?;
402        let path = home.path().to_string();
403
404        let coroutine = ListCalendars::new(&self.base_url, &self.auth, &self.user_agent, &path);
405        self.run(coroutine)
406    }
407
408    /// Creates a calendar collection under the cached
409    /// [`calendar_home_set`].
410    ///
411    /// [`calendar_home_set`]: WebdavClientStd::calendar_home_set
412    pub fn create_calendar(&mut self, calendar: &Calendar) -> Result<(), WebdavClientStdError> {
413        let home = self
414            .calendar_home_set
415            .as_ref()
416            .ok_or(WebdavClientStdError::MissingCalendarHomeSet)?;
417        let path = home.path().to_string();
418
419        let coroutine = CreateCalendar::new(
420            &self.base_url,
421            &self.auth,
422            &self.user_agent,
423            &path,
424            calendar,
425        );
426        self.run(coroutine).map(|_| ())
427    }
428
429    /// Updates a calendar collection's properties.
430    pub fn update_calendar(&mut self, calendar: &Calendar) -> Result<(), WebdavClientStdError> {
431        let home = self
432            .calendar_home_set
433            .as_ref()
434            .ok_or(WebdavClientStdError::MissingCalendarHomeSet)?;
435        let path = home.path().to_string();
436
437        let coroutine = UpdateCalendar::new(
438            &self.base_url,
439            &self.auth,
440            &self.user_agent,
441            &path,
442            calendar,
443        );
444        self.run(coroutine)
445    }
446
447    /// Deletes a calendar collection.
448    pub fn delete_calendar(&mut self, calendar_id: &str) -> Result<(), WebdavClientStdError> {
449        let home = self
450            .calendar_home_set
451            .as_ref()
452            .ok_or(WebdavClientStdError::MissingCalendarHomeSet)?;
453        let path = home.path().to_string();
454
455        let coroutine = DeleteCalendar::new(
456            &self.base_url,
457            &self.auth,
458            &self.user_agent,
459            &path,
460            calendar_id,
461        );
462        self.run(coroutine).map(|_| ())
463    }
464
465    /// Lists every iCalendar item inside `calendar_id`. `comp_filter`
466    /// is the optional VCALENDAR child filter (e.g.
467    /// `<C:comp-filter name=\"VEVENT\" />`); pass an empty string to
468    /// list every component type.
469    pub fn list_items(
470        &mut self,
471        calendar_id: &str,
472        comp_filter: &str,
473    ) -> Result<BTreeSet<ItemEntry>, WebdavClientStdError> {
474        let path = calendar_path(self.calendar_home_set.as_ref(), calendar_id)?;
475        let coroutine = ListItems::new(
476            &self.base_url,
477            &self.auth,
478            &self.user_agent,
479            &path,
480            comp_filter,
481        );
482        self.run(coroutine)
483    }
484
485    /// Reads a single calendar item's raw iCalendar bytes plus its
486    /// ETag.
487    pub fn read_item(
488        &mut self,
489        calendar_id: &str,
490        item_id: &str,
491    ) -> Result<ItemBody, WebdavClientStdError> {
492        let path = calendar_path(self.calendar_home_set.as_ref(), calendar_id)?;
493        let coroutine = ReadItem::new(&self.base_url, &self.auth, &self.user_agent, &path, item_id);
494        self.run(coroutine)
495    }
496
497    /// Creates a calendar item by id.
498    pub fn create_item(
499        &mut self,
500        calendar_id: &str,
501        id: &str,
502        ical: Vec<u8>,
503    ) -> Result<CreateItemOk, WebdavClientStdError> {
504        let path = calendar_path(self.calendar_home_set.as_ref(), calendar_id)?;
505        let coroutine = CreateItem::new(
506            &self.base_url,
507            &self.auth,
508            &self.user_agent,
509            &path,
510            id,
511            ical,
512        );
513        self.run(coroutine)
514    }
515
516    /// Updates an existing calendar item.
517    pub fn update_item(
518        &mut self,
519        calendar_id: &str,
520        id: &str,
521        ical: Vec<u8>,
522        if_match: Option<&str>,
523    ) -> Result<UpdateItemOk, WebdavClientStdError> {
524        let path = calendar_path(self.calendar_home_set.as_ref(), calendar_id)?;
525        let coroutine = UpdateItem::new(
526            &self.base_url,
527            &self.auth,
528            &self.user_agent,
529            &path,
530            id,
531            ical,
532            if_match,
533        );
534        self.run(coroutine)
535    }
536
537    /// Deletes a calendar item.
538    pub fn delete_item(
539        &mut self,
540        calendar_id: &str,
541        item_id: &str,
542        if_match: Option<&str>,
543    ) -> Result<(), WebdavClientStdError> {
544        let path = calendar_path(self.calendar_home_set.as_ref(), calendar_id)?;
545        let coroutine = DeleteItem::new(
546            &self.base_url,
547            &self.auth,
548            &self.user_agent,
549            &path,
550            item_id,
551            if_match,
552        );
553        self.run(coroutine).map(|_| ())
554    }
555
556    // ---- CardDAV (RFC 6352) ---------------------------------------------
557
558    /// Discovers the CardDAV home-set URL (RFC 6352 §7.1.1) and caches
559    /// it in [`addressbook_home_set`].
560    ///
561    /// [`addressbook_home_set`]: WebdavClientStd::addressbook_home_set
562    pub fn addressbook_home_set(&mut self) -> Result<Url, WebdavClientStdError> {
563        if let Some(url) = &self.addressbook_home_set {
564            return Ok(url.clone());
565        }
566
567        let principal = self.current_user_principal()?;
568        let path = principal.path().to_string();
569
570        let mut coroutine =
571            AddressbookHomeSet::new(&self.base_url, &self.auth, &self.user_agent, &path);
572        let url = self.run_redirect(&mut coroutine)?;
573        let url = url.ok_or(WebdavClientStdError::MissingAddressbookHomeSet)?;
574
575        self.addressbook_home_set = Some(url.clone());
576        Ok(url)
577    }
578
579    /// Lists every addressbook under the cached
580    /// [`addressbook_home_set`].
581    ///
582    /// [`addressbook_home_set`]: WebdavClientStd::addressbook_home_set
583    pub fn list_addressbooks(&mut self) -> Result<BTreeSet<Addressbook>, WebdavClientStdError> {
584        let home = self
585            .addressbook_home_set
586            .as_ref()
587            .ok_or(WebdavClientStdError::MissingAddressbookHomeSet)?;
588        let path = home.path().to_string();
589
590        let coroutine = ListAddressbooks::new(&self.base_url, &self.auth, &self.user_agent, &path);
591        self.run(coroutine)
592    }
593
594    /// Creates an addressbook collection under the cached
595    /// [`addressbook_home_set`].
596    ///
597    /// [`addressbook_home_set`]: WebdavClientStd::addressbook_home_set
598    pub fn create_addressbook(
599        &mut self,
600        addressbook: &Addressbook,
601    ) -> Result<(), WebdavClientStdError> {
602        let home = self
603            .addressbook_home_set
604            .as_ref()
605            .ok_or(WebdavClientStdError::MissingAddressbookHomeSet)?;
606        let path = home.path().to_string();
607
608        let coroutine = CreateAddressbook::new(
609            &self.base_url,
610            &self.auth,
611            &self.user_agent,
612            &path,
613            addressbook,
614        );
615        self.run(coroutine).map(|_| ())
616    }
617
618    /// Updates an addressbook collection's properties.
619    pub fn update_addressbook(
620        &mut self,
621        addressbook: &Addressbook,
622    ) -> Result<(), WebdavClientStdError> {
623        let home = self
624            .addressbook_home_set
625            .as_ref()
626            .ok_or(WebdavClientStdError::MissingAddressbookHomeSet)?;
627        let path = home.path().to_string();
628
629        let coroutine = UpdateAddressbook::new(
630            &self.base_url,
631            &self.auth,
632            &self.user_agent,
633            &path,
634            addressbook,
635        );
636        self.run(coroutine)
637    }
638
639    /// Deletes an addressbook collection.
640    pub fn delete_addressbook(&mut self, addressbook_id: &str) -> Result<(), WebdavClientStdError> {
641        let home = self
642            .addressbook_home_set
643            .as_ref()
644            .ok_or(WebdavClientStdError::MissingAddressbookHomeSet)?;
645        let path = home.path().to_string();
646
647        let coroutine = DeleteAddressbook::new(
648            &self.base_url,
649            &self.auth,
650            &self.user_agent,
651            &path,
652            addressbook_id,
653        );
654        self.run(coroutine).map(|_| ())
655    }
656
657    /// Lists every card inside `addressbook_id`.
658    pub fn list_cards(
659        &mut self,
660        addressbook_id: &str,
661    ) -> Result<BTreeSet<CardEntry>, WebdavClientStdError> {
662        let path = addressbook_path(self.addressbook_home_set.as_ref(), addressbook_id)?;
663        let coroutine = ListCards::new(&self.base_url, &self.auth, &self.user_agent, &path);
664        self.run(coroutine)
665    }
666
667    /// Enumerates card references (id plus ETag, no bodies) inside
668    /// `addressbook_id`.
669    pub fn enum_cards(
670        &mut self,
671        addressbook_id: &str,
672    ) -> Result<BTreeSet<CardRef>, WebdavClientStdError> {
673        let path = addressbook_path(self.addressbook_home_set.as_ref(), addressbook_id)?;
674        let coroutine = EnumCards::new(&self.base_url, &self.auth, &self.user_agent, &path);
675        self.run(coroutine)
676    }
677
678    /// Batch-fetches cards by id inside `addressbook_id` in a single
679    /// round-trip.
680    pub fn multiget_cards(
681        &mut self,
682        addressbook_id: &str,
683        ids: &[&str],
684    ) -> Result<Vec<CardEntry>, WebdavClientStdError> {
685        let path = addressbook_path(self.addressbook_home_set.as_ref(), addressbook_id)?;
686        let coroutine =
687            MultigetCards::new(&self.base_url, &self.auth, &self.user_agent, &path, ids);
688        self.run(coroutine)
689    }
690
691    /// Runs an incremental `sync-collection` REPORT (RFC 6578) against
692    /// `addressbook_id`, requesting ETags only. Pass [`None`] as
693    /// `sync_token` for an initial sync.
694    pub fn sync_cards(
695        &mut self,
696        addressbook_id: &str,
697        sync_token: Option<&str>,
698    ) -> Result<SyncDelta, WebdavClientStdError> {
699        let path = addressbook_path(self.addressbook_home_set.as_ref(), addressbook_id)?;
700        let coroutine = SyncCollection::new(
701            &self.base_url,
702            &self.auth,
703            &self.user_agent,
704            &path,
705            sync_token,
706            &[GETETAG],
707        );
708        self.run(coroutine)
709    }
710
711    /// Reads a single card's raw vCard bytes plus its ETag.
712    pub fn read_card(
713        &mut self,
714        addressbook_id: &str,
715        card_id: &str,
716    ) -> Result<CardBody, WebdavClientStdError> {
717        let path = addressbook_path(self.addressbook_home_set.as_ref(), addressbook_id)?;
718        let coroutine = ReadCard::new(&self.base_url, &self.auth, &self.user_agent, &path, card_id);
719        self.run(coroutine)
720    }
721
722    /// Creates a card by id.
723    pub fn create_card(
724        &mut self,
725        addressbook_id: &str,
726        id: &str,
727        vcard: Vec<u8>,
728    ) -> Result<CreateCardOk, WebdavClientStdError> {
729        let path = addressbook_path(self.addressbook_home_set.as_ref(), addressbook_id)?;
730        let coroutine = CreateCard::new(
731            &self.base_url,
732            &self.auth,
733            &self.user_agent,
734            &path,
735            id,
736            vcard,
737        );
738        self.run(coroutine)
739    }
740
741    /// Updates an existing card.
742    pub fn update_card(
743        &mut self,
744        addressbook_id: &str,
745        id: &str,
746        vcard: Vec<u8>,
747        if_match: Option<&str>,
748    ) -> Result<UpdateCardOk, WebdavClientStdError> {
749        let path = addressbook_path(self.addressbook_home_set.as_ref(), addressbook_id)?;
750        let coroutine = UpdateCard::new(
751            &self.base_url,
752            &self.auth,
753            &self.user_agent,
754            &path,
755            id,
756            vcard,
757            if_match,
758        );
759        self.run(coroutine)
760    }
761
762    /// Deletes a card.
763    pub fn delete_card(
764        &mut self,
765        addressbook_id: &str,
766        card_id: &str,
767        if_match: Option<&str>,
768    ) -> Result<(), WebdavClientStdError> {
769        let path = addressbook_path(self.addressbook_home_set.as_ref(), addressbook_id)?;
770        let coroutine = DeleteCard::new(
771            &self.base_url,
772            &self.auth,
773            &self.user_agent,
774            &path,
775            card_id,
776            if_match,
777        );
778        self.run(coroutine).map(|_| ())
779    }
780}
781
782/// Runs one standard I/O yield against the stream: writes the bytes, or
783/// reads a chunk into `buf` and returns its length.
784fn pump(
785    stream: &mut dyn WebdavStream,
786    buf: &mut [u8],
787    yielded: WebdavYield,
788) -> Result<Option<usize>, io::Error> {
789    match yielded {
790        WebdavYield::WantsRead => Ok(Some(stream.read(buf)?)),
791        WebdavYield::WantsWrite(bytes) => {
792            stream.write_all(&bytes)?;
793            Ok(None)
794        }
795    }
796}
797
798fn calendar_path(home: Option<&Url>, calendar_id: &str) -> Result<String, WebdavClientStdError> {
799    let home = home.ok_or(WebdavClientStdError::MissingCalendarHomeSet)?;
800    let base = home.path().trim_end_matches('/');
801    let id = calendar_id.trim_matches('/');
802    Ok(format!("{base}/{id}"))
803}
804
805fn addressbook_path(
806    home: Option<&Url>,
807    addressbook_id: &str,
808) -> Result<String, WebdavClientStdError> {
809    let home = home.ok_or(WebdavClientStdError::MissingAddressbookHomeSet)?;
810    let base = home.path().trim_end_matches('/');
811    let id = addressbook_id.trim_matches('/');
812    Ok(format!("{base}/{id}"))
813}
814
815/// Marker for everything the client can run against; auto-implemented
816/// for any blocking `Read + Write + Send` impl.
817pub trait WebdavStream: Read + Write + Send {}
818impl<T: Read + Write + Send + ?Sized> WebdavStream for T {}