Compare commits

...

99 Commits

Author SHA1 Message Date
Lennart
b6ef2b4c05 Update documentation given the changes to memberships 2025-06-12 20:59:50 +02:00
Lennart
32bc8c707d Add group-membership to both caldav and carddav and fix addressbook-home-set for shared principals 2025-06-12 20:55:22 +02:00
Lennart
1757bbee13 carddav: Remove members from addressbook-home-set 2025-06-12 20:12:17 +02:00
Lennart
4dbc316e64 Remove member principals from calendar-home-set 2025-06-12 20:10:14 +02:00
Lennart
4705170dbc Update .sqlx files 2025-06-12 20:05:51 +02:00
Lennart
0e2f08d7f2 caldav: Add some access control-related properties and advertise calendar-proxy 2025-06-12 19:51:02 +02:00
Lennart
feb8b3ff09 Add member search to user store 2025-06-12 19:50:32 +02:00
Lennart
41d5c72e4e Fix and simplify support-report-set 2025-06-12 17:39:42 +02:00
Lennart
89adbcf13f xml: Fix default namespace prefixing for enum variants 2025-06-12 17:38:56 +02:00
Lennart
5a3a2c0909 Fix TagList not writing the <prop> wrapper 2025-06-12 16:18:33 +02:00
Lennart
3e8fffa316 Fix xml PropName such that the rename attribute also propagates to the prop name 2025-06-12 16:07:32 +02:00
Lennart
40e7bc0f66 Fix tests 2025-06-12 15:33:49 +02:00
Lennart
f857d68760 principal: Implement principal-collection-set 2025-06-12 15:31:34 +02:00
Lennart
9e5eaa5e1c Fix bug where principal collections would return information about the requesting user instead of the principal resource 2025-06-12 15:23:02 +02:00
Lennart
7c73223877 dav: Implement some principal props for WebDAV ACL 2025-06-12 15:00:54 +02:00
Lennart K
0c1c04d1cd dav: Move displayname to common properties 2025-06-12 14:39:16 +02:00
Lennart
72961f44e0 Update Docker workflow to hopefully tag releases 2025-06-11 22:09:29 +02:00
Lennart
49ac6abf35 Update .gitattributes 2025-06-11 21:45:06 +02:00
Lennart
c855e3d6b6 Random preparation for release 2025-06-11 21:35:46 +02:00
Lennart
6ecdc6125e Iterate on documentation 2025-06-11 21:13:06 +02:00
Lennart
4eb35d6c0d caldav: Merge calendar store and birthday store into combined store 2025-06-11 19:57:04 +02:00
Lennart
bd0684dcbc Implement workaround to allow GNOME Accounts setup 2025-06-11 15:37:59 +02:00
Lennart
dac49f853a Update .sqlx files 2025-06-11 00:58:49 +02:00
Lennart
f1c61ecefa Fix insert_calendar: subscription_url not saved 2025-06-11 00:55:13 +02:00
Lennart
a20e9800bd Implement PUT method for addressbook import 2025-06-10 23:43:53 +02:00
Lennart
80cca7b7b2 Adds a licenses page to list licenses of packages used
Implements #65
2025-06-10 22:59:16 +02:00
Lennart
f04987a171 Remove some garbage code 2025-06-10 18:01:20 +02:00
Lennart
3eeef18a14 reccurence expansion: Match datetime types 2025-06-10 17:56:56 +02:00
Lennart
32225bdda8 Implement nonfunctional COPY and MOVE method
Fixes #69 for now
2025-06-10 17:42:03 +02:00
Lennart
103ac0b1f9 Implement download feature for calendars and addressbooks
Fixes #70
2025-06-10 17:23:11 +02:00
Lennart
300a0024ee Fix rrule expansion test 2025-06-10 16:14:31 +02:00
Lennart K
0dbc05345b caldav: Support MKCOL method 2025-06-10 11:43:39 +02:00
Lennart
b5f23b0f9b Resolve rrule issue 2025-06-09 23:23:41 +02:00
Lennart
5ee789bec1 RRULE expansion: Fix timezone 2025-06-09 23:14:25 +02:00
Lennart
49aab931d0 RRULE: Fix DTEND 2025-06-09 23:06:04 +02:00
Lennart
7628cdafbd Fix bug with missing trailing slash in propfind response 2025-06-09 22:36:11 +02:00
Lennart
6d6f8f20df Make sure collections have trailing slashes (py-caldav is very pedantic about that) 2025-06-09 22:23:01 +02:00
Lennart
fc590976bc Set default log level to INFO 2025-06-09 21:47:46 +02:00
Lennart
71c2f8c019 Move properties into separate files 2025-06-09 21:09:46 +02:00
Lennart
0595920809 dav: Make the get_members function more ergonomic 2025-06-09 20:35:25 +02:00
Lennart
0feaaaaca1 Add user agent to request log 2025-06-09 19:55:39 +02:00
Lennart
e000165555 Improve logging 2025-06-09 19:04:08 +02:00
Lennart
487e99216a Comment out use of webdav-push properties 2025-06-09 18:42:32 +02:00
Lennart
38dcf88f24 Stop advertising webdav push while it is not working 2025-06-09 18:39:46 +02:00
Lennart
2ce0c00f89 tracing: Update default opentelemetry log leve 2025-06-09 17:57:35 +02:00
Lennart
38de0ab268 Make sure that tracing catches all panics and shows errors better 2025-06-09 17:50:01 +02:00
Lennart
9dd5995950 Move session middleware outside such that we can access webdav endpoints from the frontend 2025-06-09 17:29:33 +02:00
Lennart
2ba0beeafc routing changes 2025-06-09 17:19:25 +02:00
Lennart
8f29a468db Improve routing 2025-06-09 16:30:14 +02:00
Lennart
764d049d3c Format Cargo.toml 2025-06-09 16:01:19 +02:00
Lennart
720e6f6115 Docker: revert to 1.86 2025-06-08 23:30:42 +02:00
Lennart
d5b43b33f4 Fix well-known carddav redirection 2025-06-08 23:08:44 +02:00
Lennart
6ae2276035 frontend: Add redirection to DAVx5 activity 2025-06-08 23:02:26 +02:00
Lennart
152bf374d7 Fix Dockerfile 2025-06-08 22:30:06 +02:00
Lennart
61f14ca072 Docker: Set default storage location and update Rust to 1.87 2025-06-08 22:22:37 +02:00
Lennart
6bcad7cc65 frontend: Add deletion buttons 2025-06-08 22:15:49 +02:00
Lennart
e58973d366 frontend: Add form to create addressbook 2025-06-08 21:54:03 +02:00
Lennart
573781310a Minor frontend improvements, feature to create calendar 2025-06-08 21:46:20 +02:00
Lennart
bbe9113f5c minor stuff 2025-06-08 20:23:53 +02:00
Lennart
ac1dbb29d8 small refactoring 2025-06-08 20:04:46 +02:00
Lennart
1d25d6cc70 Update rand to 0.9 2025-06-08 19:56:48 +02:00
Lennart
c05c330601 Update Cargo.toml 2025-06-08 19:40:40 +02:00
Lennart
00eb43f048 Implement almost all previous features 2025-06-08 19:38:33 +02:00
Lennart
95889e3df1 Checkpoint: Migration to axum 2025-06-08 14:10:12 +02:00
Lennart
790c657b08 Work on axum support 2025-06-07 20:17:50 +02:00
Lennart
57832116aa Update opentelemetry dependency 2025-06-04 20:37:25 +02:00
Lennart
0c6aef7c06 caldav: Remove calendar-no-timezone 2025-06-04 20:21:36 +02:00
Lennart
22ed278dbb TagList: Correctly write namespace 2025-06-04 20:12:47 +02:00
Lennart
1a827a164f WIP: Start implementing precondition errors 2025-06-04 20:03:30 +02:00
Lennart
e57a14cad1 WIP: Complete work of propfind parsing 2025-06-04 18:11:25 +02:00
Lennart
5ad6ee2e99 expand_recurrence remove all recurrence properties 2025-06-03 23:20:02 +02:00
Lennart
c14f98a432 slight report refactoring 2025-06-03 23:06:00 +02:00
Lennart
7f3ce01c2b Move ical-related stuff to rustical_ical crate 2025-06-03 18:15:26 +02:00
Lennart
5a6ffd3c19 some preparation for reccurence expansion 2025-06-03 17:48:07 +02:00
Lennart
cf3e213894 Comment out some code snippets that might break things at the moment 2025-06-02 22:36:40 +02:00
Lennart
13128a5caa Make tracing-actix-web optional too 2025-06-02 22:00:36 +02:00
Lennart
9836a696ad rustical_dav: Make actix-web a completely optional dependency 2025-06-02 21:58:46 +02:00
Lennart
05ff2536f6 Some work on making the dav crate framework-agnostic 2025-06-02 21:35:22 +02:00
Lennart
bcc6bef848 Fix bug 2025-06-02 20:26:34 +02:00
Lennart
088b920b68 WIP: Janky recurrence rule evaluation 2025-06-02 20:19:55 +02:00
Lennart
3c9c1c7abf slightly more refactoring 2025-06-02 20:18:59 +02:00
Lennart
b7c24fe2f0 Lots of refactoring around routing 2025-06-02 19:41:30 +02:00
Lennart
08c4bd4289 propfind: Use HashSet to prevent duplicate prop 2025-06-02 18:27:18 +02:00
Lennart K
ef33868151 Refactoring around routing and getting the principal uri (less dependence on actix) 2025-06-02 16:17:28 +02:00
Lennart
0f294cf2e1 Datetime ordering and chrono Weekdays 2025-05-18 14:35:01 +02:00
Lennart
fb8889b5f6 Implement DateLike for CalDateTime 2025-05-18 13:59:00 +02:00
Lennart
5ebcab7a19 Move ical-related stuff to dedicated rustical_ical crate 2025-05-18 13:46:08 +02:00
Lennart
3c7ee09116 WIP: Preparation for recurrence expansion 2025-05-18 11:55:25 +02:00
Lennart
f55224b21a Update dependencies 2025-05-17 10:16:07 +02:00
Lennart
0acc3c22d9 frontend: Generate random secret by default 2025-05-15 20:58:17 +02:00
Lennart
212274fce9 xml: Implement proper NamespaceOwned type 2025-05-14 20:18:45 +02:00
Lennart
1436af1f9c tiny changes to rustical_xml 2025-05-14 19:43:09 +02:00
Lennart
8f69bc839a dav: Add namespace to propname 2025-05-10 13:13:51 +02:00
Lennart
37eb6df64a xml: Add namespace deserialisation 2025-05-10 13:09:22 +02:00
Lennart
3af9b3b8b4 Decrease number of rounds for app token hash 2025-05-10 11:54:09 +02:00
Lennart
d14ded7179 Put OPTIONS handler into dedicated function 2025-05-10 11:37:28 +02:00
Lennart
de6ccdc37b Update askama 2025-05-07 13:43:37 +02:00
Lennart
86ecaef6db Comment out broken DAV Push notifier 2025-05-06 15:05:44 +02:00
Lennart
2686530024 Mention that DAV Push support is currently broken 2025-05-06 15:03:49 +02:00
229 changed files with 20975 additions and 7291 deletions

2
.gitattributes vendored Normal file
View File

@@ -0,0 +1,2 @@
# Otherwise GitHub thinks this is an HTML project
crates/frontend/public/assets/licenses.html linguist-detectable=false

View File

@@ -3,6 +3,9 @@ name: Docker
on: on:
push: push:
branches: ["main"] branches: ["main"]
release:
types: ["published"]
env: env:
REGISTRY: ghcr.io REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }} IMAGE_NAME: ${{ github.repository }}
@@ -42,7 +45,6 @@ jobs:
with: with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
# As long as we don't have releases everything on the main branch shall be tagged as latest # As long as we don't have releases everything on the main branch shall be tagged as latest
# TODO: Before first release correctly configure this
tags: | tags: |
type=raw,value=latest,enable={{is_default_branch}} type=raw,value=latest,enable={{is_default_branch}}
type=ref,event=branch type=ref,event=branch

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "DELETE FROM app_tokens WHERE (principal, id) = (?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "0195268daddd2d171577c93d1bae1b8937405bcefffa8f1f9b9c9f7f2084088f"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "UPDATE calendars SET deleted_at = NULL WHERE (principal, id) = (?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "02a9260d0ff496a6bf226fc8238ae332f8eb18dddbd80d31989c074804f31dee"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO app_tokens\n (id, principal, token, displayname)\n VALUES (?, ?, ?, ?)\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 4
},
"nullable": []
},
"hash": "0fd3167a58cbfb4ee44249dbc346d2d9077adfa04c35c8c6f2a1e24720baf753"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "UPDATE calendarobjects SET deleted_at = NULL, updated_at = datetime() WHERE (principal, cal_id, id) = (?, ?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "10325688a6601f6205cde9d9e2d582ca87a46607a1d889af155debc3073d78e1"
}

View File

@@ -1,56 +0,0 @@
{
"db_name": "SQLite",
"query": "SELECT principal, id, synctoken, displayname, description, deleted_at, push_topic\n FROM addressbooks\n WHERE (principal, id) = (?, ?)\n AND ((deleted_at IS NULL) OR ?) ",
"describe": {
"columns": [
{
"name": "principal",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "id",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "synctoken",
"ordinal": 2,
"type_info": "Integer"
},
{
"name": "displayname",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "description",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "deleted_at",
"ordinal": 5,
"type_info": "Datetime"
},
{
"name": "push_topic",
"ordinal": 6,
"type_info": "Text"
}
],
"parameters": {
"Right": 3
},
"nullable": [
false,
false,
false,
true,
true,
true,
false
]
},
"hash": "130986d03d4d78ceeb15aff6f4d6304f0be0100e4bffad9cc3f6c1a2c6c4b297"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "UPDATE addressbooks SET principal = ?, id = ?, displayname = ?, description = ?, push_topic = ?\n WHERE (principal, id) = (?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 7
},
"nullable": []
},
"hash": "16a7e0cb4527060339c168ee2528416036e401f75a03100b6bfbee687b978520"
}

View File

@@ -1,38 +0,0 @@
{
"db_name": "SQLite",
"query": "SELECT id, displayname AS name, token, created_at AS \"created_at: _\" FROM app_tokens WHERE principal = ?",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "name",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "token",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "created_at: _",
"ordinal": 3,
"type_info": "Datetime"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
true
]
},
"hash": "1ebaf3fd99bee2382abc931a1eeb29badc3aabcf6b8fd58e4cf92721588a9966"
}

View File

@@ -1,44 +0,0 @@
{
"db_name": "SQLite",
"query": "\n SELECT id, displayname, principal_type, password_hash, json_group_array(member_of) AS \"memberships: Json<Vec<Option<String>>>\"\n FROM principals\n LEFT JOIN memberships ON principals.id == memberships.principal\n GROUP BY principals.id\n ",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "displayname",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "principal_type",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "password_hash",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "memberships: Json<Vec<Option<String>>>",
"ordinal": 4,
"type_info": "Text"
}
],
"parameters": {
"Right": 0
},
"nullable": [
false,
true,
false,
true,
true
]
},
"hash": "23a07f4a732f95ff7483cd1cfe3b74af4fe6b97546a631bc96d03bdc3d764ed0"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "DELETE FROM calendars WHERE (principal, id) = (?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "2834e16e6a7acb58141a2433f7735d5e2bf913c30f9f3e7bd9fecc7d4376be0f"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "UPDATE addressobjects SET deleted_at = NULL, updated_at = datetime() WHERE (principal, addressbook_id, id) = (?, ?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "2b145d094188fab69371d98520a034c69c0b61583d4e245388d3879d290619d0"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n REPLACE INTO principals\n (id, displayname, principal_type, password_hash)\n VALUES (?, ?, ?, ?)\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 4
},
"nullable": []
},
"hash": "2f043f62a7c0eae1023e319f0bc8f35dfdcf6a8247e03b1de3e2cabb2d3ab8ae"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "DELETE FROM addressbooks WHERE (principal, id) = (?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "3885219f7e132876fa8bdfd31c0761d54fbcfe2846ffd8e4e94feb40547205be"
}

View File

@@ -1,26 +0,0 @@
{
"db_name": "SQLite",
"query": "SELECT id, vcf FROM addressobjects WHERE (principal, addressbook_id, id) = (?, ?, ?) AND ((deleted_at IS NULL) or ?)",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "vcf",
"ordinal": 1,
"type_info": "Text"
}
],
"parameters": {
"Right": 4
},
"nullable": [
false,
false
]
},
"hash": "395e40a7b3333b79bc2ad50a123d99f74bc2712a16257ee2119dd211fdb61f7e"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "DELETE FROM principals WHERE id = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "3a1dbfbe9d22a62f1830d004548b7e805bcb9fdd24b49c8c9efa93df149b1002"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "REPLACE INTO calendarobjects (principal, cal_id, id, ics, first_occurence, last_occurence, etag, object_type) VALUES (?, ?, ?, ?, date(?), date(?), ?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 8
},
"nullable": []
},
"hash": "3e1cca532372e891ab3e604ecb79311d8cd64108d4f238db4c79e9467a3b6d2e"
}

View File

@@ -1,26 +0,0 @@
{
"db_name": "SQLite",
"query": "\n SELECT DISTINCT object_id, max(0, synctoken) as \"synctoken!: i64\" from addressobjectchangelog\n WHERE synctoken > ?\n ORDER BY synctoken ASC\n ",
"describe": {
"columns": [
{
"name": "object_id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "synctoken!: i64",
"ordinal": 1,
"type_info": "Null"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
null
]
},
"hash": "41b415bfb07113cab4dc5d556d39d1d040025c33dfc24e276eb0b2a27ea1799f"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "INSERT INTO addressbooks (principal, id, displayname, description, push_topic)\n VALUES (?, ?, ?, ?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 5
},
"nullable": []
},
"hash": "508adcac37e0d6751924f65e9aed24c0185ce3b1bc39fd0eab7426f581aafe02"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO addressobjectchangelog (principal, addressbook_id, object_id, \"operation\", synctoken)\n VALUES (?1, ?2, ?3, ?4, (\n SELECT synctoken FROM addressbooks WHERE (principal, id) = (?1, ?2)\n ))",
"describe": {
"columns": [],
"parameters": {
"Right": 4
},
"nullable": []
},
"hash": "526c4a9326db7f026eee15cca358943b0546fe56fc09204f1dfe90e2614f99b9"
}

View File

@@ -1,26 +0,0 @@
{
"db_name": "SQLite",
"query": "SELECT id, ics FROM calendarobjects WHERE principal = ? AND cal_id = ? AND deleted_at IS NULL",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "ics",
"ordinal": 1,
"type_info": "Text"
}
],
"parameters": {
"Right": 2
},
"nullable": [
false,
false
]
},
"hash": "54c9c0e36a52e6963f11c6aa27f13aafb4204b8aa34b664fd825bd447db80e86"
}

View File

@@ -1,26 +0,0 @@
{
"db_name": "SQLite",
"query": "SELECT id, vcf FROM addressobjects WHERE principal = ? AND addressbook_id = ? AND deleted_at IS NULL",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "vcf",
"ordinal": 1,
"type_info": "Text"
}
],
"parameters": {
"Right": 2
},
"nullable": [
false,
false
]
},
"hash": "557344035d762f2d385e505c15288ab9c89c1572f06e7fb61073e335a801b9db"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "INSERT INTO calendarobjects (principal, cal_id, id, ics, first_occurence, last_occurence, etag, object_type) VALUES (?, ?, ?, ?, date(?), date(?), ?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 8
},
"nullable": []
},
"hash": "6327bee90e5df01536a0ddb15adcc37af3027f6902aa3786365c5ab2fbf06bda"
}

View File

@@ -1,20 +0,0 @@
{
"db_name": "SQLite",
"query": "\n UPDATE calendars\n SET synctoken = synctoken + 1\n WHERE (principal, id) = (?1, ?2)\n RETURNING synctoken",
"describe": {
"columns": [
{
"name": "synctoken",
"ordinal": 0,
"type_info": "Integer"
}
],
"parameters": {
"Right": 2
},
"nullable": [
false
]
},
"hash": "69b92e393e55b0d49d1671abf53d06551452846dd94d54ed67d85eb3ace6b568"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "INSERT OR REPLACE INTO davpush_subscriptions (id, topic, expiration, push_resource, public_key, public_key_type, auth_secret) VALUES (?, ?, ?, ?, ?, ?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 7
},
"nullable": []
},
"hash": "6d08d3a014743da9b445ab012437ec11f81fd86d3b02fc1df07a036c6b47ace2"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "\n INSERT INTO calendarobjectchangelog (principal, cal_id, object_id, \"operation\", synctoken)\n VALUES (?1, ?2, ?3, ?4, (\n SELECT synctoken FROM calendars WHERE (principal, id) = (?1, ?2)\n ))",
"describe": {
"columns": [],
"parameters": {
"Right": 4
},
"nullable": []
},
"hash": "77ad562b6d78115ebb726d58cf1e4de69df169c1872cb452488ff8c43ed983a9"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "DELETE FROM addressobjects WHERE addressbook_id = ? AND id = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "7d4348f04ea5ac82e0f362240fb677740288c963c24b85de11bad011ec5da4bc"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "UPDATE calendarobjects SET deleted_at = datetime(), updated_at = datetime() WHERE (principal, cal_id, id) = (?, ?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "7e874304170bef19ceb6f96b3d9803ce6d4553cc2bd57b05d1e546e857f995cf"
}

View File

@@ -1,56 +0,0 @@
{
"db_name": "SQLite",
"query": "SELECT id, topic, expiration, push_resource, public_key, public_key_type, auth_secret\n FROM davpush_subscriptions\n WHERE (topic) = (?)",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "topic",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "expiration",
"ordinal": 2,
"type_info": "Datetime"
},
{
"name": "push_resource",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "public_key",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "public_key_type",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "auth_secret",
"ordinal": 6,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false,
false,
false,
false
]
},
"hash": "809600b79c012e77c5efabe5d355a8b188f23826a723030807dedc96fd24fdcc"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "DELETE FROM memberships WHERE (principal, member_of) = (?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "879e2717335db3b04884fc91173c8507272f1804b27b6a7f61cbe1fbb01265cd"
}

View File

@@ -1,44 +0,0 @@
{
"db_name": "SQLite",
"query": "\n SELECT id, displayname, principal_type, password_hash, json_group_array(member_of) AS \"memberships: Json<Vec<Option<String>>>\"\n FROM (SELECT * FROM principals WHERE id = ?) AS principals\n LEFT JOIN memberships ON principals.id == memberships.principal\n GROUP BY principals.id\n ",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "displayname",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "principal_type",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "password_hash",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "memberships: Json<Vec<Option<String>>>",
"ordinal": 4,
"type_info": "Null"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
true,
false,
true,
null
]
},
"hash": "95dce97b2e3224c327690c36777e3ece84a9529551696198b745dd8c743c8a38"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "REPLACE INTO addressobjects (principal, addressbook_id, id, vcf) VALUES (?, ?, ?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 4
},
"nullable": []
},
"hash": "98dba6ddce38d166ef325bbce6055d83fb0092619262c281a271bdc783a0aed9"
}

View File

@@ -1,56 +0,0 @@
{
"db_name": "SQLite",
"query": "SELECT principal, id, synctoken, displayname, description, deleted_at, push_topic\n FROM addressbooks\n WHERE principal = ? AND deleted_at IS NULL",
"describe": {
"columns": [
{
"name": "principal",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "id",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "synctoken",
"ordinal": 2,
"type_info": "Integer"
},
{
"name": "displayname",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "description",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "deleted_at",
"ordinal": 5,
"type_info": "Datetime"
},
{
"name": "push_topic",
"ordinal": 6,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
true,
true,
true,
false
]
},
"hash": "9be5d6df7d30a9a85aece59be810bbbb203bab874860aa05eb311259e4baaf05"
}

View File

@@ -1,104 +0,0 @@
{
"db_name": "SQLite",
"query": "SELECT *\n FROM calendars\n WHERE (principal, id) = (?, ?)",
"describe": {
"columns": [
{
"name": "principal",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "id",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "synctoken",
"ordinal": 2,
"type_info": "Integer"
},
{
"name": "displayname",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "description",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "order",
"ordinal": 5,
"type_info": "Integer"
},
{
"name": "color",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "timezone",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "timezone_id",
"ordinal": 8,
"type_info": "Text"
},
{
"name": "deleted_at",
"ordinal": 9,
"type_info": "Datetime"
},
{
"name": "subscription_url",
"ordinal": 10,
"type_info": "Text"
},
{
"name": "push_topic",
"ordinal": 11,
"type_info": "Text"
},
{
"name": "comp_event",
"ordinal": 12,
"type_info": "Bool"
},
{
"name": "comp_todo",
"ordinal": 13,
"type_info": "Bool"
},
{
"name": "comp_journal",
"ordinal": 14,
"type_info": "Bool"
}
],
"parameters": {
"Right": 2
},
"nullable": [
false,
false,
false,
true,
true,
false,
true,
true,
true,
true,
true,
false,
false,
false,
false
]
},
"hash": "9f930775043a6d4571a8ffd5a981cadf7c51f3f11a189f8461505abec31076e6"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "UPDATE calendars SET deleted_at = datetime() WHERE (principal, id) = (?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "a4371f228f94afd8e6f4ac4b0f7d95b6bf86268b64e714fa7ca587eae9e5df15"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "INSERT INTO addressobjects (principal, addressbook_id, id, vcf) VALUES (?, ?, ?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 4
},
"nullable": []
},
"hash": "a8b4258868d238d9c226c44aec8c3c4d90d8e3ca526d4d60b340e868bbfd9ddb"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "UPDATE addressobjects SET deleted_at = datetime(), updated_at = datetime() WHERE (principal, addressbook_id, id) = (?, ?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 3
},
"nullable": []
},
"hash": "ac0f9e29ea8079c6900ffb0ebde699309fe8db1ac9f95c58bc9ce051b7d70299"
}

View File

@@ -1,20 +0,0 @@
{
"db_name": "SQLite",
"query": "\n UPDATE addressbooks\n SET synctoken = synctoken + 1\n WHERE (principal, id) = (?1, ?2)\n RETURNING synctoken",
"describe": {
"columns": [
{
"name": "synctoken",
"ordinal": 0,
"type_info": "Integer"
}
],
"parameters": {
"Right": 2
},
"nullable": [
false
]
},
"hash": "b89be55d6f76591f68520bbdf0ebb6d688b01ee63ae36936692cf1b4f434c7ee"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "INSERT INTO calendars (principal, id, displayname, description, \"order\", color, timezone, timezone_id, push_topic, comp_event, comp_todo, comp_journal)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 12
},
"nullable": []
},
"hash": "c4134652b1efb1dda36fb59827bf9cfee6be5bddfd352f1da4e37c6b6aa0fa7a"
}

View File

@@ -1,26 +0,0 @@
{
"db_name": "SQLite",
"query": "SELECT id, ics FROM calendarobjects\n WHERE principal = ? AND cal_id = ? AND deleted_at IS NULL\n AND (last_occurence IS NULL OR ? IS NULL OR last_occurence >= date(?))\n AND (first_occurence IS NULL OR ? IS NULL OR first_occurence <= date(?))\n ",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "ics",
"ordinal": 1,
"type_info": "Text"
}
],
"parameters": {
"Right": 6
},
"nullable": [
false,
false
]
},
"hash": "c550dbf3d5ce7069f28d767ea9045e477ef8d29d6186851760757a06dec42339"
}

View File

@@ -1,26 +0,0 @@
{
"db_name": "SQLite",
"query": "\n SELECT DISTINCT object_id, max(0, synctoken) as \"synctoken!: i64\" from calendarobjectchangelog\n WHERE synctoken > ?\n ORDER BY synctoken ASC\n ",
"describe": {
"columns": [
{
"name": "object_id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "synctoken!: i64",
"ordinal": 1,
"type_info": "Null"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
null
]
},
"hash": "cbe4be47b2ca1eba485de258f522dec14540a6a9bf383fcde294e8fe14160f22"
}

View File

@@ -1,104 +0,0 @@
{
"db_name": "SQLite",
"query": "SELECT *\n FROM calendars\n WHERE principal = ? AND deleted_at IS NOT NULL",
"describe": {
"columns": [
{
"name": "principal",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "id",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "synctoken",
"ordinal": 2,
"type_info": "Integer"
},
{
"name": "displayname",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "description",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "order",
"ordinal": 5,
"type_info": "Integer"
},
{
"name": "color",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "timezone",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "timezone_id",
"ordinal": 8,
"type_info": "Text"
},
{
"name": "deleted_at",
"ordinal": 9,
"type_info": "Datetime"
},
{
"name": "subscription_url",
"ordinal": 10,
"type_info": "Text"
},
{
"name": "push_topic",
"ordinal": 11,
"type_info": "Text"
},
{
"name": "comp_event",
"ordinal": 12,
"type_info": "Bool"
},
{
"name": "comp_todo",
"ordinal": 13,
"type_info": "Bool"
},
{
"name": "comp_journal",
"ordinal": 14,
"type_info": "Bool"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
true,
true,
false,
true,
true,
true,
true,
true,
false,
false,
false,
false
]
},
"hash": "cce62f7829bd688cd8c7928b587bc31f0e50865c214b1df113350bea2c254237"
}

View File

@@ -1,104 +0,0 @@
{
"db_name": "SQLite",
"query": "SELECT *\n FROM calendars\n WHERE principal = ? AND deleted_at IS NULL",
"describe": {
"columns": [
{
"name": "principal",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "id",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "synctoken",
"ordinal": 2,
"type_info": "Integer"
},
{
"name": "displayname",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "description",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "order",
"ordinal": 5,
"type_info": "Integer"
},
{
"name": "color",
"ordinal": 6,
"type_info": "Text"
},
{
"name": "timezone",
"ordinal": 7,
"type_info": "Text"
},
{
"name": "timezone_id",
"ordinal": 8,
"type_info": "Text"
},
{
"name": "deleted_at",
"ordinal": 9,
"type_info": "Datetime"
},
{
"name": "subscription_url",
"ordinal": 10,
"type_info": "Text"
},
{
"name": "push_topic",
"ordinal": 11,
"type_info": "Text"
},
{
"name": "comp_event",
"ordinal": 12,
"type_info": "Bool"
},
{
"name": "comp_todo",
"ordinal": 13,
"type_info": "Bool"
},
{
"name": "comp_journal",
"ordinal": 14,
"type_info": "Bool"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
true,
true,
false,
true,
true,
true,
true,
true,
false,
false,
false,
false
]
},
"hash": "cedfb82b38fdd0c7681b9873b1008abee4a2f4ca16abad1b837f256d0bf416b1"
}

View File

@@ -1,26 +0,0 @@
{
"db_name": "SQLite",
"query": "SELECT id, ics FROM calendarobjects WHERE (principal, cal_id, id) = (?, ?, ?)",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "ics",
"ordinal": 1,
"type_info": "Text"
}
],
"parameters": {
"Right": 3
},
"nullable": [
false,
false
]
},
"hash": "d2f7423e2e8f97607f6664200990dcadb927445880ec6edffba3b5aedf4e199b"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "UPDATE calendars SET principal = ?, id = ?, displayname = ?, description = ?, \"order\" = ?, color = ?, timezone = ?, timezone_id = ?, push_topic = ?, comp_event = ?, comp_todo = ?, comp_journal = ?\n WHERE (principal, id) = (?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 14
},
"nullable": []
},
"hash": "d65c9c40606e59dd816a51b9b9ac60fd2ff81aaa358fcc038134e9a68ba45ad7"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "DELETE FROM calendarobjects WHERE cal_id = ? AND id = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "e5cf59ade11c09d90899cc9f87754a78a0ca9f7781fd252ebdaf4d2180fca3ba"
}

View File

@@ -1,56 +0,0 @@
{
"db_name": "SQLite",
"query": "SELECT principal, id, synctoken, displayname, description, deleted_at, push_topic\n FROM addressbooks\n WHERE principal = ? AND deleted_at IS NOT NULL",
"describe": {
"columns": [
{
"name": "principal",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "id",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "synctoken",
"ordinal": 2,
"type_info": "Integer"
},
{
"name": "displayname",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "description",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "deleted_at",
"ordinal": 5,
"type_info": "Datetime"
},
{
"name": "push_topic",
"ordinal": 6,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
true,
true,
true,
false
]
},
"hash": "e5ded4814aae1fc033bb90d27c745f76d3799958d929e8e8d16aa3ceff98e72d"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "DELETE FROM davpush_subscriptions WHERE id = ? ",
"describe": {
"columns": [],
"parameters": {
"Right": 1
},
"nullable": []
},
"hash": "e912be3352702bbf035b3ee6e9f239bf75a1ffef20211f1b1e895a67a2310960"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "REPLACE INTO memberships (principal, member_of) VALUES (?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "e947709ba03b108765082d1c4cff3dd8cb485fba5819ac914e20cb8e97037da9"
}

View File

@@ -1,56 +0,0 @@
{
"db_name": "SQLite",
"query": "SELECT id, topic, expiration, push_resource, public_key, public_key_type, auth_secret\n FROM davpush_subscriptions\n WHERE (id) = (?)",
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Text"
},
{
"name": "topic",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "expiration",
"ordinal": 2,
"type_info": "Datetime"
},
{
"name": "push_resource",
"ordinal": 3,
"type_info": "Text"
},
{
"name": "public_key",
"ordinal": 4,
"type_info": "Text"
},
{
"name": "public_key_type",
"ordinal": 5,
"type_info": "Text"
},
{
"name": "auth_secret",
"ordinal": 6,
"type_info": "Text"
}
],
"parameters": {
"Right": 1
},
"nullable": [
false,
false,
false,
false,
false,
false,
false
]
},
"hash": "ef6cf801df2237b82b55754f1b0a5da51089810fe7a0feb0d68ea801b4e2721c"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "UPDATE addressbooks SET deleted_at = datetime() WHERE (principal, id) = (?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "fbd776efdbacf6ce039b5d9760b0de181a6f4e066c52bcac4c106118f5435fe7"
}

View File

@@ -1,12 +0,0 @@
{
"db_name": "SQLite",
"query": "UPDATE addressbooks SET deleted_at = NULL WHERE (principal, id) = (?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "fcc493f5e491abdac2c2d7d5f636c770e449daf8f5ab4873950ab126edfcce7b"
}

1988
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,7 @@ version = "0.1.0"
edition = "2024" edition = "2024"
description = "A CalDAV server" description = "A CalDAV server"
repository = "https://github.com/lennart-k/rustical" repository = "https://github.com/lennart-k/rustical"
license = "AGPL-3.0-or-later"
[package] [package]
name = "rustical" name = "rustical"
@@ -13,6 +14,7 @@ version.workspace = true
edition.workspace = true edition.workspace = true
description.workspace = true description.workspace = true
repository.workspace = true repository.workspace = true
license.workspace = true
resolver = "2" resolver = "2"
publish = false publish = false
@@ -26,17 +28,15 @@ opentelemetry = [
"dep:tracing-opentelemetry", "dep:tracing-opentelemetry",
] ]
[profile.dev] [profile.dev]
debug = 0 debug = 0
[workspace.dependencies] [workspace.dependencies]
uuid = { version = "1.11", features = ["v4", "fast-rng"] } uuid = { version = "1.11", features = ["v4", "fast-rng"] }
async-trait = "0.1" async-trait = "0.1"
actix-web = "4.9" axum = "0.8"
tracing = { version = "0.1", features = ["async-await"] } tracing = { version = "0.1", features = ["async-await"] }
tracing-actix-web = "0.7"
actix-session = { version = "0.10", features = ["cookie-session"] }
actix-web-httpauth = "0.8"
anyhow = { version = "1.0", features = ["backtrace"] } anyhow = { version = "1.0", features = ["backtrace"] }
serde = { version = "1.0", features = ["serde_derive", "derive", "rc"] } serde = { version = "1.0", features = ["serde_derive", "derive", "rc"] }
futures-util = "0.3" futures-util = "0.3"
@@ -61,9 +61,10 @@ base64 = "0.22"
thiserror = "2.0" thiserror = "2.0"
quick-xml = { version = "0.37" } quick-xml = { version = "0.37" }
rust-embed = "8.5" rust-embed = "8.5"
tower-sessions = "0.14"
futures-core = "0.3.31" futures-core = "0.3.31"
hex = { version = "0.4.3", features = ["serde"] } hex = { version = "0.4.3", features = ["serde"] }
mime_guess = "2.0.5" mime_guess = "2.0"
itertools = "0.14" itertools = "0.14"
log = "0.4" log = "0.4"
derive_more = { version = "2.0", features = [ derive_more = { version = "2.0", features = [
@@ -72,9 +73,10 @@ derive_more = { version = "2.0", features = [
"into", "into",
"deref", "deref",
"constructor", "constructor",
"display",
] } ] }
askama = { version = "0.13", features = ["serde_json"] } askama = { version = "0.14", features = ["serde_json"] }
askama_web = { version = "0.13.0", features = ["actix-web-4"] } askama_web = { version = "0.14.0", features = ["axum-0.8"] }
sqlx = { version = "0.8", default-features = false, features = [ sqlx = { version = "0.8", default-features = false, features = [
"sqlx-sqlite", "sqlx-sqlite",
"uuid", "uuid",
@@ -85,12 +87,21 @@ sqlx = { version = "0.8", default-features = false, features = [
"migrate", "migrate",
"json", "json",
] } ] }
http = "1.3"
headers = "0.4"
strum = "0.27" strum = "0.27"
strum_macros = "0.27" strum_macros = "0.27"
serde_json = { version = "1.0", features = ["raw_value"] } serde_json = { version = "1.0", features = ["raw_value"] }
sqlx-sqlite = { version = "0.8", features = ["bundled"] } sqlx-sqlite = { version = "0.8", features = ["bundled"] }
ical = { version = "0.11", features = ["generator", "serde"] } ical = { version = "0.11", features = ["generator", "serde"] }
toml = "0.8" toml = "0.8"
tower = "0.5"
tower-http = { version = "0.6", features = [
"trace",
"normalize-path",
"catch-panic",
] }
percent-encoding = "2.3"
rustical_dav = { path = "./crates/dav/" } rustical_dav = { path = "./crates/dav/" }
rustical_dav_push = { path = "./crates/dav_push/" } rustical_dav_push = { path = "./crates/dav_push/" }
rustical_store = { path = "./crates/store/" } rustical_store = { path = "./crates/store/" }
@@ -100,9 +111,12 @@ rustical_carddav = { path = "./crates/carddav/" }
rustical_frontend = { path = "./crates/frontend/" } rustical_frontend = { path = "./crates/frontend/" }
rustical_xml = { path = "./crates/xml/" } rustical_xml = { path = "./crates/xml/" }
rustical_oidc = { path = "./crates/oidc/" } rustical_oidc = { path = "./crates/oidc/" }
rustical_ical = { path = "./crates/ical/" }
chrono-tz = "0.10" chrono-tz = "0.10"
chrono-humanize = "0.2" chrono-humanize = "0.2"
rand = "0.8" rand = "0.9"
axum-extra = { version = "0.10", features = ["typed-header"] }
rrule = "0.14"
argon2 = "0.5" argon2 = "0.5"
rpassword = "7.3" rpassword = "7.3"
password-hash = { version = "0.5" } password-hash = { version = "0.5" }
@@ -123,9 +137,8 @@ clap = { version = "4.5", features = ["derive", "env"] }
rustical_store = { workspace = true } rustical_store = { workspace = true }
rustical_store_sqlite = { workspace = true } rustical_store_sqlite = { workspace = true }
rustical_caldav = { workspace = true } rustical_caldav = { workspace = true }
rustical_carddav = { workspace = true } rustical_carddav.workspace = true
rustical_frontend = { workspace = true } rustical_frontend = { workspace = true }
actix-web = { workspace = true }
toml = { workspace = true } toml = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
@@ -134,28 +147,27 @@ anyhow = { workspace = true }
clap.workspace = true clap.workspace = true
sqlx = { workspace = true } sqlx = { workspace = true }
async-trait = { workspace = true } async-trait = { workspace = true }
tracing-actix-web = { workspace = true }
uuid.workspace = true uuid.workspace = true
axum.workspace = true
opentelemetry = { version = "0.29", optional = true } opentelemetry = { version = "0.30", optional = true }
opentelemetry-otlp = { version = "0.29", optional = true, features = [ opentelemetry-otlp = { version = "0.30", optional = true, features = [
"grpc-tonic", "grpc-tonic",
] } ] }
opentelemetry_sdk = { version = "0.29", features = [ opentelemetry_sdk = { version = "0.30", features = [
"rt-tokio", "rt-tokio",
], optional = true } ], optional = true }
opentelemetry-semantic-conventions = { version = "0.29", optional = true } opentelemetry-semantic-conventions = { version = "0.30", optional = true }
tracing-opentelemetry = { version = "0.30", optional = true } tracing-opentelemetry = { version = "0.31", optional = true }
tracing-subscriber = { version = "0.3", features = [ tracing-subscriber = { version = "0.3", features = [
"env-filter", "env-filter",
"fmt", "fmt",
"registry", "registry",
] } ] }
figment = { version = "0.10", features = ["env", "toml"] } figment = { version = "0.10", features = ["env", "toml"] }
tower-sessions.workspace = true
rand.workspace = true
rpassword.workspace = true rpassword.workspace = true
tower.workspace = true
argon2.workspace = true argon2.workspace = true
pbkdf2.workspace = true pbkdf2.workspace = true
password-hash.workspace = true password-hash.workspace = true
@@ -164,3 +176,7 @@ rustical_dav.workspace = true
rustical_dav_push.workspace = true rustical_dav_push.workspace = true
rustical_oidc.workspace = true rustical_oidc.workspace = true
quick-xml.workspace = true quick-xml.workspace = true
tower-http.workspace = true
axum-extra.workspace = true
headers.workspace = true
http.workspace = true

View File

@@ -42,5 +42,7 @@ FROM scratch
COPY --from=builder /usr/local/cargo/bin/rustical /usr/local/bin/rustical COPY --from=builder /usr/local/cargo/bin/rustical /usr/local/bin/rustical
CMD ["/usr/local/bin/rustical"] CMD ["/usr/local/bin/rustical"]
ENV RUSTICAL_DATA_STORE__SQLITE__DB_URL=/var/lib/rustical/db.sqlite3
LABEL org.opencontainers.image.authors="Lennart K github.com/lennart-k" LABEL org.opencontainers.image.authors="Lennart K github.com/lennart-k"
EXPOSE 4000 EXPOSE 4000

2
Justfile Normal file
View File

@@ -0,0 +1,2 @@
licenses:
cargo about generate about.hbs > crates/frontend/public/assets/licenses.html

View File

@@ -3,15 +3,16 @@
a CalDAV/CardDAV server a CalDAV/CardDAV server
> [!WARNING] > [!WARNING]
> RustiCal is **not production-ready!** RustiCal is **not production-ready!**
> I'm just starting to use it myself so I cannot guarantee that everything will be working smoothly just yet. While I've started migrating to RustiCal and becoming more confident,
> I hope there won't be any manual migrations anymore but if you want to be an early adopter some SQL knowledge might be useful just in case. please know that bugs and rough edges will still occur.
> If you still want to play around with it in its current state, absolutely feel free to do so and to open up an issue if something is not working. :) Concretely, if you are using Apple Calendar you will want to stay away from assigning groups to users.
If you still want to play around with it in its current state, absolutely feel free to do so and to open up an issue if something is not working. :)
## Features ## Features
- easy to backup, everything saved in one SQLite database - easy to backup, everything saved in one SQLite database
- [WebDAV Push](https://github.com/bitfireAT/webdav-push/) support, so near-instant synchronisation to DAVx5 - ~~[WebDAV Push](https://github.com/bitfireAT/webdav-push/) support, so near-instant synchronisation to DAVx5~~ (currently broken)
- lightweight (the container image contains only one binary) - lightweight (the container image contains only one binary)
- adequately fast (I'd love to say blazingly fast™ :fire: but I don't have any benchmarks) - adequately fast (I'd love to say blazingly fast™ :fire: but I don't have any benchmarks)
- deleted calendars are recoverable - deleted calendars are recoverable
@@ -22,3 +23,10 @@ a CalDAV/CardDAV server
## Getting Started ## Getting Started
- Check out the [documentation](https://lennart-k.github.io/rustical/installation/) - Check out the [documentation](https://lennart-k.github.io/rustical/installation/)
## Tested Clients
- DAVx5,
- GNOME Accounts, GNOME Calendar, GNOME Contacts
- Evolution
- Apple Calendar

70
about.hbs Normal file
View File

@@ -0,0 +1,70 @@
<html>
<head>
<style>
@media (prefers-color-scheme: dark) {
body {
background: #333;
color: white;
}
a {
color: skyblue;
}
}
.container {
font-family: sans-serif;
max-width: 800px;
margin: 0 auto;
}
.intro {
text-align: center;
}
.licenses-list {
list-style-type: none;
margin: 0;
padding: 0;
}
.license-used-by {
margin-top: -10px;
}
.license-text {
max-height: 200px;
overflow-y: scroll;
white-space: pre-wrap;
}
</style>
</head>
<body>
<main class="container">
<div class="intro">
<h1>Third Party Licenses</h1>
<p>This page lists the licenses of packages used by RustiCal.</p>
</div>
<h2>Overview of licenses:</h2>
<ul class="licenses-overview">
{{#each overview}}
<li><a href="#{{id}}">{{name}}</a> ({{count}})</li>
{{/each}}
</ul>
<h2>All license text:</h2>
<ul class="licenses-list">
{{#each licenses}}
<li class="license">
<h3 id="{{id}}">{{name}}</h3>
<h4>Used by:</h4>
<ul class="license-used-by">
{{#each used_by}}
<li><a href="{{#if crate.repository}} {{crate.repository}} {{else}} https://crates.io/crates/{{crate.name}} {{/if}}">{{crate.name}} {{crate.version}}</a></li>
{{/each}}
</ul>
<pre class="license-text">{{text}}</pre>
</li>
{{/each}}
</ul>
</main>
</body>
</html>

11
about.toml Normal file
View File

@@ -0,0 +1,11 @@
accepted = [
"Apache-2.0",
"MIT",
"BSD-3-Clause",
"ISC",
"Unicode-3.0",
"CDLA-Permissive-2.0",
"Zlib",
"AGPL-3.0",
]
workarounds = ["ring", "chrono", "rustls"]

View File

@@ -4,18 +4,19 @@ version.workspace = true
edition.workspace = true edition.workspace = true
description.workspace = true description.workspace = true
repository.workspace = true repository.workspace = true
license.workspace = true
publish = false publish = false
[dependencies] [dependencies]
actix-web = { workspace = true } axum.workspace = true
axum-extra.workspace = true
tower.workspace = true
async-trait = { workspace = true } async-trait = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
quick-xml = { workspace = true } quick-xml = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
tracing-actix-web = { workspace = true }
futures-util = { workspace = true } futures-util = { workspace = true }
derive_more = { workspace = true } derive_more = { workspace = true }
actix-web-httpauth = { workspace = true }
base64 = { workspace = true } base64 = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
@@ -25,6 +26,14 @@ rustical_store = { workspace = true }
chrono = { workspace = true } chrono = { workspace = true }
chrono-tz = { workspace = true } chrono-tz = { workspace = true }
sha2 = { workspace = true } sha2 = { workspace = true }
ical.workspace = true
percent-encoding.workspace = true
rustical_xml.workspace = true rustical_xml.workspace = true
uuid.workspace = true uuid.workspace = true
rustical_dav_push.workspace = true rustical_dav_push.workspace = true
rustical_ical.workspace = true
http.workspace = true
headers.workspace = true
tower-http.workspace = true
strum.workspace = true
strum_macros.workspace = true

View File

@@ -0,0 +1,96 @@
use crate::Error;
use crate::calendar::CalendarResourceService;
use axum::body::Body;
use axum::extract::State;
use axum::{extract::Path, response::Response};
use headers::{ContentType, HeaderMapExt};
use http::{HeaderValue, StatusCode, header};
use ical::generator::{Emitter, IcalCalendarBuilder};
use ical::property::Property;
use percent_encoding::{CONTROLS, utf8_percent_encode};
use rustical_ical::{CalendarObjectComponent, EventObject, JournalObject, TodoObject};
use rustical_store::{CalendarStore, SubscriptionStore, auth::User};
use std::collections::HashMap;
use std::str::FromStr;
use tracing::instrument;
#[instrument(skip(cal_store))]
pub async fn route_get<C: CalendarStore, S: SubscriptionStore>(
Path((principal, calendar_id)): Path<(String, String)>,
State(CalendarResourceService { cal_store, .. }): State<CalendarResourceService<C, S>>,
user: User,
) -> Result<Response, Error> {
if !user.is_principal(&principal) {
return Err(crate::Error::Unauthorized);
}
let calendar = cal_store.get_calendar(&principal, &calendar_id).await?;
if !user.is_principal(&calendar.principal) {
return Err(crate::Error::Unauthorized);
}
let calendar = cal_store.get_calendar(&principal, &calendar_id).await?;
let mut timezones = HashMap::new();
let objects = cal_store.get_objects(&principal, &calendar_id).await?;
let mut ical_calendar_builder = IcalCalendarBuilder::version("4.0")
.gregorian()
.prodid("RustiCal");
if calendar.displayname.is_some() {
ical_calendar_builder = ical_calendar_builder.set(Property {
name: "X-WR-CALNAME".to_owned(),
value: calendar.displayname,
params: None,
});
}
if calendar.description.is_some() {
ical_calendar_builder = ical_calendar_builder.set(Property {
name: "X-WR-CALDESC".to_owned(),
value: calendar.description,
params: None,
});
}
if calendar.timezone_id.is_some() {
ical_calendar_builder = ical_calendar_builder.set(Property {
name: "X-WR-TIMEZONE".to_owned(),
value: calendar.timezone_id,
params: None,
});
}
let mut ical_calendar = ical_calendar_builder.build();
for object in &objects {
match object.get_data() {
CalendarObjectComponent::Event(EventObject {
event,
timezones: object_timezones,
..
}) => {
timezones.extend(object_timezones);
ical_calendar.events.push(event.clone());
}
CalendarObjectComponent::Todo(TodoObject { todo, .. }) => {
ical_calendar.todos.push(todo.clone());
}
CalendarObjectComponent::Journal(JournalObject { journal, .. }) => {
ical_calendar.journals.push(journal.clone());
}
}
}
let mut resp = Response::builder().status(StatusCode::OK);
let hdrs = resp.headers_mut().unwrap();
hdrs.typed_insert(ContentType::from_str("text/calendar").unwrap());
let filename = format!("{}_{}.ics", calendar.principal, calendar.id);
let filename = utf8_percent_encode(&filename, CONTROLS);
hdrs.insert(
header::CONTENT_DISPOSITION,
HeaderValue::from_str(&format!(
"attachement; filename*=UTF-8''{filename}; filename={filename}",
))
.unwrap(),
);
Ok(resp.body(Body::new(ical_calendar.generate())).unwrap())
}

View File

@@ -1,13 +1,14 @@
use crate::Error; use crate::Error;
use crate::calendar::CalendarResourceService;
use crate::calendar::prop::SupportedCalendarComponentSet; use crate::calendar::prop::SupportedCalendarComponentSet;
use actix_web::HttpResponse; use axum::extract::{Path, State};
use actix_web::web::{Data, Path}; use axum::response::{IntoResponse, Response};
use http::{Method, StatusCode};
use rustical_ical::CalendarObjectType;
use rustical_store::auth::User; use rustical_store::auth::User;
use rustical_store::calendar::CalendarObjectType; use rustical_store::{Calendar, CalendarStore, SubscriptionStore};
use rustical_store::{Calendar, CalendarStore};
use rustical_xml::{Unparsed, XmlDeserialize, XmlDocument, XmlRootTag}; use rustical_xml::{Unparsed, XmlDeserialize, XmlDocument, XmlRootTag};
use tracing::instrument; use tracing::instrument;
use tracing_actix_web::RootSpan;
#[derive(XmlDeserialize, Clone, Debug)] #[derive(XmlDeserialize, Clone, Debug)]
pub struct MkcolCalendarProp { pub struct MkcolCalendarProp {
@@ -48,21 +49,31 @@ struct MkcalendarRequest {
set: PropElement, set: PropElement,
} }
#[instrument(parent = root_span.id(), skip(store, root_span))] #[derive(XmlDeserialize, XmlRootTag, Clone, Debug)]
pub async fn route_mkcalendar<C: CalendarStore>( #[xml(root = b"mkcol")]
path: Path<(String, String)>, #[xml(ns = "rustical_dav::namespace::NS_DAV")]
body: String, struct MkcolRequest {
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
set: PropElement,
}
#[instrument(skip(cal_store))]
pub async fn route_mkcalendar<C: CalendarStore, S: SubscriptionStore>(
Path((principal, cal_id)): Path<(String, String)>,
user: User, user: User,
store: Data<C>, State(CalendarResourceService { cal_store, .. }): State<CalendarResourceService<C, S>>,
root_span: RootSpan, method: Method,
) -> Result<HttpResponse, Error> { body: String,
let (principal, cal_id) = path.into_inner(); ) -> Result<Response, Error> {
if !user.is_principal(&principal) { if !user.is_principal(&principal) {
return Err(Error::Unauthorized); return Err(Error::Unauthorized);
} }
let request = MkcalendarRequest::parse_str(&body)?; let request = match method.as_str() {
let request = request.set.prop; "MKCALENDAR" => MkcalendarRequest::parse_str(&body)?.set.prop,
"MKCOL" => MkcolRequest::parse_str(&body)?.set.prop,
_ => unreachable!("We never call with another method"),
};
let calendar = Calendar { let calendar = Calendar {
id: cal_id.to_owned(), id: cal_id.to_owned(),
@@ -87,17 +98,9 @@ pub async fn route_mkcalendar<C: CalendarStore>(
]), ]),
}; };
match store.insert_calendar(calendar).await { cal_store.insert_calendar(calendar).await?;
// The spec says we should return a mkcalendar-response but I don't know what goes into it. // The spec says we don't have to return a response everything was successful
// However, it works without one but breaks on iPadOS when using an empty one :) Ok(StatusCode::CREATED.into_response())
Ok(()) => Ok(HttpResponse::Created()
.insert_header(("Cache-Control", "no-cache"))
.body("")),
Err(err) => {
dbg!(err.to_string());
Err(err.into())
}
}
} }
#[cfg(test)] #[cfg(test)]
@@ -130,4 +133,31 @@ mod tests {
</CAL:mkcalendar> </CAL:mkcalendar>
"#).unwrap(); "#).unwrap();
} }
#[test]
fn test_xml_mkcol() {
MkcolRequest::parse_str(r#"
<?xml version='1.0' encoding='UTF-8' ?>
<mkcol xmlns="DAV:" xmlns:CAL="urn:ietf:params:xml:ns:caldav" xmlns:CARD="urn:ietf:params:xml:ns:carddav">
<set>
<prop>
<resourcetype>
<collection />
<CAL:calendar />
</resourcetype>
<displayname>jfs</displayname>
<CAL:calendar-description>rggg</CAL:calendar-description>
<n0:calendar-color xmlns:n0="http://apple.com/ns/ical/">#FFF8DCFF</n0:calendar-color>
<CAL:calendar-timezone-id>Europe/Berlin</CAL:calendar-timezone-id>
<CAL:supported-calendar-component-set>
<CAL:comp name="VEVENT"/>
<CAL:comp name="VTODO"/>
<CAL:comp name="VJOURNAL"/>
</CAL:supported-calendar-component-set>
<CAL:calendar-timezone>BEGIN:VCALENDAR\r\nBEGIN:VTIMEZONE\r\nTZID:Europe/Berlin\r\nLAST-MODIFIED:20240422T053450Z\r\nTZURL:https://www.tzurl.org/zoneinfo/Europe/Berlin\r\nX-LIC-LOCATION:Europe/Berlin\r\nX-PROLEPTIC-TZNAME:LMT\r\nBEGIN:STANDARD\r\nTZNAME:CET\r\nTZOFFSETFROM:+005328\r\nTZOFFSETTO:+0100\r\nDTSTART:18930401T000632\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZNAME:CEST\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nDTSTART:19160430T230000\r\nRDATE:19400401T020000\r\nRDATE:19430329T020000\r\nRDATE:19460414T020000\r\nRDATE:19470406T030000\r\nRDATE:19480418T020000\r\nRDATE:19490410T020000\r\nRDATE:19800406T020000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZNAME:CET\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nDTSTART:19161001T010000\r\nRDATE:19421102T030000\r\nRDATE:19431004T030000\r\nRDATE:19441002T030000\r\nRDATE:19451118T030000\r\nRDATE:19461007T030000\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZNAME:CEST\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nDTSTART:19170416T020000\r\nRRULE:FREQ=YEARLY;UNTIL=19180415T010000Z;BYMONTH=4;BYDAY=3MO\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZNAME:CET\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nDTSTART:19170917T030000\r\nRRULE:FREQ=YEARLY;UNTIL=19180916T010000Z;BYMONTH=9;BYDAY=3MO\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZNAME:CEST\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nDTSTART:19440403T020000\r\nRRULE:FREQ=YEARLY;UNTIL=19450402T010000Z;BYMONTH=4;BYDAY=1MO\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZNAME:CEMT\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0300\r\nDTSTART:19450524T000000\r\nRDATE:19470511T010000\r\nEND:DAYLIGHT\r\nBEGIN:DAYLIGHT\r\nTZNAME:CEST\r\nTZOFFSETFROM:+0300\r\nTZOFFSETTO:+0200\r\nDTSTART:19450924T030000\r\nRDATE:19470629T030000\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZNAME:CET\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0100\r\nDTSTART:19460101T000000\r\nRDATE:19800101T000000\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZNAME:CET\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nDTSTART:19471005T030000\r\nRRULE:FREQ=YEARLY;UNTIL=19491002T010000Z;BYMONTH=10;BYDAY=1SU\r\nEND:STANDARD\r\nBEGIN:STANDARD\r\nTZNAME:CET\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nDTSTART:19800928T030000\r\nRRULE:FREQ=YEARLY;UNTIL=19950924T010000Z;BYMONTH=9;BYDAY=-1SU\r\nEND:STANDARD\r\nBEGIN:DAYLIGHT\r\nTZNAME:CEST\r\nTZOFFSETFROM:+0100\r\nTZOFFSETTO:+0200\r\nDTSTART:19810329T020000\r\nRRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU\r\nEND:DAYLIGHT\r\nBEGIN:STANDARD\r\nTZNAME:CET\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\nDTSTART:19961027T030000\r\nRRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU\r\nEND:STANDARD\r\nEND:VTIMEZONE\r\nEND:VCALENDAR\r\n</CAL:calendar-timezone>
</prop>
</set>
</mkcol>
"#).unwrap();
}
} }

View File

@@ -1,3 +1,4 @@
pub mod mkcalendar; pub mod mkcalendar;
pub mod post; // pub mod post;
pub mod get;
pub mod report; pub mod report;

View File

@@ -1,8 +1,8 @@
use crate::Error; use crate::Error;
use crate::calendar::resource::CalendarResource; use crate::calendar::resource::{CalendarResource, CalendarResourceService};
use actix_web::http::header; use axum::extract::{Path, State};
use actix_web::web::{Data, Path}; use axum::response::{IntoResponse, Response};
use actix_web::{HttpRequest, HttpResponse}; use http::{HeaderMap, StatusCode, header};
use rustical_dav::privileges::UserPrivilege; use rustical_dav::privileges::UserPrivilege;
use rustical_dav::resource::Resource; use rustical_dav::resource::Resource;
use rustical_dav_push::register::PushRegister; use rustical_dav_push::register::PushRegister;
@@ -10,24 +10,22 @@ use rustical_store::auth::User;
use rustical_store::{CalendarStore, Subscription, SubscriptionStore}; use rustical_store::{CalendarStore, Subscription, SubscriptionStore};
use rustical_xml::XmlDocument; use rustical_xml::XmlDocument;
use tracing::instrument; use tracing::instrument;
use tracing_actix_web::RootSpan;
#[instrument(parent = root_span.id(), skip(store, subscription_store, root_span, req))] #[instrument(skip(resource_service))]
pub async fn route_post<C: CalendarStore, S: SubscriptionStore>( pub async fn route_post<C: CalendarStore, S: SubscriptionStore>(
path: Path<(String, String)>, Path((principal, cal_id)): Path<(String, String)>,
body: String,
user: User, user: User,
store: Data<C>, State(resource_service): State<CalendarResourceService<C, S>>,
subscription_store: Data<S>, body: String,
root_span: RootSpan, ) -> Result<Response, Error> {
req: HttpRequest,
) -> Result<HttpResponse, Error> {
let (principal, cal_id) = path.into_inner();
if !user.is_principal(&principal) { if !user.is_principal(&principal) {
return Err(Error::Unauthorized); return Err(Error::Unauthorized);
} }
let calendar = store.get_calendar(&principal, &cal_id).await?; let calendar = resource_service
.cal_store
.get_calendar(&principal, &cal_id)
.await?;
let calendar_resource = CalendarResource { let calendar_resource = CalendarResource {
cal: calendar, cal: calendar,
read_only: true, read_only: true,
@@ -70,12 +68,22 @@ pub async fn route_post<C: CalendarStore, S: SubscriptionStore>(
.ty, .ty,
auth_secret: request.subscription.web_push_subscription.auth_secret, auth_secret: request.subscription.web_push_subscription.auth_secret,
}; };
subscription_store.upsert_subscription(subscription).await?; resource_service
.sub_store
.upsert_subscription(subscription)
.await?;
let location = req // let location = req
.resource_map() // .resource_map()
.url_for(&req, "subscription", &[sub_id]) // .url_for(&req, "subscription", &[sub_id])
.unwrap(); // .unwrap();
//
let location = "asd";
Ok((
StatusCode::CREATED,
HeaderMap::from_iter([(header::LOCATION, location)]),
)
.into_response());
Ok(HttpResponse::Created() Ok(HttpResponse::Created()
.append_header((header::LOCATION, location.to_string())) .append_header((header::LOCATION, location.to_string()))

View File

@@ -1,18 +1,7 @@
use super::ReportPropName; use crate::{Error, calendar_object::CalendarObjectPropWrapperName};
use crate::{ use rustical_dav::xml::PropfindType;
Error, use rustical_ical::CalendarObject;
calendar_object::resource::{CalendarObjectPropWrapper, CalendarObjectResource}, use rustical_store::CalendarStore;
};
use actix_web::{
HttpRequest,
dev::{Path, ResourceDef},
http::StatusCode,
};
use rustical_dav::{
resource::Resource,
xml::{MultistatusElement, PropfindType, multistatus::ResponseElement},
};
use rustical_store::{CalendarObject, CalendarStore, auth::User};
use rustical_xml::XmlDeserialize; use rustical_xml::XmlDeserialize;
#[derive(XmlDeserialize, Clone, Debug, PartialEq)] #[derive(XmlDeserialize, Clone, Debug, PartialEq)]
@@ -20,7 +9,7 @@ use rustical_xml::XmlDeserialize;
// <!ELEMENT calendar-query ((DAV:allprop | DAV:propname | DAV:prop)?, href+)> // <!ELEMENT calendar-query ((DAV:allprop | DAV:propname | DAV:prop)?, href+)>
pub(crate) struct CalendarMultigetRequest { pub(crate) struct CalendarMultigetRequest {
#[xml(ty = "untagged")] #[xml(ty = "untagged")]
pub(crate) prop: PropfindType<ReportPropName>, pub(crate) prop: PropfindType<CalendarObjectPropWrapperName>,
#[xml(flatten)] #[xml(flatten)]
#[xml(ns = "rustical_dav::namespace::NS_DAV")] #[xml(ns = "rustical_dav::namespace::NS_DAV")]
pub(crate) href: Vec<String>, pub(crate) href: Vec<String>,
@@ -33,65 +22,27 @@ pub async fn get_objects_calendar_multiget<C: CalendarStore>(
cal_id: &str, cal_id: &str,
store: &C, store: &C,
) -> Result<(Vec<CalendarObject>, Vec<String>), Error> { ) -> Result<(Vec<CalendarObject>, Vec<String>), Error> {
let resource_def = ResourceDef::prefix(path).join(&ResourceDef::new("/{object_id}.ics"));
let mut result = vec![]; let mut result = vec![];
let mut not_found = vec![]; let mut not_found = vec![];
for href in &cal_query.href { for href in &cal_query.href {
let mut path = Path::new(href.as_str()); if let Some(filename) = href.strip_prefix(path) {
if !resource_def.capture_match_info(&mut path) { let filename = filename.trim_start_matches("/");
if let Some(object_id) = filename.strip_suffix(".ics") {
match store.get_object(principal, cal_id, object_id).await {
Ok(object) => result.push(object),
Err(rustical_store::Error::NotFound) => not_found.push(href.to_owned()),
Err(err) => return Err(err.into()),
};
} else {
not_found.push(href.to_owned());
continue;
}
} else {
not_found.push(href.to_owned()); not_found.push(href.to_owned());
continue; continue;
}; }
let object_id = path.get("object_id").unwrap();
match store.get_object(principal, cal_id, object_id).await {
Ok(object) => result.push(object),
Err(rustical_store::Error::NotFound) => not_found.push(href.to_owned()),
Err(err) => return Err(err.into()),
};
} }
Ok((result, not_found)) Ok((result, not_found))
} }
pub async fn handle_calendar_multiget<C: CalendarStore>(
cal_multiget: &CalendarMultigetRequest,
props: &[&str],
req: HttpRequest,
user: &User,
principal: &str,
cal_id: &str,
cal_store: &C,
) -> Result<MultistatusElement<CalendarObjectPropWrapper, String>, Error> {
let (objects, not_found) =
get_objects_calendar_multiget(cal_multiget, req.path(), principal, cal_id, cal_store)
.await?;
let mut responses = Vec::new();
for object in objects {
let path = format!("{}/{}.ics", req.path(), object.get_id());
responses.push(
CalendarObjectResource {
object,
principal: principal.to_owned(),
}
.propfind(&path, props, user, req.resource_map())?,
);
}
let not_found_responses = not_found
.into_iter()
.map(|path| ResponseElement {
href: path,
status: Some(StatusCode::NOT_FOUND),
..Default::default()
})
.collect();
Ok(MultistatusElement {
responses,
member_responses: not_found_responses,
..Default::default()
})
}

View File

@@ -1,21 +1,10 @@
use actix_web::HttpRequest; use crate::{Error, calendar_object::CalendarObjectPropWrapperName};
use rustical_dav::{ use rustical_dav::xml::PropfindType;
resource::Resource, use rustical_ical::{CalendarObject, UtcDateTime};
xml::{MultistatusElement, PropfindType}, use rustical_store::{CalendarStore, calendar_store::CalendarQuery};
};
use rustical_store::{
CalendarObject, CalendarStore, auth::User, calendar::UtcDateTime, calendar_store::CalendarQuery,
};
use rustical_xml::XmlDeserialize; use rustical_xml::XmlDeserialize;
use std::ops::Deref; use std::ops::Deref;
use crate::{
Error,
calendar_object::resource::{CalendarObjectPropWrapper, CalendarObjectResource},
};
use super::ReportPropName;
#[derive(XmlDeserialize, Clone, Debug, PartialEq)] #[derive(XmlDeserialize, Clone, Debug, PartialEq)]
#[allow(dead_code)] #[allow(dead_code)]
pub(crate) struct TimeRangeElement { pub(crate) struct TimeRangeElement {
@@ -181,7 +170,7 @@ impl From<&FilterElement> for CalendarQuery {
// <!ELEMENT calendar-query ((DAV:allprop | DAV:propname | DAV:prop)?, filter, timezone?)> // <!ELEMENT calendar-query ((DAV:allprop | DAV:propname | DAV:prop)?, filter, timezone?)>
pub struct CalendarQueryRequest { pub struct CalendarQueryRequest {
#[xml(ty = "untagged")] #[xml(ty = "untagged")]
pub prop: PropfindType<ReportPropName>, pub prop: PropfindType<CalendarObjectPropWrapperName>,
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")] #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
pub(crate) filter: Option<FilterElement>, pub(crate) filter: Option<FilterElement>,
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")] #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
@@ -214,36 +203,3 @@ pub async fn get_objects_calendar_query<C: CalendarStore>(
} }
Ok(objects) Ok(objects)
} }
pub async fn handle_calendar_query<C: CalendarStore>(
cal_query: &CalendarQueryRequest,
props: &[&str],
req: HttpRequest,
user: &User,
principal: &str,
cal_id: &str,
cal_store: &C,
) -> Result<MultistatusElement<CalendarObjectPropWrapper, String>, Error> {
let objects = get_objects_calendar_query(cal_query, principal, cal_id, cal_store).await?;
let mut responses = Vec::new();
for object in objects {
let path = format!(
"{}/{}.ics",
req.path().trim_end_matches('/'),
object.get_id()
);
responses.push(
CalendarObjectResource {
object,
principal: principal.to_owned(),
}
.propfind(&path, props, user, req.resource_map())?,
);
}
Ok(MultistatusElement {
responses,
..Default::default()
})
}

View File

@@ -1,14 +1,27 @@
use crate::Error; use crate::{
use actix_web::{ CalDavPrincipalUri, Error,
HttpRequest, Responder, calendar::CalendarResourceService,
web::{Data, Path}, calendar_object::{
CalendarObjectPropWrapper, CalendarObjectPropWrapperName, resource::CalendarObjectResource,
},
}; };
use calendar_multiget::{CalendarMultigetRequest, handle_calendar_multiget}; use axum::{
use calendar_query::{CalendarQueryRequest, handle_calendar_query}; Extension,
use rustical_dav::xml::{ extract::{OriginalUri, Path, State},
PropElement, PropfindType, Propname, sync_collection::SyncCollectionRequest, response::IntoResponse,
}; };
use rustical_store::{CalendarStore, auth::User}; use calendar_multiget::{CalendarMultigetRequest, get_objects_calendar_multiget};
use calendar_query::{CalendarQueryRequest, get_objects_calendar_query};
use http::StatusCode;
use rustical_dav::{
resource::{PrincipalUri, Resource},
xml::{
MultistatusElement, PropfindType, multistatus::ResponseElement,
sync_collection::SyncCollectionRequest,
},
};
use rustical_ical::CalendarObject;
use rustical_store::{CalendarStore, SubscriptionStore, auth::User};
use rustical_xml::{XmlDeserialize, XmlDocument}; use rustical_xml::{XmlDeserialize, XmlDocument};
use sync_collection::handle_sync_collection; use sync_collection::handle_sync_collection;
use tracing::instrument; use tracing::instrument;
@@ -17,34 +30,6 @@ mod calendar_multiget;
mod calendar_query; mod calendar_query;
mod sync_collection; mod sync_collection;
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
pub(crate) struct ExpandElement {
#[xml(ty = "attr")]
start: String,
#[xml(ty = "attr")]
end: String,
}
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
pub struct CalendarData {
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
comp: Option<()>,
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
expand: Option<ExpandElement>,
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
limit_recurrence_set: Option<()>,
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
limit_freebusy_set: Option<()>,
}
#[derive(XmlDeserialize, Clone, Debug, PartialEq)]
pub enum ReportPropName {
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
CalendarData(CalendarData),
#[xml(other)]
Propname(Propname),
}
#[derive(XmlDeserialize, XmlDocument, Clone, Debug, PartialEq)] #[derive(XmlDeserialize, XmlDocument, Clone, Debug, PartialEq)]
pub(crate) enum ReportRequest { pub(crate) enum ReportRequest {
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")] #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
@@ -52,44 +37,65 @@ pub(crate) enum ReportRequest {
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")] #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
CalendarQuery(CalendarQueryRequest), CalendarQuery(CalendarQueryRequest),
#[xml(ns = "rustical_dav::namespace::NS_DAV")] #[xml(ns = "rustical_dav::namespace::NS_DAV")]
SyncCollection(SyncCollectionRequest<ReportPropName>), SyncCollection(SyncCollectionRequest<CalendarObjectPropWrapperName>),
} }
impl ReportRequest { impl ReportRequest {
fn props(&self) -> Vec<&str> { fn props(&self) -> &PropfindType<CalendarObjectPropWrapperName> {
let prop_element = match self { match &self {
ReportRequest::CalendarMultiget(CalendarMultigetRequest { prop, .. }) => prop, ReportRequest::CalendarMultiget(CalendarMultigetRequest { prop, .. }) => prop,
ReportRequest::CalendarQuery(CalendarQueryRequest { prop, .. }) => prop, ReportRequest::CalendarQuery(CalendarQueryRequest { prop, .. }) => prop,
ReportRequest::SyncCollection(SyncCollectionRequest { prop, .. }) => prop, ReportRequest::SyncCollection(SyncCollectionRequest { prop, .. }) => prop,
};
match prop_element {
PropfindType::Allprop => {
vec!["allprop"]
}
PropfindType::Propname => {
vec!["propname"]
}
PropfindType::Prop(PropElement(prop_tags)) => prop_tags
.iter()
.map(|propname| match propname {
ReportPropName::Propname(propname) => propname.0.as_str(),
ReportPropName::CalendarData(_) => "calendar-data",
})
.collect(),
} }
} }
} }
#[instrument(skip(req, cal_store))] fn objects_response(
pub async fn route_report_calendar<C: CalendarStore>( objects: Vec<CalendarObject>,
path: Path<(String, String)>, not_found: Vec<String>,
body: String, path: &str,
principal: &str,
puri: &impl PrincipalUri,
user: &User,
prop: &PropfindType<CalendarObjectPropWrapperName>,
) -> Result<MultistatusElement<CalendarObjectPropWrapper, String>, Error> {
let mut responses = Vec::new();
for object in objects {
let path = format!("{}/{}.ics", path, object.get_id());
responses.push(
CalendarObjectResource {
object,
principal: principal.to_owned(),
}
.propfind(&path, prop, puri, user)?,
);
}
let not_found_responses = not_found
.into_iter()
.map(|path| ResponseElement {
href: path,
status: Some(StatusCode::NOT_FOUND),
..Default::default()
})
.collect();
Ok(MultistatusElement {
responses,
member_responses: not_found_responses,
..Default::default()
})
}
#[instrument(skip(cal_store))]
pub async fn route_report_calendar<C: CalendarStore, S: SubscriptionStore>(
Path((principal, cal_id)): Path<(String, String)>,
user: User, user: User,
req: HttpRequest, Extension(puri): Extension<CalDavPrincipalUri>,
cal_store: Data<C>, State(CalendarResourceService { cal_store, .. }): State<CalendarResourceService<C, S>>,
) -> Result<impl Responder, Error> { OriginalUri(uri): OriginalUri,
let (principal, cal_id) = path.into_inner(); body: String,
) -> Result<impl IntoResponse, Error> {
if !user.is_principal(&principal) { if !user.is_principal(&principal) {
return Err(Error::Unauthorized); return Err(Error::Unauthorized);
} }
@@ -99,34 +105,35 @@ pub async fn route_report_calendar<C: CalendarStore>(
Ok(match &request { Ok(match &request {
ReportRequest::CalendarQuery(cal_query) => { ReportRequest::CalendarQuery(cal_query) => {
handle_calendar_query( let objects =
cal_query, get_objects_calendar_query(cal_query, &principal, &cal_id, cal_store.as_ref())
&props, .await?;
req, objects_response(objects, vec![], uri.path(), &principal, &puri, &user, props)?
&user,
&principal,
&cal_id,
cal_store.as_ref(),
)
.await?
} }
ReportRequest::CalendarMultiget(cal_multiget) => { ReportRequest::CalendarMultiget(cal_multiget) => {
handle_calendar_multiget( let (objects, not_found) = get_objects_calendar_multiget(
cal_multiget, cal_multiget,
&props, uri.path(),
req,
&user,
&principal, &principal,
&cal_id, &cal_id,
cal_store.as_ref(), cal_store.as_ref(),
) )
.await? .await?;
objects_response(
objects,
not_found,
uri.path(),
&principal,
&puri,
&user,
props,
)?
} }
ReportRequest::SyncCollection(sync_collection) => { ReportRequest::SyncCollection(sync_collection) => {
handle_sync_collection( handle_sync_collection(
sync_collection, sync_collection,
&props, uri.path(),
req, &puri,
&user, &user,
&principal, &principal,
&cal_id, &cal_id,
@@ -140,10 +147,11 @@ pub async fn route_report_calendar<C: CalendarStore>(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::calendar_object::{CalendarData, CalendarObjectPropName, ExpandElement};
use calendar_query::{CompFilterElement, FilterElement, TimeRangeElement}; use calendar_query::{CompFilterElement, FilterElement, TimeRangeElement};
use rustical_dav::xml::{PropElement, PropfindType, Propname}; use rustical_dav::{extensions::CommonPropertiesPropName, xml::PropElement};
use rustical_store::calendar::UtcDateTime; use rustical_ical::UtcDateTime;
use rustical_xml::ValueDeserialize; use rustical_xml::{NamespaceOwned, ValueDeserialize};
#[test] #[test]
fn test_xml_calendar_data() { fn test_xml_calendar_data() {
@@ -152,7 +160,6 @@ mod tests {
<calendar-multiget xmlns="urn:ietf:params:xml:ns:caldav" xmlns:D="DAV:"> <calendar-multiget xmlns="urn:ietf:params:xml:ns:caldav" xmlns:D="DAV:">
<D:prop> <D:prop>
<D:getetag/> <D:getetag/>
<D:displayname/>
<calendar-data> <calendar-data>
<expand start="20250426T220000Z" end="20250503T220000Z"/> <expand start="20250426T220000Z" end="20250503T220000Z"/>
</calendar-data> </calendar-data>
@@ -165,10 +172,14 @@ mod tests {
report_request, report_request,
ReportRequest::CalendarMultiget(CalendarMultigetRequest { ReportRequest::CalendarMultiget(CalendarMultigetRequest {
prop: rustical_dav::xml::PropfindType::Prop(PropElement(vec![ prop: rustical_dav::xml::PropfindType::Prop(PropElement(vec![
ReportPropName::Propname(Propname("getetag".to_owned())), CalendarObjectPropWrapperName::CalendarObject(CalendarObjectPropName::Getetag),
ReportPropName::Propname(Propname("displayname".to_owned())), CalendarObjectPropWrapperName::CalendarObject(CalendarObjectPropName::CalendarData(
ReportPropName::CalendarData(CalendarData { comp: None, expand: Some(ExpandElement { start: "20250426T220000Z".to_owned(), end: "20250503T220000Z".to_owned() }), limit_recurrence_set: None, limit_freebusy_set: None }) CalendarData { comp: None, expand: Some(ExpandElement {
])), start: <UtcDateTime as ValueDeserialize>::deserialize("20250426T220000Z").unwrap(),
end: <UtcDateTime as ValueDeserialize>::deserialize("20250503T220000Z").unwrap(),
}), limit_recurrence_set: None, limit_freebusy_set: None }
)),
], vec![])),
href: vec![ href: vec![
"/caldav/user/user/6f787542-5256-401a-8db97003260da/ae7a998fdfd1d84a20391168962c62b".to_owned() "/caldav/user/user/6f787542-5256-401a-8db97003260da/ae7a998fdfd1d84a20391168962c62b".to_owned()
] ]
@@ -198,9 +209,12 @@ mod tests {
assert_eq!( assert_eq!(
report_request, report_request,
ReportRequest::CalendarQuery(CalendarQueryRequest { ReportRequest::CalendarQuery(CalendarQueryRequest {
prop: PropfindType::Prop(PropElement(vec![ReportPropName::Propname(Propname( prop: rustical_dav::xml::PropfindType::Prop(PropElement(
"getetag".to_owned() vec![CalendarObjectPropWrapperName::CalendarObject(
))])), CalendarObjectPropName::Getetag
),],
vec![]
)),
filter: Some(FilterElement { filter: Some(FilterElement {
comp_filter: CompFilterElement { comp_filter: CompFilterElement {
is_not_defined: None, is_not_defined: None,
@@ -238,6 +252,7 @@ mod tests {
<D:prop> <D:prop>
<D:getetag/> <D:getetag/>
<D:displayname/> <D:displayname/>
<D:invalid-prop/>
</D:prop> </D:prop>
<D:href>/caldav/user/user/6f787542-5256-401a-8db97003260da/ae7a998fdfd1d84a20391168962c62b</D:href> <D:href>/caldav/user/user/6f787542-5256-401a-8db97003260da/ae7a998fdfd1d84a20391168962c62b</D:href>
</calendar-multiget> </calendar-multiget>
@@ -247,9 +262,9 @@ mod tests {
report_request, report_request,
ReportRequest::CalendarMultiget(CalendarMultigetRequest { ReportRequest::CalendarMultiget(CalendarMultigetRequest {
prop: rustical_dav::xml::PropfindType::Prop(PropElement(vec![ prop: rustical_dav::xml::PropfindType::Prop(PropElement(vec![
ReportPropName::Propname(Propname("getetag".to_owned())), CalendarObjectPropWrapperName::CalendarObject(CalendarObjectPropName::Getetag),
ReportPropName::Propname(Propname("displayname".to_owned())) CalendarObjectPropWrapperName::Common(CommonPropertiesPropName::Displayname),
])), ], vec![(Some(NamespaceOwned(Vec::from("DAV:"))), "invalid-prop".to_string())])),
href: vec![ href: vec![
"/caldav/user/user/6f787542-5256-401a-8db97003260da/ae7a998fdfd1d84a20391168962c62b".to_owned() "/caldav/user/user/6f787542-5256-401a-8db97003260da/ae7a998fdfd1d84a20391168962c62b".to_owned()
] ]

View File

@@ -1,11 +1,12 @@
use super::ReportPropName;
use crate::{ use crate::{
Error, Error,
calendar_object::resource::{CalendarObjectPropWrapper, CalendarObjectResource}, calendar_object::{
CalendarObjectPropWrapper, CalendarObjectPropWrapperName, resource::CalendarObjectResource,
},
}; };
use actix_web::{HttpRequest, http::StatusCode}; use http::StatusCode;
use rustical_dav::{ use rustical_dav::{
resource::Resource, resource::{PrincipalUri, Resource},
xml::{ xml::{
MultistatusElement, multistatus::ResponseElement, sync_collection::SyncCollectionRequest, MultistatusElement, multistatus::ResponseElement, sync_collection::SyncCollectionRequest,
}, },
@@ -17,9 +18,9 @@ use rustical_store::{
}; };
pub async fn handle_sync_collection<C: CalendarStore>( pub async fn handle_sync_collection<C: CalendarStore>(
sync_collection: &SyncCollectionRequest<ReportPropName>, sync_collection: &SyncCollectionRequest<CalendarObjectPropWrapperName>,
props: &[&str], path: &str,
req: HttpRequest, puri: &impl PrincipalUri,
user: &User, user: &User,
principal: &str, principal: &str,
cal_id: &str, cal_id: &str,
@@ -32,22 +33,18 @@ pub async fn handle_sync_collection<C: CalendarStore>(
let mut responses = Vec::new(); let mut responses = Vec::new();
for object in new_objects { for object in new_objects {
let path = format!( let path = format!("{}/{}.ics", path, object.get_id());
"{}/{}.ics",
req.path().trim_end_matches('/'),
object.get_id()
);
responses.push( responses.push(
CalendarObjectResource { CalendarObjectResource {
object, object,
principal: principal.to_owned(), principal: principal.to_owned(),
} }
.propfind(&path, props, user, req.resource_map())?, .propfind(&path, &sync_collection.prop, puri, user)?,
); );
} }
for object_id in deleted_objects { for object_id in deleted_objects {
let path = format!("{}/{}.ics", req.path().trim_end_matches('/'), object_id); let path = format!("{path}/{object_id}.ics");
responses.push(ResponseElement { responses.push(ResponseElement {
href: path, href: path,
status: Some(StatusCode::NOT_FOUND), status: Some(StatusCode::NOT_FOUND),

View File

@@ -1,3 +1,6 @@
pub mod methods; pub mod methods;
pub mod prop; pub mod prop;
pub mod resource; pub mod resource;
mod service;
pub use service::CalendarResourceService;

View File

@@ -1,6 +1,7 @@
use derive_more::derive::{From, Into}; use derive_more::derive::{From, Into};
use rustical_store::calendar::CalendarObjectType; use rustical_ical::CalendarObjectType;
use rustical_xml::{XmlDeserialize, XmlSerialize}; use rustical_xml::{XmlDeserialize, XmlSerialize};
use strum_macros::VariantArray;
#[derive(Debug, Clone, XmlSerialize, XmlDeserialize, PartialEq, From, Into)] #[derive(Debug, Clone, XmlSerialize, XmlDeserialize, PartialEq, From, Into)]
pub struct SupportedCalendarComponent { pub struct SupportedCalendarComponent {
@@ -58,39 +59,12 @@ pub struct SupportedCalendarData {
calendar_data: CalendarData, calendar_data: CalendarData,
} }
#[derive(Debug, Clone, XmlSerialize, PartialEq)] #[derive(Debug, Clone, XmlSerialize, PartialEq, VariantArray)]
pub enum ReportMethod { pub enum ReportMethod {
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
CalendarQuery, CalendarQuery,
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
CalendarMultiget, CalendarMultiget,
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
SyncCollection, SyncCollection,
} }
#[derive(Debug, Clone, XmlSerialize, PartialEq)]
pub struct ReportWrapper {
report: ReportMethod,
}
// RFC 3253 section-3.1.5
#[derive(Debug, Clone, XmlSerialize, PartialEq)]
pub struct SupportedReportSet {
#[xml(flatten)]
supported_report: Vec<ReportWrapper>,
}
impl Default for SupportedReportSet {
fn default() -> Self {
Self {
supported_report: vec![
ReportWrapper {
report: ReportMethod::CalendarQuery,
},
ReportWrapper {
report: ReportMethod::CalendarMultiget,
},
ReportWrapper {
report: ReportMethod::SyncCollection,
},
],
}
}
}

View File

@@ -1,39 +1,25 @@
use super::methods::mkcalendar::route_mkcalendar; use super::prop::{SupportedCalendarComponentSet, SupportedCalendarData};
use super::methods::post::route_post;
use super::methods::report::route_report_calendar;
use super::prop::{SupportedCalendarComponentSet, SupportedCalendarData, SupportedReportSet};
use crate::Error; use crate::Error;
use crate::calendar_object::resource::CalendarObjectResource; use crate::calendar::prop::ReportMethod;
use crate::principal::PrincipalResource;
use actix_web::dev::ResourceMap;
use actix_web::http::Method;
use actix_web::web;
use async_trait::async_trait;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use derive_more::derive::{From, Into}; use derive_more::derive::{From, Into};
use rustical_dav::extensions::{ use rustical_dav::extensions::{
CommonPropertiesExtension, CommonPropertiesProp, SyncTokenExtension, SyncTokenExtensionProp, CommonPropertiesExtension, CommonPropertiesProp, SyncTokenExtension, SyncTokenExtensionProp,
}; };
use rustical_dav::privileges::UserPrivilegeSet; use rustical_dav::privileges::UserPrivilegeSet;
use rustical_dav::resource::{Resource, ResourceService}; use rustical_dav::resource::{PrincipalUri, Resource, ResourceName};
use rustical_dav::xml::{HrefElement, Resourcetype, ResourcetypeInner}; use rustical_dav::xml::{HrefElement, Resourcetype, ResourcetypeInner, SupportedReportSet};
use rustical_dav_push::{DavPushExtension, DavPushExtensionProp}; use rustical_dav_push::DavPushExtension;
use rustical_ical::CalDateTime;
use rustical_store::Calendar;
use rustical_store::auth::User; use rustical_store::auth::User;
use rustical_store::calendar::CalDateTime; use rustical_xml::{EnumVariants, PropName};
use rustical_store::{Calendar, CalendarStore, SubscriptionStore};
use rustical_xml::{EnumUnitVariants, EnumVariants};
use rustical_xml::{XmlDeserialize, XmlSerialize}; use rustical_xml::{XmlDeserialize, XmlSerialize};
use std::marker::PhantomData;
use std::str::FromStr; use std::str::FromStr;
use std::sync::Arc;
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, EnumUnitVariants)] #[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
#[xml(unit_variants_ident = "CalendarPropName")] #[xml(unit_variants_ident = "CalendarPropName")]
pub enum CalendarProp { pub enum CalendarProp {
// WebDAV (RFC 2518)
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
Displayname(Option<String>),
// CalDAV (RFC 4791) // CalDAV (RFC 4791)
#[xml(ns = "rustical_dav::namespace::NS_ICAL")] #[xml(ns = "rustical_dav::namespace::NS_ICAL")]
CalendarColor(Option<String>), CalendarColor(Option<String>),
@@ -55,8 +41,8 @@ pub enum CalendarProp {
#[xml(ns = "rustical_dav::namespace::NS_DAV")] #[xml(ns = "rustical_dav::namespace::NS_DAV")]
MaxResourceSize(i64), MaxResourceSize(i64),
#[xml(skip_deserializing)] #[xml(skip_deserializing)]
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")] #[xml(ns = "rustical_dav::namespace::NS_DAV")]
SupportedReportSet(SupportedReportSet), SupportedReportSet(SupportedReportSet<ReportMethod>),
#[xml(ns = "rustical_dav::namespace::NS_CALENDARSERVER")] #[xml(ns = "rustical_dav::namespace::NS_CALENDARSERVER")]
Source(Option<HrefElement>), Source(Option<HrefElement>),
#[xml(skip_deserializing)] #[xml(skip_deserializing)]
@@ -67,12 +53,12 @@ pub enum CalendarProp {
MaxDateTime(String), MaxDateTime(String),
} }
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, EnumUnitVariants)] #[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
#[xml(unit_variants_ident = "CalendarPropWrapperName", untagged)] #[xml(unit_variants_ident = "CalendarPropWrapperName", untagged)]
pub enum CalendarPropWrapper { pub enum CalendarPropWrapper {
Calendar(CalendarProp), Calendar(CalendarProp),
SyncToken(SyncTokenExtensionProp), SyncToken(SyncTokenExtensionProp),
DavPush(DavPushExtensionProp), // DavPush(DavPushExtensionProp),
Common(CommonPropertiesProp), Common(CommonPropertiesProp),
} }
@@ -82,6 +68,12 @@ pub struct CalendarResource {
pub read_only: bool, pub read_only: bool,
} }
impl ResourceName for CalendarResource {
fn get_name(&self) -> String {
self.cal.id.to_owned()
}
}
impl From<CalendarResource> for Calendar { impl From<CalendarResource> for Calendar {
fn from(value: CalendarResource) -> Self { fn from(value: CalendarResource) -> Self {
value.cal value.cal
@@ -100,15 +92,13 @@ impl DavPushExtension for CalendarResource {
} }
} }
impl CommonPropertiesExtension for CalendarResource {
type PrincipalResource = PrincipalResource;
}
impl Resource for CalendarResource { impl Resource for CalendarResource {
type Prop = CalendarPropWrapper; type Prop = CalendarPropWrapper;
type Error = Error; type Error = Error;
type Principal = User; type Principal = User;
const IS_COLLECTION: bool = true;
fn get_resourcetype(&self) -> Resourcetype { fn get_resourcetype(&self) -> Resourcetype {
if self.cal.subscription_url.is_none() { if self.cal.subscription_url.is_none() {
Resourcetype(&[ Resourcetype(&[
@@ -128,15 +118,12 @@ impl Resource for CalendarResource {
fn get_prop( fn get_prop(
&self, &self,
rmap: &ResourceMap, puri: &impl PrincipalUri,
user: &User, user: &User,
prop: &CalendarPropWrapperName, prop: &CalendarPropWrapperName,
) -> Result<Self::Prop, Self::Error> { ) -> Result<Self::Prop, Self::Error> {
Ok(match prop { Ok(match prop {
CalendarPropWrapperName::Calendar(prop) => CalendarPropWrapper::Calendar(match prop { CalendarPropWrapperName::Calendar(prop) => CalendarPropWrapper::Calendar(match prop {
CalendarPropName::Displayname => {
CalendarProp::Displayname(self.cal.displayname.clone())
}
CalendarPropName::CalendarColor => { CalendarPropName::CalendarColor => {
CalendarProp::CalendarColor(self.cal.color.clone()) CalendarProp::CalendarColor(self.cal.color.clone())
} }
@@ -164,26 +151,26 @@ impl Resource for CalendarResource {
} }
CalendarPropName::MaxResourceSize => CalendarProp::MaxResourceSize(10000000), CalendarPropName::MaxResourceSize => CalendarProp::MaxResourceSize(10000000),
CalendarPropName::SupportedReportSet => { CalendarPropName::SupportedReportSet => {
CalendarProp::SupportedReportSet(SupportedReportSet::default()) CalendarProp::SupportedReportSet(SupportedReportSet::all())
} }
CalendarPropName::Source => CalendarProp::Source( CalendarPropName::Source => CalendarProp::Source(
self.cal.subscription_url.to_owned().map(HrefElement::from), self.cal.subscription_url.to_owned().map(HrefElement::from),
), ),
CalendarPropName::MinDateTime => { CalendarPropName::MinDateTime => {
CalendarProp::MinDateTime(CalDateTime::Utc(DateTime::<Utc>::MIN_UTC).format()) CalendarProp::MinDateTime(CalDateTime::from(DateTime::<Utc>::MIN_UTC).format())
} }
CalendarPropName::MaxDateTime => { CalendarPropName::MaxDateTime => {
CalendarProp::MaxDateTime(CalDateTime::Utc(DateTime::<Utc>::MAX_UTC).format()) CalendarProp::MaxDateTime(CalDateTime::from(DateTime::<Utc>::MAX_UTC).format())
} }
}), }),
CalendarPropWrapperName::SyncToken(prop) => { CalendarPropWrapperName::SyncToken(prop) => {
CalendarPropWrapper::SyncToken(SyncTokenExtension::get_prop(self, prop)?) CalendarPropWrapper::SyncToken(SyncTokenExtension::get_prop(self, prop)?)
} }
CalendarPropWrapperName::DavPush(prop) => { // CalendarPropWrapperName::DavPush(prop) => {
CalendarPropWrapper::DavPush(DavPushExtension::get_prop(self, prop)?) // CalendarPropWrapper::DavPush(DavPushExtension::get_prop(self, prop)?)
} // }
CalendarPropWrapperName::Common(prop) => CalendarPropWrapper::Common( CalendarPropWrapperName::Common(prop) => CalendarPropWrapper::Common(
CommonPropertiesExtension::get_prop(self, rmap, user, prop)?, CommonPropertiesExtension::get_prop(self, puri, user, prop)?,
), ),
}) })
} }
@@ -194,10 +181,6 @@ impl Resource for CalendarResource {
} }
match prop { match prop {
CalendarPropWrapper::Calendar(prop) => match prop { CalendarPropWrapper::Calendar(prop) => match prop {
CalendarProp::Displayname(displayname) => {
self.cal.displayname = displayname;
Ok(())
}
CalendarProp::CalendarColor(color) => { CalendarProp::CalendarColor(color) => {
self.cal.color = color; self.cal.color = color;
Ok(()) Ok(())
@@ -243,7 +226,7 @@ impl Resource for CalendarResource {
CalendarProp::MaxDateTime(_) => Err(rustical_dav::Error::PropReadOnly), CalendarProp::MaxDateTime(_) => Err(rustical_dav::Error::PropReadOnly),
}, },
CalendarPropWrapper::SyncToken(prop) => SyncTokenExtension::set_prop(self, prop), CalendarPropWrapper::SyncToken(prop) => SyncTokenExtension::set_prop(self, prop),
CalendarPropWrapper::DavPush(prop) => DavPushExtension::set_prop(self, prop), // CalendarPropWrapper::DavPush(prop) => DavPushExtension::set_prop(self, prop),
CalendarPropWrapper::Common(prop) => CommonPropertiesExtension::set_prop(self, prop), CalendarPropWrapper::Common(prop) => CommonPropertiesExtension::set_prop(self, prop),
} }
} }
@@ -254,10 +237,6 @@ impl Resource for CalendarResource {
} }
match prop { match prop {
CalendarPropWrapperName::Calendar(prop) => match prop { CalendarPropWrapperName::Calendar(prop) => match prop {
CalendarPropName::Displayname => {
self.cal.displayname = None;
Ok(())
}
CalendarPropName::CalendarColor => { CalendarPropName::CalendarColor => {
self.cal.color = None; self.cal.color = None;
Ok(()) Ok(())
@@ -291,13 +270,21 @@ impl Resource for CalendarResource {
CalendarPropName::MaxDateTime => Err(rustical_dav::Error::PropReadOnly), CalendarPropName::MaxDateTime => Err(rustical_dav::Error::PropReadOnly),
}, },
CalendarPropWrapperName::SyncToken(prop) => SyncTokenExtension::remove_prop(self, prop), CalendarPropWrapperName::SyncToken(prop) => SyncTokenExtension::remove_prop(self, prop),
CalendarPropWrapperName::DavPush(prop) => DavPushExtension::remove_prop(self, prop), // CalendarPropWrapperName::DavPush(prop) => DavPushExtension::remove_prop(self, prop),
CalendarPropWrapperName::Common(prop) => { CalendarPropWrapperName::Common(prop) => {
CommonPropertiesExtension::remove_prop(self, prop) CommonPropertiesExtension::remove_prop(self, prop)
} }
} }
} }
fn get_displayname(&self) -> Option<&str> {
self.cal.displayname.as_deref()
}
fn set_displayname(&mut self, name: Option<String>) -> Result<(), rustical_dav::Error> {
self.cal.displayname = name;
Ok(())
}
fn get_owner(&self) -> Option<&str> { fn get_owner(&self) -> Option<&str> {
Some(&self.cal.principal) Some(&self.cal.principal)
} }
@@ -314,90 +301,3 @@ impl Resource for CalendarResource {
)) ))
} }
} }
pub struct CalendarResourceService<C: CalendarStore, S: SubscriptionStore> {
cal_store: Arc<C>,
__phantom_sub: PhantomData<S>,
}
impl<C: CalendarStore, S: SubscriptionStore> CalendarResourceService<C, S> {
pub fn new(cal_store: Arc<C>) -> Self {
Self {
cal_store,
__phantom_sub: PhantomData,
}
}
}
#[async_trait(?Send)]
impl<C: CalendarStore, S: SubscriptionStore> ResourceService for CalendarResourceService<C, S> {
type MemberType = CalendarObjectResource;
type PathComponents = (String, String); // principal, calendar_id
type Resource = CalendarResource;
type Error = Error;
type Principal = User;
async fn get_resource(
&self,
(principal, cal_id): &Self::PathComponents,
) -> Result<Self::Resource, Error> {
let calendar = self.cal_store.get_calendar(principal, cal_id).await?;
Ok(CalendarResource {
cal: calendar,
read_only: self.cal_store.is_read_only(),
})
}
async fn get_members(
&self,
(principal, cal_id): &Self::PathComponents,
) -> Result<Vec<(String, Self::MemberType)>, Self::Error> {
Ok(self
.cal_store
.get_objects(principal, cal_id)
.await?
.into_iter()
.map(|object| {
(
format!("{}.ics", object.get_id()),
CalendarObjectResource {
object,
principal: principal.to_owned(),
},
)
})
.collect())
}
async fn save_resource(
&self,
(principal, cal_id): &Self::PathComponents,
file: Self::Resource,
) -> Result<(), Self::Error> {
self.cal_store
.update_calendar(principal.to_owned(), cal_id.to_owned(), file.into())
.await?;
Ok(())
}
async fn delete_resource(
&self,
(principal, cal_id): &Self::PathComponents,
use_trashbin: bool,
) -> Result<(), Self::Error> {
self.cal_store
.delete_calendar(principal, cal_id, use_trashbin)
.await?;
Ok(())
}
#[inline]
fn actix_additional_routes(res: actix_web::Resource) -> actix_web::Resource {
let report_method = web::method(Method::from_str("REPORT").unwrap());
let mkcalendar_method = web::method(Method::from_str("MKCALENDAR").unwrap());
res.route(report_method.to(route_report_calendar::<C>))
.route(mkcalendar_method.to(route_mkcalendar::<C>))
.post(route_post::<C, S>)
}
}

View File

@@ -0,0 +1,143 @@
use crate::calendar::methods::get::route_get;
use crate::calendar::methods::mkcalendar::route_mkcalendar;
use crate::calendar::methods::report::route_report_calendar;
use crate::calendar::resource::CalendarResource;
use crate::calendar_object::CalendarObjectResourceService;
use crate::calendar_object::resource::CalendarObjectResource;
use crate::{CalDavPrincipalUri, Error};
use async_trait::async_trait;
use axum::Router;
use axum::extract::Request;
use axum::handler::Handler;
use axum::response::Response;
use futures_util::future::BoxFuture;
use rustical_dav::resource::{AxumMethods, ResourceService};
use rustical_store::auth::User;
use rustical_store::{CalendarStore, SubscriptionStore};
use std::convert::Infallible;
use std::sync::Arc;
use tower::Service;
pub struct CalendarResourceService<C: CalendarStore, S: SubscriptionStore> {
pub(crate) cal_store: Arc<C>,
pub(crate) sub_store: Arc<S>,
}
impl<C: CalendarStore, S: SubscriptionStore> Clone for CalendarResourceService<C, S> {
fn clone(&self) -> Self {
Self {
cal_store: self.cal_store.clone(),
sub_store: self.sub_store.clone(),
}
}
}
impl<C: CalendarStore, S: SubscriptionStore> CalendarResourceService<C, S> {
pub fn new(cal_store: Arc<C>, sub_store: Arc<S>) -> Self {
Self {
cal_store,
sub_store,
}
}
}
#[async_trait]
impl<C: CalendarStore, S: SubscriptionStore> ResourceService for CalendarResourceService<C, S> {
type MemberType = CalendarObjectResource;
type PathComponents = (String, String); // principal, calendar_id
type Resource = CalendarResource;
type Error = Error;
type Principal = User;
type PrincipalUri = CalDavPrincipalUri;
const DAV_HEADER: &str = "1, 3, access-control, calendar-access, calendar-proxy";
async fn get_resource(
&self,
(principal, cal_id): &Self::PathComponents,
) -> Result<Self::Resource, Error> {
let calendar = self.cal_store.get_calendar(principal, cal_id).await?;
Ok(CalendarResource {
cal: calendar,
read_only: self.cal_store.is_read_only(cal_id),
})
}
async fn get_members(
&self,
(principal, cal_id): &Self::PathComponents,
) -> Result<Vec<Self::MemberType>, Self::Error> {
Ok(self
.cal_store
.get_objects(principal, cal_id)
.await?
.into_iter()
.map(|object| CalendarObjectResource {
object,
principal: principal.to_owned(),
})
.collect())
}
async fn save_resource(
&self,
(principal, cal_id): &Self::PathComponents,
file: Self::Resource,
) -> Result<(), Self::Error> {
self.cal_store
.update_calendar(principal.to_owned(), cal_id.to_owned(), file.into())
.await?;
Ok(())
}
async fn delete_resource(
&self,
(principal, cal_id): &Self::PathComponents,
use_trashbin: bool,
) -> Result<(), Self::Error> {
self.cal_store
.delete_calendar(principal, cal_id, use_trashbin)
.await?;
Ok(())
}
fn axum_router<State: Send + Sync + Clone + 'static>(self) -> axum::Router<State> {
Router::new()
.nest(
"/{object_id}",
CalendarObjectResourceService::new(self.cal_store.clone()).axum_router(),
)
.route_service("/", self.axum_service())
}
}
impl<C: CalendarStore, S: SubscriptionStore> AxumMethods for CalendarResourceService<C, S> {
fn report() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> {
Some(|state, req| {
let mut service = Handler::with_state(route_report_calendar::<C, S>, state);
Box::pin(Service::call(&mut service, req))
})
}
fn get() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> {
Some(|state, req| {
let mut service = Handler::with_state(route_get::<C, S>, state);
Box::pin(Service::call(&mut service, req))
})
}
fn mkcalendar() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>>
{
Some(|state, req| {
let mut service = Handler::with_state(route_mkcalendar::<C, S>, state);
Box::pin(Service::call(&mut service, req))
})
}
fn mkcol() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> {
Some(|state, req| {
let mut service = Handler::with_state(route_mkcalendar::<C, S>, state);
Box::pin(Service::call(&mut service, req))
})
}
}

View File

@@ -1,74 +1,85 @@
use crate::Error; use crate::Error;
use actix_web::HttpRequest; use crate::calendar_object::{CalendarObjectPathComponents, CalendarObjectResourceService};
use actix_web::HttpResponse; use crate::error::Precondition;
use actix_web::http::header; use axum::body::Body;
use actix_web::http::header::HeaderValue; use axum::extract::{Path, State};
use actix_web::web::{Data, Path}; use axum::response::{IntoResponse, Response};
use axum_extra::TypedHeader;
use headers::{ContentType, ETag, HeaderMapExt, IfNoneMatch};
use http::{HeaderMap, StatusCode};
use rustical_ical::CalendarObject;
use rustical_store::CalendarStore;
use rustical_store::auth::User; use rustical_store::auth::User;
use rustical_store::{CalendarObject, CalendarStore}; use std::str::FromStr;
use tracing::instrument; use tracing::instrument;
use tracing_actix_web::RootSpan;
use super::resource::CalendarObjectPathComponents; #[instrument(skip(cal_store))]
#[instrument(parent = root_span.id(), skip(store, root_span))]
pub async fn get_event<C: CalendarStore>( pub async fn get_event<C: CalendarStore>(
path: Path<CalendarObjectPathComponents>, Path(CalendarObjectPathComponents {
store: Data<C>,
user: User,
root_span: RootSpan,
) -> Result<HttpResponse, Error> {
let CalendarObjectPathComponents {
principal, principal,
calendar_id, calendar_id,
object_id, object_id,
} = path.into_inner(); }): Path<CalendarObjectPathComponents>,
State(CalendarObjectResourceService { cal_store }): State<CalendarObjectResourceService<C>>,
user: User,
) -> Result<Response, Error> {
if !user.is_principal(&principal) { if !user.is_principal(&principal) {
return Ok(HttpResponse::Unauthorized().body("")); return Err(crate::Error::Unauthorized);
} }
let calendar = store.get_calendar(&principal, &calendar_id).await?; let calendar = cal_store.get_calendar(&principal, &calendar_id).await?;
if !user.is_principal(&calendar.principal) { if !user.is_principal(&calendar.principal) {
return Ok(HttpResponse::Unauthorized().body("")); return Err(crate::Error::Unauthorized);
} }
let event = store let event = cal_store
.get_object(&principal, &calendar_id, &object_id) .get_object(&principal, &calendar_id, &object_id)
.await?; .await?;
Ok(HttpResponse::Ok() let mut resp = Response::builder().status(StatusCode::OK);
.insert_header(("ETag", event.get_etag())) let hdrs = resp.headers_mut().unwrap();
.insert_header(("Content-Type", "text/calendar")) hdrs.typed_insert(ETag::from_str(&event.get_etag()).unwrap());
.body(event.get_ics().to_owned())) hdrs.typed_insert(ContentType::from_str("text/calendar").unwrap());
Ok(resp.body(Body::new(event.get_ics().to_owned())).unwrap())
} }
#[instrument(parent = root_span.id(), skip(store, req, root_span))] #[instrument(skip(cal_store))]
pub async fn put_event<C: CalendarStore>( pub async fn put_event<C: CalendarStore>(
path: Path<CalendarObjectPathComponents>, Path(CalendarObjectPathComponents {
store: Data<C>,
body: String,
user: User,
req: HttpRequest,
root_span: RootSpan,
) -> Result<HttpResponse, Error> {
let CalendarObjectPathComponents {
principal, principal,
calendar_id, calendar_id,
object_id, object_id,
} = path.into_inner(); }): Path<CalendarObjectPathComponents>,
State(CalendarObjectResourceService { cal_store }): State<CalendarObjectResourceService<C>>,
user: User,
mut if_none_match: Option<TypedHeader<IfNoneMatch>>,
header_map: HeaderMap,
body: String,
) -> Result<Response, Error> {
if !user.is_principal(&principal) { if !user.is_principal(&principal) {
return Ok(HttpResponse::Unauthorized().body("")); return Err(crate::Error::Unauthorized);
} }
let overwrite = // https://github.com/hyperium/headers/issues/204
Some(&HeaderValue::from_static("*")) != req.headers().get(header::IF_NONE_MATCH); if !header_map.contains_key("If-None-Match") {
if_none_match = None;
}
let object = CalendarObject::from_ics(object_id, body)?; let overwrite = if let Some(TypedHeader(if_none_match)) = if_none_match {
store if_none_match == IfNoneMatch::any()
} else {
true
};
let object = match CalendarObject::from_ics(object_id, body) {
Ok(obj) => obj,
Err(_) => {
return Err(Error::PreconditionFailed(Precondition::ValidCalendarData));
}
};
cal_store
.put_object(principal, calendar_id, object, overwrite) .put_object(principal, calendar_id, object, overwrite)
.await?; .await?;
Ok(HttpResponse::Created().body("")) Ok(StatusCode::CREATED.into_response())
} }

View File

@@ -1,2 +1,6 @@
pub mod methods; pub mod methods;
pub mod resource; pub mod resource;
mod service;
pub use service::*;
mod prop;
pub use prop::*;

View File

@@ -0,0 +1,45 @@
use rustical_dav::extensions::CommonPropertiesProp;
use rustical_ical::UtcDateTime;
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
#[xml(unit_variants_ident = "CalendarObjectPropName")]
pub enum CalendarObjectProp {
// WebDAV (RFC 2518)
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
Getetag(String),
#[xml(ns = "rustical_dav::namespace::NS_DAV", skip_deserializing)]
Getcontenttype(&'static str),
// CalDAV (RFC 4791)
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
#[xml(prop = "CalendarData")]
CalendarData(String),
}
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
#[xml(unit_variants_ident = "CalendarObjectPropWrapperName", untagged)]
pub enum CalendarObjectPropWrapper {
CalendarObject(CalendarObjectProp),
Common(CommonPropertiesProp),
}
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq, Hash)]
pub(crate) struct ExpandElement {
#[xml(ty = "attr")]
pub(crate) start: UtcDateTime,
#[xml(ty = "attr")]
pub(crate) end: UtcDateTime,
}
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Default, Eq, Hash)]
pub struct CalendarData {
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
pub(crate) comp: Option<()>,
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
pub(crate) expand: Option<ExpandElement>,
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
pub(crate) limit_recurrence_set: Option<()>,
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
pub(crate) limit_freebusy_set: Option<()>,
}

View File

@@ -1,49 +1,14 @@
use super::methods::{get_event, put_event}; use super::prop::*;
use crate::{Error, principal::PrincipalResource}; use crate::Error;
use actix_web::dev::ResourceMap;
use async_trait::async_trait;
use derive_more::derive::{From, Into}; use derive_more::derive::{From, Into};
use rustical_dav::{ use rustical_dav::{
extensions::{CommonPropertiesExtension, CommonPropertiesProp}, extensions::CommonPropertiesExtension,
privileges::UserPrivilegeSet, privileges::UserPrivilegeSet,
resource::{Resource, ResourceService}, resource::{PrincipalUri, Resource, ResourceName},
xml::Resourcetype, xml::Resourcetype,
}; };
use rustical_store::{CalendarObject, CalendarStore, auth::User}; use rustical_ical::CalendarObject;
use rustical_xml::{EnumUnitVariants, EnumVariants, XmlDeserialize, XmlSerialize}; use rustical_store::auth::User;
use serde::Deserialize;
use std::sync::Arc;
pub struct CalendarObjectResourceService<C: CalendarStore> {
cal_store: Arc<C>,
}
impl<C: CalendarStore> CalendarObjectResourceService<C> {
pub fn new(cal_store: Arc<C>) -> Self {
Self { cal_store }
}
}
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, EnumUnitVariants)]
#[xml(unit_variants_ident = "CalendarObjectPropName")]
pub enum CalendarObjectProp {
// WebDAV (RFC 2518)
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
Getetag(String),
#[xml(ns = "rustical_dav::namespace::NS_DAV", skip_deserializing)]
Getcontenttype(&'static str),
// CalDAV (RFC 4791)
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
CalendarData(String),
}
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, EnumUnitVariants)]
#[xml(unit_variants_ident = "CalendarObjectPropWrapperName", untagged)]
pub enum CalendarObjectPropWrapper {
CalendarObject(CalendarObjectProp),
Common(CommonPropertiesProp),
}
#[derive(Clone, From, Into)] #[derive(Clone, From, Into)]
pub struct CalendarObjectResource { pub struct CalendarObjectResource {
@@ -51,8 +16,10 @@ pub struct CalendarObjectResource {
pub principal: String, pub principal: String,
} }
impl CommonPropertiesExtension for CalendarObjectResource { impl ResourceName for CalendarObjectResource {
type PrincipalResource = PrincipalResource; fn get_name(&self) -> String {
format!("{}.ics", self.object.get_id())
}
} }
impl Resource for CalendarObjectResource { impl Resource for CalendarObjectResource {
@@ -60,13 +27,15 @@ impl Resource for CalendarObjectResource {
type Error = Error; type Error = Error;
type Principal = User; type Principal = User;
const IS_COLLECTION: bool = false;
fn get_resourcetype(&self) -> Resourcetype { fn get_resourcetype(&self) -> Resourcetype {
Resourcetype(&[]) Resourcetype(&[])
} }
fn get_prop( fn get_prop(
&self, &self,
rmap: &ResourceMap, puri: &impl PrincipalUri,
user: &User, user: &User,
prop: &CalendarObjectPropWrapperName, prop: &CalendarObjectPropWrapperName,
) -> Result<Self::Prop, Self::Error> { ) -> Result<Self::Prop, Self::Error> {
@@ -76,8 +45,15 @@ impl Resource for CalendarObjectResource {
CalendarObjectPropName::Getetag => { CalendarObjectPropName::Getetag => {
CalendarObjectProp::Getetag(self.object.get_etag()) CalendarObjectProp::Getetag(self.object.get_etag())
} }
CalendarObjectPropName::CalendarData => { CalendarObjectPropName::CalendarData(CalendarData { expand, .. }) => {
CalendarObjectProp::CalendarData(self.object.get_ics().to_owned()) CalendarObjectProp::CalendarData(if let Some(expand) = expand.as_ref() {
self.object.expand_recurrence(
Some(expand.start.to_utc()),
Some(expand.end.to_utc()),
)?
} else {
self.object.get_ics().to_owned()
})
} }
CalendarObjectPropName::Getcontenttype => { CalendarObjectPropName::Getcontenttype => {
CalendarObjectProp::Getcontenttype("text/calendar;charset=utf-8") CalendarObjectProp::Getcontenttype("text/calendar;charset=utf-8")
@@ -85,11 +61,16 @@ impl Resource for CalendarObjectResource {
}) })
} }
CalendarObjectPropWrapperName::Common(prop) => CalendarObjectPropWrapper::Common( CalendarObjectPropWrapperName::Common(prop) => CalendarObjectPropWrapper::Common(
CommonPropertiesExtension::get_prop(self, rmap, user, prop)?, CommonPropertiesExtension::get_prop(self, puri, user, prop)?,
), ),
}) })
} }
fn get_displayname(&self) -> Option<&str> {
// TODO: Extract summary from object
None
}
fn get_owner(&self) -> Option<&str> { fn get_owner(&self) -> Option<&str> {
Some(&self.principal) Some(&self.principal)
} }
@@ -104,57 +85,3 @@ impl Resource for CalendarObjectResource {
)) ))
} }
} }
#[derive(Debug, Clone, Deserialize)]
pub struct CalendarObjectPathComponents {
pub principal: String,
pub calendar_id: String,
pub object_id: String,
}
#[async_trait(?Send)]
impl<C: CalendarStore> ResourceService for CalendarObjectResourceService<C> {
type PathComponents = CalendarObjectPathComponents;
type Resource = CalendarObjectResource;
type MemberType = CalendarObjectResource;
type Error = Error;
type Principal = User;
async fn get_resource(
&self,
CalendarObjectPathComponents {
principal,
calendar_id,
object_id,
}: &Self::PathComponents,
) -> Result<Self::Resource, Self::Error> {
let object = self
.cal_store
.get_object(principal, calendar_id, object_id)
.await?;
Ok(CalendarObjectResource {
object,
principal: principal.to_owned(),
})
}
async fn delete_resource(
&self,
CalendarObjectPathComponents {
principal,
calendar_id,
object_id,
}: &Self::PathComponents,
use_trashbin: bool,
) -> Result<(), Self::Error> {
self.cal_store
.delete_object(principal, calendar_id, object_id, use_trashbin)
.await?;
Ok(())
}
#[inline]
fn actix_additional_routes(res: actix_web::Resource) -> actix_web::Resource {
res.get(get_event::<C>).put(put_event::<C>)
}
}

View File

@@ -0,0 +1,113 @@
use crate::{
CalDavPrincipalUri, Error,
calendar_object::{
methods::{get_event, put_event},
resource::CalendarObjectResource,
},
};
use async_trait::async_trait;
use axum::{extract::Request, handler::Handler, response::Response};
use futures_util::future::BoxFuture;
use rustical_dav::resource::{AxumMethods, ResourceService};
use rustical_store::{CalendarStore, auth::User};
use serde::{Deserialize, Deserializer};
use std::{convert::Infallible, sync::Arc};
use tower::Service;
#[derive(Debug, Clone, Deserialize)]
pub struct CalendarObjectPathComponents {
pub principal: String,
pub calendar_id: String,
#[serde(deserialize_with = "deserialize_ics_name")]
pub object_id: String,
}
pub struct CalendarObjectResourceService<C: CalendarStore> {
pub(crate) cal_store: Arc<C>,
}
impl<C: CalendarStore> Clone for CalendarObjectResourceService<C> {
fn clone(&self) -> Self {
Self {
cal_store: self.cal_store.clone(),
}
}
}
impl<C: CalendarStore> CalendarObjectResourceService<C> {
pub fn new(cal_store: Arc<C>) -> Self {
Self { cal_store }
}
}
#[async_trait]
impl<C: CalendarStore> ResourceService for CalendarObjectResourceService<C> {
type PathComponents = CalendarObjectPathComponents;
type Resource = CalendarObjectResource;
type MemberType = CalendarObjectResource;
type Error = Error;
type Principal = User;
type PrincipalUri = CalDavPrincipalUri;
const DAV_HEADER: &str = "1, 3, access-control, calendar-access";
async fn get_resource(
&self,
CalendarObjectPathComponents {
principal,
calendar_id,
object_id,
}: &Self::PathComponents,
) -> Result<Self::Resource, Self::Error> {
let object = self
.cal_store
.get_object(principal, calendar_id, object_id)
.await?;
Ok(CalendarObjectResource {
object,
principal: principal.to_owned(),
})
}
async fn delete_resource(
&self,
CalendarObjectPathComponents {
principal,
calendar_id,
object_id,
}: &Self::PathComponents,
use_trashbin: bool,
) -> Result<(), Self::Error> {
self.cal_store
.delete_object(principal, calendar_id, object_id, use_trashbin)
.await?;
Ok(())
}
}
impl<C: CalendarStore> AxumMethods for CalendarObjectResourceService<C> {
fn get() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> {
Some(|state, req| {
let mut service = Handler::with_state(get_event::<C>, state);
Box::pin(Service::call(&mut service, req))
})
}
fn put() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> {
Some(|state, req| {
let mut service = Handler::with_state(put_event::<C>, state);
Box::pin(Service::call(&mut service, req))
})
}
}
fn deserialize_ics_name<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: Deserializer<'de>,
{
let name: String = Deserialize::deserialize(deserializer)?;
if let Some(object_id) = name.strip_suffix(".ics") {
Ok(object_id.to_owned())
} else {
Err(serde::de::Error::custom("Missing .ics extension"))
}
}

View File

@@ -1,115 +0,0 @@
use crate::Error;
use crate::calendar::resource::CalendarResource;
use crate::principal::PrincipalResource;
use actix_web::dev::ResourceMap;
use async_trait::async_trait;
use rustical_dav::extensions::{CommonPropertiesExtension, CommonPropertiesProp};
use rustical_dav::privileges::UserPrivilegeSet;
use rustical_dav::resource::{Resource, ResourceService};
use rustical_dav::xml::{Resourcetype, ResourcetypeInner};
use rustical_store::CalendarStore;
use rustical_store::auth::User;
use rustical_xml::{EnumUnitVariants, EnumVariants, XmlDeserialize, XmlSerialize};
use std::sync::Arc;
#[derive(Clone)]
pub struct CalendarSetResource {
pub(crate) principal: String,
pub(crate) read_only: bool,
}
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, EnumUnitVariants)]
#[xml(unit_variants_ident = "PrincipalPropWrapperName", untagged)]
pub enum PrincipalPropWrapper {
Common(CommonPropertiesProp),
}
impl CommonPropertiesExtension for CalendarSetResource {
type PrincipalResource = PrincipalResource;
}
impl Resource for CalendarSetResource {
type Prop = PrincipalPropWrapper;
type Error = Error;
type Principal = User;
fn get_resourcetype(&self) -> Resourcetype {
Resourcetype(&[ResourcetypeInner(
Some(rustical_dav::namespace::NS_DAV),
"collection",
)])
}
fn get_prop(
&self,
rmap: &ResourceMap,
user: &User,
prop: &PrincipalPropWrapperName,
) -> Result<Self::Prop, Self::Error> {
Ok(match prop {
PrincipalPropWrapperName::Common(prop) => PrincipalPropWrapper::Common(
<Self as CommonPropertiesExtension>::get_prop(self, rmap, user, prop)?,
),
})
}
fn get_owner(&self) -> Option<&str> {
Some(&self.principal)
}
fn get_user_privileges(&self, user: &User) -> Result<UserPrivilegeSet, Self::Error> {
Ok(if self.read_only {
UserPrivilegeSet::owner_read(user.is_principal(&self.principal))
} else {
UserPrivilegeSet::owner_only(user.is_principal(&self.principal))
})
}
}
pub struct CalendarSetResourceService<C: CalendarStore> {
cal_store: Arc<C>,
}
impl<C: CalendarStore> CalendarSetResourceService<C> {
pub fn new(cal_store: Arc<C>) -> Self {
Self { cal_store }
}
}
#[async_trait(?Send)]
impl<C: CalendarStore> ResourceService for CalendarSetResourceService<C> {
type PathComponents = (String,);
type MemberType = CalendarResource;
type Resource = CalendarSetResource;
type Error = Error;
type Principal = User;
async fn get_resource(
&self,
(principal,): &Self::PathComponents,
) -> Result<Self::Resource, Self::Error> {
Ok(CalendarSetResource {
principal: principal.to_owned(),
read_only: self.cal_store.is_read_only(),
})
}
async fn get_members(
&self,
(principal,): &Self::PathComponents,
) -> Result<Vec<(String, Self::MemberType)>, Self::Error> {
let calendars = self.cal_store.get_calendars(principal).await?;
Ok(calendars
.into_iter()
.map(|cal| {
(
cal.id.to_owned(),
CalendarResource {
cal,
read_only: self.cal_store.is_read_only(),
},
)
})
.collect())
}
}

View File

@@ -1,6 +1,34 @@
use actix_web::{HttpResponse, http::StatusCode}; use axum::{
body::Body,
response::{IntoResponse, Response},
};
use headers::{ContentType, HeaderMapExt};
use http::StatusCode;
use rustical_xml::{XmlSerialize, XmlSerializeRoot};
use tracing::error; use tracing::error;
#[derive(Debug, thiserror::Error, XmlSerialize)]
pub enum Precondition {
#[error("valid-calendar-data")]
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
ValidCalendarData,
}
impl IntoResponse for Precondition {
fn into_response(self) -> axum::response::Response {
let mut output: Vec<_> = b"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n".into();
let mut writer = quick_xml::Writer::new_with_indent(&mut output, b' ', 4);
let error = rustical_dav::xml::ErrorElement(&self);
if let Err(err) = error.serialize_root(&mut writer) {
return rustical_dav::Error::from(err).into_response();
}
let mut res = Response::builder().status(StatusCode::PRECONDITION_FAILED);
res.headers_mut().unwrap().typed_insert(ContentType::xml());
res.body(Body::from(output)).unwrap()
}
}
#[derive(Debug, thiserror::Error)] #[derive(Debug, thiserror::Error)]
pub enum Error { pub enum Error {
#[error("Unauthorized")] #[error("Unauthorized")]
@@ -23,32 +51,38 @@ pub enum Error {
#[error(transparent)] #[error(transparent)]
XmlDecodeError(#[from] rustical_xml::XmlError), XmlDecodeError(#[from] rustical_xml::XmlError),
#[error(transparent)]
IcalError(#[from] rustical_ical::Error),
#[error(transparent)]
PreconditionFailed(Precondition),
} }
impl actix_web::ResponseError for Error { impl Error {
fn status_code(&self) -> actix_web::http::StatusCode { pub fn status_code(&self) -> StatusCode {
match self { match self {
Error::StoreError(err) => match err { Error::StoreError(err) => match err {
rustical_store::Error::NotFound => StatusCode::NOT_FOUND, rustical_store::Error::NotFound => StatusCode::NOT_FOUND,
rustical_store::Error::InvalidData(_) => StatusCode::BAD_REQUEST,
rustical_store::Error::AlreadyExists => StatusCode::CONFLICT, rustical_store::Error::AlreadyExists => StatusCode::CONFLICT,
rustical_store::Error::ParserError(_) => StatusCode::BAD_REQUEST,
rustical_store::Error::ReadOnly => StatusCode::FORBIDDEN, rustical_store::Error::ReadOnly => StatusCode::FORBIDDEN,
_ => StatusCode::INTERNAL_SERVER_ERROR, _ => StatusCode::INTERNAL_SERVER_ERROR,
}, },
Error::ChronoParseError(_) => StatusCode::INTERNAL_SERVER_ERROR, Error::ChronoParseError(_) => StatusCode::INTERNAL_SERVER_ERROR,
Error::DavError(err) => err.status_code(), Error::DavError(err) => StatusCode::try_from(err.status_code().as_u16())
.expect("Just converting between versions"),
Error::Unauthorized => StatusCode::UNAUTHORIZED, Error::Unauthorized => StatusCode::UNAUTHORIZED,
Error::XmlDecodeError(_) => StatusCode::BAD_REQUEST, Error::XmlDecodeError(_) => StatusCode::BAD_REQUEST,
Error::NotImplemented => StatusCode::INTERNAL_SERVER_ERROR, Error::NotImplemented => StatusCode::INTERNAL_SERVER_ERROR,
Error::NotFound => StatusCode::NOT_FOUND, Error::NotFound => StatusCode::NOT_FOUND,
} Error::IcalError(err) => err.status_code(),
} Error::PreconditionFailed(_err) => StatusCode::PRECONDITION_FAILED,
fn error_response(&self) -> actix_web::HttpResponse<actix_web::body::BoxBody> {
error!("Error: {self}");
match self {
Error::DavError(err) => err.error_response(),
_ => HttpResponse::build(self.status_code()).body(self.to_string()),
} }
} }
} }
impl IntoResponse for Error {
fn into_response(self) -> axum::response::Response {
(self.status_code(), self.to_string()).into_response()
}
}

View File

@@ -1,99 +1,57 @@
use actix_web::HttpResponse; use axum::response::Redirect;
use actix_web::dev::{HttpServiceFactory, ServiceResponse}; use axum::routing::any;
use actix_web::http::header::{self, HeaderName, HeaderValue}; use axum::{Extension, Router};
use actix_web::http::{Method, StatusCode}; use derive_more::Constructor;
use actix_web::middleware::{ErrorHandlerResponse, ErrorHandlers}; use principal::PrincipalResourceService;
use actix_web::web::{self, Data}; use rustical_dav::resource::{PrincipalUri, ResourceService};
use calendar::resource::CalendarResourceService;
use calendar_object::resource::CalendarObjectResourceService;
use calendar_set::CalendarSetResourceService;
use principal::{PrincipalResource, PrincipalResourceService};
use rustical_dav::resource::{NamedRoute, ResourceService, ResourceServiceRoute};
use rustical_dav::resources::RootResourceService; use rustical_dav::resources::RootResourceService;
use rustical_store::auth::{AuthenticationMiddleware, AuthenticationProvider, User}; use rustical_store::auth::middleware::AuthenticationLayer;
use rustical_store::{AddressbookStore, CalendarStore, ContactBirthdayStore, SubscriptionStore}; use rustical_store::auth::{AuthenticationProvider, User};
use rustical_store::{CalendarStore, SubscriptionStore};
use std::sync::Arc; use std::sync::Arc;
use subscription::subscription_resource;
pub mod calendar; pub mod calendar;
pub mod calendar_object; pub mod calendar_object;
pub mod calendar_set;
pub mod error; pub mod error;
pub mod principal; pub mod principal;
mod subscription; // mod subscription;
pub use error::Error; pub use error::Error;
pub fn caldav_service< #[derive(Debug, Clone, Constructor)]
AP: AuthenticationProvider, pub struct CalDavPrincipalUri(&'static str);
AS: AddressbookStore,
C: CalendarStore, impl PrincipalUri for CalDavPrincipalUri {
S: SubscriptionStore, fn principal_collection(&self) -> String {
>( format!("{}/principal/", self.0)
}
fn principal_uri(&self, principal: &str) -> String {
format!("{}{}/", self.principal_collection(), principal)
}
}
pub fn caldav_router<AP: AuthenticationProvider, C: CalendarStore, S: SubscriptionStore>(
prefix: &'static str,
auth_provider: Arc<AP>, auth_provider: Arc<AP>,
store: Arc<C>, store: Arc<C>,
addr_store: Arc<AS>,
subscription_store: Arc<S>, subscription_store: Arc<S>,
) -> impl HttpServiceFactory { ) -> Router {
let birthday_store = Arc::new(ContactBirthdayStore::new(addr_store)); let principal_service = PrincipalResourceService {
auth_provider: auth_provider.clone(),
sub_store: subscription_store.clone(),
cal_store: store.clone(),
};
web::scope("") Router::new()
.wrap(AuthenticationMiddleware::new(auth_provider.clone())) .nest(
.wrap( prefix,
ErrorHandlers::new().handler(StatusCode::METHOD_NOT_ALLOWED, |res| { RootResourceService::<_, User, CalDavPrincipalUri>::new(principal_service.clone())
Ok(ErrorHandlerResponse::Response( .axum_router()
if res.request().method() == Method::OPTIONS { .layer(AuthenticationLayer::new(auth_provider))
let mut response = HttpResponse::Ok(); .layer(Extension(CalDavPrincipalUri(prefix))),
response.insert_header(( )
HeaderName::from_static("dav"), .route(
// https://datatracker.ietf.org/doc/html/rfc4918#section-18 "/.well-known/caldav",
HeaderValue::from_static( any(async || Redirect::permanent(prefix)),
"1, 3, access-control, calendar-access, extended-mkcol, calendar-no-timezone, webdav-push", )
),
));
if let Some(allow) = res.headers().get(header::ALLOW) {
response.insert_header((header::ALLOW, allow.to_owned()));
}
ServiceResponse::new(res.into_parts().0, response.finish()).map_into_right_body()
} else {
res.map_into_left_body()
},
))
}),
)
.app_data(Data::from(store.clone()))
.app_data(Data::from(birthday_store.clone()))
.app_data(Data::from(subscription_store))
.service(RootResourceService::<PrincipalResource, User>::default().actix_resource())
.service(
web::scope("/principal").service(
web::scope("/{principal}")
.service(PrincipalResourceService{auth_provider, home_set: &[
("calendar", false), ("birthdays", true)
]}.actix_resource().name(PrincipalResource::route_name()))
.service(web::scope("/calendar")
.service(CalendarSetResourceService::new(store.clone()).actix_resource())
.service(
web::scope("/{calendar_id}")
.service(
ResourceServiceRoute(CalendarResourceService::<_, S>::new(store.clone()))
)
.service(web::scope("/{object_id}.ics").service(CalendarObjectResourceService::new(store.clone()).actix_resource()
))
)
)
.service(web::scope("/birthdays")
.service(CalendarSetResourceService::new(birthday_store.clone()).actix_resource())
.service(
web::scope("/{calendar_id}")
.service(
ResourceServiceRoute(CalendarResourceService::<_, S>::new(birthday_store.clone()))
)
.service(web::scope("/{object_id}.ics").service(CalendarObjectResourceService::new(birthday_store.clone()).actix_resource()
))
)
)
),
).service(subscription_resource::<S>())
} }

View File

@@ -1,101 +1,55 @@
use std::sync::Arc;
use crate::Error; use crate::Error;
use crate::calendar_set::CalendarSetResource; use rustical_dav::extensions::CommonPropertiesExtension;
use actix_web::dev::ResourceMap;
use async_trait::async_trait;
use rustical_dav::extensions::{CommonPropertiesExtension, CommonPropertiesProp};
use rustical_dav::privileges::UserPrivilegeSet; use rustical_dav::privileges::UserPrivilegeSet;
use rustical_dav::resource::{NamedRoute, Resource, ResourceService}; use rustical_dav::resource::{PrincipalUri, Resource, ResourceName};
use rustical_dav::xml::{HrefElement, Resourcetype, ResourcetypeInner}; use rustical_dav::xml::{
use rustical_store::auth::user::PrincipalType; GroupMemberSet, GroupMembership, Resourcetype, ResourcetypeInner, SupportedReportSet,
use rustical_store::auth::{AuthenticationProvider, User}; };
use rustical_xml::{EnumUnitVariants, EnumVariants, XmlDeserialize, XmlSerialize}; use rustical_store::auth::User;
mod service;
pub use service::*;
mod prop;
pub use prop::*;
#[derive(Clone)] #[derive(Clone)]
pub struct PrincipalResource { pub struct PrincipalResource {
principal: User, principal: User,
home_set: &'static [(&'static str, bool)], members: Vec<String>,
} }
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone)] impl ResourceName for PrincipalResource {
pub struct CalendarHomeSet(#[xml(ty = "untagged", flatten)] Vec<HrefElement>); fn get_name(&self) -> String {
self.principal.id.to_owned()
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, EnumUnitVariants)]
#[xml(unit_variants_ident = "PrincipalPropName")]
pub enum PrincipalProp {
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
Displayname(String),
// Scheduling Extensions to CalDAV (RFC 6638)
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", skip_deserializing)]
CalendarUserType(PrincipalType),
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
CalendarUserAddressSet(HrefElement),
// WebDAV Access Control (RFC 3744)
#[xml(ns = "rustical_dav::namespace::NS_DAV", rename = b"principal-URL")]
PrincipalUrl(HrefElement),
// CalDAV (RFC 4791)
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
CalendarHomeSet(CalendarHomeSet),
}
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, EnumUnitVariants)]
#[xml(unit_variants_ident = "PrincipalPropWrapperName", untagged)]
pub enum PrincipalPropWrapper {
Principal(PrincipalProp),
Common(CommonPropertiesProp),
}
impl PrincipalResource {
pub fn get_principal_url(rmap: &ResourceMap, principal: &str) -> String {
Self::get_url(rmap, vec![principal]).unwrap()
} }
} }
impl NamedRoute for PrincipalResource {
fn route_name() -> &'static str {
"caldav_principal"
}
}
impl CommonPropertiesExtension for PrincipalResource {
type PrincipalResource = Self;
}
impl Resource for PrincipalResource { impl Resource for PrincipalResource {
type Prop = PrincipalPropWrapper; type Prop = PrincipalPropWrapper;
type Error = Error; type Error = Error;
type Principal = User; type Principal = User;
const IS_COLLECTION: bool = true;
fn get_resourcetype(&self) -> Resourcetype { fn get_resourcetype(&self) -> Resourcetype {
Resourcetype(&[ Resourcetype(&[
ResourcetypeInner(Some(rustical_dav::namespace::NS_DAV), "collection"), ResourcetypeInner(Some(rustical_dav::namespace::NS_DAV), "collection"),
ResourcetypeInner(Some(rustical_dav::namespace::NS_DAV), "principal"), ResourcetypeInner(Some(rustical_dav::namespace::NS_DAV), "principal"),
// https://github.com/apple/ccs-calendarserver/blob/13c706b985fb728b9aab42dc0fef85aae21921c3/doc/Extensions/caldav-proxy.txt
ResourcetypeInner(
Some(rustical_dav::namespace::NS_CALENDARSERVER),
"calendar-proxy-write",
),
]) ])
} }
fn get_prop( fn get_prop(
&self, &self,
rmap: &ResourceMap, puri: &impl PrincipalUri,
user: &User, user: &User,
prop: &PrincipalPropWrapperName, prop: &PrincipalPropWrapperName,
) -> Result<Self::Prop, Self::Error> { ) -> Result<Self::Prop, Self::Error> {
let principal_url = Self::get_url(rmap, vec![&self.principal.id]).unwrap(); let principal_url = puri.principal_uri(&self.principal.id);
let home_set = CalendarHomeSet(
user.memberships()
.into_iter()
.map(|principal| Self::get_url(rmap, vec![principal]).unwrap())
.flat_map(|principal_url| {
self.home_set.iter().map(move |&(home_name, _read_only)| {
HrefElement::new(format!("{}/{}", &principal_url, home_name))
})
})
.collect(),
);
Ok(match prop { Ok(match prop {
PrincipalPropWrapperName::Principal(prop) => { PrincipalPropWrapperName::Principal(prop) => {
@@ -103,27 +57,56 @@ impl Resource for PrincipalResource {
PrincipalPropName::CalendarUserType => { PrincipalPropName::CalendarUserType => {
PrincipalProp::CalendarUserType(self.principal.principal_type.to_owned()) PrincipalProp::CalendarUserType(self.principal.principal_type.to_owned())
} }
PrincipalPropName::Displayname => PrincipalProp::Displayname(
self.principal
.displayname
.to_owned()
.unwrap_or(self.principal.id.to_owned()),
),
PrincipalPropName::PrincipalUrl => { PrincipalPropName::PrincipalUrl => {
PrincipalProp::PrincipalUrl(principal_url.into()) PrincipalProp::PrincipalUrl(principal_url.into())
} }
PrincipalPropName::CalendarHomeSet => PrincipalProp::CalendarHomeSet(home_set), PrincipalPropName::CalendarHomeSet => {
PrincipalProp::CalendarHomeSet(principal_url.into())
}
PrincipalPropName::CalendarUserAddressSet => { PrincipalPropName::CalendarUserAddressSet => {
PrincipalProp::CalendarUserAddressSet(principal_url.into()) PrincipalProp::CalendarUserAddressSet(principal_url.into())
} }
PrincipalPropName::GroupMemberSet => {
PrincipalProp::GroupMemberSet(GroupMemberSet(
self.members
.iter()
.map(|principal| puri.principal_uri(principal).into())
.collect(),
))
}
PrincipalPropName::GroupMembership => {
PrincipalProp::GroupMembership(GroupMembership(
self.principal
.memberships_without_self()
.iter()
.map(|principal| puri.principal_uri(principal).into())
.collect(),
))
}
PrincipalPropName::AlternateUriSet => PrincipalProp::AlternateUriSet,
// PrincipalPropName::PrincipalCollectionSet => {
// PrincipalProp::PrincipalCollectionSet(puri.principal_collection().into())
// }
PrincipalPropName::SupportedReportSet => {
PrincipalProp::SupportedReportSet(SupportedReportSet::all())
}
}) })
} }
PrincipalPropWrapperName::Common(prop) => PrincipalPropWrapper::Common( PrincipalPropWrapperName::Common(prop) => PrincipalPropWrapper::Common(
<Self as CommonPropertiesExtension>::get_prop(self, rmap, user, prop)?, <Self as CommonPropertiesExtension>::get_prop(self, puri, user, prop)?,
), ),
}) })
} }
fn get_displayname(&self) -> Option<&str> {
Some(
self.principal
.displayname
.as_ref()
.unwrap_or(&self.principal.id),
)
}
fn get_owner(&self) -> Option<&str> { fn get_owner(&self) -> Option<&str> {
Some(&self.principal.id) Some(&self.principal.id)
} }
@@ -134,51 +117,3 @@ impl Resource for PrincipalResource {
)) ))
} }
} }
pub struct PrincipalResourceService<AP: AuthenticationProvider> {
pub auth_provider: Arc<AP>,
pub home_set: &'static [(&'static str, bool)],
}
#[async_trait(?Send)]
impl<AP: AuthenticationProvider> ResourceService for PrincipalResourceService<AP> {
type PathComponents = (String,);
type MemberType = CalendarSetResource;
type Resource = PrincipalResource;
type Error = Error;
type Principal = User;
async fn get_resource(
&self,
(principal,): &Self::PathComponents,
) -> Result<Self::Resource, Self::Error> {
let user = self
.auth_provider
.get_principal(principal)
.await?
.ok_or(crate::Error::NotFound)?;
Ok(PrincipalResource {
principal: user,
home_set: self.home_set,
})
}
async fn get_members(
&self,
(principal,): &Self::PathComponents,
) -> Result<Vec<(String, Self::MemberType)>, Self::Error> {
Ok(self
.home_set
.iter()
.map(|&(set_name, read_only)| {
(
set_name.to_string(),
CalendarSetResource {
principal: principal.to_owned(),
read_only,
},
)
})
.collect())
}
}

View File

@@ -0,0 +1,49 @@
use rustical_dav::{
extensions::CommonPropertiesProp,
xml::{GroupMemberSet, GroupMembership, HrefElement, SupportedReportSet},
};
use rustical_store::auth::user::PrincipalType;
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
use strum_macros::VariantArray;
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
#[xml(unit_variants_ident = "PrincipalPropName")]
pub enum PrincipalProp {
// Scheduling Extensions to CalDAV (RFC 6638)
#[xml(ns = "rustical_dav::namespace::NS_CALDAV", skip_deserializing)]
CalendarUserType(PrincipalType),
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
CalendarUserAddressSet(HrefElement),
// WebDAV Access Control (RFC 3744)
#[xml(ns = "rustical_dav::namespace::NS_DAV", rename = b"principal-URL")]
PrincipalUrl(HrefElement),
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
GroupMembership(GroupMembership),
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
GroupMemberSet(GroupMemberSet),
#[xml(ns = "rustical_dav::namespace::NS_DAV", rename = b"alternate-URI-set")]
AlternateUriSet,
// #[xml(ns = "rustical_dav::namespace::NS_DAV")]
// PrincipalCollectionSet(HrefElement),
#[xml(ns = "rustical_dav::namespace::NS_DAV", skip_deserializing)]
SupportedReportSet(SupportedReportSet<ReportMethod>),
// CalDAV (RFC 4791)
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
CalendarHomeSet(HrefElement),
}
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
#[xml(unit_variants_ident = "PrincipalPropWrapperName", untagged)]
pub enum PrincipalPropWrapper {
Principal(PrincipalProp),
Common(CommonPropertiesProp),
}
#[derive(XmlSerialize, PartialEq, Clone, VariantArray)]
pub enum ReportMethod {
// We don't actually support principal-match
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
PrincipalMatch,
}

View File

@@ -0,0 +1,92 @@
use crate::calendar::CalendarResourceService;
use crate::calendar::resource::CalendarResource;
use crate::principal::PrincipalResource;
use crate::{CalDavPrincipalUri, Error};
use async_trait::async_trait;
use axum::Router;
use rustical_dav::resource::{AxumMethods, ResourceService};
use rustical_store::auth::{AuthenticationProvider, User};
use rustical_store::{CalendarStore, SubscriptionStore};
use std::sync::Arc;
#[derive(Debug)]
pub struct PrincipalResourceService<
AP: AuthenticationProvider,
S: SubscriptionStore,
CS: CalendarStore,
> {
pub(crate) auth_provider: Arc<AP>,
pub(crate) sub_store: Arc<S>,
pub(crate) cal_store: Arc<CS>,
}
impl<AP: AuthenticationProvider, S: SubscriptionStore, CS: CalendarStore> Clone
for PrincipalResourceService<AP, S, CS>
{
fn clone(&self) -> Self {
Self {
auth_provider: self.auth_provider.clone(),
sub_store: self.sub_store.clone(),
cal_store: self.cal_store.clone(),
}
}
}
#[async_trait]
impl<AP: AuthenticationProvider, S: SubscriptionStore, CS: CalendarStore> ResourceService
for PrincipalResourceService<AP, S, CS>
{
type PathComponents = (String,);
type MemberType = CalendarResource;
type Resource = PrincipalResource;
type Error = Error;
type Principal = User;
type PrincipalUri = CalDavPrincipalUri;
const DAV_HEADER: &str = "1, 3, access-control, calendar-access, calendar-proxy";
async fn get_resource(
&self,
(principal,): &Self::PathComponents,
) -> Result<Self::Resource, Self::Error> {
let user = self
.auth_provider
.get_principal(principal)
.await?
.ok_or(crate::Error::NotFound)?;
Ok(PrincipalResource {
members: self.auth_provider.list_members(&user.id).await?,
principal: user,
})
}
async fn get_members(
&self,
(principal,): &Self::PathComponents,
) -> Result<Vec<Self::MemberType>, Self::Error> {
let calendars = self.cal_store.get_calendars(principal).await?;
Ok(calendars
.into_iter()
.map(|cal| CalendarResource {
read_only: self.cal_store.is_read_only(&cal.id),
cal,
})
.collect())
}
fn axum_router<State: Send + Sync + Clone + 'static>(self) -> axum::Router<State> {
Router::new()
.nest(
"/{calendar_id}",
CalendarResourceService::new(self.cal_store.clone(), self.sub_store.clone())
.axum_router(),
)
.route_service("/", self.axum_service())
}
}
impl<AP: AuthenticationProvider, S: SubscriptionStore, CS: CalendarStore> AxumMethods
for PrincipalResourceService<AP, S, CS>
{
}

View File

@@ -1,6 +1,8 @@
use std::sync::Arc;
use actix_web::{ use actix_web::{
web::{self, Data, Path},
HttpResponse, HttpResponse,
web::{self, Data, Path},
}; };
use rustical_dav::xml::multistatus::PropstatElement; use rustical_dav::xml::multistatus::PropstatElement;
use rustical_store::SubscriptionStore; use rustical_store::SubscriptionStore;
@@ -17,8 +19,9 @@ async fn handle_delete<S: SubscriptionStore>(
Ok(HttpResponse::NoContent().body("Unregistered")) Ok(HttpResponse::NoContent().body("Unregistered"))
} }
pub fn subscription_resource<S: SubscriptionStore>() -> actix_web::Resource { pub fn subscription_resource<S: SubscriptionStore>(sub_store: Arc<S>) -> actix_web::Resource {
web::resource("/subscription/{id}") web::resource("/subscription/{id}")
.app_data(Data::from(sub_store))
.name("subscription") .name("subscription")
.delete(handle_delete::<S>) .delete(handle_delete::<S>)
} }

View File

@@ -4,18 +4,19 @@ version.workspace = true
edition.workspace = true edition.workspace = true
description.workspace = true description.workspace = true
repository.workspace = true repository.workspace = true
license.workspace = true
publish = false publish = false
[dependencies] [dependencies]
actix-web = { workspace = true } axum.workspace = true
axum-extra.workspace = true
tower.workspace = true
async-trait = { workspace = true } async-trait = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
quick-xml = { workspace = true } quick-xml = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
tracing-actix-web = { workspace = true }
futures-util = { workspace = true } futures-util = { workspace = true }
derive_more = { workspace = true } derive_more = { workspace = true }
actix-web-httpauth = { workspace = true }
base64 = { workspace = true } base64 = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
@@ -26,3 +27,10 @@ chrono = { workspace = true }
rustical_xml.workspace = true rustical_xml.workspace = true
uuid.workspace = true uuid.workspace = true
rustical_dav_push.workspace = true rustical_dav_push.workspace = true
rustical_ical.workspace = true
http.workspace = true
tower-http.workspace = true
percent-encoding.workspace = true
ical.workspace = true
strum.workspace = true
strum_macros.workspace = true

View File

@@ -1,36 +1,36 @@
use super::resource::AddressObjectPathComponents; use super::AddressObjectPathComponents;
use super::AddressObjectResourceService;
use crate::Error; use crate::Error;
use crate::addressbook::resource::AddressbookResource; use crate::addressbook::resource::AddressbookResource;
use actix_web::HttpRequest; use axum::body::Body;
use actix_web::HttpResponse; use axum::extract::{Path, State};
use actix_web::http::header; use axum::response::{IntoResponse, Response};
use actix_web::http::header::HeaderValue; use axum_extra::TypedHeader;
use actix_web::web::{Data, Path}; use axum_extra::headers::{ContentType, ETag, HeaderMapExt, IfNoneMatch};
use http::{HeaderMap, StatusCode};
use rustical_dav::privileges::UserPrivilege; use rustical_dav::privileges::UserPrivilege;
use rustical_dav::resource::Resource; use rustical_dav::resource::Resource;
use rustical_ical::AddressObject;
use rustical_store::AddressbookStore;
use rustical_store::auth::User; use rustical_store::auth::User;
use rustical_store::{AddressObject, AddressbookStore}; use std::str::FromStr;
use tracing::instrument; use tracing::instrument;
use tracing_actix_web::RootSpan;
#[instrument(parent = root_span.id(), skip(store, root_span))] #[instrument(skip(addr_store))]
pub async fn get_object<AS: AddressbookStore>( pub async fn get_object<AS: AddressbookStore>(
path: Path<AddressObjectPathComponents>, Path(AddressObjectPathComponents {
store: Data<AS>,
user: User,
root_span: RootSpan,
) -> Result<HttpResponse, Error> {
let AddressObjectPathComponents {
principal, principal,
addressbook_id, addressbook_id,
object_id, object_id,
} = path.into_inner(); }): Path<AddressObjectPathComponents>,
State(AddressObjectResourceService { addr_store }): State<AddressObjectResourceService<AS>>,
user: User,
) -> Result<Response, Error> {
if !user.is_principal(&principal) { if !user.is_principal(&principal) {
return Err(Error::Unauthorized); return Err(Error::Unauthorized);
} }
let addressbook = store let addressbook = addr_store
.get_addressbook(&principal, &addressbook_id, false) .get_addressbook(&principal, &addressbook_id, false)
.await?; .await?;
let addressbook_resource = AddressbookResource(addressbook); let addressbook_resource = AddressbookResource(addressbook);
@@ -41,42 +41,49 @@ pub async fn get_object<AS: AddressbookStore>(
return Err(Error::Unauthorized); return Err(Error::Unauthorized);
} }
let object = store let object = addr_store
.get_object(&principal, &addressbook_id, &object_id, false) .get_object(&principal, &addressbook_id, &object_id, false)
.await?; .await?;
Ok(HttpResponse::Ok() let mut resp = Response::builder().status(StatusCode::OK);
.insert_header(("ETag", object.get_etag())) let hdrs = resp.headers_mut().unwrap();
.insert_header(("Content-Type", "text/vcard")) hdrs.typed_insert(ETag::from_str(&object.get_etag()).unwrap());
.body(object.get_vcf().to_owned())) hdrs.typed_insert(ContentType::from_str("text/vcard").unwrap());
Ok(resp.body(Body::new(object.get_vcf().to_owned())).unwrap())
} }
#[instrument(parent = root_span.id(), skip(store, req, root_span))] #[instrument(skip(addr_store, body))]
pub async fn put_object<AS: AddressbookStore>( pub async fn put_object<AS: AddressbookStore>(
path: Path<AddressObjectPathComponents>, Path(AddressObjectPathComponents {
store: Data<AS>,
body: String,
user: User,
req: HttpRequest,
root_span: RootSpan,
) -> Result<HttpResponse, Error> {
let AddressObjectPathComponents {
principal, principal,
addressbook_id, addressbook_id,
object_id, object_id,
} = path.into_inner(); }): Path<AddressObjectPathComponents>,
State(AddressObjectResourceService { addr_store }): State<AddressObjectResourceService<AS>>,
user: User,
mut if_none_match: Option<TypedHeader<IfNoneMatch>>,
header_map: HeaderMap,
body: String,
) -> Result<Response, Error> {
if !user.is_principal(&principal) { if !user.is_principal(&principal) {
return Err(Error::Unauthorized); return Err(Error::Unauthorized);
} }
let overwrite = // https://github.com/hyperium/headers/issues/204
Some(&HeaderValue::from_static("*")) != req.headers().get(header::IF_NONE_MATCH); if !header_map.contains_key("If-None-Match") {
if_none_match = None;
}
let overwrite = if let Some(TypedHeader(if_none_match)) = if_none_match {
if_none_match == IfNoneMatch::any()
} else {
true
};
let object = AddressObject::from_vcf(object_id, body)?; let object = AddressObject::from_vcf(object_id, body)?;
store addr_store
.put_object(principal, addressbook_id, object, overwrite) .put_object(principal, addressbook_id, object, overwrite)
.await?; .await?;
Ok(HttpResponse::Created().finish()) Ok(StatusCode::CREATED.into_response())
} }

View File

@@ -1,2 +1,6 @@
pub mod methods; pub mod methods;
pub mod resource; pub mod resource;
mod service;
pub use service::*;
mod prop;
pub use prop::*;

View File

@@ -0,0 +1,23 @@
use rustical_dav::extensions::CommonPropertiesProp;
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
#[xml(unit_variants_ident = "AddressObjectPropName")]
pub enum AddressObjectProp {
// WebDAV (RFC 2518)
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
Getetag(String),
#[xml(ns = "rustical_dav::namespace::NS_DAV", skip_deserializing)]
Getcontenttype(&'static str),
// CalDAV (RFC 4791)
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
AddressData(String),
}
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
#[xml(unit_variants_ident = "AddressObjectPropWrapperName", untagged)]
pub enum AddressObjectPropWrapper {
AddressObject(AddressObjectProp),
Common(CommonPropertiesProp),
}

View File

@@ -1,45 +1,19 @@
use crate::{Error, principal::PrincipalResource}; use crate::{
use actix_web::dev::ResourceMap; Error,
use async_trait::async_trait; address_object::{
use derive_more::derive::{Constructor, From, Into}; AddressObjectProp, AddressObjectPropName, AddressObjectPropWrapper,
AddressObjectPropWrapperName,
},
};
use derive_more::derive::{From, Into};
use rustical_dav::{ use rustical_dav::{
extensions::{CommonPropertiesExtension, CommonPropertiesProp}, extensions::CommonPropertiesExtension,
privileges::UserPrivilegeSet, privileges::UserPrivilegeSet,
resource::{Resource, ResourceService}, resource::{PrincipalUri, Resource, ResourceName},
xml::Resourcetype, xml::Resourcetype,
}; };
use rustical_store::{AddressObject, AddressbookStore, auth::User}; use rustical_ical::AddressObject;
use rustical_xml::{EnumUnitVariants, EnumVariants, XmlDeserialize, XmlSerialize}; use rustical_store::auth::User;
use serde::Deserialize;
use std::sync::Arc;
use super::methods::{get_object, put_object};
#[derive(Constructor)]
pub struct AddressObjectResourceService<AS: AddressbookStore> {
addr_store: Arc<AS>,
}
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, EnumUnitVariants)]
#[xml(unit_variants_ident = "AddressObjectPropName")]
pub enum AddressObjectProp {
// WebDAV (RFC 2518)
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
Getetag(String),
#[xml(ns = "rustical_dav::namespace::NS_DAV", skip_deserializing)]
Getcontenttype(&'static str),
// CalDAV (RFC 4791)
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
AddressData(String),
}
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, EnumUnitVariants)]
#[xml(unit_variants_ident = "AddressObjectPropWrapperName", untagged)]
pub enum AddressObjectPropWrapper {
AddressObject(AddressObjectProp),
Common(CommonPropertiesProp),
}
#[derive(Clone, From, Into)] #[derive(Clone, From, Into)]
pub struct AddressObjectResource { pub struct AddressObjectResource {
@@ -47,8 +21,10 @@ pub struct AddressObjectResource {
pub principal: String, pub principal: String,
} }
impl CommonPropertiesExtension for AddressObjectResource { impl ResourceName for AddressObjectResource {
type PrincipalResource = PrincipalResource; fn get_name(&self) -> String {
format!("{}.vcf", self.object.get_id())
}
} }
impl Resource for AddressObjectResource { impl Resource for AddressObjectResource {
@@ -56,13 +32,15 @@ impl Resource for AddressObjectResource {
type Error = Error; type Error = Error;
type Principal = User; type Principal = User;
const IS_COLLECTION: bool = false;
fn get_resourcetype(&self) -> Resourcetype { fn get_resourcetype(&self) -> Resourcetype {
Resourcetype(&[]) Resourcetype(&[])
} }
fn get_prop( fn get_prop(
&self, &self,
rmap: &ResourceMap, puri: &impl PrincipalUri,
user: &User, user: &User,
prop: &AddressObjectPropWrapperName, prop: &AddressObjectPropWrapperName,
) -> Result<Self::Prop, Self::Error> { ) -> Result<Self::Prop, Self::Error> {
@@ -81,11 +59,15 @@ impl Resource for AddressObjectResource {
}) })
} }
AddressObjectPropWrapperName::Common(prop) => AddressObjectPropWrapper::Common( AddressObjectPropWrapperName::Common(prop) => AddressObjectPropWrapper::Common(
CommonPropertiesExtension::get_prop(self, rmap, user, prop)?, CommonPropertiesExtension::get_prop(self, puri, user, prop)?,
), ),
}) })
} }
fn get_displayname(&self) -> Option<&str> {
self.object.get_full_name()
}
fn get_owner(&self) -> Option<&str> { fn get_owner(&self) -> Option<&str> {
Some(&self.principal) Some(&self.principal)
} }
@@ -100,57 +82,3 @@ impl Resource for AddressObjectResource {
)) ))
} }
} }
#[derive(Debug, Clone, Deserialize)]
pub struct AddressObjectPathComponents {
pub principal: String,
pub addressbook_id: String,
pub object_id: String,
}
#[async_trait(?Send)]
impl<AS: AddressbookStore> ResourceService for AddressObjectResourceService<AS> {
type PathComponents = AddressObjectPathComponents;
type Resource = AddressObjectResource;
type MemberType = AddressObjectResource;
type Error = Error;
type Principal = User;
async fn get_resource(
&self,
AddressObjectPathComponents {
principal,
addressbook_id,
object_id,
}: &Self::PathComponents,
) -> Result<Self::Resource, Self::Error> {
let object = self
.addr_store
.get_object(principal, addressbook_id, object_id, false)
.await?;
Ok(AddressObjectResource {
object,
principal: principal.to_owned(),
})
}
async fn delete_resource(
&self,
AddressObjectPathComponents {
principal,
addressbook_id,
object_id,
}: &Self::PathComponents,
use_trashbin: bool,
) -> Result<(), Self::Error> {
self.addr_store
.delete_object(principal, addressbook_id, object_id, use_trashbin)
.await?;
Ok(())
}
#[inline]
fn actix_additional_routes(res: actix_web::Resource) -> actix_web::Resource {
res.get(get_object::<AS>).put(put_object::<AS>)
}
}

View File

@@ -0,0 +1,105 @@
use super::methods::{get_object, put_object};
use crate::{CardDavPrincipalUri, Error, address_object::resource::AddressObjectResource};
use async_trait::async_trait;
use axum::{extract::Request, handler::Handler, response::Response};
use derive_more::derive::Constructor;
use futures_util::future::BoxFuture;
use rustical_dav::resource::{AxumMethods, ResourceService};
use rustical_store::{AddressbookStore, auth::User};
use serde::{Deserialize, Deserializer};
use std::{convert::Infallible, sync::Arc};
use tower::Service;
#[derive(Constructor)]
pub struct AddressObjectResourceService<AS: AddressbookStore> {
pub(crate) addr_store: Arc<AS>,
}
impl<AS: AddressbookStore> Clone for AddressObjectResourceService<AS> {
fn clone(&self) -> Self {
Self {
addr_store: self.addr_store.clone(),
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct AddressObjectPathComponents {
pub principal: String,
pub addressbook_id: String,
#[serde(deserialize_with = "deserialize_vcf_name")]
pub object_id: String,
}
#[async_trait]
impl<AS: AddressbookStore> ResourceService for AddressObjectResourceService<AS> {
type PathComponents = AddressObjectPathComponents;
type Resource = AddressObjectResource;
type MemberType = AddressObjectResource;
type Error = Error;
type Principal = User;
type PrincipalUri = CardDavPrincipalUri;
const DAV_HEADER: &str = "1, 3, access-control, addressbook";
async fn get_resource(
&self,
AddressObjectPathComponents {
principal,
addressbook_id,
object_id,
}: &Self::PathComponents,
) -> Result<Self::Resource, Self::Error> {
let object = self
.addr_store
.get_object(principal, addressbook_id, object_id, false)
.await?;
Ok(AddressObjectResource {
object,
principal: principal.to_owned(),
})
}
async fn delete_resource(
&self,
AddressObjectPathComponents {
principal,
addressbook_id,
object_id,
}: &Self::PathComponents,
use_trashbin: bool,
) -> Result<(), Self::Error> {
self.addr_store
.delete_object(principal, addressbook_id, object_id, use_trashbin)
.await?;
Ok(())
}
}
impl<AS: AddressbookStore> AxumMethods for AddressObjectResourceService<AS> {
fn get() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> {
Some(|state, req| {
let mut service = Handler::with_state(get_object::<AS>, state);
Box::pin(Service::call(&mut service, req))
})
}
fn put() -> Option<fn(Self, Request) -> BoxFuture<'static, Result<Response, Infallible>>> {
Some(|state, req| {
let mut service = Handler::with_state(put_object::<AS>, state);
Box::pin(Service::call(&mut service, req))
})
}
}
fn deserialize_vcf_name<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: Deserializer<'de>,
{
let name: String = Deserialize::deserialize(deserializer)?;
if let Some(object_id) = name.strip_suffix(".vcf") {
Ok(object_id.to_owned())
} else {
Err(serde::de::Error::custom("Missing .vcf extension"))
}
}

View File

@@ -0,0 +1,59 @@
use crate::Error;
use crate::addressbook::AddressbookResourceService;
use crate::addressbook::resource::AddressbookResource;
use axum::body::Body;
use axum::extract::{Path, State};
use axum::response::Response;
use axum_extra::headers::{ContentType, HeaderMapExt};
use http::{HeaderValue, StatusCode, header};
use percent_encoding::{CONTROLS, utf8_percent_encode};
use rustical_dav::privileges::UserPrivilege;
use rustical_dav::resource::Resource;
use rustical_ical::AddressObject;
use rustical_store::auth::User;
use rustical_store::{AddressbookStore, SubscriptionStore};
use std::str::FromStr;
use tracing::instrument;
#[instrument(skip(addr_store))]
pub async fn route_get<AS: AddressbookStore, S: SubscriptionStore>(
Path((principal, addressbook_id)): Path<(String, String)>,
State(AddressbookResourceService { addr_store, .. }): State<AddressbookResourceService<AS, S>>,
user: User,
) -> Result<Response, Error> {
if !user.is_principal(&principal) {
return Err(Error::Unauthorized);
}
let addressbook = addr_store
.get_addressbook(&principal, &addressbook_id, false)
.await?;
let addressbook_resource = AddressbookResource(addressbook);
if !addressbook_resource
.get_user_privileges(&user)?
.has(&UserPrivilege::Read)
{
return Err(Error::Unauthorized);
}
let objects = addr_store.get_objects(&principal, &addressbook_id).await?;
let vcf = objects
.iter()
.map(AddressObject::get_vcf)
.collect::<Vec<_>>()
.join("\r\n");
let mut resp = Response::builder().status(StatusCode::OK);
let hdrs = resp.headers_mut().unwrap();
hdrs.typed_insert(ContentType::from_str("text/vcard").unwrap());
let filename = format!("{}_{}.vcf", principal, addressbook_id);
let filename = utf8_percent_encode(&filename, CONTROLS);
hdrs.insert(
header::CONTENT_DISPOSITION,
HeaderValue::from_str(&format!(
"attachement; filename*=UTF-8''{filename}; filename={filename}",
))
.unwrap(),
);
Ok(resp.body(Body::new(vcf)).unwrap())
}

View File

@@ -1,10 +1,12 @@
use crate::Error; use crate::{Error, addressbook::AddressbookResourceService};
use actix_web::web::Path; use axum::{
use actix_web::{HttpResponse, web::Data}; extract::{Path, State},
use rustical_store::{Addressbook, AddressbookStore, auth::User}; response::{IntoResponse, Response},
};
use http::StatusCode;
use rustical_store::{Addressbook, AddressbookStore, SubscriptionStore, auth::User};
use rustical_xml::{XmlDeserialize, XmlDocument, XmlRootTag}; use rustical_xml::{XmlDeserialize, XmlDocument, XmlRootTag};
use tracing::instrument; use tracing::instrument;
use tracing_actix_web::RootSpan;
#[derive(XmlDeserialize, Clone, Debug, PartialEq)] #[derive(XmlDeserialize, Clone, Debug, PartialEq)]
pub struct Resourcetype { pub struct Resourcetype {
@@ -39,15 +41,13 @@ struct MkcolRequest {
set: PropElement<MkcolAddressbookProp>, set: PropElement<MkcolAddressbookProp>,
} }
#[instrument(parent = root_span.id(), skip(store, root_span))] #[instrument(skip(addr_store))]
pub async fn route_mkcol<AS: AddressbookStore>( pub async fn route_mkcol<AS: AddressbookStore, S: SubscriptionStore>(
path: Path<(String, String)>, Path((principal, addressbook_id)): Path<(String, String)>,
body: String,
user: User, user: User,
store: Data<AS>, State(AddressbookResourceService { addr_store, .. }): State<AddressbookResourceService<AS, S>>,
root_span: RootSpan, body: String,
) -> Result<HttpResponse, Error> { ) -> Result<Response, Error> {
let (principal, addressbook_id) = path.into_inner();
if !user.is_principal(&principal) { if !user.is_principal(&principal) {
return Err(Error::Unauthorized); return Err(Error::Unauthorized);
} }
@@ -65,7 +65,7 @@ pub async fn route_mkcol<AS: AddressbookStore>(
push_topic: uuid::Uuid::new_v4().to_string(), push_topic: uuid::Uuid::new_v4().to_string(),
}; };
match store match addr_store
.get_addressbook(&principal, &addressbook_id, true) .get_addressbook(&principal, &addressbook_id, true)
.await .await
{ {
@@ -74,7 +74,11 @@ pub async fn route_mkcol<AS: AddressbookStore>(
} }
Ok(_) => { Ok(_) => {
// oh no, there's a conflict // oh no, there's a conflict
return Ok(HttpResponse::Conflict().body("An addressbook already exists at this URI")); return Ok((
StatusCode::CONFLICT,
"An addressbook already exists at this URI",
)
.into_response());
} }
Err(err) => { Err(err) => {
// some other error // some other error
@@ -82,12 +86,10 @@ pub async fn route_mkcol<AS: AddressbookStore>(
} }
} }
match store.insert_addressbook(addressbook).await { match addr_store.insert_addressbook(addressbook).await {
// TODO: The spec says we should return a mkcol-response. // TODO: The spec says we should return a mkcol-response.
// However, it works without one but breaks on iPadOS when using an empty one :) // However, it works without one but breaks on iPadOS when using an empty one :)
Ok(()) => Ok(HttpResponse::Created() Ok(()) => Ok(StatusCode::CREATED.into_response()),
.insert_header(("Cache-Control", "no-cache"))
.body("")),
Err(err) => { Err(err) => {
dbg!(err.to_string()); dbg!(err.to_string());
Err(err.into()) Err(err.into())

View File

@@ -1,3 +1,5 @@
pub mod mkcol; pub mod mkcol;
pub mod post; // pub mod post;
pub mod get;
pub mod put;
pub mod report; pub mod report;

View File

@@ -1,4 +1,5 @@
use crate::Error; use crate::Error;
use crate::addressbook::resource::AddressbookResourceService;
use actix_web::http::header; use actix_web::http::header;
use actix_web::web::{Data, Path}; use actix_web::web::{Data, Path};
use actix_web::{HttpRequest, HttpResponse}; use actix_web::{HttpRequest, HttpResponse};
@@ -9,13 +10,12 @@ use rustical_xml::XmlDocument;
use tracing::instrument; use tracing::instrument;
use tracing_actix_web::RootSpan; use tracing_actix_web::RootSpan;
#[instrument(parent = root_span.id(), skip(store, subscription_store, root_span, req))] #[instrument(parent = root_span.id(), skip(resource_service, root_span, req))]
pub async fn route_post<A: AddressbookStore, S: SubscriptionStore>( pub async fn route_post<A: AddressbookStore, S: SubscriptionStore>(
path: Path<(String, String)>, path: Path<(String, String)>,
body: String, body: String,
user: User, user: User,
store: Data<A>, resource_service: Data<AddressbookResourceService<A, S>>,
subscription_store: Data<S>,
root_span: RootSpan, root_span: RootSpan,
req: HttpRequest, req: HttpRequest,
) -> Result<HttpResponse, Error> { ) -> Result<HttpResponse, Error> {
@@ -24,7 +24,8 @@ pub async fn route_post<A: AddressbookStore, S: SubscriptionStore>(
return Err(Error::Unauthorized); return Err(Error::Unauthorized);
} }
let addressbook = store let addressbook = resource_service
.addr_store
.get_addressbook(&principal, &addressbook_id, false) .get_addressbook(&principal, &addressbook_id, false)
.await?; .await?;
let request = PushRegister::parse_str(&body)?; let request = PushRegister::parse_str(&body)?;
@@ -57,7 +58,10 @@ pub async fn route_post<A: AddressbookStore, S: SubscriptionStore>(
.ty, .ty,
auth_secret: request.subscription.web_push_subscription.auth_secret, auth_secret: request.subscription.web_push_subscription.auth_secret,
}; };
subscription_store.upsert_subscription(subscription).await?; resource_service
.sub_store
.upsert_subscription(subscription)
.await?;
let location = req let location = req
.resource_map() .resource_map()

View File

@@ -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<AS: AddressbookStore, S: SubscriptionStore>(
Path((principal, addressbook_id)): Path<(String, String)>,
State(AddressbookResourceService { addr_store, .. }): State<AddressbookResourceService<AS, S>>,
user: User,
body: String,
) -> Result<Response, Error> {
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())
}

View File

@@ -1,17 +1,16 @@
use crate::{ use crate::{
Error, Error,
address_object::resource::{AddressObjectPropWrapper, AddressObjectResource}, address_object::{
}; AddressObjectPropWrapper, AddressObjectPropWrapperName, resource::AddressObjectResource,
use actix_web::{ },
HttpRequest,
dev::{Path, ResourceDef},
http::StatusCode,
}; };
use http::StatusCode;
use rustical_dav::{ use rustical_dav::{
resource::Resource, resource::{PrincipalUri, Resource},
xml::{MultistatusElement, PropfindType, multistatus::ResponseElement}, xml::{MultistatusElement, PropfindType, multistatus::ResponseElement},
}; };
use rustical_store::{AddressObject, AddressbookStore, auth::User}; use rustical_ical::AddressObject;
use rustical_store::{AddressbookStore, auth::User};
use rustical_xml::XmlDeserialize; use rustical_xml::XmlDeserialize;
#[derive(XmlDeserialize, Clone, Debug, PartialEq)] #[derive(XmlDeserialize, Clone, Debug, PartialEq)]
@@ -19,7 +18,7 @@ use rustical_xml::XmlDeserialize;
#[xml(ns = "rustical_dav::namespace::NS_DAV")] #[xml(ns = "rustical_dav::namespace::NS_DAV")]
pub struct AddressbookMultigetRequest { pub struct AddressbookMultigetRequest {
#[xml(ns = "rustical_dav::namespace::NS_DAV", ty = "untagged")] #[xml(ns = "rustical_dav::namespace::NS_DAV", ty = "untagged")]
pub(crate) prop: PropfindType, pub(crate) prop: PropfindType<AddressObjectPropWrapperName>,
#[xml(ns = "rustical_dav::namespace::NS_DAV", flatten)] #[xml(ns = "rustical_dav::namespace::NS_DAV", flatten)]
pub(crate) href: Vec<String>, pub(crate) href: Vec<String>,
} }
@@ -31,27 +30,29 @@ pub async fn get_objects_addressbook_multiget<AS: AddressbookStore>(
addressbook_id: &str, addressbook_id: &str,
store: &AS, store: &AS,
) -> Result<(Vec<AddressObject>, Vec<String>), Error> { ) -> Result<(Vec<AddressObject>, Vec<String>), Error> {
let resource_def = ResourceDef::prefix(path).join(&ResourceDef::new("/{object_id}.vcf"));
let mut result = vec![]; let mut result = vec![];
let mut not_found = vec![]; let mut not_found = vec![];
for href in &addressbook_multiget.href { for href in &addressbook_multiget.href {
let mut path = Path::new(href.as_str()); if let Some(filename) = href.strip_prefix(path) {
if !resource_def.capture_match_info(&mut path) { let filename = filename.trim_start_matches("/");
if let Some(object_id) = filename.strip_suffix(".vcf") {
match store
.get_object(principal, addressbook_id, object_id, false)
.await
{
Ok(object) => result.push(object),
Err(rustical_store::Error::NotFound) => not_found.push(href.to_owned()),
Err(err) => return Err(err.into()),
};
} else {
not_found.push(href.to_owned());
continue;
}
} else {
not_found.push(href.to_owned()); not_found.push(href.to_owned());
continue; continue;
}; }
let object_id = path.get("object_id").unwrap();
match store
.get_object(principal, addressbook_id, object_id, false)
.await
{
Ok(object) => result.push(object),
Err(rustical_store::Error::NotFound) => not_found.push(href.to_owned()),
// TODO: Maybe add error handling on a per-object basis
Err(err) => return Err(err.into()),
};
} }
Ok((result, not_found)) Ok((result, not_found))
@@ -59,26 +60,27 @@ pub async fn get_objects_addressbook_multiget<AS: AddressbookStore>(
pub async fn handle_addressbook_multiget<AS: AddressbookStore>( pub async fn handle_addressbook_multiget<AS: AddressbookStore>(
addr_multiget: &AddressbookMultigetRequest, addr_multiget: &AddressbookMultigetRequest,
props: &[&str], prop: &PropfindType<AddressObjectPropWrapperName>,
req: HttpRequest, path: &str,
puri: &impl PrincipalUri,
user: &User, user: &User,
principal: &str, principal: &str,
cal_id: &str, cal_id: &str,
addr_store: &AS, addr_store: &AS,
) -> Result<MultistatusElement<AddressObjectPropWrapper, String>, Error> { ) -> Result<MultistatusElement<AddressObjectPropWrapper, String>, Error> {
let (objects, not_found) = let (objects, not_found) =
get_objects_addressbook_multiget(addr_multiget, req.path(), principal, cal_id, addr_store) get_objects_addressbook_multiget(addr_multiget, path, principal, cal_id, addr_store)
.await?; .await?;
let mut responses = Vec::new(); let mut responses = Vec::new();
for object in objects { for object in objects {
let path = format!("{}/{}.vcf", req.path(), object.get_id()); let path = format!("{}/{}.vcf", path, object.get_id());
responses.push( responses.push(
AddressObjectResource { AddressObjectResource {
object, object,
principal: principal.to_owned(), principal: principal.to_owned(),
} }
.propfind(&path, props, user, req.resource_map())?, .propfind(&path, prop, puri, user)?,
); );
} }

View File

@@ -1,11 +1,15 @@
use crate::Error; use crate::{
use actix_web::{ CardDavPrincipalUri, Error, address_object::AddressObjectPropWrapperName,
HttpRequest, Responder, addressbook::AddressbookResourceService,
web::{Data, Path},
}; };
use addressbook_multiget::{AddressbookMultigetRequest, handle_addressbook_multiget}; use addressbook_multiget::{AddressbookMultigetRequest, handle_addressbook_multiget};
use rustical_dav::xml::{PropElement, PropfindType, sync_collection::SyncCollectionRequest}; use axum::{
use rustical_store::{AddressbookStore, auth::User}; Extension,
extract::{OriginalUri, Path, State},
response::IntoResponse,
};
use rustical_dav::xml::{PropfindType, sync_collection::SyncCollectionRequest};
use rustical_store::{AddressbookStore, SubscriptionStore, auth::User};
use rustical_xml::{XmlDeserialize, XmlDocument}; use rustical_xml::{XmlDeserialize, XmlDocument};
use sync_collection::handle_sync_collection; use sync_collection::handle_sync_collection;
use tracing::instrument; use tracing::instrument;
@@ -18,53 +22,40 @@ pub(crate) enum ReportRequest {
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")] #[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
AddressbookMultiget(AddressbookMultigetRequest), AddressbookMultiget(AddressbookMultigetRequest),
#[xml(ns = "rustical_dav::namespace::NS_DAV")] #[xml(ns = "rustical_dav::namespace::NS_DAV")]
SyncCollection(SyncCollectionRequest), SyncCollection(SyncCollectionRequest<AddressObjectPropWrapperName>),
} }
impl ReportRequest { impl ReportRequest {
fn props(&self) -> Vec<&str> { fn props(&self) -> &PropfindType<AddressObjectPropWrapperName> {
let prop_element = match self { match self {
ReportRequest::AddressbookMultiget(AddressbookMultigetRequest { prop, .. }) => prop, ReportRequest::AddressbookMultiget(AddressbookMultigetRequest { prop, .. }) => prop,
ReportRequest::SyncCollection(SyncCollectionRequest { prop, .. }) => prop, ReportRequest::SyncCollection(SyncCollectionRequest { prop, .. }) => prop,
};
match prop_element {
PropfindType::Allprop => {
vec!["allprop"]
}
PropfindType::Propname => {
vec!["propname"]
}
PropfindType::Prop(PropElement(prop_tags)) => prop_tags
.iter()
.map(|propname| propname.0.as_str())
.collect(),
} }
} }
} }
#[instrument(skip(req, addr_store))] #[instrument(skip(addr_store))]
pub async fn route_report_addressbook<AS: AddressbookStore>( pub async fn route_report_addressbook<AS: AddressbookStore, S: SubscriptionStore>(
path: Path<(String, String)>, Path((principal, addressbook_id)): Path<(String, String)>,
body: String,
user: User, user: User,
req: HttpRequest, OriginalUri(uri): OriginalUri,
addr_store: Data<AS>, Extension(puri): Extension<CardDavPrincipalUri>,
) -> Result<impl Responder, Error> { State(AddressbookResourceService { addr_store, .. }): State<AddressbookResourceService<AS, S>>,
let (principal, addressbook_id) = path.into_inner(); body: String,
) -> Result<impl IntoResponse, Error> {
if !user.is_principal(&principal) { if !user.is_principal(&principal) {
return Err(Error::Unauthorized); return Err(Error::Unauthorized);
} }
let request = ReportRequest::parse_str(&body)?; let request = ReportRequest::parse_str(&body)?;
let props = request.props();
Ok(match &request { Ok(match &request {
ReportRequest::AddressbookMultiget(addr_multiget) => { ReportRequest::AddressbookMultiget(addr_multiget) => {
handle_addressbook_multiget( handle_addressbook_multiget(
addr_multiget, addr_multiget,
&props, request.props(),
req, uri.path(),
&puri,
&user, &user,
&principal, &principal,
&addressbook_id, &addressbook_id,
@@ -75,8 +66,8 @@ pub async fn route_report_addressbook<AS: AddressbookStore>(
ReportRequest::SyncCollection(sync_collection) => { ReportRequest::SyncCollection(sync_collection) => {
handle_sync_collection( handle_sync_collection(
sync_collection, sync_collection,
&props, uri.path(),
req, &puri,
&user, &user,
&principal, &principal,
&addressbook_id, &addressbook_id,
@@ -89,9 +80,9 @@ pub async fn route_report_addressbook<AS: AddressbookStore>(
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use rustical_dav::xml::{PropElement, Propname, sync_collection::SyncLevel};
use super::*; use super::*;
use crate::address_object::AddressObjectPropName;
use rustical_dav::xml::{PropElement, sync_collection::SyncLevel};
#[test] #[test]
fn test_xml_sync_collection() { fn test_xml_sync_collection() {
@@ -112,9 +103,12 @@ mod tests {
ReportRequest::SyncCollection(SyncCollectionRequest { ReportRequest::SyncCollection(SyncCollectionRequest {
sync_token: "".to_owned(), sync_token: "".to_owned(),
sync_level: SyncLevel::One, sync_level: SyncLevel::One,
prop: rustical_dav::xml::PropfindType::Prop(PropElement(vec![Propname( prop: rustical_dav::xml::PropfindType::Prop(PropElement(
"getetag".to_owned() vec![AddressObjectPropWrapperName::AddressObject(
)])), AddressObjectPropName::Getetag
)],
vec![]
)),
limit: None limit: None
}) })
) )
@@ -137,9 +131,13 @@ mod tests {
report_request, report_request,
ReportRequest::AddressbookMultiget(AddressbookMultigetRequest { ReportRequest::AddressbookMultiget(AddressbookMultigetRequest {
prop: rustical_dav::xml::PropfindType::Prop(PropElement(vec![ prop: rustical_dav::xml::PropfindType::Prop(PropElement(vec![
Propname("getetag".to_owned()), AddressObjectPropWrapperName::AddressObject(
Propname("address-data".to_owned()) AddressObjectPropName::Getetag
])), ),
AddressObjectPropWrapperName::AddressObject(
AddressObjectPropName::AddressData
),
], vec![])),
href: vec![ href: vec![
"/carddav/user/user/6f787542-5256-401a-8db97003260da/ae7a998fdfd1d84a20391168962c62b".to_owned() "/carddav/user/user/6f787542-5256-401a-8db97003260da/ae7a998fdfd1d84a20391168962c62b".to_owned()
] ]

View File

@@ -1,10 +1,12 @@
use crate::{ use crate::{
Error, Error,
address_object::resource::{AddressObjectPropWrapper, AddressObjectResource}, address_object::{
AddressObjectPropWrapper, AddressObjectPropWrapperName, resource::AddressObjectResource,
},
}; };
use actix_web::{HttpRequest, http::StatusCode}; use http::StatusCode;
use rustical_dav::{ use rustical_dav::{
resource::Resource, resource::{PrincipalUri, Resource},
xml::{ xml::{
MultistatusElement, multistatus::ResponseElement, sync_collection::SyncCollectionRequest, MultistatusElement, multistatus::ResponseElement, sync_collection::SyncCollectionRequest,
}, },
@@ -16,9 +18,9 @@ use rustical_store::{
}; };
pub async fn handle_sync_collection<AS: AddressbookStore>( pub async fn handle_sync_collection<AS: AddressbookStore>(
sync_collection: &SyncCollectionRequest, sync_collection: &SyncCollectionRequest<AddressObjectPropWrapperName>,
props: &[&str], path: &str,
req: HttpRequest, puri: &impl PrincipalUri,
user: &User, user: &User,
principal: &str, principal: &str,
addressbook_id: &str, addressbook_id: &str,
@@ -31,22 +33,18 @@ pub async fn handle_sync_collection<AS: AddressbookStore>(
let mut responses = Vec::new(); let mut responses = Vec::new();
for object in new_objects { for object in new_objects {
let path = format!( let path = format!("{}/{}.vcf", path.trim_end_matches('/'), object.get_id());
"{}/{}.vcf",
req.path().trim_end_matches('/'),
object.get_id()
);
responses.push( responses.push(
AddressObjectResource { AddressObjectResource {
object, object,
principal: principal.to_owned(), principal: principal.to_owned(),
} }
.propfind(&path, props, user, req.resource_map())?, .propfind(&path, &sync_collection.prop, puri, user)?,
); );
} }
for object_id in deleted_objects { for object_id in deleted_objects {
let path = format!("{}/{}.vcf", req.path().trim_end_matches('/'), object_id); let path = format!("{}/{}.vcf", path.trim_end_matches('/'), object_id);
responses.push(ResponseElement { responses.push(ResponseElement {
href: path, href: path,
status: Some(StatusCode::NOT_FOUND), status: Some(StatusCode::NOT_FOUND),

View File

@@ -1,3 +1,5 @@
pub mod methods; pub mod methods;
pub mod prop; pub mod prop;
pub mod resource; pub mod resource;
mod service;
pub use service::*;

View File

@@ -1,4 +1,33 @@
use rustical_xml::XmlSerialize; use rustical_dav::{
extensions::{CommonPropertiesProp, SyncTokenExtensionProp},
xml::SupportedReportSet,
};
use rustical_dav_push::DavPushExtensionProp;
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
use strum_macros::VariantArray;
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
#[xml(unit_variants_ident = "AddressbookPropName")]
pub enum AddressbookProp {
// CardDAV (RFC 6352)
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
AddressbookDescription(Option<String>),
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV", skip_deserializing)]
SupportedAddressData(SupportedAddressData),
#[xml(ns = "rustical_dav::namespace::NS_DAV", skip_deserializing)]
SupportedReportSet(SupportedReportSet<ReportMethod>),
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
MaxResourceSize(i64),
}
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)]
#[xml(unit_variants_ident = "AddressbookPropWrapperName", untagged)]
pub enum AddressbookPropWrapper {
Addressbook(AddressbookProp),
SyncToken(SyncTokenExtensionProp),
DavPush(DavPushExtensionProp),
Common(CommonPropertiesProp),
}
#[derive(Debug, Clone, XmlSerialize, PartialEq)] #[derive(Debug, Clone, XmlSerialize, PartialEq)]
pub struct AddressDataType { pub struct AddressDataType {
@@ -31,37 +60,10 @@ impl Default for SupportedAddressData {
} }
} }
#[derive(Debug, Clone, XmlSerialize, PartialEq)] #[derive(Debug, Clone, XmlSerialize, PartialEq, VariantArray)]
pub enum ReportMethod { pub enum ReportMethod {
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")] #[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
AddressbookMultiget, AddressbookMultiget,
#[xml(ns = "rustical_dav::namespace::NS_DAV")]
SyncCollection, SyncCollection,
} }
#[derive(Debug, Clone, XmlSerialize, PartialEq)]
pub struct SupportedReportWrapper {
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV")]
report: ReportMethod,
}
// RFC 3253 section-3.1.5
#[derive(Debug, Clone, XmlSerialize, PartialEq)]
pub struct SupportedReportSet {
#[xml(ns = "rustical_dav::namespace::NS_CARDDAV", flatten)]
supported_report: &'static [SupportedReportWrapper],
}
impl Default for SupportedReportSet {
fn default() -> Self {
Self {
supported_report: &[
SupportedReportWrapper {
report: ReportMethod::AddressbookMultiget,
},
SupportedReportWrapper {
report: ReportMethod::SyncCollection,
},
],
}
}
}

Some files were not shown because too many files have changed in this diff Show More