Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package Radicale for openSUSE:Factory 
checked in at 2026-08-29 17:43:43
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/Radicale (Old)
 and      /work/SRC/openSUSE:Factory/.Radicale.new.1265 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "Radicale"

Sat Aug 29 17:43:43 2026 rev:32 rq:1374593 version:3.7.8

Changes:
--------
--- /work/SRC/openSUSE:Factory/Radicale/Radicale.changes        2026-08-04 
22:48:31.022753200 +0200
+++ /work/SRC/openSUSE:Factory/.Radicale.new.1265/Radicale.changes      
2026-08-29 17:44:47.842213746 +0200
@@ -1,0 +2,15 @@
+Sat Aug 22 08:58:49 UTC 2026 - Ákos Szőts <[email protected]>
+
+- Warning: upcoming 3.8.0 will be enhanced with sharing-by-group/realm 
feature, internal technical limitations leads to required enhanced username 
syntax check, following (unusual) usernames will be no longer supported:
+  * username starts or ends with @
+  * username containing more than one @
+  * username containing :
+- Update to 3.7.8
+  * Fix: time-range filter on a VTODO having DTSTART/DUE and also 
CREATED/COMPLETED used the CREATED->COMPLETED duration instead of the 
DTSTART->DUE one, so completed tasks were missing from (or wrongly returned by) 
calendar-query REPORT results
+  * Fix: sharing/proppatch: reject in case of write-access but 'p' is in 
permissions
+  * Fix: sharing/by-map: catch collection path without trailing / (supporting 
"pimsync")
+  * Improvements
+  * Add: [report] max_expand_occurrence option to separate from 
max_freebusy_occurrence
+  * Add: [system] max_vevent_rrule_occurrence option to catch DoS by 
problematic RRULE early enough (workaround for missing protection in current 
vobject version)
+
+-------------------------------------------------------------------

Old:
----
  Radicale-3.7.7.tar.gz

New:
----
  Radicale-3.7.8.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ Radicale.spec ++++++
--- /var/tmp/diff_new_pack.XIxU7S/_old  2026-08-29 17:44:48.610240912 +0200
+++ /var/tmp/diff_new_pack.XIxU7S/_new  2026-08-29 17:44:48.611240948 +0200
@@ -28,7 +28,7 @@
 %define py_min_ver 3.9
 %define vo_min_ver 0.9.6
 Name:           Radicale
-Version:        3.7.7
+Version:        3.7.8
 Release:        0
 Summary:        A CalDAV calendar and CardDav contact server
 License:        GPL-3.0-or-later

++++++ Radicale-3.7.7.tar.gz -> Radicale-3.7.8.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.7.7/CHANGELOG.md 
new/Radicale-3.7.8/CHANGELOG.md
--- old/Radicale-3.7.7/CHANGELOG.md     2026-07-19 16:31:27.000000000 +0200
+++ new/Radicale-3.7.8/CHANGELOG.md     2026-08-06 08:01:32.000000000 +0200
@@ -1,5 +1,12 @@
 # Changelog
 
+## 3.7.8
+* Fix: time-range filter on a VTODO having DTSTART/DUE and also 
CREATED/COMPLETED used the CREATED->COMPLETED duration instead of the 
DTSTART->DUE one, so completed tasks were missing from (or wrongly returned by) 
calendar-query REPORT results
+* Fix: sharing/proppatch: reject in case of write-access but 'p' is in 
permissions
+* Fix: sharing/by-map: catch collection path without trailing / (supporting 
"pimsync")
+* Add: [report] max_expand_occurrence option to separate from 
max_freebusy_occurrence
+* Add: [system] max_vevent_rrule_occurrence option to catch DoS by problematic 
RRULE early enough (workaround for missing protection in current vobject 
version)
+
 ## 3.7.7
 * Fix: web plugin helpers httputils.serve_resource/serve_folder ignored their 
mimetypes and fallback_mimetype parameters and always used the built-in 
mapping, so custom web plugins could not serve additional file types with a 
correct Content-Type
 * Fix: free-busy REPORT always failed with HTTP 400 ("FREEBUSY occurrences 
limit of 0 hit") when [reporting] max_freebusy_occurrence is set to 0 (limit 
disabled), because the limit check did not honor the disabled limit
@@ -8,7 +15,6 @@
 * Fix: text-match filter on a structured property (e.g. vCard N or ADR) 
crashed with HTTP 500 (AttributeError: 'Name'/'Address' object has no attribute 
'lower') because vobject parses these into non-string objects; their text 
representation is now used
 * Fix: sharing bday-to-ICS conversion assigned the empty-FN fallback marker to 
the {nickname} placeholder instead of {fn}, so a VCARD with an empty FN got its 
NICKNAME overwritten with "!fn!" in the generated SUMMARY/DESCRIPTION and the 
{fn} fallback never resolved
 * Fix: sharing/delete: add forgotten backmap
-* Fix: sharing/proppatch: reject in case of write-access but 'p' is in 
permissions
 
 ## 3.7.6
 * Extension: item verification on commandline
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.7.7/DOCUMENTATION.md 
new/Radicale-3.7.8/DOCUMENTATION.md
--- old/Radicale-3.7.7/DOCUMENTATION.md 2026-07-19 16:31:27.000000000 +0200
+++ new/Radicale-3.7.8/DOCUMENTATION.md 2026-08-06 08:01:32.000000000 +0200
@@ -866,6 +866,19 @@
 
 Announced to clients requesting "max-resource-size" via PROPFIND.
 
+##### max_vevent_rrule_occurrence
+
+_(>= 3.7.8)_
+
+The maximum of occurrence by an rrule of a vevent.
+Large time frames defined in RRULE by COUNT or UNTIL could
+generate a lot of occurrences based on the time frame supplied. This
+setting limits the lookup to prevent potential denial of service
+attacks on large time frames. If the limit is reached, an HTTP error
+is thrown instead of accepting the item.
+
+Default: `10000`
+
 ##### timeout
 
 Socket timeout. (seconds)
@@ -2191,6 +2204,19 @@
 
 #### [reporting]
 
+##### max_expand_occurrence
+
+_(>= 3.7.8)_
+
+When returning an expanded report, a list of occurrences are
+generated based on a given time frame. Large time frames could
+generate a lot of occurrences based on the time frame supplied. This
+setting limits the lookup to prevent potential denial of service
+attacks on large time frames. If the limit is reached, an HTTP error
+is thrown instead of returning the results.
+
+Default: 10000
+
 ##### max_freebusy_occurrence
 
 _(>= 3.2.3)_
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.7.7/config new/Radicale-3.7.8/config
--- old/Radicale-3.7.7/config   2026-07-19 16:31:27.000000000 +0200
+++ new/Radicale-3.7.8/config   2026-08-06 08:01:32.000000000 +0200
@@ -33,6 +33,9 @@
 # Announced to clients requesting "max-resource-size" via PROPFIND
 #max_resource_size = 10000000
 
+# Max occurrence by an RRULE, limit the number to prevent DoS attacks.
+#max_vevent_rrule_occurrence = 10000
+
 # Socket timeout (seconds)
 #timeout = 30
 
@@ -499,6 +502,10 @@
 
 [reporting]
 
+# When returning an expanded report, limit the number of returned
+# occurences per event to prevent DoS attacks.
+#max_expand_occurrence = 10000
+
 # When returning a free-busy report, limit the number of returned
 # occurences per event to prevent DoS attacks.
 #max_freebusy_occurrence = 10000
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.7.7/pyproject.toml 
new/Radicale-3.7.8/pyproject.toml
--- old/Radicale-3.7.7/pyproject.toml   2026-07-19 16:31:27.000000000 +0200
+++ new/Radicale-3.7.8/pyproject.toml   2026-08-06 08:01:32.000000000 +0200
@@ -3,7 +3,7 @@
 # When the version is updated, a new section in the CHANGELOG.md file must be
 # added too.
 readme = "README.md"
-version = "3.7.7"
+version = "3.7.8"
 authors = [{name = "Guillaume Ayoub", email = "[email protected]"}, 
{name = "Unrud", email = "[email protected]"}, {name = "Peter Bieringer", email 
= "[email protected]"}]
 license = {text = "GNU GPL v3"}
 description = "CalDAV and CardDAV Server"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.7.7/radicale/__main__.py 
new/Radicale-3.7.8/radicale/__main__.py
--- old/Radicale-3.7.7/radicale/__main__.py     2026-07-19 16:31:27.000000000 
+0200
+++ new/Radicale-3.7.8/radicale/__main__.py     2026-08-06 08:01:32.000000000 
+0200
@@ -200,9 +200,10 @@
 
     if args_ns.verify_item:
         encoding = configuration.get("encoding", "stock")
+        max_vevent_rrule_occurrence = configuration.get("server", 
"max_vevent_rrule_occurrence")
         logger.info("Item verification start using 'stock' encoding: %s", 
encoding)
         try:
-            if not item.verify(args_ns.verify_item[0], encoding):
+            if not item.verify(args_ns.verify_item[0], encoding, 
max_vevent_rrule_occurrence):
                 logger.critical("Item verification failed")
                 sys.exit(1)
         except Exception as e:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.7.7/radicale/app/__init__.py 
new/Radicale-3.7.8/radicale/app/__init__.py
--- old/Radicale-3.7.7/radicale/app/__init__.py 2026-07-19 16:31:27.000000000 
+0200
+++ new/Radicale-3.7.8/radicale/app/__init__.py 2026-08-06 08:01:32.000000000 
+0200
@@ -79,6 +79,7 @@
     _internal_server: bool
     _max_content_length: int
     _max_resource_size: int
+    _max_vevent_rrule_occurrence: int
     _auth_realm: str
     _auth_type: str
     _web_type: str
@@ -120,6 +121,8 @@
             self._max_resource_size = max_resource_size_limited
         else:
             logger.info("max_resource_size set to: %d bytes (%sbytes)", 
self._max_resource_size, utils.format_unit(self._max_resource_size, 
binary=True))
+        self._max_vevent_rrule_occurrence = configuration.get("server", 
"max_vevent_rrule_occurrence")
+        logger.info("max_vevent_rrule_occurrence set to: %d", 
self._max_vevent_rrule_occurrence)
         self._bad_put_request_content = configuration.get("logging", 
"bad_put_request_content")
         logger.info("log bad put request content: %s", 
self._bad_put_request_content)
         self._request_header_on_debug = configuration.get("logging", 
"request_header_on_debug")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.7.7/radicale/app/base.py 
new/Radicale-3.7.8/radicale/app/base.py
--- old/Radicale-3.7.7/radicale/app/base.py     2026-07-19 16:31:27.000000000 
+0200
+++ new/Radicale-3.7.8/radicale/app/base.py     2026-08-06 08:01:32.000000000 
+0200
@@ -122,6 +122,7 @@
     _sharing: sharing.BaseSharing
     _encoding: str
     _max_resource_size: int
+    _max_vevent_rrule_occurrence: int
     _permit_delete_collection: bool
     _permit_overwrite_collection: bool
     _strict_preconditions: bool
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.7.7/radicale/app/proppatch.py 
new/Radicale-3.7.8/radicale/app/proppatch.py
--- old/Radicale-3.7.7/radicale/app/proppatch.py        2026-07-19 
16:31:27.000000000 +0200
+++ new/Radicale-3.7.8/radicale/app/proppatch.py        2026-08-06 
08:01:32.000000000 +0200
@@ -142,6 +142,12 @@
         else:
             logger.trace("PROPPATCH/xml_proppatch: write-access: %r", path)
             if share:
+                if "p" in share['Permissions']:
+                    logger.notice("PROPPATCH request on shared %r: 
write-permissions, but disabled by share permission 'p'", path_orig)
+                    return httputils.NOT_ALLOWED
+                elif "p" in raw_permissions:
+                    logger.notice("PROPPATCH request on shared %r: 
write-permissions, but disabled by rights permission 'p'", path_orig)
+                    return httputils.NOT_ALLOWED
                 # write access -> check for enforced properties overlay
                 logger.trace("PROPPATCH/xml_proppatch: write-access/sharing: 
%r", path_orig)
                 if self._sharing.enforce_properties_overlay:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.7.7/radicale/app/put.py 
new/Radicale-3.7.8/radicale/app/put.py
--- old/Radicale-3.7.7/radicale/app/put.py      2026-07-19 16:31:27.000000000 
+0200
+++ new/Radicale-3.7.8/radicale/app/put.py      2026-08-06 08:01:32.000000000 
+0200
@@ -47,6 +47,7 @@
 
 def prepare(vobject_items: List[vobject.base.Component], path: str,
             content_type: str, permission: bool, parent_permission: bool, 
max_resource_size: int,
+            max_vevent_rrule_occurrence: int,
             tag: Optional[str] = None,
             write_whole_collection: Optional[bool] = None) -> Tuple[
                 Iterator[radicale_item.Item],  # items
@@ -73,7 +74,9 @@
     try:
         if tag and write_whole_collection is not None:
             radicale_item.check_and_sanitize_items(
-                vobject_items, is_collection=write_whole_collection, tag=tag)
+                vobject_items,
+                max_vevent_rrule_occurrence=max_vevent_rrule_occurrence,
+                is_collection=write_whole_collection, tag=tag)
             if write_whole_collection and tag == "VCALENDAR":
                 vobject_components: List[vobject.base.Component] = []
                 vobject_item, = vobject_items
@@ -224,7 +227,9 @@
              vobject_items, path, content_type,
              bool(rights.intersect(access.permissions, "Ww")),
              bool(rights.intersect(access.parent_permissions, "w")),
-             self._max_resource_size)
+             self._max_resource_size,
+             self._max_vevent_rrule_occurrence,
+             )
 
         with self._storage.acquire_lock("w", user, path=path, request="PUT"):
             item = next(iter(self._storage.discover(path)), None)
@@ -289,6 +294,7 @@
                      bool(rights.intersect(access.permissions, "Ww")),
                      bool(rights.intersect(access.parent_permissions, "w")),
                      self._max_resource_size,
+                     self._max_vevent_rrule_occurrence,
                      tag, write_whole_collection)
             props = prepared_props
             if prepared_exc_info:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.7.7/radicale/app/report.py 
new/Radicale-3.7.8/radicale/app/report.py
--- old/Radicale-3.7.7/radicale/app/report.py   2026-07-19 16:31:27.000000000 
+0200
+++ new/Radicale-3.7.8/radicale/app/report.py   2026-08-06 08:01:32.000000000 
+0200
@@ -895,9 +895,9 @@
                 assert item.collection is not None
                 collection = item.collection
 
-            max_occurrence = self.configuration.get("reporting", 
"max_freebusy_occurrence")
             if xml_content is not None and \
                xml_content.tag == xmlutils.make_clark("C:free-busy-query"):
+                max_occurrence = self.configuration.get("reporting", 
"max_freebusy_occurrence")
                 try:
                     status, body = free_busy_report(
                         base_prefix, path, xml_content, collection, 
self._encoding,
@@ -909,6 +909,7 @@
                 headers = {"Content-Type": "text/calendar; charset=%s" % 
self._encoding}
                 return status, headers, str(body), 
xmlutils.pretty_xml(xml_content)
             else:
+                max_occurrence = self.configuration.get("reporting", 
"max_expand_occurrence")
                 try:
                     status, xml_answer = xml_report(
                         base_prefix, path, xml_content, collection, 
self._encoding,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.7.7/radicale/config.py 
new/Radicale-3.7.8/radicale/config.py
--- old/Radicale-3.7.7/radicale/config.py       2026-07-19 16:31:27.000000000 
+0200
+++ new/Radicale-3.7.8/radicale/config.py       2026-08-06 08:01:32.000000000 
+0200
@@ -254,6 +254,10 @@
             "value": "10000000",
             "help": "maximum size of resource (default: 10 Mbyte)",
             "type": positive_int}),
+        ("max_vevent_rrule_occurrence", {
+            "value": "10000",
+            "help": "maximum occurrence by an RRULE (default: 10000)",
+            "type": positive_int}),
         ("timeout", {
             "value": "30",
             "help": "socket timeout",
@@ -813,9 +817,13 @@
     ("headers", OrderedDict([
         ("_allow_extra", str)])),
     ("reporting", OrderedDict([
+        ("max_expand_occurrence", {
+            "value": "10000",
+            "help": "number of expand occurrences per event when reporting",
+            "type": positive_int}),
         ("max_freebusy_occurrence", {
             "value": "10000",
-            "help": "number of occurrences per event when reporting",
+            "help": "number of free-busy occurrences per event when reporting",
             "type": positive_int})]))
     ])
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.7.7/radicale/item/__init__.py 
new/Radicale-3.7.8/radicale/item/__init__.py
--- old/Radicale-3.7.7/radicale/item/__init__.py        2026-07-19 
16:31:27.000000000 +0200
+++ new/Radicale-3.7.8/radicale/item/__init__.py        2026-08-06 
08:01:32.000000000 +0200
@@ -53,6 +53,17 @@
 # List of BDAY years acting as flag for "no year specified"
 VCF_TO_ICS_BDAY_NO_YEAR: list = ["1604"]
 
+# List of RRULE frequencies and their interval in seconds
+RRULE_FREQUENCIES_TO_INTERVAL: dict[str, float] = {
+                                  "YEARLY": 60*60*24*365,
+                                  "MONTHLY": 60*60*24*365/12,
+                                  "WEEKLY": 60*60*24*7,
+                                  "DAILY": 60*60*24,
+                                  "HOURLY": 60*60,
+                                  "MINUTELY": 60,
+                                  "SECONDLY": 1,
+                                  }
+
 
 def read_components(s: str) -> List[vobject.base.Component]:
     """Wrapper for vobject.readComponents"""
@@ -103,6 +114,7 @@
 
 def check_and_sanitize_items(
         vobject_items: List[vobject.base.Component],
+        max_vevent_rrule_occurrence: int,
         is_collection: bool = False, tag: str = "") -> None:
     """Check vobject items for common errors and add missing UIDs.
 
@@ -232,11 +244,77 @@
                     if ref_value_param is not None:
                         dates.params["VALUE"] = ref_value_param
             # vobject interprets recurrence rules on demand
-            try:
-                component.rruleset
-            except Exception as e:
-                raise ValueError("Invalid recurrence rules in %s in object %r"
-                                 % (component.name, component_uid)) from e
+            if hasattr(component, "rrule"):
+                # workaround for vobject < 1.0.0 as it has no limiter in 
"getrruleset"
+                logger.trace("Recurrence rule found in %s in object %r: %r", 
component.name, component_uid, component.rrule.value)
+                if not hasattr(component, "dtstart"):
+                    # e.g. VTODO
+                    rrule = 
vobject.icalendar.rrule.rrulestr(component.rrule.value)
+                else:
+                    dtstart = 
radicale_filter.date_to_datetime(component.dtstart.value)
+                    ignoretz = (
+                        not isinstance(dtstart, datetime.datetime)
+                        or dtstart.tzinfo is None
+                    )
+                    rrule = 
vobject.icalendar.rrule.rrulestr(component.rrule.value, ignoretz=ignoretz)
+                # early check of maximum of COUNT to avoid DoS (workaround)
+                if hasattr(rrule, "_count") and rrule._count is not None:
+                    logger.trace("Recurrence rule %r in %s in object %r 
contains: COUNT=%d", component.rrule.value, component.name, component_uid, 
rrule._count)
+                    if max_vevent_rrule_occurrence > 0 and rrule._count > 
max_vevent_rrule_occurrence:
+                        logger.error("Recurrence rule %r count in %s in object 
%r: %d (REJECTED/limit: %d)", component.rrule.value, component.name, 
component_uid, rrule._count, max_vevent_rrule_occurrence)
+                        raise ValueError("Too many recurrence rule entries in 
%s in object %r: %d (limit: %d)"
+                                         % (component.name, component_uid, 
rrule._count, max_vevent_rrule_occurrence))
+                    else:
+                        logger.debug("Recurrence rule count in %s in object 
%r: %d (PASSED/limit: %d)" % (component.name, component_uid, rrule._count, 
max_vevent_rrule_occurrence))
+                else:
+                    logger.trace("Recurrence rule %r in %s in object %r 
doesn't contain: COUNT", component.rrule.value, component.name, component_uid)
+                # early check of maximum of (UNTIL-DTSTART)/interval(FREQ) to 
avoid DoS (ugly workaround with some guessing)
+                if hasattr(rrule, "_freq") and rrule._freq is not None and 
hasattr(rrule, "_until") and rrule._until is not None and hasattr(component, 
"dtstart"):
+                    if vobject.icalendar.FREQUENCIES[rrule._freq] not in 
RRULE_FREQUENCIES_TO_INTERVAL:
+                        raise ValueError("Unsupported FREQ in recurrence rule 
in %s in object %r: %r"
+                                         % (component.name, component_uid, 
rrule._freq))
+                    # RRULE has known FREQ+UNTIL+DTSTART
+                    # TZ code taken from vobject/icalendar.py/getrruleset
+                    if rrule._until.tzinfo is None:
+                        rrule._until = 
rrule._until.replace(tzinfo=dtstart.tzinfo)
+                    if dtstart.tzinfo is not None:
+                        rrule._until = rrule._until.astimezone(dtstart.tzinfo)
+                    delta = rrule._until - dtstart
+                    seconds = delta.total_seconds()
+                    if seconds < 0:
+                        # UNTIL < DTSTART
+                        logger.error("Recurrence rule %r in %s in object %r 
REJECTED, UNTIL < DTSTART", component.rrule.value, component.name, 
component_uid)
+                        raise ValueError("Recurrence rule in %s in object %r 
has UNTIL < DTSTART"
+                                         % (component.name, component_uid))
+                    rrule_entries = seconds / 
RRULE_FREQUENCIES_TO_INTERVAL[vobject.icalendar.FREQUENCIES[rrule._freq]]
+                    if max_vevent_rrule_occurrence > 0 and rrule_entries > 
max_vevent_rrule_occurrence:
+                        logger.warning("Recurrence rule %r entries in %s in 
object %r: %d (estimated/REJECTED/limit: %d)", component.rrule.value, 
component.name, component_uid, rrule_entries, max_vevent_rrule_occurrence)
+                        raise ValueError("Too many recurrence rule entries in 
%s in object %r: %d (limit: %d)"
+                                         % (component.name, component_uid, 
rrule_entries, max_vevent_rrule_occurrence))
+                    else:
+                        logger.debug("Recurrence rule %r entries in %s in 
object %r: %d (estimated/PASSED/limit: %d)" % (component.rrule.value, 
component.name, component_uid, rrule_entries, max_vevent_rrule_occurrence))
+                else:
+                    logger.trace("Recurrence rule %r in %s in object %r 
doesn't contain: FREQ+UNTIL", component.rrule.value, component.name, 
component_uid)
+                # generic check by vobject
+                try:
+                    rruleset = component.rruleset
+                except Exception as e:
+                    raise ValueError("Invalid recurrence rules in %s in object 
%r"
+                                     % (component.name, component_uid)) from e
+                # check limit (last resort)
+                infinite = False
+                if (";UNTIL=" not in component.rrule.value and
+                        ";COUNT=" not in component.rrule.value):
+                    infinite = True
+
+                if infinite is False:
+                    rrule_entries = len(list(rruleset))
+                    if max_vevent_rrule_occurrence > 0 and rrule_entries > 
max_vevent_rrule_occurrence:
+                        logger.warning("Recurrence rule %r entries in %s in 
object %r: %d (calculated/REJECTED/limit: %d)", component.rrule.value, 
component.name, component_uid, rrule_entries, max_vevent_rrule_occurrence)
+                        raise ValueError("Too many recurrence rule entries in 
%s in object %r: %d (limit: %d)"
+                                         % (component.name, component_uid, 
rrule_entries, max_vevent_rrule_occurrence))
+                    else:
+                        logger.debug("Recurrence rule %r entries in %s in 
object %r: %d (calculated/PASSED/limit: %d)", component.rrule.value, 
component.name, component_uid, rrule_entries, max_vevent_rrule_occurrence)
     elif tag == "VADDRESSBOOK":
         # https://tools.ietf.org/html/rfc6352#section-5.1
         object_uids = set()
@@ -387,7 +465,7 @@
     return math.floor(start.timestamp()), math.ceil(end.timestamp())
 
 
-def verify(file: str, encoding: str):
+def verify(file: str, encoding: str, max_vevent_rrule_occurrence: int):
     logger.info("Verifying item: %s", file)
     with open(file, "rb") as f:
         content_raw = f.read()
@@ -407,7 +485,7 @@
     try:
         tag = radicale_item.predict_tag_of_whole_collection(vobject_items)
         if tag is not None:
-            radicale_item.check_and_sanitize_items(vobject_items, tag=tag)
+            radicale_item.check_and_sanitize_items(vobject_items, tag=tag, 
max_vevent_rrule_occurrence=max_vevent_rrule_occurrence)
         else:
             raise ValueError("collection tag cannot be predicted")
     except Exception as e:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.7.7/radicale/item/filter.py 
new/Radicale-3.7.8/radicale/item/filter.py
--- old/Radicale-3.7.7/radicale/item/filter.py  2026-07-19 16:31:27.000000000 
+0200
+++ new/Radicale-3.7.8/radicale/item/filter.py  2026-08-06 08:01:32.000000000 
+0200
@@ -442,7 +442,10 @@
                 completed = date_to_datetime(completed.value)
                 if created is not None:
                     created = date_to_datetime(created.value)
-                    original_duration = (completed - created).total_seconds()
+                    # NOTE: kept separate from "original_duration", otherwise a
+                    # VTODO with DTSTART+DUE and also CREATED+COMPLETED would
+                    # lose its DTSTART->DUE duration (see line 2 below)
+                    completed_duration = (completed - created).total_seconds()
             elif created is not None:
                 created = date_to_datetime(created.value)
 
@@ -500,7 +503,7 @@
                 elif completed is not None and created is not None:
                     # Line 5
                     completed = reference_date + timedelta(
-                        seconds=original_duration)
+                        seconds=completed_duration)
                     if (range_fn(reference_date - SECOND,
                                  reference_date + SECOND,
                                  is_recurrence) or
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.7.7/radicale/sharing/__init__.py 
new/Radicale-3.7.8/radicale/sharing/__init__.py
--- old/Radicale-3.7.7/radicale/sharing/__init__.py     2026-07-19 
16:31:27.000000000 +0200
+++ new/Radicale-3.7.8/radicale/sharing/__init__.py     2026-08-06 
08:01:32.000000000 +0200
@@ -628,6 +628,7 @@
         """ returning dict with PathMapped, Owner, Permissions or None if 
invalid"""
         if self.sharing_collection_by_map:
             logger.trace("sharing/map/resolver: check path: %r", path)
+            # check collection path
             result = self.database_get_sharing(
                 ShareType="map",
                 PathOrToken=path,
@@ -635,7 +636,7 @@
                 User=user)
 
             if not result:
-                # fallback to parent path
+                # assume item path, fallback to parent path
                 parent_path = pathutils.parent_path(path)
                 logger.trace("sharing/map/resolver: check parent path: %r", 
parent_path)
                 result = self.database_get_sharing(
@@ -646,9 +647,19 @@
                 if result:
                     result['PathMapped'] = path.replace(parent_path, 
result['PathMapped'])
                     logger.trace("sharing/map/resolver: PathMapped=%r 
Permissions=%r by parent_path=%r", result['PathMapped'], result['Permissions'], 
parent_path)
-                else:
-                    logger.trace("sharing/map/resolver: not found")
-                    return None
+
+            if not result and not path.endswith("/"):
+                # assume collection path with missing trailing /
+                path_with_trailing_slash = path + "/"
+                logger.trace("sharing/map/resolver: check path having '/' 
appended: %r", path_with_trailing_slash)
+                result = self.database_get_sharing(
+                        ShareType="map",
+                        PathOrToken=path_with_trailing_slash,
+                        OnlyEnabled=False,
+                        User=user)
+                if result:
+                    path = path_with_trailing_slash
+                    logger.trace("sharing/map/resolver: PathMapped=%r 
Permissions=%r by path_with_trailing_slash=%r", result['PathMapped'], 
result['Permissions'], path_with_trailing_slash)
 
             if result:
                 if result['EnabledByOwner'] is not True:
@@ -666,6 +677,7 @@
                 logger.info("sharing/%s: resolved path %r->%r, user %r->%r, 
Permissions=%r Conversion=%r", "map", result['PathOrToken'], 
result['PathMapped'], user, result['Owner'], result['Permissions'], 
result['Conversion'])
                 return result
 
+            logger.trace("sharing/map/resolver: not found")
             return None
         else:
             logger.trace("sharing/map: not active")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/Radicale-3.7.7/radicale/storage/multifilesystem/base.py 
new/Radicale-3.7.8/radicale/storage/multifilesystem/base.py
--- old/Radicale-3.7.7/radicale/storage/multifilesystem/base.py 2026-07-19 
16:31:27.000000000 +0200
+++ new/Radicale-3.7.8/radicale/storage/multifilesystem/base.py 2026-08-06 
08:01:32.000000000 +0200
@@ -106,6 +106,7 @@
             "logging", "storage_cache_actions_on_debug")
         self._max_resource_size = configuration.get(
             "server", "max_resource_size")
+        self._max_vevent_rrule_occurrence = configuration.get("server", 
"max_vevent_rrule_occurrence")
 
     def _get_collection_root_folder(self) -> str:
         return os.path.join(self._filesystem_folder, "collection-root")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/Radicale-3.7.7/radicale/storage/multifilesystem/get.py 
new/Radicale-3.7.8/radicale/storage/multifilesystem/get.py
--- old/Radicale-3.7.7/radicale/storage/multifilesystem/get.py  2026-07-19 
16:31:27.000000000 +0200
+++ new/Radicale-3.7.8/radicale/storage/multifilesystem/get.py  2026-08-06 
08:01:32.000000000 +0200
@@ -124,7 +124,7 @@
                         vobject_items = radicale_item.read_components(
                             raw_text.decode(self._encoding))
                         radicale_item.check_and_sanitize_items(
-                            vobject_items, tag=self.tag)
+                            vobject_items, tag=self.tag, 
max_vevent_rrule_occurrence=self._storage._max_vevent_rrule_occurrence)
                         vobject_item, = vobject_items
                         temp_item = radicale_item.Item(
                             collection=self, vobject_item=vobject_item)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/Radicale-3.7.7/radicale/tests/static/event1_y2040.ics 
new/Radicale-3.7.8/radicale/tests/static/event1_y2040.ics
--- old/Radicale-3.7.7/radicale/tests/static/event1_y2040.ics   1970-01-01 
01:00:00.000000000 +0100
+++ new/Radicale-3.7.8/radicale/tests/static/event1_y2040.ics   2026-08-06 
08:01:32.000000000 +0200
@@ -0,0 +1,35 @@
+BEGIN:VCALENDAR
+PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
+VERSION:2.0
+BEGIN:VTIMEZONE
+TZID:Europe/Paris
+X-LIC-LOCATION:Europe/Paris
+BEGIN:DAYLIGHT
+TZOFFSETFROM:+0100
+TZOFFSETTO:+0200
+TZNAME:CEST
+DTSTART:19700329T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=3
+END:DAYLIGHT
+BEGIN:STANDARD
+TZOFFSETFROM:+0200
+TZOFFSETTO:+0100
+TZNAME:CET
+DTSTART:19701025T030000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+CREATED:20130902T150157Z
+LAST-MODIFIED:20130902T150158Z
+DTSTAMP:20130902T150158Z
+UID:event1-Y2040
+SUMMARY:Event in year 2040
+CATEGORIES:some_category1,another_category2
+ORGANIZER:mailto:[email protected]
+ATTENDEE;ROLE=REQ-PARTICIPANT;PARTSTAT=TENTATIVE;CN=Jane 
Doe:MAILTO:[email protected]
+ATTENDEE;ROLE=REQ-PARTICIPANT;DELEGATED-FROM="MAILTO:[email protected]";PARTSTAT=ACCEPTED;CN=John
 Doe:MAILTO:[email protected]
+DTSTART;TZID=Europe/Paris:20400901T180000
+DTEND;TZID=Europe/Paris:20400901T190000
+END:VEVENT
+END:VCALENDAR
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/Radicale-3.7.7/radicale/tests/static/event_full_day_rrule_count_500.ics 
new/Radicale-3.7.8/radicale/tests/static/event_full_day_rrule_count_500.ics
--- old/Radicale-3.7.7/radicale/tests/static/event_full_day_rrule_count_500.ics 
1970-01-01 01:00:00.000000000 +0100
+++ new/Radicale-3.7.8/radicale/tests/static/event_full_day_rrule_count_500.ics 
2026-08-06 08:01:32.000000000 +0200
@@ -0,0 +1,31 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
+BEGIN:VTIMEZONE
+LAST-MODIFIED:20040110T032845Z
+TZID:US/Eastern
+BEGIN:DAYLIGHT
+DTSTART:20000404T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:20001026T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+DTSTART;TZID=US/Eastern:20060102
+DTEND;TZID=US/Eastern:20060103
+RRULE:FREQ=DAILY;COUNT=500
+SUMMARY:Recurring event with count 500
+UID:event_full_day_rrule_count_500
+DTSTAMP:20060102T094829Z
+END:VEVENT
+END:VCALENDAR
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/Radicale-3.7.7/radicale/tests/static/event_full_day_rrule_until_2y.ics 
new/Radicale-3.7.8/radicale/tests/static/event_full_day_rrule_until_2y.ics
--- old/Radicale-3.7.7/radicale/tests/static/event_full_day_rrule_until_2y.ics  
1970-01-01 01:00:00.000000000 +0100
+++ new/Radicale-3.7.8/radicale/tests/static/event_full_day_rrule_until_2y.ics  
2026-08-06 08:01:32.000000000 +0200
@@ -0,0 +1,31 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
+BEGIN:VTIMEZONE
+LAST-MODIFIED:20040110T032845Z
+TZID:US/Eastern
+BEGIN:DAYLIGHT
+DTSTART:20000404T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:20001026T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+DTSTART;TZID=US/Eastern:20060102
+DTEND;TZID=US/Eastern:20060103
+RRULE:FREQ=DAILY;UNTIL=20080101
+SUMMARY:Recurring event with until +2y
+UID:event_full_day_rrule_until_2y
+DTSTAMP:20060102T094829Z
+END:VEVENT
+END:VCALENDAR
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/Radicale-3.7.7/radicale/tests/static/event_full_day_rrule_until_5000y.ics 
new/Radicale-3.7.8/radicale/tests/static/event_full_day_rrule_until_5000y.ics
--- 
old/Radicale-3.7.7/radicale/tests/static/event_full_day_rrule_until_5000y.ics   
    1970-01-01 01:00:00.000000000 +0100
+++ 
new/Radicale-3.7.8/radicale/tests/static/event_full_day_rrule_until_5000y.ics   
    2026-08-06 08:01:32.000000000 +0200
@@ -0,0 +1,31 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
+BEGIN:VTIMEZONE
+LAST-MODIFIED:20040110T032845Z
+TZID:US/Eastern
+BEGIN:DAYLIGHT
+DTSTART:20000404T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:20001026T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+DTSTART;TZID=US/Eastern:20060102
+DTEND;TZID=US/Eastern:20060103
+RRULE:FREQ=DAILY;UNTIL=70060101
+SUMMARY:Recurring event with until +5000y
+UID:event_full_day_rrule_until_5000y
+DTSTAMP:20060102T094829Z
+END:VEVENT
+END:VCALENDAR
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/Radicale-3.7.7/radicale/tests/static/event_full_day_rrule_until_50y.ics 
new/Radicale-3.7.8/radicale/tests/static/event_full_day_rrule_until_50y.ics
--- old/Radicale-3.7.7/radicale/tests/static/event_full_day_rrule_until_50y.ics 
1970-01-01 01:00:00.000000000 +0100
+++ new/Radicale-3.7.8/radicale/tests/static/event_full_day_rrule_until_50y.ics 
2026-08-06 08:01:32.000000000 +0200
@@ -0,0 +1,31 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
+BEGIN:VTIMEZONE
+LAST-MODIFIED:20040110T032845Z
+TZID:US/Eastern
+BEGIN:DAYLIGHT
+DTSTART:20000404T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:20001026T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+DTSTART;TZID=US/Eastern:20060102
+DTEND;TZID=US/Eastern:20060103
+RRULE:FREQ=DAILY;UNTIL=20560101
+SUMMARY:Recurring event with until +50y
+UID:event_full_day_rrule_until_50y
+DTSTAMP:20060102T094829Z
+END:VEVENT
+END:VCALENDAR
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/Radicale-3.7.7/radicale/tests/static/event_full_day_rrule_until_before_dtstart.ics
 
new/Radicale-3.7.8/radicale/tests/static/event_full_day_rrule_until_before_dtstart.ics
--- 
old/Radicale-3.7.7/radicale/tests/static/event_full_day_rrule_until_before_dtstart.ics
      1970-01-01 01:00:00.000000000 +0100
+++ 
new/Radicale-3.7.8/radicale/tests/static/event_full_day_rrule_until_before_dtstart.ics
      2026-08-06 08:01:32.000000000 +0200
@@ -0,0 +1,31 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
+BEGIN:VTIMEZONE
+LAST-MODIFIED:20040110T032845Z
+TZID:US/Eastern
+BEGIN:DAYLIGHT
+DTSTART:20000404T020000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:20001026T020000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+DTSTART;TZID=US/Eastern:20060102
+DTEND;TZID=US/Eastern:20060103
+RRULE:FREQ=DAILY;UNTIL=20050101
+SUMMARY:Recurring event with until in the past
+UID:event_full_day_rrule_until_in_the_past
+DTSTAMP:20060102T094829Z
+END:VEVENT
+END:VCALENDAR
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.7.7/radicale/tests/static/todo10.ics 
new/Radicale-3.7.8/radicale/tests/static/todo10.ics
--- old/Radicale-3.7.7/radicale/tests/static/todo10.ics 1970-01-01 
01:00:00.000000000 +0100
+++ new/Radicale-3.7.8/radicale/tests/static/todo10.ics 2026-08-06 
08:01:32.000000000 +0200
@@ -0,0 +1,13 @@
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//Radicale//NONSGML Radicale Server//EN
+BEGIN:VTODO
+UID:todo10
+SUMMARY:todo10
+DTSTART:20130901T180000Z
+DUE:20130903T180000Z
+CREATED:20130901T170000Z
+COMPLETED:20130901T173000Z
+STATUS:COMPLETED
+END:VTODO
+END:VCALENDAR
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.7.7/radicale/tests/test_base.py 
new/Radicale-3.7.8/radicale/tests/test_base.py
--- old/Radicale-3.7.7/radicale/tests/test_base.py      2026-07-19 
16:31:27.000000000 +0200
+++ new/Radicale-3.7.8/radicale/tests/test_base.py      2026-08-06 
08:01:32.000000000 +0200
@@ -151,6 +151,19 @@
         assert "Event" in answer
         assert "UID:event" in answer
 
+    def test_add_event_y2040(self) -> None:
+        """Add an event with year 2040."""
+        self.mkcalendar("/calendar.ics/")
+        event = get_file_content("event1_y2040.ics")
+        path = "/calendar.ics/event1_y2040.ics"
+        self.put(path, event)
+        _, headers, answer = self.request("GET", path, check=200)
+        assert "ETag" in headers
+        assert headers["Content-Type"] == "text/calendar; charset=utf-8"
+        assert "VEVENT" in answer
+        assert "Event" in answer
+        assert "UID:event" in answer
+
     @pytest.mark.skipif(not utils.vobject_supports_period(), reason="vobject 
<= 0.9.9 does not support PERIOD")
     def test_add_event_with_rdate_period_start_duration_single(self) -> None:
         """Add an event with RDATE/PERIOD with start+duration."""
@@ -328,6 +341,72 @@
         event = get_file_content("event_mixed_datetime_and_date.ics")
         self.put("/calendar.ics/event.ics", event)
 
+    def test_add_event_with_rrule_count_500_limit_100(self) -> None:
+        """Test event with RRULE COUNT=500 and limit 100."""
+        self.configure({"server": {"max_vevent_rrule_occurrence": 100}})
+        self.mkcalendar("/calendar.ics/")
+        event = get_file_content("event_full_day_rrule_count_500.ics")
+        self.put("/calendar.ics/event_full_day_rrule_count_500.ics", event, 
check=400)
+
+    def test_add_event_with_rrule_count_500_limit_600(self) -> None:
+        """Test event with RRULE COUNT=500 and limit 600."""
+        self.configure({"server": {"max_vevent_rrule_occurrence": 600}})
+        self.mkcalendar("/calendar.ics/")
+        event = get_file_content("event_full_day_rrule_count_500.ics")
+        self.put("/calendar.ics/event_full_day_rrule_count_500.ics", event)
+
+    def test_add_event_with_rrule_until_2y_limit_100(self) -> None:
+        """Test event with RRULE UNTIL=+2y and limit 100."""
+        self.configure({"server": {"max_vevent_rrule_occurrence": 100}})
+        self.mkcalendar("/calendar.ics/")
+        event = get_file_content("event_full_day_rrule_until_2y.ics")
+        self.put("/calendar.ics/event_full_day_rrule_until_2y.ics", event, 
check=400)
+
+    def test_add_event_with_rrule_until_2y_limit_800(self) -> None:
+        """Test event with RRULE UNTIL=+2y and limit 800."""
+        self.configure({"server": {"max_vevent_rrule_occurrence": 800}})
+        self.mkcalendar("/calendar.ics/")
+        event = get_file_content("event_full_day_rrule_until_2y.ics")
+        self.put("/calendar.ics/event_full_day_rrule_until_2y.ics", event)
+
+    def test_add_event_with_rrule_until_before_dtstart(self) -> None:
+        """Test event with RRULE UNTIL < DTSTART."""
+        self.mkcalendar("/calendar.ics/")
+        event = 
get_file_content("event_full_day_rrule_until_before_dtstart.ics")
+        
self.put("/calendar.ics/event_full_day_rrule_until_before_dtstart.ics", event, 
check=400)
+
+    @pytest.mark.skipif(sys.maxsize <= 2**32, reason="So far not working on on 
32-bit platform")
+    def test_add_event_with_rrule_until_50y_limit_100(self) -> None:
+        """Test event with RRULE UNTIL=+50y and limit 100."""
+        self.configure({"server": {"max_vevent_rrule_occurrence": 100}})
+        self.mkcalendar("/calendar.ics/")
+        event = get_file_content("event_full_day_rrule_until_50y.ics")
+        self.put("/calendar.ics/event_full_day_rrule_until_50y.ics", event, 
check=400)
+
+    @pytest.mark.skipif(sys.maxsize <= 2**32, reason="So far not working on on 
32-bit platform")
+    def test_add_event_with_rrule_until_50y_limit_20000(self) -> None:
+        """Test event with RRULE UNTIL=+50y and limit 20000."""
+        self.configure({"server": {"max_vevent_rrule_occurrence": 20000}})
+        self.mkcalendar("/calendar.ics/")
+        event = get_file_content("event_full_day_rrule_until_50y.ics")
+        self.put("/calendar.ics/event_full_day_rrule_until_50y.ics", event)
+
+    @pytest.mark.skipif(sys.maxsize <= 2**32, reason="So far not working on on 
32-bit platform")
+    def test_add_event_with_rrule_until_5000y_limit_100(self) -> None:
+        """Test event with RRULE UNTIL=+5000y and limit 100."""
+        self.configure({"server": {"max_vevent_rrule_occurrence": 100}})
+        self.mkcalendar("/calendar.ics/")
+        event = get_file_content("event_full_day_rrule_until_5000y.ics")
+        self.put("/calendar.ics/event_full_day_rrule_until_5000y.ics", event, 
check=400)
+
+    @pytest.mark.skipif(sys.maxsize <= 2**32, reason="So far not working on on 
32-bit platform")
+    def test_add_event_with_rrule_until_5000y_limit_2000000(self) -> None:
+        """Test event with RRULE UNTIL=+5000y and limit 2000000."""
+        self.configure({"server": {"max_vevent_rrule_occurrence": 2000000}})
+        self.mkcalendar("/calendar.ics/")
+        event = get_file_content("event_full_day_rrule_until_5000y.ics")
+        self.put("/calendar.ics/event_full_day_rrule_until_5000y.ics", event)
+
     def test_add_event_with_exdate_without_rrule(self) -> None:
         """Test event with EXDATE but not having RRULE."""
         self.mkcalendar("/calendar.ics/")
@@ -2004,6 +2083,27 @@
 </C:comp-filter>"""], "todo", items=range(1, 9))
         assert "/calendar.ics/todo7.ics" in answer
 
+    def test_time_range_filter_todos_dtstart_due_completed(self) -> None:
+        """Report request with time-range filter on a completed todo which
+           also has DTSTART and DUE (rfc4791-9.9: DTSTART/DUE take
+           precedence over CREATED/COMPLETED)."""
+        # inside DTSTART..DUE, but outside CREATED..COMPLETED
+        answer = self._test_filter(["""\
+<C:comp-filter name="VCALENDAR">
+    <C:comp-filter name="VTODO">
+        <C:time-range start="20130902T000000Z" end="20130903T000000Z"/>
+    </C:comp-filter>
+</C:comp-filter>"""], "todo", items=(10,))
+        assert "/calendar.ics/todo10.ics" in answer
+        # entirely before DTSTART and CREATED
+        answer = self._test_filter(["""\
+<C:comp-filter name="VCALENDAR">
+    <C:comp-filter name="VTODO">
+        <C:time-range start="20130801T000000Z" end="20130901T000000Z"/>
+    </C:comp-filter>
+</C:comp-filter>"""], "todo", items=(10,))
+        assert "/calendar.ics/todo10.ics" not in answer
+
     def test_time_range_filter_events_valarm(self) -> None:
         """Report request with time-range filter on events having absolute 
VALARM."""
         answer = self._test_filter(["""\
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.7.7/radicale/tests/test_expand.py 
new/Radicale-3.7.8/radicale/tests/test_expand.py
--- old/Radicale-3.7.7/radicale/tests/test_expand.py    2026-07-19 
16:31:27.000000000 +0200
+++ new/Radicale-3.7.8/radicale/tests/test_expand.py    2026-08-06 
08:01:32.000000000 +0200
@@ -128,7 +128,8 @@
                      expected_start_times: List[str],
                      expected_end_times: List[str],
                      only_dates: bool,
-                     nr_uids: int) -> None:
+                     nr_uids: int,
+                     check: int = 207) -> None:
         _, responses = self.report("/calendar.ics/",
                                    self._req_without_expand(expected_uid, 
start, end))
         assert len(responses) == 1
@@ -154,7 +155,9 @@
         assert len(uids) == nr_uids
 
         _, responses = self.report("/calendar.ics/",
-                                   self._req_with_expand(expected_uid, start, 
end))
+                                   self._req_with_expand(expected_uid, start, 
end), check=check)
+        if check != 207:
+            return
 
         assert len(responses) == 1
 
@@ -176,17 +179,21 @@
                 uids.append(line)
 
             if line.startswith("RECURRENCE-ID:"):
-                assert line in expected_recurrence_ids
+                if expected_recurrence_ids:
+                    assert line in expected_recurrence_ids
                 recurrence_ids.append(line)
 
             if line.startswith("DTSTART:"):
-                assert line in expected_start_times
+                if expected_start_times:
+                    assert line in expected_start_times
 
             if line.startswith("DTEND:"):
-                assert line in expected_end_times
+                if expected_end_times:
+                    assert line in expected_end_times
 
-        assert len(uids) == len(expected_recurrence_ids)
-        assert len(set(recurrence_ids)) == len(expected_recurrence_ids)
+        if expected_recurrence_ids:
+            assert len(uids) == len(expected_recurrence_ids)
+            assert len(set(recurrence_ids)) == len(expected_recurrence_ids)
 
     def _test_expand_max(self,
                          expected_uid: str,
@@ -289,6 +296,35 @@
             1
         )
 
+    def test_report_with_expand_property_all_day_count_500_event_pass(self) -> 
None:
+        """Test report with expand property for all day count 500 events"""
+        self.configure({"reporting": {"max_expand_occurrence": 501}})
+        self._test_expand(
+            "event_full_day_rrule_count_500",
+            "20060103T000000Z",
+            "20080105T000000Z",
+            [],
+            [],
+            [],
+            ONLY_DATES,
+            1
+        )
+
+    def test_report_with_expand_property_all_day_count_500_event_reject(self) 
-> None:
+        """Test report with expand property for all day count 500 events"""
+        self.configure({"reporting": {"max_expand_occurrence": 10}})
+        self._test_expand(
+            "event_full_day_rrule_count_500",
+            "20060103T000000Z",
+            "20080105T000000Z",
+            [],
+            [],
+            [],
+            ONLY_DATES,
+            1,
+            400
+        )
+
     def test_report_with_expand_property_overridden(self) -> None:
         """Test report with expand property with overridden events"""
         self._test_expand(
@@ -326,7 +362,7 @@
 
     def test_report_with_expand_property_max_occur(self) -> None:
         """Test report with expand property too many vevents"""
-        self.configure({"reporting": {"max_freebusy_occurrence": 100}})
+        self.configure({"reporting": {"max_expand_occurrence": 100}})
         self._test_expand_max(
             "event_daily_rrule_forever",
             "20060103T000000Z",
@@ -336,7 +372,7 @@
 
     def test_report_with_max_occur(self) -> None:
         """Test report with too many vevents"""
-        self.configure({"reporting": {"max_freebusy_occurrence": 10}})
+        self.configure({"reporting": {"max_expand_occurrence": 10}})
 
         uid = "event_multiple_too_many"
         start = "20130901T000000Z"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.7.7/radicale/tests/test_sharing.py 
new/Radicale-3.7.8/radicale/tests/test_sharing.py
--- old/Radicale-3.7.7/radicale/tests/test_sharing.py   2026-07-19 
16:31:27.000000000 +0200
+++ new/Radicale-3.7.8/radicale/tests/test_sharing.py   2026-08-06 
08:01:32.000000000 +0200
@@ -1988,12 +1988,36 @@
             json_dict['PathOrToken'] = path_shared
             _, headers, answer = self._sharing_api_json("map", "enable", 
check=200, login="user:userpw", json_dict=json_dict)
 
+            # check GET as user
+            logging.info("\n*** GET collection user -> ok")
+            _, headers, answer = self.request("GET", path_shared, check=200, 
login="user:userpw")
+
             # check REPORT as user
             logging.info("\n*** REPORT collection user -> ok")
             _, responses = self.report(path_shared, """\
 <?xml version="1.0" encoding="utf-8" ?>
 <C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
     <D:prop xmlns:D="DAV:">
+        <D:getetag />
+    </D:prop>
+</C:calendar-query>""", login="user:userpw")
+            assert len(responses) == 1
+            logging.info("response: %r", responses)
+            response = responses[path_shared_item]
+            assert isinstance(response, dict)
+            status, prop = response["D:getetag"]
+            assert status == 200 and prop.text
+
+            # check GET as user without trailing /
+            logging.info("\n*** GET collection user (without trailing /) -> 
ok")
+            _, headers, answer = self.request("GET", 
path_shared.removesuffix("/"), check=200, login="user:userpw")
+
+            # check REPORT as user without trailing /
+            logging.info("\n*** REPORT collection user (without trailing /) -> 
ok")
+            _, responses = self.report(path_shared.removesuffix("/"), """\
+<?xml version="1.0" encoding="utf-8" ?>
+<C:calendar-query xmlns:C="urn:ietf:params:xml:ns:caldav">
+    <D:prop xmlns:D="DAV:">
         <D:getetag />
     </D:prop>
 </C:calendar-query>""", login="user:userpw")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.7.7/setup.py.legacy 
new/Radicale-3.7.8/setup.py.legacy
--- old/Radicale-3.7.7/setup.py.legacy  2026-07-19 16:31:27.000000000 +0200
+++ new/Radicale-3.7.8/setup.py.legacy  2026-08-06 08:01:32.000000000 +0200
@@ -21,7 +21,7 @@
 
 # When the version is updated, a new section in the CHANGELOG.md file must be
 # added too.
-VERSION = "3.7.7"
+VERSION = "3.7.8"
 
 with open("README.md", encoding="utf-8") as f:
     long_description = f.read()

Reply via email to