diff --git a/Cargo.lock b/Cargo.lock index f93be36..6018f0c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2724,6 +2724,7 @@ dependencies = [ "derive_more", "futures-util", "http", + "ical", "percent-encoding", "quick-xml", "rustical_dav", diff --git a/crates/carddav/Cargo.toml b/crates/carddav/Cargo.toml index c899639..df7351e 100644 --- a/crates/carddav/Cargo.toml +++ b/crates/carddav/Cargo.toml @@ -30,3 +30,4 @@ rustical_ical.workspace = true http.workspace = true tower-http.workspace = true percent-encoding.workspace = true +ical.workspace = true diff --git a/crates/carddav/src/addressbook/methods/mod.rs b/crates/carddav/src/addressbook/methods/mod.rs index 3aa53ec..bcefbc9 100644 --- a/crates/carddav/src/addressbook/methods/mod.rs +++ b/crates/carddav/src/addressbook/methods/mod.rs @@ -1,4 +1,5 @@ pub mod mkcol; // pub mod post; pub mod get; +pub mod put; pub mod report; diff --git a/crates/carddav/src/addressbook/methods/put.rs b/crates/carddav/src/addressbook/methods/put.rs new file mode 100644 index 0000000..b632fcc --- /dev/null +++ b/crates/carddav/src/addressbook/methods/put.rs @@ -0,0 +1,47 @@ +use crate::Error; +use crate::addressbook::AddressbookResourceService; +use axum::response::IntoResponse; +use axum::{ + extract::{Path, State}, + response::Response, +}; +use http::StatusCode; +use ical::VcardParser; +use rustical_ical::AddressObject; +use rustical_store::Addressbook; +use rustical_store::{AddressbookStore, SubscriptionStore, auth::User}; +use tracing::instrument; + +#[instrument(skip(addr_store))] +pub async fn route_put( + Path((principal, addressbook_id)): Path<(String, String)>, + State(AddressbookResourceService { addr_store, .. }): State>, + user: User, + body: String, +) -> Result { + if !user.is_principal(&principal) { + return Err(Error::Unauthorized); + } + + let mut objects = vec![]; + for object in VcardParser::new(body.as_bytes()) { + let object = object.map_err(rustical_ical::Error::from)?; + objects.push(AddressObject::try_from(object)?); + } + + let addressbook = Addressbook { + id: addressbook_id.clone(), + principal: principal.clone(), + displayname: None, + description: None, + deleted_at: None, + synctoken: Default::default(), + push_topic: uuid::Uuid::new_v4().to_string(), + }; + + addr_store + .import_addressbook(principal.clone(), addressbook, objects) + .await?; + + Ok(StatusCode::CREATED.into_response()) +} diff --git a/crates/carddav/src/addressbook/service.rs b/crates/carddav/src/addressbook/service.rs index 6901c60..f158f81 100644 --- a/crates/carddav/src/addressbook/service.rs +++ b/crates/carddav/src/addressbook/service.rs @@ -3,6 +3,7 @@ use super::methods::report::route_report_addressbook; use crate::address_object::AddressObjectResourceService; use crate::address_object::resource::AddressObjectResource; use crate::addressbook::methods::get::route_get; +use crate::addressbook::methods::put::route_put; use crate::addressbook::resource::AddressbookResource; use crate::{CardDavPrincipalUri, Error}; use async_trait::async_trait; @@ -129,6 +130,13 @@ impl AxumMethods for AddressbookReso }) } + fn put() -> Option BoxFuture<'static, Result>> { + Some(|state, req| { + let mut service = Handler::with_state(route_put::, state); + Box::pin(Service::call(&mut service, req)) + }) + } + fn mkcol() -> Option BoxFuture<'static, Result>> { Some(|state, req| { let mut service = Handler::with_state(route_mkcol::, state); diff --git a/crates/ical/src/address_object.rs b/crates/ical/src/address_object.rs index 77e6c0a..0407ade 100644 --- a/crates/ical/src/address_object.rs +++ b/crates/ical/src/address_object.rs @@ -1,6 +1,7 @@ use crate::{CalDateTime, LOCAL_DATE}; use crate::{CalendarObject, Error}; use chrono::Datelike; +use ical::generator::Emitter; use ical::parser::{ Component, vcard::{self, component::VcardContact}, @@ -15,6 +16,21 @@ pub struct AddressObject { vcard: VcardContact, } +impl TryFrom for AddressObject { + type Error = Error; + + fn try_from(vcard: VcardContact) -> Result { + let id = vcard + .get_property("UID") + .ok_or(Error::InvalidData("Missing UID".to_owned()))? + .value + .clone() + .ok_or(Error::InvalidData("Missing UID".to_owned()))?; + let vcf = vcard.generate(); + Ok(Self { id, vcf, vcard }) + } +} + impl AddressObject { pub fn from_vcf(object_id: String, vcf: String) -> Result { let mut parser = vcard::VcardParser::new(BufReader::new(vcf.as_bytes())); diff --git a/crates/store/src/addressbook_store.rs b/crates/store/src/addressbook_store.rs index 0d936dd..5453703 100644 --- a/crates/store/src/addressbook_store.rs +++ b/crates/store/src/addressbook_store.rs @@ -67,4 +67,11 @@ pub trait AddressbookStore: Send + Sync + 'static { addressbook_id: &str, object_id: &str, ) -> Result<(), Error>; + + async fn import_addressbook( + &self, + principal: String, + addressbook: Addressbook, + objects: Vec, + ) -> Result<(), Error>; } diff --git a/crates/store_sqlite/src/addressbook_store.rs b/crates/store_sqlite/src/addressbook_store.rs index 0645420..f926dcb 100644 --- a/crates/store_sqlite/src/addressbook_store.rs +++ b/crates/store_sqlite/src/addressbook_store.rs @@ -584,6 +584,33 @@ impl AddressbookStore for SqliteAddressbookStore { Ok(()) } + + #[instrument(skip(objects))] + async fn import_addressbook( + &self, + principal: String, + addressbook: Addressbook, + objects: Vec, + ) -> Result<(), Error> { + let mut tx = self.db.begin().await.map_err(crate::Error::from)?; + + let addressbook_id = addressbook.id.clone(); + Self::_insert_addressbook(&mut *tx, addressbook).await?; + + for object in objects { + Self::_put_object( + &mut *tx, + principal.clone(), + addressbook_id.clone(), + object, + false, + ) + .await?; + } + + tx.commit().await.map_err(crate::Error::from)?; + Ok(()) + } } // Logs an operation to an address object