diff --git a/doc/guix.texi b/doc/guix.texi
index 858d5751bfe..096f6bcacdf 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -28252,21 +28252,201 @@ Mail Services
 server whose value should be a @code{radicale-configuration}.
 @end defvar
 
+@c %start of fragment
+
 @deftp {Data Type} radicale-configuration
-Data type representing the configuration of @command{radicale}.
+Data type representing the configuration of @command{radicale}. See the
+@uref{https://radicale.org/v3.html#configuration, configuration
+documentation} for default values.  Available
+@code{radicale-configuration} fields are:
 
 @table @asis
-@item @code{package} (default: @code{radicale})
-The package that provides @command{radicale}.
+@item @code{package} (default: @code{radicale}) (type: package)
+Package that provides @command{radicale}.
 
-@item @code{config-file} (default: @code{%default-radicale-config-file})
-File-like object of the configuration file to use, by default it will listen
-on TCP port 5232 of @code{localhost} and use the @code{htpasswd} file at
-@file{/var/lib/radicale/users} with no (@code{plain}) encryption.
+@item @code{auth} (type: maybe-auth-config)
+Configuration for auth-related variables.  This should be a
+@code{radicale-auth-configuration}.
+
+@deftp {Data Type} radicale-auth-configuration
+Data type representing the @code{auth} section of a @command{radicale}
+configuration file.  Available @code{radicale-auth-configuration} fields
+are:
+
+@table @asis
+@item @code{type} (type: maybe-symbol)
+The method to verify usernames and passwords.  Options are @code{none},
+@code{htpasswd}, @code{remote-user}, and @code{http-x-remote-user}.
+This value is tied to @code{htpasswd-filename} and
+@code{htpasswd-encryption}.
+
+@item @code{htpasswd-filename} (type: maybe-file-name)
+Path to the htpasswd file.  Use htpasswd or similar to generate this
+file.
+
+@item @code{htpasswd-encryption} (type: maybe-symbol)
+Encryption method used in the htpasswd file.  Options are @code{plain},
+@code{bcrypt}, and @code{md5}.
+
+@item @code{delay} (type: maybe-non-negative-integer)
+Average delay after failed login attempts in seconds.
+
+@item @code{realm} (type: maybe-string)
+Message displayed in the client when a password is needed.
+
+@end table
+
+@end deftp
+
+@item @code{encoding} (type: maybe-encoding-config)
+Configuration for encoding-related variables.  This should be a
+@code{radicale-encoding-configuration}.
+
+@deftp {Data Type} radicale-encoding-configuration
+Data type representing the @code{encoding} section of a
+@command{radicale} configuration file.  Available
+@code{radicale-encoding-configuration} fields are:
+
+@table @asis
+@item @code{request} (type: maybe-symbol)
+Encoding for responding requests.
+
+@item @code{stock} (type: maybe-symbol)
+Encoding for storing local collections.
 
 @end table
+
 @end deftp
 
+@item @code{headers-file} (type: maybe-text-config)
+Custom HTTP headers.  This should be a file-like object.
+
+@item @code{logging} (type: maybe-logging-config)
+Configuration for logging-related variables.  This should be a
+@code{radicale-logging-configuration}.
+
+@deftp {Data Type} radicale-logging-configuration
+Data type representing the @code{logging} section of a
+@command{radicale} configuration file.  Available
+@code{radicale-logging-configuration} fields are:
+
+@table @asis
+@item @code{level} (type: maybe-symbol)
+Set the logging level.  One of @code{debug}, @code{info},
+@code{warning}, @code{error}, or @code{critical}.
+
+@item @code{mask-passwords?} (type: maybe-boolean)
+Whether to include passwords in logs.
+
+@end table
+
+@end deftp
+
+@item @code{rights} (type: maybe-rights-config)
+Configuration for rights-related variables.  This should be a
+@code{radicale-rights-configuration}.
+
+@deftp {Data Type} radicale-rights-configuration
+Data type representing the @code{rights} section of a @command{radicale}
+configuration file.  Available @code{radicale-rights-configuration}
+fields are:
+
+@table @asis
+@item @code{type} (type: maybe-symbol)
+Backend used to check collection access rights.  The recommended backend
+is @code{owner-only}.  If access to calendars and address books outside
+the home directory of users is granted, clients won't detect these
+collections and will not show them to the user.  Choosing any other
+method is only useful if you access calendars and address books directly
+via URL.  Options are @code{authenticate}, @code{owner-only},
+@code{owner-write}, and @code{from-file}.
+
+@item @code{file} (type: maybe-file-name)
+File for the rights backend @code{from-file}.
+
+@end table
+
+@end deftp
+
+@item @code{server} (type: maybe-server-config)
+Configuration for server-related variables.  Ignored if WSGI is used.
+This should be a @code{radicale-server-configuration}.
+
+@deftp {Data Type} radicale-server-configuration
+Data type representing the @code{server} section of a @command{radicale}
+configuration file.  Available @code{radicale-server-configuration}
+fields are:
+
+@table @asis
+@item @code{hosts} (type: maybe-comma-separated-string-list)
+List of IP addresses that the server will bind to.
+
+@item @code{max-connections} (type: maybe-non-negative-integer)
+Maximum number of parallel connections.  Set to 0 to disable the limit.
+
+@item @code{max-content-length} (type: maybe-non-negative-integer)
+Maximum size of the request body in byetes.
+
+@item @code{timeout} (type: maybe-non-negative-integer)
+Socket timeout in seconds.
+
+@item @code{ssl?} (type: maybe-boolean)
+Whether to enable transport layer encryption.
+
+@item @code{certificate} (type: maybe-file-name)
+Path of the SSL certificate.
+
+@item @code{key} (type: maybe-file-name)
+Path to the private key for SSL.  Only effective if @code{ssl?} is
+@code{#t}.
+
+@item @code{certificate-authority} (type: maybe-file-name)
+Path to CA certificate for validating client certificates.  This can be
+used to secure TCP traffic between Radicale and a reverse proxy.  If you
+want to authenticate users with client-side certificates, you also have
+to write an authentication plugin that extracts the username from the
+certificate.
+
+@end table
+
+@end deftp
+
+@item @code{storage} (type: maybe-storage-config)
+Configuration for storage-related variables.  This should be a
+@code{radicale-storage-configuration}.
+
+@deftp {Data Type} radicale-storage-configuration
+Data type representing the @code{storage} section of a
+@command{radicale} configuration file.  Available
+@code{radicale-storage-configuration} fields are:
+
+@table @asis
+@item @code{type} (type: maybe-symbol)
+Backend used to store data.  Options are @code{multifilesystem} and
+@code{multifilesystem-nolock}.
+
+@item @code{filesystem-folder} (type: maybe-file-name)
+Folder for storing local collections.  Created if not present.
+
+@item @code{max-sync-token-age} (type: maybe-non-negative-integer)
+Delete sync-tokens that are older than the specified time in seconds.
+
+@item @code{hook} (type: maybe-string)
+Command run after changes to storage.
+
+@end table
+
+@end deftp
+
+@item @code{web-interface?} (type: maybe-boolean)
+Whether to use Radicale's built-in web interface.
+
+@end table
+
+@end deftp
+
+@c %end of fragment
+
 @subsubheading Rspamd Service
 @cindex email
 @cindex spam
diff --git a/gnu/services/mail.scm b/gnu/services/mail.scm
index afe1bb60169..2499be87722 100644
--- a/gnu/services/mail.scm
+++ b/gnu/services/mail.scm
@@ -38,10 +38,12 @@ (define-module (gnu services mail)
   #:use-module (gnu packages dav)
   #:use-module (gnu packages tls)
   #:use-module (guix deprecation)
+  #:use-module (guix diagnostics)
   #:use-module (guix modules)
   #:use-module (guix records)
   #:use-module (guix packages)
   #:use-module (guix gexp)
+  #:use-module (ice-9 curried-definitions)
   #:use-module (ice-9 match)
   #:use-module (ice-9 format)
   #:use-module (srfi srfi-1)
@@ -79,10 +81,21 @@ (define-module (gnu services mail)
             imap4d-service-type
             %default-imap4d-config-file
 
+            radicale-auth-configuration
+            radicale-auth-configuration?
+            radicale-encoding-configuration
+            radicale-encoding-configuration?
+            radicale-logging-configuration
+            radicale-logging-configuration?
+            radicale-rights-configuration
+            radicale-rights-configuration?
+            radicale-server-configuration
+            radicale-server-configuration?
+            radicale-storage-configuration
+            radicale-storage-configuration?
             radicale-configuration
             radicale-configuration?
             radicale-service-type
-            %default-radicale-config-file
 
             rspamd-configuration
             rspamd-service-type
@@ -1929,23 +1942,255 @@ (define imap4d-service-type
 ;;; Radicale.
 ;;;
 
-(define-record-type* <radicale-configuration>
-  radicale-configuration make-radicale-configuration
-  radicale-configuration?
-  (package     radicale-configuration-package
-               (default radicale))
-  (config-file radicale-configuration-config-file
-               (default %default-radicale-config-file)))
+;; Maybe types
 
-(define %default-radicale-config-file
-  (plain-file "radicale.conf" "
-[auth]
-type = htpasswd
-htpasswd_filename = /var/lib/radicale/users
-htpasswd_encryption = plain
+(define-maybe boolean (prefix radicale-))
+(define-maybe comma-separated-string-list (prefix radicale-))
+(define-maybe file-name (prefix radicale-))
+(define-maybe non-negative-integer (prefix radicale-))
+(define-maybe string (prefix radicale-))
+(define-maybe symbol (prefix radicale-))
+(define-maybe text-config)
 
-[server]
-hosts = localhost:5232"))
+;; Serializers and sanitizers
+
+(define (radicale-serialize-field field-name value)
+  ;; XXX We quote the un-gexp form here because otherwise symbol-literals are
+  ;; treated as variables. We can get away with this because all of our other
+  ;; field value types are primitives by the time they get here so are printed
+  ;; the same whether or not they are quoted.
+  #~(format #f "~a = ~a\n" #$(uglify-field-name field-name) '#$value))
+
+(define (radicale-serialize-boolean field-name value?)
+  (radicale-serialize-field field-name (if value? "True" "False")))
+
+(define (radicale-serialize-comma-separated-string-list field-name value)
+  (radicale-serialize-field field-name (string-join value ", ")))
+
+(define radicale-serialize-file-name radicale-serialize-field)
+
+(define radicale-serialize-non-negative-integer radicale-serialize-field)
+
+(define radicale-serialize-string radicale-serialize-field)
+
+(define radicale-serialize-symbol radicale-serialize-field)
+
+(define ((sanitize-delimited-symbols syms location field) value)
+  (cond
+   ((not (maybe-value-set? value))
+    value)
+   ((member value syms)
+    (uglify-field-name value))
+   (else
+    (configuration-field-error (source-properties->location location)
+                               field
+                               value))))
+
+;; Section configuration types
+
+(define-configuration radicale-auth-configuration
+  (type
+   maybe-symbol
+   "The method to verify usernames and passwords. Options are @code{none},
+@code{htpasswd}, @code{remote-user}, and @code{http-x-remote-user}.
+
+This value is tied to @code{htpasswd-filename} and @code{htpasswd-encryption}."
+   (sanitizer
+    (sanitize-delimited-symbols '(none htpasswd remote-user http-x-remote-user)
+                                (current-source-location)
+                                'type))
+   (serializer radicale-serialize-field))
+  (htpasswd-filename
+   maybe-file-name
+   "Path to the htpasswd file. Use htpasswd or similar to generate this file.")
+  (htpasswd-encryption
+   maybe-symbol
+   "Encryption method used in the htpasswd file. Options are @code{plain},
+@code{bcrypt}, and @code{md5}."
+   (sanitizer
+    (sanitize-delimited-symbols '(plain bcrypt md5)
+                                (current-source-location)
+                                'htpasswd-encryption))
+   (serializer radicale-serialize-field))
+  (delay
+   maybe-non-negative-integer
+   "Average delay after failed login attempts in seconds.")
+  (realm
+   maybe-string
+   "Message displayed in the client when a password is needed.")
+  (prefix radicale-))
+
+(define-configuration radicale-encoding-configuration
+  (request
+   maybe-symbol
+   "Encoding for responding requests.")
+  (stock
+   maybe-symbol
+   "Encoding for storing local collections.")
+  (prefix radicale-))
+
+(define-configuration radicale-logging-configuration
+  (level
+   maybe-symbol
+   "Set the logging level. One of @code{debug}, @code{info}, @code{warning},
+@code{error}, or @code{critical}."
+   (sanitizer (sanitize-delimited-symbols '(debug info warning error critical)
+                                          (current-source-location)
+                                          'level))
+   (serializer radicale-serialize-field))
+  (mask-passwords?
+   maybe-boolean
+   "Whether to include passwords in logs.")
+  (prefix radicale-))
+
+(define-configuration radicale-rights-configuration
+  (type
+   maybe-symbol
+   "Backend used to check collection access rights. The recommended backend is
+@code{owner-only}. If access to calendars and address books outside the home
+directory of users is granted, clients won't detect these collections and will
+not show them to the user. Choosing any other method is only useful if you
+access calendars and address books directly via URL. Options are
+@code{authenticate}, @code{owner-only}, @code{owner-write}, and
+@code{from-file}."
+   (sanitizer
+    (sanitize-delimited-symbols '(authenticate owner-only owner-write from-file)
+                                (current-source-location)
+                                'type))
+   (serializer radicale-serialize-field))
+  (file
+   maybe-file-name
+   "File for the rights backend @code{from-file}.")
+  (prefix radicale-))
+
+(define-configuration radicale-server-configuration
+  (hosts
+   maybe-comma-separated-string-list
+   "List of IP addresses that the server will bind to.")
+  (max-connections
+   maybe-non-negative-integer
+   "Maximum number of parallel connections. Set to 0 to disable the limit.")
+  (max-content-length
+   maybe-non-negative-integer
+   "Maximum size of the request body in byetes.")
+  (timeout
+   maybe-non-negative-integer
+   "Socket timeout in seconds.")
+  (ssl?
+   maybe-boolean
+   "Whether to enable transport layer encryption.")
+  (certificate
+   maybe-file-name
+   "Path of the SSL certificate.")
+  (key
+   maybe-file-name
+   "Path to the private key for SSL. Only effective if @code{ssl?} is
+@code{#t}.")
+  (certificate-authority
+   maybe-file-name
+   "Path to CA certificate for validating client certificates. This can be used
+to secure TCP traffic between Radicale and a reverse proxy. If you want to
+authenticate users with client-side certificates, you also have to write an
+authentication plugin that extracts the username from the certificate.")
+  (prefix radicale-))
+
+(define-configuration radicale-storage-configuration
+  (type
+   maybe-symbol
+   "Backend used to store data. Options are @code{multifilesystem} and
+@code{multifilesystem-nolock}."
+   (sanitizer
+    (sanitize-delimited-symbols '(multifilesystem multifilesystem-nolock)
+                                (current-source-location)
+                                'type))
+   (serializer radicale-serialize-field))
+  (filesystem-folder
+   maybe-file-name
+   "Folder for storing local collections. Created if not present.")
+  (max-sync-token-age
+   maybe-non-negative-integer
+   "Delete sync-tokens that are older than the specified time in seconds.")
+  (hook
+   maybe-string
+   "Command run after changes to storage.")
+  (prefix radicale-))
+
+;; Helpers for using section configurations in the main configuration
+
+;; XXX These indirections are necessary to avoid creating semantic ambiguity
+(define auth-config? radicale-auth-configuration?)
+(define encoding-config? radicale-encoding-configuration?)
+(define logging-config? radicale-logging-configuration?)
+(define rights-config? radicale-rights-configuration?)
+(define server-config? radicale-server-configuration?)
+(define storage-config? radicale-storage-configuration?)
+
+(define-maybe auth-config)
+(define-maybe encoding-config)
+(define-maybe logging-config)
+(define-maybe rights-config)
+(define-maybe server-config)
+(define-maybe storage-config)
+
+(define ((serialize-radicale-section name fields) _ cfg)
+  #~(format #f "[~a]\n~a\n" #$name #$(serialize-configuration cfg fields)))
+
+(define serialize-auth-config
+  (serialize-radicale-section "auth" radicale-auth-configuration-fields))
+(define serialize-encoding-config
+  (serialize-radicale-section "encoding" radicale-encoding-configuration-fields))
+(define serialize-logging-config
+  (serialize-radicale-section "logging" radicale-logging-configuration-fields))
+(define serialize-rights-config
+  (serialize-radicale-section "rights" radicale-rights-configuration-fields))
+(define serialize-server-config
+  (serialize-radicale-section "server" radicale-server-configuration-fields))
+(define serialize-storage-config
+  (serialize-radicale-section "storage" radicale-storage-configuration-fields))
+
+(define (serialize-radicale-configuration cfg)
+  (mixed-text-file
+   "radicale.conf"
+   (serialize-configuration cfg radicale-configuration-fields)))
+
+(define-configuration radicale-configuration
+  ;; Only fields whose default value does not match upstream are not maybe-types
+  (package
+   (package radicale)
+   "Package that provides @command{radicale}.")
+  (auth
+   maybe-auth-config
+   "Configuration for auth-related variables. This should be a
+@code{radicale-auth-configuration}.")
+  (encoding
+   maybe-encoding-config
+   "Configuration for encoding-related variables. This should be a
+@code{radicale-encoding-configuration}.")
+  (headers-file
+   maybe-text-config
+   "Custom HTTP headers. This should be a file-like object.")
+  (logging
+   maybe-logging-config
+   "Configuration for logging-related variables. This should be a
+@code{radicale-logging-configuration}.")
+  (rights
+   maybe-rights-config
+   "Configuration for rights-related variables. This should be a
+@code{radicale-rights-configuration}.")
+  (server
+   maybe-server-config
+   "Configuration for server-related variables. Ignored if WSGI is used. This
+should be a @code{radicale-server-configuration}.")
+  (storage
+   maybe-storage-config
+   "Configuration for storage-related variables. This should be a
+@code{radicale-storage-configuration}.")
+  (web-interface?
+   maybe-boolean
+   "Whether to use Radicale's built-in web interface."
+   (serializer
+    (lambda (_ use?)
+      #~(format #f "[web]\ntype = ~a\n\n" #$(if use? "internal" "none"))))))
 
 (define %radicale-accounts
   (list (user-group
@@ -1959,43 +2204,96 @@ (define %radicale-accounts
          (home-directory "/var/empty")
          (shell (file-append shadow "/sbin/nologin")))))
 
-(define radicale-shepherd-service
-  (match-lambda
-    (($ <radicale-configuration> package config-file)
-     (list (shepherd-service
-            (provision '(radicale))
-            (documentation "Run the radicale daemon.")
-            (requirement '(networking))
-            (start #~(make-forkexec-constructor
-                      (list #$(file-append package "/bin/radicale")
-                        "-C" #$config-file)
-                      #:user "radicale"
-                      #:group "radicale"))
-            (stop #~(make-kill-destructor)))))))
+(define (radicale-shepherd-service cfg)
+  (list (shepherd-service
+         (provision '(radicale))
+         (documentation "Run the radicale daemon.")
+         (requirement '(networking))
+         (start #~(make-forkexec-constructor
+                   (list #$(file-append (radicale-configuration-package cfg)
+                                        "/bin/radicale")
+                         "-C" #$(serialize-radicale-configuration cfg))
+                   #:user "radicale"
+                   #:group "radicale"))
+         (stop #~(make-kill-destructor)))))
 
 (define radicale-activation
   (match-lambda
-    (($ <radicale-configuration> package config-file)
+    (($ <radicale-configuration> _ auth-config _ _ _ _ _ storage-config _)
+     ;; Get values for the collections directory
+     ;; See https://radicale.org/v3.html#running-as-a-service
+     (define filesystem-folder-val
+       (if (maybe-value-set? storage-config)
+           (radicale-storage-configuration-filesystem-folder storage-config)
+           storage-config))
+     (define collections-dir
+       (if (maybe-value-set? filesystem-folder-val)
+           filesystem-folder-val
+           "/var/lib/radicale/collections"))
+     (define collections-parent-dir (dirname collections-dir))
+     ;; Get values for the password file directory
+     ;; This might be the parent directory for the collections directory,
+     ;; but we shouldn't assume that.
+     (define auth-value-set? (maybe-value-set? auth-config))
+     ;; If auth's type is 'none or unset, that means there is no authentication
+     ;; and we don't need to setup files for it
+     (define auth?
+       (and auth-value-set?
+            (not (eq? (radicale-auth-configuration-type auth-config) 'none))))
+     (define password-file-val
+       (if auth-value-set?
+           (radicale-auth-configuration-htpasswd-filename auth-config)
+           auth-config))
+     (define password-file-dir
+       (if (maybe-value-set? password-file-val)
+           (dirname password-file-val)
+           "/etc/radicale"))
      (with-imported-modules '((guix build utils))
        #~(begin
            (use-modules (guix build utils))
            (let ((uid (passwd:uid (getpw "radicale")))
                  (gid (group:gid (getgr "radicale"))))
-             (mkdir-p "/var/lib/radicale/collections")
-             (chown "/var/lib/radicale" uid gid)
-             (chown "/var/lib/radicale/collections" uid gid)
-             (chmod "/var/lib/radicale" #o700)))))))
+             ;; Collections directory perms
+             (mkdir-p #$collections-dir)
+             (chown #$collections-parent-dir uid gid)
+             (chown #$collections-dir uid gid)
+             (chmod #$collections-parent-dir #o700)
+             ;; Password file perms
+             (when #$auth?
+               ;; In theory, the password file and thus this directory should already
+               ;; exist because the user has to make them by hand
+               (mkdir-p #$password-file-dir)
+               (chown #$password-file-dir uid gid)
+               (chmod #$password-file-dir #o700))))))))
 
 (define radicale-service-type
   (service-type
    (name 'radicale)
-   (description "Run radicale, a small CalDAV and CardDAV server.")
+   (description "Run Radicale, a small CalDAV and CardDAV server.")
    (extensions
     (list (service-extension shepherd-root-service-type radicale-shepherd-service)
           (service-extension account-service-type (const %radicale-accounts))
           (service-extension activation-service-type radicale-activation)))
    (default-value (radicale-configuration))))
 
+(define (generate-radicale-documentation)
+  (generate-documentation
+   `((radicale-configuration
+      ,radicale-configuration-fields
+      (auth     radicale-auth-configuration)
+      (encoding radicale-encoding-configuration)
+      (logging  radicale-logging-configuration)
+      (rights   radicale-rights-configuration)
+      (server   radicale-server-configuration)
+      (storage  radicale-storage-configuration))
+     (radicale-auth-configuration     ,radicale-auth-configuration-fields)
+     (radicale-encoding-configuration ,radicale-encoding-configuration-fields)
+     (radicale-logging-configuration  ,radicale-logging-configuration-fields)
+     (radicale-rights-configuration   ,radicale-rights-configuration-fields)
+     (radicale-server-configuration   ,radicale-server-configuration-fields)
+     (radicale-storage-configuration  ,radicale-storage-configuration-fields))
+   'radicale-configuration))
+
 ;;;
 ;;; Rspamd.
 ;;;
