libdav/carddav.rs
1// Copyright 2023-2024 Hugo Osvaldo Barrera
2//
3// SPDX-License-Identifier: ISC
4
5use std::ops::Deref;
6
7use http::Response;
8use hyper::Uri;
9use hyper::body::Incoming;
10use tower_service::Service;
11
12use crate::common::{ServiceForUrlError, check_support, parse_find_multiple_collections};
13use crate::dav::WebDavClient;
14use crate::dav::{FoundCollection, WebDavError, check_status};
15use crate::sd::{BootstrapError, DiscoverableService, find_context_url};
16use crate::xmlutils::escape_xml_entities;
17use crate::{CheckSupportError, FetchedResource};
18use crate::{Depth, FindHomeSetError, names};
19
20/// Client to communicate with a CardDAV server.
21///
22/// Instances are usually created via [`CardDavClient::new`]:
23///
24/// ```rust
25/// # use libdav::CardDavClient;
26/// # use libdav::dav::WebDavClient;
27/// use http::Uri;
28/// use hyper_rustls::HttpsConnectorBuilder;
29/// use hyper_util::{client::legacy::Client, rt::TokioExecutor};
30/// use tower_http::auth::AddAuthorization;
31///
32/// # tokio::runtime::Builder::new_current_thread().build().unwrap().block_on(async {
33/// let uri = Uri::try_from("https://example.com").unwrap();
34///
35/// let https_connector = HttpsConnectorBuilder::new()
36/// .with_native_roots()
37/// .unwrap()
38/// .https_or_http()
39/// .enable_http1()
40/// .build();
41/// let https_client = Client::builder(TokioExecutor::new()).build(https_connector);
42/// let https_client = AddAuthorization::basic(https_client, "user", "secret");
43/// let webdav = WebDavClient::new(uri, https_client);
44/// let client = CardDavClient::new(webdav);
45/// # })
46/// ```
47///
48/// If the real CardDAV server needs to be resolved via automated service discovery, use
49/// [`CardDavClient::bootstrap_via_service_discovery`].
50///
51/// For setting the `Authorization` header or applying other changes to outgoing requests, see the
52/// documentation on [`WebDavClient`].
53#[derive(Debug)]
54pub struct CardDavClient<C>
55where
56 C: Service<http::Request<String>, Response = Response<Incoming>> + Sync + Send + 'static,
57{
58 /// A WebDAV client used to send requests.
59 pub webdav_client: WebDavClient<C>,
60}
61
62impl<C> Deref for CardDavClient<C>
63where
64 C: Service<http::Request<String>, Response = Response<Incoming>> + Sync + Send,
65{
66 type Target = WebDavClient<C>;
67
68 fn deref(&self) -> &Self::Target {
69 &self.webdav_client
70 }
71}
72
73impl<C> CardDavClient<C>
74where
75 C: Service<http::Request<String>, Response = Response<Incoming>> + Sync + Send,
76 <C as Service<http::Request<String>>>::Error: std::error::Error + Send + Sync,
77{
78 /// Create a new client instance.
79 ///
80 /// # See also
81 ///
82 /// [`CardDavClient::bootstrap_via_service_discovery`].
83 pub fn new(webdav_client: WebDavClient<C>) -> CardDavClient<C> {
84 CardDavClient { webdav_client }
85 }
86
87 /// Automatically bootstrap a new client instance.
88 ///
89 /// Creates a new client, with its `base_url` set to the context path retrieved using service
90 /// discovery via [`find_context_url`].
91 ///
92 /// # Errors
93 ///
94 /// Returns an error if:
95 ///
96 /// - The URL has an invalid schema.
97 /// - The underlying call to [`find_context_url`] returns an error.
98 pub async fn bootstrap_via_service_discovery(
99 mut webdav_client: WebDavClient<C>,
100 ) -> Result<CardDavClient<C>, BootstrapError> {
101 let service = service_for_url(&webdav_client.base_url)?;
102 match find_context_url(&webdav_client, service).await {
103 crate::sd::FindContextUrlResult::BaseUrl => {}
104 crate::sd::FindContextUrlResult::Found(url) => webdav_client.base_url = url,
105 crate::sd::FindContextUrlResult::NoneFound => return Err(BootstrapError::NoUsableUrl),
106 crate::sd::FindContextUrlResult::Error(err) => return Err(err.into()),
107 }
108 Ok(CardDavClient { webdav_client })
109 }
110
111 /// Queries the server for the address book home set.
112 ///
113 /// See: <https://www.rfc-editor.org/rfc/rfc4791#section-6.2.1>
114 ///
115 /// # Errors
116 ///
117 /// If there are any network errors or the response could not be parsed.
118 pub async fn find_address_book_home_set(
119 &self,
120 principal: &Uri,
121 ) -> Result<Vec<Uri>, FindHomeSetError> {
122 // If obtaining a principal fails, the specification says we should query the user. This
123 // tries to use the `base_url` first, since the user might have provided it for a reason.
124 self.find_home_sets(principal, &names::ADDRESSBOOK_HOME_SET)
125 .await
126 .map_err(FindHomeSetError)
127 }
128
129 /// Find address book collections under the given `url`.
130 ///
131 /// If `url` is not specified, this client's current `base_url` shall be used instead. When
132 /// using a client bootstrapped via automatic discovery, passing `None` will usually yield the
133 /// expected results.
134 ///
135 /// # Errors
136 ///
137 /// If the HTTP call fails or parsing the XML response fails.
138 pub async fn find_addressbooks(
139 &self,
140 address_book_home_set: &Uri,
141 ) -> Result<Vec<FoundCollection>, WebDavError> {
142 let props = [
143 &names::RESOURCETYPE,
144 &names::GETETAG,
145 &names::SUPPORTED_REPORT_SET,
146 ];
147 let (head, body) = self
148 .propfind(address_book_home_set, &props, Depth::One)
149 .await?;
150 check_status(head.status)?;
151
152 parse_find_multiple_collections(&body, &names::ADDRESSBOOK)
153 }
154
155 /// Fetches existing vcard resources.
156 ///
157 /// If the `getetag` property is missing for an item, it will be reported as
158 /// [`http::StatusCode::NOT_FOUND`]. This should not be an actual issue with in practice, since
159 /// support for `getetag` is mandatory for CalDAV implementations.
160 ///
161 /// # Errors
162 ///
163 /// If there are any network errors or the response could not be parsed.
164 pub async fn get_address_book_resources(
165 &self,
166 addressbook_href: &str,
167 hrefs: impl IntoIterator<Item = impl AsRef<str>>,
168 ) -> Result<Vec<FetchedResource>, WebDavError> {
169 let mut body = String::from(
170 r#"
171 <C:addressbook-multiget xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:carddav">
172 <D:prop>
173 <D:getetag/>
174 <C:address-data/>
175 </D:prop>"#,
176 );
177 for href in hrefs {
178 body.push_str("<D:href>");
179 body.push_str(&escape_xml_entities(href.as_ref()));
180 body.push_str("</D:href>");
181 }
182 body.push_str("</C:addressbook-multiget>");
183
184 self.multi_get(addressbook_href.as_ref(), body, &names::ADDRESS_DATA)
185 .await
186 }
187
188 /// Checks that the given URI advertises carddav support.
189 ///
190 /// See: <https://www.rfc-editor.org/rfc/rfc6352#section-6.1>
191 ///
192 /// # Known Issues
193 ///
194 /// - Nextcloud's implementation seems broken. [Bug report][nextcloud].
195 ///
196 /// [nextcloud]: https://github.com/nextcloud/server/issues/37374
197 ///
198 /// # Errors
199 ///
200 /// If there are any network issues or if the server does not explicitly advertise carddav
201 /// support.
202 pub async fn check_support(&self, url: &Uri) -> Result<(), CheckSupportError> {
203 check_support(&self.webdav_client, url, "addressbook").await
204 }
205
206 /// Create an address book collection.
207 ///
208 /// # Errors
209 ///
210 /// Returns an error in case of network errors or if the server returns a failure status code.
211 pub async fn create_addressbook(&self, href: &str) -> Result<(), WebDavError> {
212 self.webdav_client
213 .create_collection(href, &[&names::ADDRESSBOOK])
214 .await
215 }
216}
217
218impl<C> Clone for CardDavClient<C>
219where
220 C: Service<http::Request<String>, Response = Response<Incoming>> + Sync + Send + Clone,
221{
222 fn clone(&self) -> CardDavClient<C> {
223 CardDavClient {
224 webdav_client: self.webdav_client.clone(),
225 }
226 }
227}
228
229/// Return the service type based on a URL's scheme.
230///
231/// # Errors
232///
233/// If `url` is missing a scheme or has a scheme invalid for CardDAV usage.
234pub fn service_for_url(url: &Uri) -> Result<DiscoverableService, ServiceForUrlError> {
235 match url
236 .scheme()
237 .ok_or(ServiceForUrlError::MissingScheme)?
238 .as_ref()
239 {
240 "https" | "carddavs" => Ok(DiscoverableService::CardDavs),
241 "http" | "carddav" => Ok(DiscoverableService::CardDav),
242 _ => Err(ServiceForUrlError::UnknownScheme),
243 }
244}