-
Notifications
You must be signed in to change notification settings - Fork 112
Expand file tree
/
Copy pathcompatibility_hints.py
More file actions
1488 lines (1343 loc) · 81 KB
/
compatibility_hints.py
File metadata and controls
1488 lines (1343 loc) · 81 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# fmt: off
"""
This file serves as a database of different compatibility issues we've
encountered while working on the caldav library, and descriptions on
how the well-known servers behave.
TODO: it should probably be split with the "feature definitions",
"server implementation details" and "feature database logic" in three separate files.
"""
import copy
import warnings
# Valid support levels for features
VALID_SUPPORT_LEVELS = frozenset({
"full", # Feature works as expected
"unsupported", # Feature not available (may be silently ignored)
"fragile", # Sometimes works, sometimes not
"quirk", # Supported but needs special handling
"broken", # Server does unexpected things
"ungraceful", # Server throws errors (actually most graceful for error handling)
"unknown", # Not yet tested/determined
})
## TODO: this file should probably be split in two (or three) as there
## are three different concerns in this file - the "feature
## definitions", some code logic, and the "database" of known server
## implementation compatibilities.
## TODO: Lots of silly comments in the compatibility matrixes now as I
## at one point managed to check out the wrong version of the
## caldav_server_checker, and things got messy from there. We should
## double-check the values where there is any doubt, and clean up.
## NEW STYLE
## (we're gradually moving stuff from the good old
## "incompatibility_description" below over to
## "compatibility_features")
class FeatureSet:
"""Work in progress ... TODO: write a better class description.
This class holds the description of different behaviour observed in
a class constant.
An object of this class describes the feature set of a server.
TODO: use enums? TODO: describe the different types TODO: think more through the different types, consolidate?
type -> "client-feature", "client-hints", "server-peculiarity", "tests-behaviour", "server-observation", "server-feature" (last is default)
support -> "full" (default), "unsupported", "fragile", "quirk", "broken", "ungraceful"
unsupported means that attempts to use the feature will be silently ignored (this may actually be the worst option, as it may cause data loss). quirk means that the feature is suppored, but special handling needs to be done towards the server. fragile means that it sometimes works and sometimes not - either it's arbitrary, or we didn't spend enough time doing research into the patterns. My idea behind broken was that the server should do completely unexpected things. Probably a lot of things classified as "unsupported" today should rather be classified as "broken". Some AI-generated code is using"broken". TODO: look through and clean up. "ungraceful" means the server will throw some error (this may indeed be the most graceful, as the client may catch the error and handle it in the best possible way).
types:
* client-feature means the client is supposed to do special things (like, rate-limiting). While the need for rate-limiting may be set by the server, it may not be possible to reliably establish it by probling the server, and the value may differ for different clients.
* server-peculiarity - weird behaviour detected at the server side, behaviour that is too odd to be described as "missing support for a feature". Example: there is some cache working, causing a delay from some object is sent to the server and until it can be retrieved. The difference between an "unsupported server-feature" and a "server-peculiarity" may be a bit floating - like, arguably "instant updates" may be considered a feature.
* tests-behaviour - configuration for the tests. Like, it's OK to wipe everyhting from the test calendar, location of test calendar, rate-limiting that only should apply to test runs, etc.
* server-observation - not features, but other facts found about the server
* server-feature - some feature (preferably rooted with a pointer to some specific section of the RFC)
* "support" -> "quirk" if we have a server-peculiarity where it's needed with special care to get the request through.
IMPORTANT NOTE: The dotted format sort of represents a hierarchy - say, one may have foo.bar and foo.zoo. If foo has an explicit default given in the FEATURES below, it will be considered an independent feature, otherwise it will be considered to only exist to group bar and zoo together. This matters if bar and zoo is not supported. Without an explicit default below, the default for foo will also be "unsupported".
"""
FEATURES = {
"auto-connect": {
## Nothing here - everything is under auto-connect.url as for now.
## Other connection details - like what auth method to use - could also
## be under the auto-connect umbrella
"type": "client-hints",
},
"auto-connect.url": {
"description": "Instruction for how to access DAV. I.e. `/remote.php/dav` - see also https://github.com/python-caldav/caldav/issues/463. To be used in the get_davclient method if the URL only contains a domain",
"type": "client-hints",
"extra_keys": {
"basepath": "The path to append to the domain",
"domain": "Domain name may be given through the features - useful for well-known cloud solutions",
"scheme": "The scheme to prepend to the domain. Defaults to https",
## TODO: in the future, templates for the principal URL, calendar URLs etc may also be added.
}
},
"get-current-user-principal": {
"description": "Support for RFC5397, current principal extension. Most CalDAV servers have this, but it is an extension to the DAV standard"},
"get-current-user-principal.has-calendar": {
"type": "server-observation",
"description": "Principal has one or more calendars. Some servers and providers comes with a pre-defined calendar for each user, for other servers a calendar has to be explicitly created (supported means there exists a calendar - it may be because the calendar was already provisioned together with the principal, or it may be because a calendar was created manually, the checks can't see the difference)"},
"rate-limit": {
"type": "client-feature",
"description": "client (or test code) must sleep a bit between requests. Pro-active rate limiting is done through interval and count, server-flagged rate-limiting is controlled through default_sleep/max_sleep",
"extra_keys": {
"interval": "Rate limiting window, in seconds",
"count": "Max number of requests to send within the interval",
"max_sleep": "Max sleep when hitting a 429 or 503 with retry-after, in seconds",
"default_sleep": "Sleep for this long when hitting a 429, in seconds"
}},
"search-cache": {
"type": "server-peculiarity",
"description": "The server delivers search results from a cache which is not immediately updated when an object is changed. Hence recent changes may not be reflected in search results",
"extra_keys": {
"delay": "after this number of seconds, we may be reasonably sure that the search results are updated",
}
},
"tests-cleanup-calendar": {
"type": "tests-behaviour",
"description": "Deleting a calendar does not delete the objects, or perhaps create/delete of calendars does not work at all. For each test run, every calendar resource object should be deleted for every test run",
},
"create-calendar": {
"default": { "support": "full" },
"description": "RFC4791 says that \"support for MKCALENDAR on the server is only RECOMMENDED and not REQUIRED because some calendar stores only support one calendar per user (or principal), and those are typically pre-created for each account\". Hence a conformant server may opt to not support creating calendars, this is often seen for cloud services (some services allows extra calendars to be made, but not through the CalDAV protocol). (RFC4791 also says that the server MAY support MKCOL in section 8.5.2. I do read it as MKCOL may be used for creating calendars - which is weird, since section 8.5.2 is titled \"external attachments\". We should consider testing this as well)",
},
"create-calendar.auto": {
"default": { "support": "unsupported" },
"description": "Accessing a calendar which does not exist automatically creates it",
},
"create-calendar.set-displayname": {
"description": "It's possible to set the displayname on a calendar upon creation"
},
"delete-calendar": {
"description": "RFC4791 says nothing about deletion of calendars, so the server implementation is free to choose weather this should be supported or not. Section 3.2.3.2 in RFC 6638 says that if a calendar is deleted, all the calendarobjectresources on the calendar should also be deleted - but it's a bit unclear if this only applies to scheduling objects or not. Some calendar servers moves the object to a trashcan rather than deleting it"
},
"delete-calendar.free-namespace": {
"description": "The delete operations clears the namespace, so that another calendar with the same ID/name can be created"
},
"http": { },
"http.multiplexing": {
"description": "chulka/baikal:nginx is having Problems with using HTTP/2 with multiplexing, ref https://github.com/python-caldav/caldav/issues/564. I haven't (yet) been able to reproduce this locally, so no check for this yet. Due to caution and friendly advice from the niquests team, the default now is to NOT support http multiplexing.",
"default": { "support": "fragile" },
},
"save-load": {
"description": "it's possible to save and load objects to the calendar"
},
"save-load.event": {"description": "it's possible to save and load events to the calendar"},
"save-load.event.recurrences": {"description": "it's possible to save and load recurring events to the calendar - events with an RRULE property set, including recurrence sets"},
"save-load.event.recurrences.count": {"description": "The server will receive and store a recurring event with a count set in the RRULE", "default": {"support": "full"}},
## This was Claude's suggestion and it works as of today, the
## "unsupported" description matches the behaviour of the Stalwart server.
## Stalwart apparently (in a breach with the RFC) stores the exception
## information as a separate CalendarObjectResource.
## Currently the search logic will do server-side expansion
## if this flag is set to "unsupported", which is the correct behaviour for Stalwart.
## The problem is that logically, this feature would also be "unsupported" if the exception
## information was simply discarded, and the current search behaviour would in
## such a case be incorrect if the exception is simply discarded.
"save-load.event.recurrences.exception": {"description": "When a VCALENDAR containing a master VEVENT (with RRULE) and exception VEVENT(s) (with RECURRENCE-ID) is stored, the server keeps them together as a single calendar object resource. When unsupported, the server splits exception VEVENTs into separate calendar objects, making client-side expansion unreliable (the master expands without knowing about its exceptions)."},
"save-load.todo": {"description": "it's possible to save and load tasks to the calendar"},
"save-load.todo.recurrences": {"description": "it's possible to save and load recurring tasks to the calendar"},
"save-load.todo.recurrences.count": {"description": "The server will receive and store a recurring task with a count set in the RRULE", "default": {"support": "full"}},
"save-load.todo.recurrences.thisandfuture": {"description": "Completing a recurring task with rrule_mode='thisandfuture' works (modifies RRULE and saves back to server)", "default": {"support": "full"}},
"save-load.todo.mixed-calendar": {"description": "The same calendar may contain both events and tasks (Zimbra only allows tasks to be placed on special task lists)", "default": {"support": "full"}},
"save-load.journal": {"description": "The server will even accept journals"},
## TODO: zimbra cannot mix events and tasks, but then davis surprised me by not allowing journals on the same calendar. But this may be a miss in the checking script - it may be that mixing is allowed, but that the calendar has to be set up from scratch with explicit support for both VJOURNAL and other things
"save-load.journal.mixed-calendar": {"description": "The same calendar may contain events, tasks and journals (some servers require journals on a dedicated VJOURNAL calendar)", "default": {"support": "full"}},
"save-load.get-by-url": {
"description": "GET requests to calendar object resource URLs work correctly. When unsupported, the server returns 404 on GET even for valid object URLs. The client works around this by falling back to UID-based lookup.",
},
"save-load.reuse-deleted-uid": {
"description": "After deleting an event, the server allows creating a new event with the same UID. When 'broken', the server keeps deleted events in a trashbin with a soft-delete flag, causing unique constraint violations on UID reuse. See https://github.com/nextcloud/server/issues/30096"
},
"save-load.event.timezone": {
"description": "The server accepts events with non-UTC timezone information. When unsupported or broken, the server may reject events with timezone data (e.g., return 403 Forbidden). Related to GitHub issue https://github.com/python-caldav/caldav/issues/372."
},
"search": {
"description": "calendar MUST support searching for objects using the REPORT method, as specified in RFC4791, section 7"
},
"search.comp-type-optional": {
"description": "In all the search examples in the RFC, comptype is given during a search, the client specifies if it's event or tasks or journals that is wanted. However, as I read the RFC this is not required. If omitted, the server should deliver all objects. Many servers will not return anything if the COMPTYPE filter is not set. Other servers will return 404"
},
"search.comp-type": {
"type": "server-peculiarity",
"description": "Server correctly filters calendar-query results by component type. When 'broken', server may misclassify component types (e.g., returning TODOs when VEVENTs are requested). The library will perform client-side filtering to work around this issue",
"default": {"support": "full"}
},
## TODO - there is still quite a lot of search-related
## stuff that hasn't been moved from the old "quirk list"
"search.time-range": {
"description": "Search for time or date ranges should work. This is specified in RFC4791, section 7.4 and section 9.9"},
"search.time-range.accurate": {
"description": "Time-range searches should only return events/todos that actually fall within the requested time range. Some servers incorrectly return recurring events whose recurrences fall outside (after) the search interval, or events with no recurrences in the requested time range at all. RFC4791 section 9.9 specifies that a VEVENT component overlaps a time range if the condition (start < search_end AND end > search_start) is true.",
"links": ["https://datatracker.ietf.org/doc/html/rfc4791#section-9.9"],
},
"search.time-range.todo": {"description": "basic time range searches for tasks works", "default": {"support": "full"}},
"search.time-range.todo.old-dates": {"description": "time range searches for tasks with old dates (e.g. year 2000) work - some servers enforce a min-date-time restriction"},
"search.time-range.event": {"description": "basic time range searches for event works", "default": {"support": "full"}},
"search.time-range.event.old-dates": {"description": "time range searches for events with old dates (e.g. year 2000) work - some servers enforce a min-date-time restriction"},
"search.time-range.journal": {"description": "basic time range searches for journal works"},
"search.time-range.alarm": {"description": "Time range searches for alarms work. The server supports searching for events based on when their alarms trigger, as specified in RFC4791 section 9.9"},
"search.is-not-defined": {
"description": "Supports searching for objects where properties is-not-defined according to rfc4791 section 9.7.4",
"default": {"support": "full"}
},
"search.is-not-defined.category": {
"description": "Supports searching for objects where the CATEGORIES property is not defined (RFC4791 section 9.7.4). Some servers support is-not-defined for other properties (e.g. CLASS) but silently return wrong results or nothing when applied to CATEGORIES"
},
"search.is-not-defined.dtend": {
"description": "Supports searching for objects where the DTEND property is not defined (RFC4791 section 9.7.4). Some servers support is-not-defined for some properties but not DTEND"
},
"search.text": {
"description": "Search for text attributes should work"
},
"search.text.case-sensitive": {
"description": "In RFC4791, section-9.7.5, a text-match may pass a collation, and i;ascii-casemap MUST be the default, this is not checked (yet - TODO) by the caldav-server-checker project. Section 7.5 describes that the servers also are REQUIRED to support i;octet. The definitions of those collations are given in RFC4790, i;octet is a case-sensitive byte-by-byte comparition (fastest). search.text.case-sensitive is supported if passing the i;octet collation to search causes the search to be case-sensitive."
},
"search.text.case-insensitive": {
"description": "The i;ascii-casemap requires ascii-characters to be case-insensitive, while non-ascii characters are compared byte-by-byte (case-sensitive). Proper unicode case-insensitive searches may be supported by the server, but it's not a requirement in the RFC. As for now, we consider case-insensitive searches to be supported if the i;ascii-casemap collation does what it's supposed to do.. In the future we may consider adding a search.text.case-insensitive.unicode. (i;unicode-casemap is defined in RFC5051)"
},
"search.text.substring": {
"description": "According to RFC4791 the search done should be a substring search. The search.text.substring feature is set if the calendar server does this (as opposed to only return full matches). Substring matches does not always make sense, but it's mandated by the RFC. When a server does a substring match on some properties but an exact match on others, the support should be marked as fragile. Except for categories, which are handled in search.text.category.substring"
},
"search.text.category": {
"description": "Search for category should work. This is not explicitly specified in RFC4791, but covered in section 9.7.5. No examples targets categories explicitly, but there are some text match examples in section 7.8.6 and following sections"},
"search.text.category.substring": {
"description": "Substring search for category should work according to the RFC. I.e., search for mil should match family,finance",
},
"search.text.by-uid": {
"description": "The server supports searching for objects by UID property. When unsupported, calendar.get_object_by_uid(uid) will not work. This may be removed in the feature - the checker-script is not checking the right thing (check TODO-comments), probably search by uid is no special case for any server implementations"
},
"search.recurrences": {
"description": "Support for recurrences in search"
},
"search.recurrences.includes-implicit": {
"description": "RFC 4791, section 7.4 says that the server MUST expand recurring components to determine whether any recurrence instances overlap the specified time range. Considered supported i.e. if a search for 2005 yields a yearly event happening first time in 2004.",
"links": ["https://datatracker.ietf.org/doc/html/rfc4791#section-7.4"],
},
"search.recurrences.includes-implicit.todo": {
"description": "tasks can also be recurring"
},
"search.recurrences.includes-implicit.todo.pending": {
"description": "a future recurrence of a pending task should always be pending and appear in searches for pending tasks",
"default": {"support": "full"},
},
"search.recurrences.includes-implicit.event": {
"description": "support for events"
},
"search.recurrences.includes-implicit.infinite-scope": {
"description": "Needless to say, search on any future date range, no matter how far out in the future, should yield the recurring object"
},
"search.combined-is-logical-and": {
"description": "Multiple search filters should yield only those that passes all filters"
## For "unsupported", we could also add a "behaviour" (returns everything, returns nothing, returns logical OR, etc).
},
"search.recurrences.expanded": {
"description": "According to RFC 4791, the server MUST expand recurrence objects if asked for it - but many server doesn't do that. Some servers don't do expand at all, others deliver broken data, typically missing RECURRENCE-ID. The python caldav client library (from 2.0) does the expand-operation client-side no matter if it's supported or not",
"links": ["https://datatracker.ietf.org/doc/html/rfc4791#section-9.6.5"],
},
"search.recurrences.expanded.todo": {
"description": "expanding tasks"
},
"search.recurrences.expanded.event": {
"description": "exanding events"
},
"search.recurrences.expanded.exception": {
"description": "Server expand should work correctly also if a recurrence set with exceptions is given"
},
"sync-token": {
"description": "RFC6578 sync-collection reports are supported. Server provides sync tokens that can be used to efficiently retrieve only changed objects since last sync. Support can be 'full', 'fragile' (occasionally returns more content than expected), or 'unsupported'. Behaviour 'time-based' indicates second-precision tokens requiring sleep(1) between operations"
},
"sync-token.delete": {
"description": "Server correctly handles sync-collection reports after objects have been deleted from the calendar (solved in Nextcloud in https://github.com/nextcloud/server/pull/44130)"
},
'freebusy-query': {'description': "freebusy queries come in two flavors, one query can be done towards a CalDAV server as defined in RFC4791, another query can be done through the scheduling framework, RFC 6638. Only RFC4791 is tested for as today"},
"freebusy-query.rfc4791": {
"description": "Server supports free/busy-query REPORT as specified in RFC4791 section 7.10. The REPORT allows clients to query for free/busy time information for a time range. Servers without this support will typically return an error (often 500 Internal Server Error or 501 Not Implemented). Note: RFC6638 defines a different freebusy mechanism for scheduling",
"links": ["https://datatracker.ietf.org/doc/html/rfc4791#section-7.10"],
},
"principal-search": {
"description": "Server supports searching for principals (CalDAV users). Principal search may be restricted for privacy/security reasons on many servers. (not to be confused with get-current-user-principal)"
},
"principal-search.by-name": {
"description": "Server supports searching for principals by display name. Testing this properly requires setting up another user with a known name, so this check is not yet implemented"
},
"principal-search.by-name.self": {
"description": "Server allows searching for own principal by display name. Some servers block this for privacy reasons even when general principal search works"
},
"principal-search.list-all": {
"description": "Server allows listing all principals without a name filter. Often blocked for privacy/security reasons"
},
"wrong-password-check": {
"description": "Server rejects requests with wrong password by returning an authorization error. Some servers may not properly reject wrong passwords in certain configurations."
},
"save": {},
"save.duplicate-uid": {},
"save.duplicate-uid.cross-calendar": {
"description": "Server allows events with the same UID to exist in different calendars and treats them as separate entities. Support can be 'full' (allowed), 'ungraceful' (rejected with error), or 'unsupported' (silently ignored or moved). Behaviour 'silently-ignored' means the duplicate is not saved but no error is thrown. Behaviour 'moved-instead-of-copied' means the event is moved from the original calendar to the new calendar (Zimbra behavior)"
},
## TODO: as for now, the tests will run towards the first calendar it will find, and most of the tests will assume the calendar is empty. This is bad.
"test-calendar": {
"type": "tests-behaviour",
"description": "if the server does not allow creating new calendars, then use the calendar with the given name for running tests (NOT SUPPORTED YET!), wipe the calendar between each test run (alternative for calendars not supporting the creation of new calendars is a very expensive delete objects one-by-one by uid)",
"extra_keys": { "name": "calendar name", "cleanup-regime": "thorough|pre|post|light|wipe-calendar" }
},
"test-calendar.compatibility-tests": {
"type": "tests-behaviour",
"description": "if the server does not allow creating new calendars, then use the calendar with the given name for running the compatibility tests",
"extra_keys": { "name": "calendar name", "cleanup": "Set to True to clean up the calendar after compatibility run" } ## if needed, pad up with cal_id, url, etc
} ## if needed we may pad up with test-calendar.compatibility-tests.events, etc, etc
}
def __init__(self, feature_set_dict=None):
"""
TODO: describe the feature_set better.
Should be a dict on the same style as self.FEATURES, but different.
Shortcuts accepted in the dict, like:
{
"recurrences.search-includes-implicit-recurrences.infinite-scope":
"unsupported" }
is equivalent with
{
"recurrences": {
"features": {
"search-includes-inplicit-recurrences": {
"infinite-scope":
"support": "unsupported" }}}}
(TODO: is this sane? Am I reinventing a configuration language?)
"""
if isinstance(feature_set_dict, FeatureSet):
self._server_features = copy.deepcopy(feature_set_dict._server_features)
self.backward_compatibility_mode = feature_set_dict.backward_compatibility_mode
self._old_flags = copy.copy(feature_set_dict._old_flags) if hasattr(feature_set_dict, '_old_flags') else []
return
## TODO: copy the FEATURES dict, or just the feature_set dict?
## (anyways, that is an internal design decision that may be
## changed ... but we need test code in place)
self.backward_compatibility_mode = feature_set_dict is None
self._server_features = {}
## TODO: remove this when it can be removed
self._old_flags = []
if feature_set_dict:
self.copyFeatureSet(feature_set_dict, collapse=False)
def set_feature(self, feature, value=True):
if isinstance(value, dict):
fc = {feature: value}
elif isinstance(value, str):
fc = {feature: {"support": value}}
elif value is True:
fc = {feature: {"support": "full"}}
elif value is False:
fc = {feature: {"support": "unsupported"}}
elif value is None:
fc = {feature: {"support": "unknown"}}
else:
assert False
self.copyFeatureSet(fc, collapse=False)
feat_def = self.find_feature(feature)
feat_type = feat_def.get('type', 'server-feature')
sup = fc[feature].get('support', feat_def.get('default', 'full'))
## TODO: Why is this camelCase while every other method is with under_score? rename ...
def copyFeatureSet(self, feature_set, collapse=True):
for feature in feature_set:
## TODO: temp - should be removed
if feature == 'old_flags':
self._old_flags = feature_set[feature]
continue
try:
feature_info = self.find_feature(feature)
except (AssertionError, KeyError):
warnings.warn(
f"Unknown feature '{feature}' in configuration. "
"This might be a typo. Check caldav/compatibility_hints.py for valid features.",
UserWarning,
stacklevel=3,
)
feature_info = {}
value = feature_set[feature]
if feature not in self._server_features:
self._server_features[feature] = {}
server_node = self._server_features[feature]
if isinstance(value, bool):
server_node['support'] = "full" if value else "unsupported"
elif isinstance(value, str) and 'support' not in server_node:
self._validate_support_level(value, feature)
server_node['support'] = value
elif isinstance(value, dict):
if 'support' in value:
self._validate_support_level(value['support'], feature)
server_node.update(value)
else:
assert False
if collapse:
self.collapse()
def _validate_support_level(self, level, feature_name):
"""Validate that a support level is valid, warn if not."""
if level not in VALID_SUPPORT_LEVELS:
warnings.warn(
f"Feature '{feature_name}' has invalid support level '{level}'. "
f"Valid levels: {', '.join(sorted(VALID_SUPPORT_LEVELS))}",
UserWarning,
stacklevel=4,
)
def _collapse_key(self, feature_dict):
"""
Extract the key part of a feature dictionary for comparison during collapse.
For collapse purposes, we compare the 'support' level (or 'enable', 'behaviour', 'observed')
but ignore differences in detailed behaviour messages, as those are often implementation-specific
error messages that shouldn't prevent collapsing.
"""
if not isinstance(feature_dict, dict):
return feature_dict
# Return a tuple of the main status fields, ignoring detailed messages
return (
feature_dict.get('support'),
feature_dict.get('enable'),
feature_dict.get('observed'),
)
def collapse(self):
"""
If all subfeatures are the same, it should be collapsed into the parent
Messy and complex logic :-(
"""
features = list(self._server_features.keys())
parents = set()
for feature in features:
if '.' in feature:
parents.add(feature[:feature.rfind('.')])
parents = list(parents)
## Parents needs to be ordered by the number of dots. We proceed those with most dots first.
parents.sort(key = lambda x: (-x.count('.'), x))
for parent in parents:
parent_info = self.find_feature(parent)
if len(parent_info['subfeatures']):
foo = self.is_supported(parent, return_type=dict, return_defaults=False)
if len(parent_info['subfeatures']) > 1 or foo is not None:
dont_collapse = False
foo_key = self._collapse_key(foo) if foo is not None else None
for sub in parent_info['subfeatures']:
bar = self._server_features.get(f"{parent}.{sub}")
if bar is None:
dont_collapse = True
break
bar_key = self._collapse_key(bar)
if foo is None:
foo = bar
foo_key = bar_key
elif bar_key != foo_key:
dont_collapse = True
break
if not dont_collapse:
if parent not in self._server_features:
self._server_features[parent] = {}
for sub in parent_info['subfeatures']:
self._server_features.pop(f"{parent}.{sub}")
self.copyFeatureSet({parent: foo})
def _default(self, feature_info):
if isinstance(feature_info, str):
feature_info = self.find_feature(feature_info)
if 'default' in feature_info:
return feature_info['default']
feature_type = feature_info.get('type', 'server-feature')
## TODO: move the default values up to some constant dict probably, like self.DEFAULTS = { "server-feature": {...}}
if feature_type == 'server-feature':
return { "support": "full" }
elif feature_type == 'client-feature':
return { "enable": False }
elif feature_type == 'server-peculiarity':
return { "behaviour": "normal" }
elif feature_type == 'server-observation':
return { "observed": True }
elif feature_type in ('tests-behaviour', 'client-hints'):
return { }
else:
raise ValueError(f"Unknown feature type: {feature_type!r}")
def is_supported(self, feature, return_type=bool, return_defaults=True, accept_fragile=False):
"""Work in progress
TODO: write a better docstring
The dotted features is essentially a tree. If feature foo
is unsupported it basically means that feature foo.bar is also
unsupported. Hence the extra logic visiting "nodes".
"""
feature_info = self.find_feature(feature)
feature_ = feature
while True:
if feature_ in self._server_features:
return self._convert_node(self._server_features[feature_], feature_info, return_type, accept_fragile)
# Try deriving status from subfeatures at this level
current_info = feature_info if feature_ == feature else self.find_feature(feature_)
if 'default' not in current_info:
derived = self._derive_from_subfeatures(feature_, current_info, return_type, accept_fragile)
if derived is not None:
return derived
if '.' not in feature_:
if not return_defaults:
return None
# For features WITHOUT an explicit default (i.e. pure grouping features),
# derive status from subfeatures. Features WITH a default represent
# independent capabilities and their default should not be overridden
# by subfeature statuses (e.g. create-calendar is supported even if
# create-calendar.set-displayname is not).
if 'default' not in feature_info:
derived = self._derive_from_subfeatures(feature_, feature_info, return_type, accept_fragile)
if derived is not None:
return derived
return self._convert_node(self._default(feature_info), feature_info, return_type, accept_fragile)
feature_ = feature_[:feature_.rfind('.')]
_POSITIVE_STATUSES = frozenset({'full', 'quirk'})
def _derive_from_subfeatures(self, feature, feature_info, return_type, accept_fragile=False):
"""
Derive parent feature status from explicitly set subfeatures.
Logic:
- Only consider subfeatures WITHOUT explicit defaults (those are independent features)
- If ANY relevant subfeature has a positive status (full/quirk) → derive as that status
(any support means the parent has some support)
- If ALL relevant subfeatures are set AND all have the same negative status → use that status
- If only a PARTIAL set of subfeatures is configured with all negative statuses →
return None (incomplete information, fall through to default)
- Mixed statuses (some positive, some negative) → "unknown"
Returns None if no relevant subfeatures are explicitly set or if
derivation is inconclusive due to partial information.
"""
if 'subfeatures' not in feature_info or not feature_info['subfeatures']:
return None
# Count relevant subfeatures (those without explicit defaults) and collect statuses
total_relevant = 0
subfeature_statuses = []
for sub in feature_info['subfeatures']:
subfeature_key = f"{feature}.{sub}"
# Skip subfeatures with explicit defaults - they represent independent behaviors
try:
subfeature_info = self.find_feature(subfeature_key)
if 'default' in subfeature_info:
continue
except:
pass
total_relevant += 1
if subfeature_key in self._server_features:
sub_dict = self._server_features[subfeature_key]
# Extract the support level (or enable/behaviour/observed)
status = sub_dict.get('support', sub_dict.get('enable', sub_dict.get('behaviour', sub_dict.get('observed'))))
if status:
subfeature_statuses.append(status)
# If no relevant subfeatures are explicitly set, return None (use default)
if not subfeature_statuses:
return None
has_positive = any(s in self._POSITIVE_STATUSES for s in subfeature_statuses)
all_same = all(s == subfeature_statuses[0] for s in subfeature_statuses)
is_complete = len(subfeature_statuses) >= total_relevant
if has_positive:
if all_same:
derived_status = subfeature_statuses[0]
else:
# Mixed positive/negative → unknown
derived_status = 'unknown'
elif is_complete and all_same:
# All relevant subfeatures set, all the same negative status
derived_status = subfeature_statuses[0]
elif is_complete:
# All relevant subfeatures set, mixed non-positive statuses
derived_status = 'unknown'
else:
# Partial set with only non-positive statuses → inconclusive,
# the unset siblings might have different (positive) status
return None
# Create a node dict with the derived status
derived_node = {'support': derived_status}
return self._convert_node(derived_node, feature_info, return_type, accept_fragile)
def _convert_node(self, node, feature_info, return_type, accept_fragile=False):
"""
Return the information in a "node" given the wished return_type
(The dotted feature format was an afterthought, the first
iteration of this code the feature tree was actually a
hierarchical dict, hence the naming of the method. I
considered it too complicated though)
"""
if return_type == str:
## TODO: consider feature_info['type'], be smarter about it
return node.get('support', node.get('enable', node.get('behaviour')))
elif return_type == dict:
return node
elif return_type == bool:
## TODO: consider feature_info['type'], be smarter about this
support = node.get('support', 'full')
if support == 'quirk':
return True
if accept_fragile and support == 'fragile':
support = 'full'
if feature_info.get('type', 'server-feature') == 'server-feature':
return support == 'full'
else:
## TODO: this may be improved
return not node.get('enable') and not node.get('behaviour') and not node.get('observed')
else:
assert False
@classmethod
def find_feature(cls, feature: str) -> dict:
"""
Feature should be a string like feature.subfeature.subsubfeature.
Looks through the FEATURES list and returns the relevant section.
Will raise an Error if feature is not found
(this is very simple now - used to be a hierarchy dict to be traversed)
"""
assert feature in cls.FEATURES ## A feature in the configured feature-list does not exist. TODO ... raise a better exception?
if 'name' not in cls.FEATURES[feature]:
cls.FEATURES[feature]['name'] = feature
if '.' in feature and 'parent' not in cls.FEATURES[feature]:
cls.FEATURES[feature]['parent'] = cls.find_feature(feature[:feature.rfind('.')])
if 'subfeatures' not in cls.FEATURES[feature]:
tree = cls.feature_tree()
for x in feature.split('.'):
tree = tree[x]
cls.FEATURES[feature]['subfeatures'] = tree
return cls.FEATURES[feature]
@classmethod
def _dots_to_tree(cls, target, source):
for feat in source:
node = target
path = feat.split('.')
for part in path:
if part not in node:
node[part] = {}
node = node[part]
return target
@classmethod
def feature_tree(cls) -> dict:
"""TODO: is this in use at all? Can it be deprecated already?
TODO: the description may be outdated as I decided to refactor
things from "overly complex" to "just sufficiently complex".
Or maybe it's still a bit too complex.
A "path" may have several "subpaths" in self.FEATURES
(i.e. feat.subfeat.A, feat.subfeat.B, feat.subfeat.C)
This method will return `{'feat': { 'subfeat': {'A': {}, ...}}}`
making it possible to traverse the feature tree
"""
## I'm an old fart, grown up in an age where CPU-cycles was considered
## expensive ... so I always cache things when possible ...
if hasattr(cls, '_feature_tree'):
return cls._feature_tree
cls._feature_tree = {}
cls._dots_to_tree(cls._feature_tree, cls.FEATURES)
return cls._feature_tree
def dotted_feature_set_list(self, compact=False):
ret = {}
if compact:
self.collapse()
for x in self._server_features:
feature = self._server_features[x]
if compact and feature == self._default(x):
continue
ret[x] = feature.copy()
return ret
#### OLD STYLE
## THE LIST BELOW IS TO BE REMOVED COMPLETELY. DO NOT USE IT.
## It's not considered to be part of the public API (though, it should
## have been prefixed with _ to make it clear). The list is being
## removed little-by-little, without regards of SemVer.
## The lists below are specifying what tests should be skipped or
## modified to accept non-conforming resultsets from the different
## calendar servers. In addition there are some hacks in the library
## code itself to work around some known compatibility issues, like
## the caldav.lib.vcal.fix function.
## Here is a list of all observed (in)compatibility issues the test framework needs to know about
## TODO:
## * references to the relevant parts of the RFC would be nice.
## * Research should be done to triple-check that the issue is on the server side, and not on the client side
## * Some of the things below should be possible to probe the server for.
## * Perhaps some more readable format should be considered (yaml?).
## * Consider how to get this into the documentation
incompatibility_description = {
'no_current-user-principal':
"""Current user principal not supported by the server (flag is ignored by the tests as for now - pass the principal URL as the testing URL and it will work, albeit with one warning""",
'no_scheduling':
"""RFC6833 is not supported""",
'no_scheduling_mailbox':
"""Parts of RFC6833 is supported, but not the existence of inbox/mailbox""",
'no_scheduling_calendar_user_address_set':
"""Parts of RFC6833 is supported, but not getting the calendar users addresses""",
'no_default_calendar':
"""The given user starts without an assigned default calendar """
"""(or without pre-defined calendars at all)""",
'no_freebusy_rfc6638':
"""Server does not support a freebusy-request as per RFC6638""",
'calendar_order':
"""Server supports (nonstandard) calendar ordering property""",
'calendar_color':
"""Server supports (nonstandard) calendar color property""",
'duplicates_not_allowed':
"""Duplication of an event in the same calendar not allowed """
"""(even with different uid)""",
'event_by_url_is_broken':
"""A GET towards a valid calendar object resource URL will yield 404 (wtf?)""",
'no_delete_event':
"""Zimbra does not support deleting an event, probably because event_by_url is broken""",
'propfind_allprop_failure':
"""The propfind test fails ... """
"""it asserts DAV:allprop response contains the text 'resourcetype', """
"""possibly this assert is wrong""",
'vtodo_datesearch_nodtstart_task_is_skipped':
"""date searches for todo-items will not find tasks without a dtstart""",
'vtodo_datesearch_nodtstart_task_is_skipped_in_closed_date_range':
"""only open-ended date searches for todo-items will find tasks without a dtstart""",
'vtodo_datesearch_notime_task_is_skipped':
"""date searches for todo-items will (only) find tasks that has either """
"""a dtstart or due set""",
'vtodo_datesearch_nostart_future_tasks_delivered':
"""Future tasks are yielded when doing a date search with some end timestamp and without start timestamp and the task contains both dtstart and due, but not duration (xandikos 0.2.12)""",
'vtodo_no_due_infinite_duration':
"""date search will find todo-items without due if dtstart is """
"""before the date search interval. This is in breach of rfc4791"""
"""section 9.9""",
'vtodo_no_dtstart_infinite_duration':
"""date search will find todo-items without dtstart if due is """
"""after the date search interval. This is in breach of rfc4791"""
"""section 9.9""",
'vtodo_no_dtstart_search_weirdness':
"""Zimbra is weird""",
'vtodo_no_duration_search_weirdness':
"""Zimbra is weird""",
'vtodo_with_due_weirdness':
"""Zimbra is weird""",
'vtodo-cannot-be-uncompleted':
"""If a VTODO object has been set with STATUS:COMPLETE, it's not possible to delete the COMPLTEDED attribute and change back to STATUS:IN-ACTION""",
'unique_calendar_ids':
"""For every test, generate a new and unique calendar id""",
'sticky_events':
"""Events should be deleted before the calendar is deleted, """
"""and/or deleting a calendar may not have immediate effect""",
'no_overwrite':
"""events cannot be edited""",
'dav_not_supported':
"""when asked, the server may claim it doesn't support the DAV protocol. Observed by one baikal server, should be investigated more (TODO) and robur""",
'text_search_is_case_insensitive':
"""Probably not supporting the collation used by the caldav library""",
'date_search_ignores_duration':
"""Date search with search interval overlapping event interval works on events with dtstart and dtend, but not on events with dtstart and due""",
'date_todo_search_ignores_duration':
"""Same as above, but specifically for tasks""",
'fastmail_buggy_noexpand_date_search':
"""The 'blissful anniversary' recurrent example event is returned when asked for a no-expand date search for some timestamps covering a completely different date""",
'non_existing_raises_other':
"""Robur raises AuthorizationError when trying to access a non-existing resource (while 404 is expected). Probably so one shouldn't probe a public name space?""",
'no_supported_components_support':
"""The supported components prop query does not work""",
'no_relships':
"""The calendar server does not support child/parent relationships between calendar components""",
'robur_rrule_freq_yearly_expands_monthly':
"""Robur expands a yearly event into a monthly event. I believe I've reported this one upstream at some point, but can't find back to it""",
'no_search_openended':
"""An open-ended search will not work""",
}
## This is for Xandikos 0.2.12.
## Lots of development going on as of summer 2025, so expect the list to become shorter soon!
xandikos_v0_2_12 = {
## this only applies for very simple installations
"auto-connect.url": {"domain": "localhost", "scheme": "http", "basepath": "/"},
'search.recurrences.includes-implicit': {'support': 'unsupported'},
'search.recurrences.expanded': {'support': 'unsupported'},
'search.time-range.todo': {'support': 'unsupported'},
'search.time-range.alarm': {'support': 'ungraceful', 'behaviour': '500 internal server error'},
'search.comp-type-optional': {'support': 'ungraceful'},
"search.text.substring": {"support": "unsupported"},
"search.text.category.substring": {"support": "unsupported"},
'principal-search': {'support': 'unsupported'},
'freebusy-query.rfc4791': {'support': 'ungraceful', 'behaviour': '500 internal server error'},
"old_flags": [
## https://github.com/jelmer/xandikos/issues/8
'date_todo_search_ignores_duration',
'vtodo_datesearch_nostart_future_tasks_delivered',
## scheduling is not supported
"no_scheduling",
## The test with an rrule and an overridden event passes as
## long as it's with timestamps. With dates, xandikos gets
## into troubles. I've chosen to edit the test to use timestamp
## rather than date, just to have the test exercised ... but we
## should report this upstream
#'broken_expand_on_exceptions',
]
}
xandikos_v0_3 = {
## this only applies for very simple installations
"auto-connect.url": {"domain": "localhost", "scheme": "http", "basepath": "/"},
'search.comp-type-optional': {'support': 'unsupported'},
## This suddenly disappeared. Should probably look more into the checks ...
#"search.recurrences.includes-implicit.todo.pending": {"support": "unsupported"},
'search.recurrences.expanded.todo': {'support': 'unsupported'},
'search.recurrences.expanded.exception': {'support': 'unsupported'},
'principal-search': {'support': 'ungraceful'},
'freebusy-query.rfc4791': {'support': 'ungraceful', 'behaviour': '500 internal server error'},
"old_flags": [
## https://github.com/jelmer/xandikos/issues/8
'date_todo_search_ignores_duration',
'vtodo_datesearch_nostart_future_tasks_delivered',
## scheduling is not supported
"no_scheduling",
## The test with an rrule and an overridden event passes as
## long as it's with timestamps. With dates, xandikos gets
## into troubles. I've chosen to edit the test to use timestamp
## rather than date, just to have the test exercised ... but we
## should report this upstream
#'broken_expand_on_exceptions',
]
}
xandikos_main = xandikos_v0_3.copy()
## woot ... a regression?
## xandikos did support freebusy-query.rfc4791 for a while, now it trips on a traceback
## TODO: do research into this and report
#xandikos_main.pop('freebusy-query.rfc4791')
xandikos = xandikos_main
## This seems to work as of version 3.5.4 of Radicale.
## There is much development going on at Radicale as of summar 2025,
## so I'm expecting this list to shrink a lot soon.
radicale = {
"search.is-not-defined": {"support": "full"},
"search.text.case-sensitive": {"support": "unsupported"},
"search.recurrences.includes-implicit.todo.pending": {"support": "fragile", "behaviour": "inconsistent results between runs"},
"search.recurrences.expanded.todo": {"support": "unsupported"},
"search.recurrences.expanded.exception": {"support": "unsupported"},
"principal-search": {"support": "unsupported"},
## this only applies for very simple installations
"auto-connect.url": {"domain": "localhost", "scheme": "http", "basepath": "/"},
## freebusy is not supported yet, but on the long-term road map
'old_flags': [
## calendar listings and calendar creation works a bit
## "weird" on radicale
'no_scheduling',
'no_search_openended',
#'text_search_is_exact_match_sometimes',
## extra features not specified in RFC5545
"calendar_order",
"calendar_color"
]
}
## Be aware that nextcloud by default have different rate limits, including how often a user is allowed to create a new calendar. This may break test runs badly.
nextcloud = {
'auto-connect.url': {
'basepath': '/remote.php/dav',
},
## I'm surprised, I'm quite sure this was reported ungraceful earlier. Passed with caldav commit a98d50490b872e9b9d8e93e2e401c936ad193003, caldav server checker commit 3cae24cf99da1702b851b5a74a9b88c8e5317dad 2026-02-15. The commit 3cae24cf99da1702b851b5a74a9b88c8e5317dad was however development done on the wrong branch and has been force-pushed awway. It was again observed ungraceful at commits be26d42b1ca3ff3b4fd183761b4a9b024ce12b84 / 537a23b145487006bb987dee5ab9e00cdebb0492
'search.comp-type-optional': {'support': 'ungraceful'},
'search.recurrences.expanded.todo': {'support': 'unsupported'},
'search.recurrences.expanded.exception': {'support': 'unsupported'}, ## TODO: verify
'delete-calendar': {
'support': 'fragile',
'behaviour': 'Deleting a recently created calendar fails'},
'delete-calendar.free-namespace': { ## TODO: not caught by server-tester
'behaviour': "deleting a calendar moves it to a trashbin, thrashbin has to be manually 'emptied' from the web-ui before the namespace is freed up",
'support': 'fragile',
},
'search.recurrences.includes-implicit.todo': {'support': 'unsupported'},
#'save-load.todo.mixed-calendar': {'support': 'unsupported'}, ## Why? It started complaining about this just recently.
'principal-search.by-name.self': {'support': 'unsupported'},
'principal-search': {'support': 'ungraceful'},
'old_flags': ['unique_calendar_ids'],
## I'm surprised, I'm quite sure this was passing earlier. Caldav commit a98d50490b872e9b9d8e93e2e401c936ad193003, caldav server checker commit 3cae24cf99da1702b851b5a74a9b88c8e5317dad
'search.combined-is-logical-and': False
}
## TODO: Latest - mismatch between config and test script in delete-calendar.free-namespace ... and create-calendar.set-displayname?
ecloud = nextcloud | {
#'search.is-not-defined': {'support': 'unsupported'}, ## observed to work at 4bc0de765a2b53e6f223e0b9ac51c653bac11fb7 (caldav) / 3cae24cf99da1702b851b5a74a9b88c8e5317dad (server checker)
#'search.text.case-sensitive': {'support': 'unsupported'}, ## observed to work at 4bc0de765a2b53e6f223e0b9ac51c653bac11fb7 (caldav) / 3cae24cf99da1702b851b5a74a9b88c8e5317dad (server checker)
## TODO: this applies only to test runs, not to ordinary usage
'rate-limit': {
'enable': True,
'interval': 2,
'count': 1,
'default_sleep': 4,
'max_sleep': 120,
'description': "It's needed to manually empty trashbin frequently when running tests. Since this operation takes some time and/or there are some caches, it's needed to run tests slowly, even when hammering the 'empty thrashbin' frequently",
},
'auto-connect.url': {
'basepath': '/remote.php/dav',
'domain': 'ecloud.global',
'scheme': 'https',
},
}
## Zimbra is not very good at it's caldav support
zimbra = {
'auto-connect.url': {'basepath': '/dav/'},
'delete-calendar': {'support': 'fragile', 'behaviour': 'may move to trashbin instead of deleting immediately'},
'save-load.get-by-url': {'support': 'fragile', 'behaviour': '404 most of the time - but sometimes 200. Weird, should be investigated more'},
## Zimbra treats same-UID events across calendars as aliases of the same event
'save.duplicate-uid.cross-calendar': {'support': 'unsupported'},
'search.recurrences.expanded.exception': {'support': 'unsupported'}, ## TODO: verify
'create-calendar.set-displayname': {'support': 'unsupported'},
'save-load.todo.mixed-calendar': {'support': 'unsupported'},
'save-load.todo.recurrences.count': {'support': 'unsupported'}, ## This is a new problem?
'save-load.journal': {'support': 'ungraceful'},
'sync-token': {'support': 'fragile'},
'search.is-not-defined': {'support': 'unsupported'},
'search.text': {'support': 'unsupported'},
# sometimes throws a 500
'search.text.category': {'support': 'ungraceful'},
'search.recurrences.expanded.todo': { "support": "unsupported" },
'search.comp-type-optional': {'support': 'fragile'}, ## TODO: more research on this, looks like a bug in the checker,
'search.time-range.alarm': {'support': 'unsupported'},
'principal-search': "unsupported",
"old_flags": [
## apparently, zimbra has no journal support
## setting display name in zimbra does not work (display name,
## calendar-ID and URL is the same, the display name cannot be
## changed, it can only be given if no calendar-ID is given. In
## earlier versions of Zimbra display-name could be changed, but
## then the calendar would not be available on the old URL
## anymore)
## 'event_by_url_is_broken' removed - works in zimbra/zcs-foss:latest
'no_delete_event',
'vtodo_datesearch_notime_task_is_skipped',
'no_relships',
## TODO: I just discovered that when searching for a date some