Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package Radicale for openSUSE:Factory 
checked in at 2024-09-06 17:18:53
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/Radicale (Old)
 and      /work/SRC/openSUSE:Factory/.Radicale.new.10096 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "Radicale"

Fri Sep  6 17:18:53 2024 rev:15 rq:1199117 version:3.2.3

Changes:
--------
--- /work/SRC/openSUSE:Factory/Radicale/Radicale.changes        2024-06-27 
16:01:00.093520438 +0200
+++ /work/SRC/openSUSE:Factory/.Radicale.new.10096/Radicale.changes     
2024-09-06 17:19:16.805723976 +0200
@@ -1,0 +2,16 @@
+Fri Aug 30 04:42:28 UTC 2024 - Ákos Szőts <[email protected]>
+
+- Cleaned up unnecessary BuildRequires
+- Update to 3.2.3
+  * Add: support for Python 3.13
+  * Fix: Using icalendar's tzinfo on created datetime to fix issue with 
icalendar
+  * Fix: typos in code
+  * Enhancement: Added free-busy report
+  * Enhancement: Added 'max_freebusy_occurrences` setting to avoid potential 
DOS on reports
+  * Enhancement: remove unexpected control codes from uploaded items
+  * Enhancement: add 'strip_domain' setting for username handling
+  * Enhancement: add option to toggle debug log of rights rule with doesn't 
match
+  * Drop: remove unused requirement "typeguard"
+  * Improve: Refactored some date parsing code
+
+-------------------------------------------------------------------

Old:
----
  v3.2.2.tar.gz

New:
----
  v3.2.3.tar.gz

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

Other differences:
------------------
++++++ Radicale.spec ++++++
--- /var/tmp/diff_new_pack.0OfFAq/_old  2024-09-06 17:19:17.333745918 +0200
+++ /var/tmp/diff_new_pack.0OfFAq/_new  2024-09-06 17:19:17.333745918 +0200
@@ -27,7 +27,7 @@
 %define du_min_ver 2.7.3
 %define pk_min_ver 1.1.0
 Name:           Radicale
-Version:        3.2.2
+Version:        3.2.3
 Release:        0
 Summary:        A CalDAV calendar and CardDav contact server
 License:        GPL-3.0-or-later
@@ -42,12 +42,7 @@
 BuildRequires:  firewall-macros
 BuildRequires:  pkgconfig
 BuildRequires:  python-rpm-macros
-BuildRequires:  python3-defusedxml
-BuildRequires:  python3-passlib
-BuildRequires:  python3-pika >= %{pk_min_ver}
-BuildRequires:  python3-python-dateutil >= %{du_min_ver}
 BuildRequires:  python3-setuptools
-BuildRequires:  python3-vobject >= %{vo_min_ver}
 BuildRequires:  systemd-rpm-macros
 BuildRequires:  sysuser-tools
 BuildRequires:  pkgconfig(python3) >= %{py_min_ver}

++++++ v3.2.2.tar.gz -> v3.2.3.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.2.2/.github/workflows/test.yml 
new/Radicale-3.2.3/.github/workflows/test.yml
--- old/Radicale-3.2.2/.github/workflows/test.yml       2024-06-18 
20:24:12.000000000 +0200
+++ new/Radicale-3.2.3/.github/workflows/test.yml       2024-08-30 
06:19:51.000000000 +0200
@@ -6,7 +6,7 @@
     strategy:
       matrix:
         os: [ubuntu-latest, macos-latest, windows-latest]
-        python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', pypy-3.8, 
pypy-3.9]
+        python-version: ['3.8', '3.9', '3.10', '3.11', '3.12.3', 
'3.13.0-beta.4', pypy-3.8, pypy-3.9]
         exclude:
           - os: windows-latest
             python-version: pypy-3.8
@@ -21,7 +21,7 @@
       - name: Install Test dependencies
         run: pip install tox
       - name: Test
-        run: tox
+        run: tox -e py
       - name: Install Coveralls
         if: github.event_name == 'push'
         run: pip install coveralls
@@ -46,3 +46,15 @@
         env:
           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
         run: coveralls --service=github --finish
+
+  lint:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - uses: actions/setup-python@v5
+        with:
+          python-version: '3.12'
+      - name: Install tox
+        run: pip install tox
+      - name: Lint
+        run: tox -e flake8,mypy,isort
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.2.2/CHANGELOG.md 
new/Radicale-3.2.3/CHANGELOG.md
--- old/Radicale-3.2.2/CHANGELOG.md     2024-06-18 20:24:12.000000000 +0200
+++ new/Radicale-3.2.3/CHANGELOG.md     2024-08-30 06:19:51.000000000 +0200
@@ -1,6 +1,16 @@
 # Changelog
 
-## 3.dev
+## 3.2.3
+* Add: support for Python 3.13
+* Fix: Using icalendar's tzinfo on created datetime to fix issue with icalendar
+* Fix: typos in code
+* Enhancement: Added free-busy report
+* Enhancement: Added 'max_freebusy_occurrences` setting to avoid potential DOS 
on reports
+* Enhancement: remove unexpected control codes from uploaded items
+* Enhancement: add 'strip_domain' setting for username handling
+* Enhancement: add option to toggle debug log of rights rule with doesn't match
+* Drop: remove unused requirement "typeguard"
+* Improve: Refactored some date parsing code
 
 ## 3.2.2
 * Enhancement: add support for auth.type=denyall (will be default for security 
reasons in upcoming releases)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.2.2/DOCUMENTATION.md 
new/Radicale-3.2.3/DOCUMENTATION.md
--- old/Radicale-3.2.2/DOCUMENTATION.md 2024-06-18 20:24:12.000000000 +0200
+++ new/Radicale-3.2.3/DOCUMENTATION.md 2024-08-30 06:19:51.000000000 +0200
@@ -350,16 +350,13 @@
 }
 ```
 
-Example **Caddy** configuration with basicauth from Caddy:
+Example **Caddy** configuration:
 
-```Caddy
-handle_path /radicale* {
-    basicauth {
-        user hash
-    }
+```
+handle_path /radicale/* {
+    uri strip_prefix /radicale
     reverse_proxy localhost:5232 {
-        header_up +X-Script-Name "/radicale"
-        header_up +X-remote-user "{http.auth.user.id}"
+        header_up X-Script-Name /radicale
     }
 }
 ```
@@ -440,6 +437,21 @@
 }
 ```
 
+Example **Caddy** configuration:
+
+```
+handle_path /radicale/* {
+    uri strip_prefix /radicale
+    basicauth {
+        USER HASH
+    }
+    reverse_proxy localhost:5232 {
+        header_up X-Script-Name /radicale
+        header_up X-remote-user {http.auth.user.id}
+    }
+}
+```
+
 Example **Apache** configuration:
 
 ```apache
@@ -795,6 +807,12 @@
 
 Default: `False`
 
+##### strip_domain
+
+Strip domain from username
+
+Default: `False`
+
 #### rights
 
 ##### type
@@ -865,7 +883,7 @@
 
 Default: `2592000`
 
-#### skip_broken_item
+##### skip_broken_item
 
 Skip broken item instead of triggering an exception
 
@@ -960,6 +978,12 @@
 
 Default: `False`
 
+##### rights_rule_doesnt_match_on_debug = True
+
+Log rights rule which doesn't match on level=debug
+
+Default: `False`
+
 #### headers
 
 In this section additional HTTP headers that are sent to clients can be
@@ -1005,6 +1029,18 @@
 
 Default: classic
 
+#### reporting
+##### max_freebusy_occurrence
+
+When returning a free-busy report, a list of busy time 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
+
 ## Supported Clients
 
 Radicale has been tested with:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.2.2/config new/Radicale-3.2.3/config
--- old/Radicale-3.2.2/config   2024-06-18 20:24:12.000000000 +0200
+++ new/Radicale-3.2.3/config   2024-08-30 06:19:51.000000000 +0200
@@ -73,6 +73,8 @@
 # Convert username to lowercase, must be true for case-insensitive auth 
providers
 #lc_username = False
 
+# Strip domain name from username
+#strip_domain = False
 
 [rights]
 
@@ -156,6 +158,8 @@
 # Log response content on level=debug
 #response_content_on_debug = False
 
+# Log rights rule which doesn't match on level=debug
+#rights_rule_doesnt_match_on_debug = False
 
 [headers]
 
@@ -170,3 +174,9 @@
 #rabbitmq_endpoint =
 #rabbitmq_topic =
 #rabbitmq_queue_type = classic
+
+[reporting]
+
+# 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.2.2/contrib/apache/radicale.conf 
new/Radicale-3.2.3/contrib/apache/radicale.conf
--- old/Radicale-3.2.2/contrib/apache/radicale.conf     2024-06-18 
20:24:12.000000000 +0200
+++ new/Radicale-3.2.3/contrib/apache/radicale.conf     2024-08-30 
06:19:51.000000000 +0200
@@ -57,13 +57,15 @@
                Require all granted
                </IfDefine>
 
-               ## You may want to use apache's authentication (config: [auth] 
type = remote_user)
+               ## You may want to use apache's authentication (config: [auth] 
type = http_x_remote_user)
+               ##  e.g. create a new file with a testuser: htpasswd -c -B 
/etc/httpd/conf/htpasswd-radicale testuser
                #AuthBasicProvider file
                #AuthType Basic
                #AuthName "Enter your credentials"
-               #AuthUserFile /path/to/httpdfile/
+               #AuthUserFile /etc/httpd/conf/htpasswd-radicale
                #AuthGroupFile /dev/null
                #Require valid-user
+               #RequestHeader set X-Remote-User expr=%{REMOTE_USER}
 
                <IfDefine RADICALE_ENFORCE_SSL>
                        <IfModule !ssl_module>
@@ -106,13 +108,15 @@
                Require all granted
                </IfDefine>
 
-               ## You may want to use apache's authentication (config: [auth] 
type = remote_user)
+               ## You may want to use apache's authentication (config: [auth] 
type = http_x_remote_user)
+               ##  e.g. create a new file with a testuser: htpasswd -c -B 
/etc/httpd/conf/htpasswd-radicale testuser
                #AuthBasicProvider file
                #AuthType Basic
                #AuthName "Enter your credentials"
-               #AuthUserFile /path/to/httpdfile/
+               #AuthUserFile /etc/httpd/conf/htpasswd-radicale
                #AuthGroupFile /dev/null
                #Require valid-user
+               #RequestHeader set X-Remote-User expr=%{REMOTE_USER}
 
                <IfDefine RADICALE_ENFORCE_SSL>
                        <IfModule !ssl_module>
@@ -179,11 +183,12 @@
                Require all granted
                </IfDefine>
 
-               ## You may want to use apache's authentication (config: [auth] 
type = remote_user)
+               ## You may want to use apache's authentication (config: [auth] 
type = http_x_remote_user)
+               ##  e.g. create a new file with a testuser: htpasswd -c -B 
/etc/httpd/conf/htpasswd-radicale testuser
                #AuthBasicProvider file
                #AuthType Basic
                #AuthName "Enter your credentials"
-               #AuthUserFile /path/to/httpdfile/
+               #AuthUserFile /etc/httpd/conf/htpasswd-radicale
                #AuthGroupFile /dev/null
                #Require valid-user
        </Location>
@@ -221,11 +226,12 @@
                Require all granted
                </IfDefine>
 
-               ## You may want to use apache's authentication (config: [auth] 
type = remote_user)
+               ## You may want to use apache's authentication (config: [auth] 
type = http_x_remote_user)
+               ##  e.g. create a new file with a testuser: htpasswd -c -B 
/etc/httpd/conf/htpasswd-radicale testuser
                #AuthBasicProvider file
                #AuthType Basic
                #AuthName "Enter your credentials"
-               #AuthUserFile /path/to/httpdfile/
+               #AuthUserFile /etc/httpd/conf/htpasswd-radicale
                #AuthGroupFile /dev/null
                #Require valid-user
        </Location>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.2.2/radicale/__init__.py 
new/Radicale-3.2.3/radicale/__init__.py
--- old/Radicale-3.2.2/radicale/__init__.py     2024-06-18 20:24:12.000000000 
+0200
+++ new/Radicale-3.2.3/radicale/__init__.py     2024-08-30 06:19:51.000000000 
+0200
@@ -61,7 +61,7 @@
                     if not miss and source != "default config":
                         default_config_active = False
                 if default_config_active:
-                    logger.warn("%s", "No config file found/readable - only 
default config is active")
+                    logger.warning("%s", "No config file found/readable - only 
default config is active")
                 _application_instance = Application(configuration)
     if _application_config_path != config_path:
         raise ValueError("RADICALE_CONFIG must not change: %r != %r" %
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.2.2/radicale/__main__.py 
new/Radicale-3.2.3/radicale/__main__.py
--- old/Radicale-3.2.2/radicale/__main__.py     2024-06-18 20:24:12.000000000 
+0200
+++ new/Radicale-3.2.3/radicale/__main__.py     2024-08-30 06:19:51.000000000 
+0200
@@ -175,7 +175,7 @@
             default_config_active = False
 
     if default_config_active:
-        logger.warn("%s", "No config file found/readable - only default config 
is active")
+        logger.warning("%s", "No config file found/readable - only default 
config is active")
 
     if args_ns.verify_storage:
         logger.info("Verifying storage")
@@ -183,7 +183,7 @@
             storage_ = storage.load(configuration)
             with storage_.acquire_lock("r"):
                 if not storage_.verify():
-                    logger.critical("Storage verifcation failed")
+                    logger.critical("Storage verification failed")
                     sys.exit(1)
         except Exception as e:
             logger.critical("An exception occurred during storage "
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.2.2/radicale/app/__init__.py 
new/Radicale-3.2.3/radicale/app/__init__.py
--- old/Radicale-3.2.2/radicale/app/__init__.py 2024-06-18 20:24:12.000000000 
+0200
+++ new/Radicale-3.2.3/radicale/app/__init__.py 2024-08-30 06:19:51.000000000 
+0200
@@ -146,7 +146,7 @@
                     if self._response_content_on_debug:
                         logger.debug("Response content:\n%s", answer)
                     else:
-                        logger.debug("Response content: suppressed by 
config/option [auth] response_content_on_debug")
+                        logger.debug("Response content: suppressed by 
config/option [logging] response_content_on_debug")
                     headers["Content-Type"] += "; charset=%s" % self._encoding
                     answer = answer.encode(self._encoding)
                 accept_encoding = [
@@ -196,7 +196,7 @@
             logger.debug("Request header:\n%s",
                          pprint.pformat(self._scrub_headers(environ)))
         else:
-            logger.debug("Request header: suppressed by config/option [auth] 
request_header_on_debug")
+            logger.debug("Request header: suppressed by config/option 
[logging] request_header_on_debug")
 
         # SCRIPT_NAME is already removed from PATH_INFO, according to the
         # WSGI specification.
@@ -232,7 +232,7 @@
                 path.rstrip("/").endswith("/.well-known/carddav")):
             return response(*httputils.redirect(
                 base_prefix + "/", client.MOVED_PERMANENTLY))
-        # Return NOT FOUND for all other paths containing ".well-knwon"
+        # Return NOT FOUND for all other paths containing ".well-known"
         if path.endswith("/.well-known") or "/.well-known/" in path:
             return response(*httputils.NOT_FOUND)
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.2.2/radicale/app/base.py 
new/Radicale-3.2.3/radicale/app/base.py
--- old/Radicale-3.2.2/radicale/app/base.py     2024-06-18 20:24:12.000000000 
+0200
+++ new/Radicale-3.2.3/radicale/app/base.py     2024-08-30 06:19:51.000000000 
+0200
@@ -51,6 +51,7 @@
         self._encoding = configuration.get("encoding", "request")
         self._log_bad_put_request_content = configuration.get("logging", 
"bad_put_request_content")
         self._response_content_on_debug = configuration.get("logging", 
"response_content_on_debug")
+        self._request_content_on_debug = configuration.get("logging", 
"request_content_on_debug")
         self._hook = hook.load(configuration)
 
     def _read_xml_request_body(self, environ: types.WSGIEnviron
@@ -66,17 +67,20 @@
             logger.debug("Request content (Invalid XML):\n%s", content)
             raise RuntimeError("Failed to parse XML: %s" % e) from e
         if logger.isEnabledFor(logging.DEBUG):
-            logger.debug("Request content:\n%s",
-                         xmlutils.pretty_xml(xml_content))
+            if self._request_content_on_debug:
+                logger.debug("Request content (XML):\n%s",
+                             xmlutils.pretty_xml(xml_content))
+            else:
+                logger.debug("Request content (XML): suppressed by 
config/option [logging] request_content_on_debug")
         return xml_content
 
     def _xml_response(self, xml_content: ET.Element) -> bytes:
         if logger.isEnabledFor(logging.DEBUG):
             if self._response_content_on_debug:
-                logger.debug("Response content:\n%s",
+                logger.debug("Response content (XML):\n%s",
                              xmlutils.pretty_xml(xml_content))
             else:
-                logger.debug("Response content: suppressed by config/option 
[auth] response_content_on_debug")
+                logger.debug("Response content (XML): suppressed by 
config/option [logging] response_content_on_debug")
         f = io.BytesIO()
         ET.ElementTree(xml_content).write(f, encoding=self._encoding,
                                           xml_declaration=True)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.2.2/radicale/app/propfind.py 
new/Radicale-3.2.3/radicale/app/propfind.py
--- old/Radicale-3.2.2/radicale/app/propfind.py 2024-06-18 20:24:12.000000000 
+0200
+++ new/Radicale-3.2.3/radicale/app/propfind.py 2024-08-30 06:19:51.000000000 
+0200
@@ -322,13 +322,13 @@
 
         responses[404 if is404 else 200].append(element)
 
-    for status_code, childs in responses.items():
-        if not childs:
+    for status_code, children in responses.items():
+        if not children:
             continue
         propstat = ET.Element(xmlutils.make_clark("D:propstat"))
         response.append(propstat)
         prop = ET.Element(xmlutils.make_clark("D:prop"))
-        prop.extend(childs)
+        prop.extend(children)
         propstat.append(prop)
         status = ET.Element(xmlutils.make_clark("D:status"))
         status.text = xmlutils.make_response(status_code)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.2.2/radicale/app/put.py 
new/Radicale-3.2.3/radicale/app/put.py
--- old/Radicale-3.2.2/radicale/app/put.py      2024-06-18 20:24:12.000000000 
+0200
+++ new/Radicale-3.2.3/radicale/app/put.py      2024-08-30 06:19:51.000000000 
+0200
@@ -150,7 +150,7 @@
             if self._log_bad_put_request_content:
                 logger.warning("Bad PUT request content of %r:\n%s", path, 
content)
             else:
-                logger.debug("Bad PUT request content: suppressed by 
config/option [auth] bad_put_request_content")
+                logger.debug("Bad PUT request content: suppressed by 
config/option [logging] bad_put_request_content")
             return httputils.BAD_REQUEST
         (prepared_items, prepared_tag, prepared_write_whole_collection,
          prepared_props, prepared_exc_info) = prepare(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.2.2/radicale/app/report.py 
new/Radicale-3.2.3/radicale/app/report.py
--- old/Radicale-3.2.2/radicale/app/report.py   2024-06-18 20:24:12.000000000 
+0200
+++ new/Radicale-3.2.3/radicale/app/report.py   2024-08-30 06:19:51.000000000 
+0200
@@ -28,6 +28,7 @@
                     Sequence, Tuple, Union)
 from urllib.parse import unquote, urlparse
 
+import vobject
 import vobject.base
 from vobject.base import ContentLine
 
@@ -38,11 +39,110 @@
 from radicale.log import logger
 
 
+def free_busy_report(base_prefix: str, path: str, xml_request: 
Optional[ET.Element],
+                     collection: storage.BaseCollection, encoding: str,
+                     unlock_storage_fn: Callable[[], None],
+                     max_occurrence: int
+                     ) -> Tuple[int, Union[ET.Element, str]]:
+    # NOTE: this function returns both an Element and a string because
+    # free-busy reports are an edge-case on the return type according
+    # to the spec.
+
+    multistatus = ET.Element(xmlutils.make_clark("D:multistatus"))
+    if xml_request is None:
+        return client.MULTI_STATUS, multistatus
+    root = xml_request
+    if (root.tag == xmlutils.make_clark("C:free-busy-query") and
+            collection.tag != "VCALENDAR"):
+        logger.warning("Invalid REPORT method %r on %r requested",
+                       xmlutils.make_human_tag(root.tag), path)
+        return client.FORBIDDEN, xmlutils.webdav_error("D:supported-report")
+
+    time_range_element = root.find(xmlutils.make_clark("C:time-range"))
+    assert isinstance(time_range_element, ET.Element)
+
+    # Build a single filter from the free busy query for retrieval
+    # TODO: filter for VFREEBUSY in additional to VEVENT but
+    # test_filter doesn't support that yet.
+    vevent_cf_element = ET.Element(xmlutils.make_clark("C:comp-filter"),
+                                   attrib={'name': 'VEVENT'})
+    vevent_cf_element.append(time_range_element)
+    vcalendar_cf_element = ET.Element(xmlutils.make_clark("C:comp-filter"),
+                                      attrib={'name': 'VCALENDAR'})
+    vcalendar_cf_element.append(vevent_cf_element)
+    filter_element = ET.Element(xmlutils.make_clark("C:filter"))
+    filter_element.append(vcalendar_cf_element)
+    filters = (filter_element,)
+
+    # First pull from storage
+    retrieved_items = list(collection.get_filtered(filters))
+    # !!! Don't access storage after this !!!
+    unlock_storage_fn()
+
+    cal = vobject.iCalendar()
+    collection_tag = collection.tag
+    while retrieved_items:
+        # Second filtering before evaluating occurrences.
+        # ``item.vobject_item`` might be accessed during filtering.
+        # Don't keep reference to ``item``, because VObject requires a lot of
+        # memory.
+        item, filter_matched = retrieved_items.pop(0)
+        if not filter_matched:
+            try:
+                if not test_filter(collection_tag, item, filter_element):
+                    continue
+            except ValueError as e:
+                raise ValueError("Failed to free-busy filter item %r from %r: 
%s" %
+                                 (item.href, collection.path, e)) from e
+            except Exception as e:
+                raise RuntimeError("Failed to free-busy filter item %r from 
%r: %s" %
+                                   (item.href, collection.path, e)) from e
+
+        fbtype = None
+        if item.component_name == 'VEVENT':
+            transp = getattr(item.vobject_item.vevent, 'transp', None)
+            if transp and transp.value != 'OPAQUE':
+                continue
+
+            status = getattr(item.vobject_item.vevent, 'status', None)
+            if not status or status.value == 'CONFIRMED':
+                fbtype = 'BUSY'
+            elif status.value == 'CANCELLED':
+                fbtype = 'FREE'
+            elif status.value == 'TENTATIVE':
+                fbtype = 'BUSY-TENTATIVE'
+            else:
+                # Could do fbtype = status.value for x-name, I prefer this
+                fbtype = 'BUSY'
+
+        # TODO: coalesce overlapping periods
+
+        if max_occurrence > 0:
+            n_occurrences = max_occurrence+1
+        else:
+            n_occurrences = 0
+        occurrences = radicale_filter.time_range_fill(item.vobject_item,
+                                                      time_range_element,
+                                                      "VEVENT",
+                                                      n=n_occurrences)
+        if len(occurrences) >= max_occurrence:
+            raise ValueError("FREEBUSY occurrences limit of {} hit"
+                             .format(max_occurrence))
+
+        for occurrence in occurrences:
+            vfb = cal.add('vfreebusy')
+            vfb.add('dtstamp').value = item.vobject_item.vevent.dtstamp.value
+            vfb.add('dtstart').value, vfb.add('dtend').value = occurrence
+            if fbtype:
+                vfb.add('fbtype').value = fbtype
+    return (client.OK, cal.serialize())
+
+
 def xml_report(base_prefix: str, path: str, xml_request: Optional[ET.Element],
                collection: storage.BaseCollection, encoding: str,
                unlock_storage_fn: Callable[[], None]
                ) -> Tuple[int, ET.Element]:
-    """Read and answer REPORT requests.
+    """Read and answer REPORT requests that return XML.
 
     Read rfc3253-3.6 for info.
 
@@ -271,7 +371,7 @@
     if hasattr(item.vobject_item.vevent, 'rrule'):
         rruleset = vevent.getrruleset()
 
-    # There is something strage behavour during serialization native datetime, 
so converting manualy
+    # There is something strange behaviour during serialization native 
datetime, so converting manually
     vevent.dtstart.value = vevent.dtstart.value.strftime(dt_format)
     if dt_end is not None:
         vevent.dtend.value = vevent.dtend.value.strftime(dt_format)
@@ -426,13 +526,28 @@
             else:
                 assert item.collection is not None
                 collection = item.collection
-            try:
-                status, xml_answer = xml_report(
-                    base_prefix, path, xml_content, collection, self._encoding,
-                    lock_stack.close)
-            except ValueError as e:
-                logger.warning(
-                    "Bad REPORT request on %r: %s", path, e, exc_info=True)
-                return httputils.BAD_REQUEST
-        headers = {"Content-Type": "text/xml; charset=%s" % self._encoding}
-        return status, headers, self._xml_response(xml_answer)
+
+            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,
+                        lock_stack.close, max_occurrence)
+                except ValueError as e:
+                    logger.warning(
+                        "Bad REPORT request on %r: %s", path, e, exc_info=True)
+                    return httputils.BAD_REQUEST
+                headers = {"Content-Type": "text/calendar; charset=%s" % 
self._encoding}
+                return status, headers, str(body)
+            else:
+                try:
+                    status, xml_answer = xml_report(
+                        base_prefix, path, xml_content, collection, 
self._encoding,
+                        lock_stack.close)
+                except ValueError as e:
+                    logger.warning(
+                        "Bad REPORT request on %r: %s", path, e, exc_info=True)
+                    return httputils.BAD_REQUEST
+                headers = {"Content-Type": "text/xml; charset=%s" % 
self._encoding}
+                return status, headers, self._xml_response(xml_answer)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.2.2/radicale/auth/__init__.py 
new/Radicale-3.2.3/radicale/auth/__init__.py
--- old/Radicale-3.2.2/radicale/auth/__init__.py        2024-06-18 
20:24:12.000000000 +0200
+++ new/Radicale-3.2.3/radicale/auth/__init__.py        2024-08-30 
06:19:51.000000000 +0200
@@ -52,6 +52,7 @@
 class BaseAuth:
 
     _lc_username: bool
+    _strip_domain: bool
 
     def __init__(self, configuration: "config.Configuration") -> None:
         """Initialize BaseAuth.
@@ -63,6 +64,7 @@
         """
         self.configuration = configuration
         self._lc_username = configuration.get("auth", "lc_username")
+        self._strip_domain = configuration.get("auth", "strip_domain")
 
     def get_external_login(self, environ: types.WSGIEnviron) -> Union[
             Tuple[()], Tuple[str, str]]:
@@ -91,4 +93,8 @@
         raise NotImplementedError
 
     def login(self, login: str, password: str) -> str:
-        return self._login(login, password).lower() if self._lc_username else 
self._login(login, password)
+        if self._lc_username:
+            login = login.lower()
+        if self._strip_domain:
+            login = login.split('@')[0]
+        return self._login(login, password)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.2.2/radicale/auth/htpasswd.py 
new/Radicale-3.2.3/radicale/auth/htpasswd.py
--- old/Radicale-3.2.2/radicale/auth/htpasswd.py        2024-06-18 
20:24:12.000000000 +0200
+++ new/Radicale-3.2.3/radicale/auth/htpasswd.py        2024-08-30 
06:19:51.000000000 +0200
@@ -36,7 +36,7 @@
 the password encryption method specified via the ``htpasswd_encryption``
 configuration value.
 
-The following htpasswd password encrpytion methods are supported by Radicale
+The following htpasswd password encryption methods are supported by Radicale
 out-of-the-box:
     - plain-text (created by htpasswd -p ...) -- INSECURE
     - MD5-APR1   (htpasswd -m ...) -- htpasswd's default method, INSECURE
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.2.2/radicale/config.py 
new/Radicale-3.2.3/radicale/config.py
--- old/Radicale-3.2.2/radicale/config.py       2024-06-18 20:24:12.000000000 
+0200
+++ new/Radicale-3.2.3/radicale/config.py       2024-08-30 06:19:51.000000000 
+0200
@@ -191,6 +191,10 @@
             "value": "1",
             "help": "incorrect authentication delay",
             "type": positive_float}),
+        ("strip_domain", {
+            "value": "False",
+            "help": "strip domain from username",
+            "type": bool}),
         ("lc_username", {
             "value": "False",
             "help": "convert username to lowercase, must be true for 
case-insensitive auth providers",
@@ -288,12 +292,22 @@
             "value": "False",
             "help": "log response content on level=debug",
             "type": bool}),
+        ("rights_rule_doesnt_match_on_debug", {
+            "value": "False",
+            "help": "log rights rules which doesn't match on level=debug",
+            "type": bool}),
         ("mask_passwords", {
             "value": "True",
             "help": "mask passwords in logs",
             "type": bool})])),
     ("headers", OrderedDict([
-        ("_allow_extra", str)]))])
+        ("_allow_extra", str)])),
+    ("reporting", OrderedDict([
+        ("max_freebusy_occurrence", {
+            "value": "10000",
+            "help": "number of occurrences per event when reporting",
+            "type": positive_int})]))
+    ])
 
 
 def parse_compound_paths(*compound_paths: Optional[str]
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.2.2/radicale/hook/__init__.py 
new/Radicale-3.2.3/radicale/hook/__init__.py
--- old/Radicale-3.2.2/radicale/hook/__init__.py        2024-06-18 
20:24:12.000000000 +0200
+++ new/Radicale-3.2.3/radicale/hook/__init__.py        2024-08-30 
06:19:51.000000000 +0200
@@ -3,14 +3,23 @@
 from typing import Sequence
 
 from radicale import pathutils, utils
+from radicale.log import logger
 
 INTERNAL_TYPES: Sequence[str] = ("none", "rabbitmq")
 
 
 def load(configuration):
     """Load the storage module chosen in configuration."""
-    return utils.load_plugin(
-        INTERNAL_TYPES, "hook", "Hook", BaseHook, configuration)
+    try:
+        return utils.load_plugin(
+            INTERNAL_TYPES, "hook", "Hook", BaseHook, configuration)
+    except Exception as e:
+        logger.warning(e)
+        logger.warning("Hook \"%s\" failed to load, falling back to \"none\"." 
% configuration.get("hook", "type"))
+        configuration = configuration.copy()
+        configuration.update({"hook": {"type": "none"}}, "hook", 
privileged=True)
+        return utils.load_plugin(
+            INTERNAL_TYPES, "hook", "Hook", BaseHook, configuration)
 
 
 class BaseHook:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.2.2/radicale/httputils.py 
new/Radicale-3.2.3/radicale/httputils.py
--- old/Radicale-3.2.2/radicale/httputils.py    2024-06-18 20:24:12.000000000 
+0200
+++ new/Radicale-3.2.3/radicale/httputils.py    2024-08-30 06:19:51.000000000 
+0200
@@ -146,7 +146,7 @@
     if configuration.get("logging", "request_content_on_debug"):
         logger.debug("Request content:\n%s", content)
     else:
-        logger.debug("Request content: suppressed by config/option [auth] 
request_content_on_debug")
+        logger.debug("Request content: suppressed by config/option [logging] 
request_content_on_debug")
     return content
 
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.2.2/radicale/item/__init__.py 
new/Radicale-3.2.3/radicale/item/__init__.py
--- old/Radicale-3.2.2/radicale/item/__init__.py        2024-06-18 
20:24:12.000000000 +0200
+++ new/Radicale-3.2.3/radicale/item/__init__.py        2024-08-30 
06:19:51.000000000 +0200
@@ -49,6 +49,12 @@
     s = re.sub(r"^(PHOTO(?:;[^:\r\n]*)?;ENCODING=b(?:;[^:\r\n]*)?:)"
                r"data:[^;,\r\n]*;base64,", r"\1", s,
                flags=re.MULTILINE | re.IGNORECASE)
+    # Workaround for bug with malformed ICS files containing control codes
+    # Filter out all control codes except those we expect to find:
+    #  * 0x09 Horizontal Tab
+    #  * 0x0A Line Feed
+    #  * 0x0D Carriage Return
+    s = re.sub(r'[\x00-\x08\x0B\x0C\x0E-\x1F]', '', s)
     return list(vobject.readComponents(s, allowQP=True))
 
 
@@ -298,7 +304,7 @@
     Returns a tuple (``start``, ``end``) where ``start`` and ``end`` are
     POSIX timestamps.
 
-    This is intened to be used for matching against simplified prefilters.
+    This is intended to be used for matching against simplified prefilters.
 
     """
     if not tag:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.2.2/radicale/item/filter.py 
new/Radicale-3.2.3/radicale/item/filter.py
--- old/Radicale-3.2.2/radicale/item/filter.py  2024-06-18 20:24:12.000000000 
+0200
+++ new/Radicale-3.2.3/radicale/item/filter.py  2024-08-30 06:19:51.000000000 
+0200
@@ -48,10 +48,34 @@
     if not isinstance(d, datetime):
         d = datetime.combine(d, datetime.min.time())
     if not d.tzinfo:
-        d = d.replace(tzinfo=timezone.utc)
+        # NOTE: using vobject's UTC as it wasn't playing well with datetime's.
+        d = d.replace(tzinfo=vobject.icalendar.utc)
     return d
 
 
+def parse_time_range(time_filter: ET.Element) -> Tuple[datetime, datetime]:
+    start_text = time_filter.get("start")
+    end_text = time_filter.get("end")
+    if start_text:
+        start = datetime.strptime(
+            start_text, "%Y%m%dT%H%M%SZ").replace(
+                tzinfo=timezone.utc)
+    else:
+        start = DATETIME_MIN
+    if end_text:
+        end = datetime.strptime(
+            end_text, "%Y%m%dT%H%M%SZ").replace(
+                tzinfo=timezone.utc)
+    else:
+        end = DATETIME_MAX
+    return start, end
+
+
+def time_range_timestamps(time_filter: ET.Element) -> Tuple[int, int]:
+    start, end = parse_time_range(time_filter)
+    return (math.floor(start.timestamp()), math.ceil(end.timestamp()))
+
+
 def comp_match(item: "item.Item", filter_: ET.Element, level: int = 0) -> bool:
     """Check whether the ``item`` matches the comp ``filter_``.
 
@@ -147,21 +171,10 @@
     """Check whether the component/property ``child_name`` of
        ``vobject_item`` matches the time-range ``filter_``."""
 
-    start_text = filter_.get("start")
-    end_text = filter_.get("end")
-    if not start_text and not end_text:
+    if not filter_.get("start") and not filter_.get("end"):
         return False
-    if start_text:
-        start = datetime.strptime(start_text, "%Y%m%dT%H%M%SZ")
-    else:
-        start = datetime.min
-    if end_text:
-        end = datetime.strptime(end_text, "%Y%m%dT%H%M%SZ")
-    else:
-        end = datetime.max
-    start = start.replace(tzinfo=timezone.utc)
-    end = end.replace(tzinfo=timezone.utc)
 
+    start, end = parse_time_range(filter_)
     matched = False
 
     def range_fn(range_start: datetime, range_end: datetime,
@@ -181,6 +194,35 @@
     return matched
 
 
+def time_range_fill(vobject_item: vobject.base.Component,
+                    filter_: ET.Element, child_name: str, n: int = 1
+                    ) -> List[Tuple[datetime, datetime]]:
+    """Create a list of ``n`` occurances from the component/property 
``child_name``
+       of ``vobject_item``."""
+    if not filter_.get("start") and not filter_.get("end"):
+        return []
+
+    start, end = parse_time_range(filter_)
+    ranges: List[Tuple[datetime, datetime]] = []
+
+    def range_fn(range_start: datetime, range_end: datetime,
+                 is_recurrence: bool) -> bool:
+        nonlocal ranges
+        if start < range_end and range_start < end:
+            ranges.append((range_start, range_end))
+            if n > 0 and len(ranges) >= n:
+                return True
+        if end < range_start and not is_recurrence:
+            return True
+        return False
+
+    def infinity_fn(range_start: datetime) -> bool:
+        return False
+
+    visit_time_ranges(vobject_item, child_name, range_fn, infinity_fn)
+    return ranges
+
+
 def visit_time_ranges(vobject_item: vobject.base.Component, child_name: str,
                       range_fn: Callable[[datetime, datetime, bool], bool],
                       infinity_fn: Callable[[datetime], bool]) -> None:
@@ -199,7 +241,7 @@
 
     """
 
-    # HACK: According to rfc5545-3.8.4.4 an recurrance that is resheduled
+    # HACK: According to rfc5545-3.8.4.4 a recurrence that is rescheduled
     # with Recurrence ID affects the recurrence itself and all following
     # recurrences too. This is not respected and client don't seem to bother
     # either.
@@ -543,20 +585,7 @@
                 if time_filter.tag != xmlutils.make_clark("C:time-range"):
                     simple = False
                     continue
-                start_text = time_filter.get("start")
-                end_text = time_filter.get("end")
-                if start_text:
-                    start = math.floor(datetime.strptime(
-                        start_text, "%Y%m%dT%H%M%SZ").replace(
-                            tzinfo=timezone.utc).timestamp())
-                else:
-                    start = TIMESTAMP_MIN
-                if end_text:
-                    end = math.ceil(datetime.strptime(
-                        end_text, "%Y%m%dT%H%M%SZ").replace(
-                            tzinfo=timezone.utc).timestamp())
-                else:
-                    end = TIMESTAMP_MAX
+                start, end = time_range_timestamps(time_filter)
                 return tag, start, end, simple
             return tag, TIMESTAMP_MIN, TIMESTAMP_MAX, simple
     return None, TIMESTAMP_MIN, TIMESTAMP_MAX, simple
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.2.2/radicale/rights/from_file.py 
new/Radicale-3.2.3/radicale/rights/from_file.py
--- old/Radicale-3.2.2/radicale/rights/from_file.py     2024-06-18 
20:24:12.000000000 +0200
+++ new/Radicale-3.2.3/radicale/rights/from_file.py     2024-08-30 
06:19:51.000000000 +0200
@@ -22,7 +22,7 @@
 The login is matched against the "user" key, and the collection path
 is matched against the "collection" key. In the "collection" regex you can use
 `{user}` and get groups from the "user" regex with `{0}`, `{1}`, etc.
-In consequence of the parameter subsitution you have to write `{{` and `}}`
+In consequence of the parameter substitution you have to write `{{` and `}}`
 if you want to use regular curly braces in the "user" and "collection" regexes.
 
 For example, for the "user" key, ".+" means "authenticated user" and ".*"
@@ -48,6 +48,7 @@
     def __init__(self, configuration: config.Configuration) -> None:
         super().__init__(configuration)
         self._filename = configuration.get("rights", "file")
+        self._log_rights_rule_doesnt_match_on_debug = 
configuration.get("logging", "rights_rule_doesnt_match_on_debug")
 
     def authorization(self, user: str, path: str) -> str:
         user = user or ""
@@ -61,6 +62,8 @@
         except Exception as e:
             raise RuntimeError("Failed to load rights file %r: %s" %
                                (self._filename, e)) from e
+        if not self._log_rights_rule_doesnt_match_on_debug:
+            logger.debug("logging of rules which doesn't match suppressed by 
config/option [logging] rights_rule_doesnt_match_on_debug")
         for section in rights_config.sections():
             try:
                 user_pattern = rights_config.get(section, "user")
@@ -80,8 +83,9 @@
                              user, sane_path, user_pattern,
                              collection_pattern, section, permission)
                 return permission
-            logger.debug("Rule %r:%r doesn't match %r:%r from section %r",
-                         user, sane_path, user_pattern, collection_pattern,
-                         section)
+            if self._log_rights_rule_doesnt_match_on_debug:
+                logger.debug("Rule %r:%r doesn't match %r:%r from section %r",
+                             user, sane_path, user_pattern, collection_pattern,
+                             section)
         logger.info("Rights: %r:%r doesn't match any section", user, sane_path)
         return ""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.2.2/radicale/server.py 
new/Radicale-3.2.3/radicale/server.py
--- old/Radicale-3.2.2/radicale/server.py       2024-06-18 20:24:12.000000000 
+0200
+++ new/Radicale-3.2.3/radicale/server.py       2024-08-30 06:19:51.000000000 
+0200
@@ -291,7 +291,7 @@
             try:
                 getaddrinfo = socket.getaddrinfo(address_port[0], 
address_port[1], 0, socket.SOCK_STREAM, socket.IPPROTO_TCP)
             except OSError as e:
-                logger.warn("cannot retrieve IPv4 or IPv6 address of '%s': %s" 
% (format_address(address_port), e))
+                logger.warning("cannot retrieve IPv4 or IPv6 address of '%s': 
%s" % (format_address(address_port), e))
                 continue
             logger.debug("getaddrinfo of '%s': %s" % 
(format_address(address_port), getaddrinfo))
             for (address_family, socket_kind, socket_proto, socket_flags, 
socket_address) in getaddrinfo:
@@ -299,7 +299,7 @@
                 try:
                     server = server_class(configuration, address_family, 
(socket_address[0], socket_address[1]), RequestHandler)
                 except OSError as e:
-                    logger.warn("cannot create server socket on '%s': %s" % 
(format_address(socket_address), e))
+                    logger.warning("cannot create server socket on '%s': %s" % 
(format_address(socket_address), e))
                     continue
                 servers[server.socket] = server
                 server.set_app(application)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/Radicale-3.2.2/radicale/storage/multifilesystem/get.py 
new/Radicale-3.2.3/radicale/storage/multifilesystem/get.py
--- old/Radicale-3.2.2/radicale/storage/multifilesystem/get.py  2024-06-18 
20:24:12.000000000 +0200
+++ new/Radicale-3.2.3/radicale/storage/multifilesystem/get.py  2024-08-30 
06:19:51.000000000 +0200
@@ -84,7 +84,7 @@
         cache_content = self._load_item_cache(href, cache_hash)
         if cache_content is None:
             with self._acquire_cache_lock("item"):
-                # Lock the item cache to prevent multpile processes from
+                # Lock the item cache to prevent multiple processes from
                 # generating the same data in parallel.
                 # This improves the performance for multiple requests.
                 if self._storage._lock.locked == "r":
@@ -127,7 +127,7 @@
 
     def get_multi(self, hrefs: Iterable[str]
                   ) -> Iterator[Tuple[str, Optional[radicale_item.Item]]]:
-        # It's faster to check for file name collissions here, because
+        # It's faster to check for file name collisions here, because
         # we only need to call os.listdir once.
         files = None
         for href in hrefs:
@@ -146,7 +146,7 @@
 
     def get_all(self) -> Iterator[radicale_item.Item]:
         for href in self._list():
-            # We don't need to check for collissions, because the file names
+            # We don't need to check for collisions, because the file names
             # are from os.listdir.
             item = self._get(href, verify_href=False)
             if item is not None:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.2.2/radicale/tests/__init__.py 
new/Radicale-3.2.3/radicale/tests/__init__.py
--- old/Radicale-3.2.2/radicale/tests/__init__.py       2024-06-18 
20:24:12.000000000 +0200
+++ new/Radicale-3.2.3/radicale/tests/__init__.py       2024-08-30 
06:19:51.000000000 +0200
@@ -31,11 +31,12 @@
 from typing import Any, Dict, List, Optional, Tuple, Union
 
 import defusedxml.ElementTree as DefusedET
+import vobject
 
 import radicale
 from radicale import app, config, types, xmlutils
 
-RESPONSES = Dict[str, Union[int, Dict[str, Tuple[int, ET.Element]]]]
+RESPONSES = Dict[str, Union[int, Dict[str, Tuple[int, ET.Element]], 
vobject.base.Component]]
 
 # Enable debug output
 radicale.log.logger.setLevel(logging.DEBUG)
@@ -107,12 +108,11 @@
     def parse_responses(text: str) -> RESPONSES:
         xml = DefusedET.fromstring(text)
         assert xml.tag == xmlutils.make_clark("D:multistatus")
-        path_responses: Dict[str, Union[
-            int, Dict[str, Tuple[int, ET.Element]]]] = {}
+        path_responses: RESPONSES = {}
         for response in xml.findall(xmlutils.make_clark("D:response")):
             href = response.find(xmlutils.make_clark("D:href"))
             assert href.text not in path_responses
-            prop_respones: Dict[str, Tuple[int, ET.Element]] = {}
+            prop_responses: Dict[str, Tuple[int, ET.Element]] = {}
             for propstat in response.findall(
                     xmlutils.make_clark("D:propstat")):
                 status = propstat.find(xmlutils.make_clark("D:status"))
@@ -121,16 +121,22 @@
                 for element in propstat.findall(
                         "./%s/*" % xmlutils.make_clark("D:prop")):
                     human_tag = xmlutils.make_human_tag(element.tag)
-                    assert human_tag not in prop_respones
-                    prop_respones[human_tag] = (status_code, element)
+                    assert human_tag not in prop_responses
+                    prop_responses[human_tag] = (status_code, element)
             status = response.find(xmlutils.make_clark("D:status"))
             if status is not None:
-                assert not prop_respones
+                assert not prop_responses
                 assert status.text.startswith("HTTP/1.1 ")
                 status_code = int(status.text.split(" ")[1])
                 path_responses[href.text] = status_code
             else:
-                path_responses[href.text] = prop_respones
+                path_responses[href.text] = prop_responses
+        return path_responses
+
+    @staticmethod
+    def parse_free_busy(text: str) -> RESPONSES:
+        path_responses: RESPONSES = {}
+        path_responses[""] = vobject.readOne(text)
         return path_responses
 
     def get(self, path: str, check: Optional[int] = 200, **kwargs
@@ -177,13 +183,18 @@
         return status, responses
 
     def report(self, path: str, data: str, check: Optional[int] = 207,
+               is_xml: Optional[bool] = True,
                **kwargs) -> Tuple[int, RESPONSES]:
         status, _, answer = self.request("REPORT", path, data, check=check,
                                          **kwargs)
         if status < 200 or 300 <= status:
             return status, {}
         assert answer is not None
-        return status, self.parse_responses(answer)
+        if is_xml:
+            parsed = self.parse_responses(answer)
+        else:
+            parsed = self.parse_free_busy(answer)
+        return status, parsed
 
     def delete(self, path: str, check: Optional[int] = 200, **kwargs
                ) -> Tuple[int, RESPONSES]:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.2.2/radicale/tests/static/event10.ics 
new/Radicale-3.2.3/radicale/tests/static/event10.ics
--- old/Radicale-3.2.2/radicale/tests/static/event10.ics        1970-01-01 
01:00:00.000000000 +0100
+++ new/Radicale-3.2.3/radicale/tests/static/event10.ics        2024-08-30 
06:19:51.000000000 +0200
@@ -0,0 +1,36 @@
+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:event10
+SUMMARY:Event
+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:20130901T180000
+DTEND;TZID=Europe/Paris:20130901T190000
+STATUS:CANCELLED
+END:VEVENT
+END:VCALENDAR
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.2.2/radicale/tests/test_auth.py 
new/Radicale-3.2.3/radicale/tests/test_auth.py
--- old/Radicale-3.2.2/radicale/tests/test_auth.py      2024-06-18 
20:24:12.000000000 +0200
+++ new/Radicale-3.2.3/radicale/tests/test_auth.py      2024-08-30 
06:19:51.000000000 +0200
@@ -115,6 +115,16 @@
     def test_htpasswd_comment(self) -> None:
         self._test_htpasswd("plain", "#comment\n #comment\n \ntmp:bepo\n\n")
 
+    def test_htpasswd_lc_username(self) -> None:
+        self.configure({"auth": {"lc_username": "True"}})
+        self._test_htpasswd("plain", "tmp:bepo", (
+            ("tmp", "bepo", True), ("TMP", "bepo", True), ("tmp1", "bepo", 
False)))
+
+    def test_htpasswd_strip_domain(self) -> None:
+        self.configure({"auth": {"strip_domain": "True"}})
+        self._test_htpasswd("plain", "tmp:bepo", (
+            ("tmp", "bepo", True), ("[email protected]", "bepo", True), 
("tmp1", "bepo", False)))
+
     def test_remote_user(self) -> None:
         self.configure({"auth": {"type": "remote_user"}})
         _, responses = self.propfind("/", """\
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.2.2/radicale/tests/test_base.py 
new/Radicale-3.2.3/radicale/tests/test_base.py
--- old/Radicale-3.2.2/radicale/tests/test_base.py      2024-06-18 
20:24:12.000000000 +0200
+++ new/Radicale-3.2.3/radicale/tests/test_base.py      2024-08-30 
06:19:51.000000000 +0200
@@ -25,6 +25,7 @@
 from typing import Any, Callable, ClassVar, Iterable, List, Optional, Tuple
 
 import defusedxml.ElementTree as DefusedET
+import vobject
 
 from radicale import storage, xmlutils
 from radicale.tests import RESPONSES, BaseTest
@@ -359,7 +360,7 @@
         self.get(path1, check=404)
         self.get(path2)
 
-    def test_move_between_colections(self) -> None:
+    def test_move_between_collections(self) -> None:
         """Move a item."""
         self.mkcalendar("/calendar1.ics/")
         self.mkcalendar("/calendar2.ics/")
@@ -372,7 +373,7 @@
         self.get(path1, check=404)
         self.get(path2)
 
-    def test_move_between_colections_duplicate_uid(self) -> None:
+    def test_move_between_collections_duplicate_uid(self) -> None:
         """Move a item to a collection which already contains the UID."""
         self.mkcalendar("/calendar1.ics/")
         self.mkcalendar("/calendar2.ics/")
@@ -388,7 +389,7 @@
         assert xml.tag == xmlutils.make_clark("D:error")
         assert xml.find(xmlutils.make_clark("C:no-uid-conflict")) is not None
 
-    def test_move_between_colections_overwrite(self) -> None:
+    def test_move_between_collections_overwrite(self) -> None:
         """Move a item to a collection which already contains the item."""
         self.mkcalendar("/calendar1.ics/")
         self.mkcalendar("/calendar2.ics/")
@@ -402,8 +403,8 @@
         self.request("MOVE", path1, check=204, HTTP_OVERWRITE="T",
                      HTTP_DESTINATION="http://127.0.0.1/"+path2)
 
-    def test_move_between_colections_overwrite_uid_conflict(self) -> None:
-        """Move a item to a collection which already contains the item with
+    def test_move_between_collections_overwrite_uid_conflict(self) -> None:
+        """Move an item to a collection which already contains the item with
            a different UID."""
         self.mkcalendar("/calendar1.ics/")
         self.mkcalendar("/calendar2.ics/")
@@ -1360,10 +1361,45 @@
 </C:calendar-query>""")
         assert len(responses) == 1
         response = responses[event_path]
-        assert not isinstance(response, int)
+        assert isinstance(response, dict)
         status, prop = response["D:getetag"]
         assert status == 200 and prop.text
 
+    def test_report_free_busy(self) -> None:
+        """Test free busy report on a few items"""
+        calendar_path = "/calendar.ics/"
+        self.mkcalendar(calendar_path)
+        for i in (1, 2, 10):
+            filename = "event{}.ics".format(i)
+            event = get_file_content(filename)
+            self.put(posixpath.join(calendar_path, filename), event)
+        code, responses = self.report(calendar_path, """\
+<?xml version="1.0" encoding="utf-8" ?>
+<C:free-busy-query xmlns:C="urn:ietf:params:xml:ns:caldav">
+    <C:time-range start="20130901T140000Z" end="20130908T220000Z"/>
+</C:free-busy-query>""", 200, is_xml=False)
+        for response in responses.values():
+            assert isinstance(response, vobject.base.Component)
+        assert len(responses) == 1
+        vcalendar = list(responses.values())[0]
+        assert isinstance(vcalendar, vobject.base.Component)
+        assert len(vcalendar.vfreebusy_list) == 3
+        types = {}
+        for vfb in vcalendar.vfreebusy_list:
+            fbtype_val = vfb.fbtype.value
+            if fbtype_val not in types:
+                types[fbtype_val] = 0
+            types[fbtype_val] += 1
+        assert types == {'BUSY': 2, 'FREE': 1}
+
+        # Test max_freebusy_occurrence limit
+        self.configure({"reporting": {"max_freebusy_occurrence": 1}})
+        code, responses = self.report(calendar_path, """\
+<?xml version="1.0" encoding="utf-8" ?>
+<C:free-busy-query xmlns:C="urn:ietf:params:xml:ns:caldav">
+    <C:time-range start="20130901T140000Z" end="20130908T220000Z"/>
+</C:free-busy-query>""", 400, is_xml=False)
+
     def _report_sync_token(
             self, calendar_path: str, sync_token: Optional[str] = None
             ) -> Tuple[str, RESPONSES]:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.2.2/setup.cfg new/Radicale-3.2.3/setup.cfg
--- old/Radicale-3.2.2/setup.cfg        2024-06-18 20:24:12.000000000 +0200
+++ new/Radicale-3.2.3/setup.cfg        2024-08-30 06:19:51.000000000 +0200
@@ -1,26 +1,31 @@
 [tool:pytest]
-addopts = --typeguard-packages=radicale
 
 [tox:tox]
+min_version = 4.0
+envlist = py, flake8, isort, mypy
 
 [testenv]
-extras = test
+extras =
+    test
 deps =
-    flake8
-    isort
-    # mypy installation fails with pypy<3.9
-    mypy; implementation_name!='pypy' or python_version>='3.9'
-    types-setuptools
+    pytest
     pytest-cov
-commands =
-    flake8 .
-    isort --check --diff .
-    # Run mypy if it's installed
-    python -c 'import importlib.util, subprocess, sys; \
-               importlib.util.find_spec("mypy") \
-                   and sys.exit(subprocess.run(["mypy", "."]).returncode) \
-                   or print("Skipped: mypy is not installed")'
-    pytest -r s --cov --cov-report=term --cov-report=xml .
+commands = pytest -r s --cov --cov-report=term --cov-report=xml .
+
+[testenv:flake8]
+deps = flake8==7.1.0
+commands = flake8 .
+skip_install = True
+
+[testenv:isort]
+deps = isort==5.13.2
+commands = isort --check --diff .
+skip_install = True
+
+[testenv:mypy]
+deps = mypy==1.11.0
+commands = mypy .
+skip_install = True
 
 [tool:isort]
 known_standard_library = 
_dummy_thread,_thread,abc,aifc,argparse,array,ast,asynchat,asyncio,asyncore,atexit,audioop,base64,bdb,binascii,binhex,bisect,builtins,bz2,cProfile,calendar,cgi,cgitb,chunk,cmath,cmd,code,codecs,codeop,collections,colorsys,compileall,concurrent,configparser,contextlib,contextvars,copy,copyreg,crypt,csv,ctypes,curses,dataclasses,datetime,dbm,decimal,difflib,dis,distutils,doctest,dummy_threading,email,encodings,ensurepip,enum,errno,faulthandler,fcntl,filecmp,fileinput,fnmatch,formatter,fpectl,fractions,ftplib,functools,gc,getopt,getpass,gettext,glob,grp,gzip,hashlib,heapq,hmac,html,http,imaplib,imghdr,imp,importlib,inspect,io,ipaddress,itertools,json,keyword,lib2to3,linecache,locale,logging,lzma,macpath,mailbox,mailcap,marshal,math,mimetypes,mmap,modulefinder,msilib,msvcrt,multiprocessing,netrc,nis,nntplib,ntpath,numbers,operator,optparse,os,ossaudiodev,parser,pathlib,pdb,pickle,pickletools,pipes,pkgutil,platform,plistlib,poplib,posix,posixpath,pprint,profile,p
 
stats,pty,pwd,py_compile,pyclbr,pydoc,queue,quopri,random,re,readline,reprlib,resource,rlcompleter,runpy,sched,secrets,select,selectors,shelve,shlex,shutil,signal,site,smtpd,smtplib,sndhdr,socket,socketserver,spwd,sqlite3,sre,sre_compile,sre_constants,sre_parse,ssl,stat,statistics,string,stringprep,struct,subprocess,sunau,symbol,symtable,sys,sysconfig,syslog,tabnanny,tarfile,telnetlib,tempfile,termios,test,textwrap,threading,time,timeit,tkinter,token,tokenize,trace,traceback,tracemalloc,tty,turtle,turtledemo,types,typing,unicodedata,unittest,urllib,uu,uuid,venv,warnings,wave,weakref,webbrowser,winreg,winsound,wsgiref,xdrlib,xml,xmlrpc,zipapp,zipfile,zipimport,zlib
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/Radicale-3.2.2/setup.py new/Radicale-3.2.3/setup.py
--- old/Radicale-3.2.2/setup.py 2024-06-18 20:24:12.000000000 +0200
+++ new/Radicale-3.2.3/setup.py 2024-08-30 06:19:51.000000000 +0200
@@ -19,7 +19,7 @@
 
 # When the version is updated, a new section in the CHANGELOG.md file must be
 # added too.
-VERSION = "3.2.2"
+VERSION = "3.2.3"
 
 with open("README.md", encoding="utf-8") as f:
     long_description = f.read()
@@ -38,9 +38,9 @@
 install_requires = ["defusedxml", "passlib", "vobject>=0.9.6",
                     "python-dateutil>=2.7.3",
                     "pika>=1.1.0",
-                    "setuptools; python_version<'3.9'"]
+                    ]
 bcrypt_requires = ["bcrypt"]
-test_requires = ["pytest>=7", "typeguard<4.3", "waitress", *bcrypt_requires]
+test_requires = ["pytest>=7", "waitress", *bcrypt_requires]
 
 setup(
     name="Radicale",
@@ -75,6 +75,7 @@
         "Programming Language :: Python :: 3.10",
         "Programming Language :: Python :: 3.11",
         "Programming Language :: Python :: 3.12",
+        "Programming Language :: Python :: 3.13",
         "Programming Language :: Python :: Implementation :: CPython",
         "Programming Language :: Python :: Implementation :: PyPy",
         "Topic :: Office/Business :: Groupware"])

Reply via email to