[PATCH] services: radicale: Use define-configuration.

  • Open
  • quality assurance status badge
Details
2 participants
  • Juliana Sims
  • Liliana Marie Prikler
Owner
unassigned
Submitted by
Juliana Sims
Severity
normal
J
J
Juliana Sims wrote on 11 Mar 03:04 +0100
(address . guix-patches@gnu.org)(name . Juliana Sims)(address . juli@incana.org)
17984bb7ca32629fed475db7e782acd90e538077.1710122683.git.juli@incana.org
Hello,

This beast of a patch ports the radicale system service to the
define-configuration framework for writing configuration files, instead of
simply accepting a handwritten file. It also includes documentation updates
(generated with the amazing generate-documentation procedure). The changelog
below should cover all changes to all exported symbols, but I'll give an
overview of the semantics of what I did and why for the benefit of reviews.

Essentially, I took each section of the Radicale configuration file[1] and made
it its own define-configuration object (henceforth "configuration"). I made each
field using these configurations a maybe type so that the serializer for the
configurations could write out the section header iff it was needed. I then made
each field of each configuration a maybe type as well so that the default
Radicale values are used unless the user overrides them. The logic here is that
users will be checking upstream documentation while configuring this service,
and we should match upstream's defaults as closely as possible. In the end, the
only non-maybe field is the Radicale package itself. This has the mildly annoying
effect that a default radicale-service-type produces an empty file on disk as
opposed to no file, but the alternative is to query each field individually in a
serialization guard which would complicate logic and therefore maintainability.
Simplifying maintainibility by providing users with automated checking for
configuration values is the primary goal here, and the separate configurations
were chosen specifically to avoid the increased cognitive overhead of
serialization guards, so this tradeoff feels worth it to me. I also modified the
radicale-activation function to query the configurations for the paths of the
directories it creates rather than hardcoding the default values.

I have tested this code fairly thoroughly, generating system vms that used at
least one field of each distinct type, including each section configuration.
I've also verified that radicale-activation works as expected and the
configuration file is correctly passed to the shepherd service. That said,
there's still a good chance I missed something because this is a lot of code.

There is one important thing to note about these changes for existing users.
Besides removing the option to pass a configuration file verbatim, the defaults
of a radicale-service-type have changed to match upstream. Previously the
defaults were not in line with upstream. This means users with
radicale-service-type and no custom configuration will see silent breakage. I
considered adding a news item to alert folks to this, but wasn't sure of the
protocol on that so I didn't. I think introducing these breakages is reasonable
because the defaults now match upstream and standard filesystem patterns -- /var
is not longer being used for static configuration files, for example.

I hope folks find this useful. I'm excited to deploy this when it's merged :)

Thanks,
Juli


* doc/guix.texi (radicale-configuration): Update documentation to reflect new
configuration, add new symbols.
* gnu/services/mail.scm (%default-radicale-config-file): Delete.
(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?): New symbol.
(radicale-configuration, radicale-configuration?): Use define-configuration.
(radicale-activation, radicale-shepherd-service): Update for new
configuration format.
(radicale-activation): Use user-defined values for service files.
(radicale-service-type): Capitalize "Radicale" in description.
---
doc/guix.texi | 194 +++++++++++++++++++++-
gnu/services/mail.scm | 368 ++++++++++++++++++++++++++++++++++++++----
2 files changed, 520 insertions(+), 42 deletions(-)

Toggle diff (446 lines)
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
This message was truncated. Download the full message here.
L
L
Liliana Marie Prikler wrote on 11 Mar 06:20 +0100
525f302593642aba838cb3ee966482794302cffb.camel@gmail.com
Hi Juliana,

Am Sonntag, dem 10.03.2024 um 22:04 -0400 schrieb Juliana Sims:
Toggle quote (12 lines)
> [...]
>
> +(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))
You should not need to quote symbols here.

Toggle quote (45 lines)
> +(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))
Note the mismatch between documented and stored type: Your type is
"maybe-symbol", but you store strings. Instead, you can a) move the
the logic to uglify the name to radicale-serialize-symbol or define a
radicale-serialize-uglified-symbol variant if you need the normal
radicale-serialize-symbol, or b) document that you actually take
strings. Of course, you can also think of other solutions.

Hint: I'd personally prefer solution a).


Cheers
J
J
Juliana Sims wrote on 12 Mar 02:14 +0100
[PATCH v2] services: radicale: Use define-configuration.
(address . liliana.prikler@gmail.com)
31d0eb8638da64260378c9756f00bd8a062b04ca.1710206046.git.juli@incana.org
Hi,

First and foremost, thanks for such a quick review!

Last night as I was going to sleep I thought to myself, "I feel like there's
something I forgot to check in that patch." And this morning I woke up to the
thought, "I forgot to test serializing headers!" Importantly, I'd forgotten to
write out the section header during serialization. Further, the text-config type
I was using expects a list rather than individual file-like objects. I've
changed the code so the headers-file field now accepts a single file-like
object.

However, in figuring out how best to do this, I noticed that the dovecot
configuration uses fields with the file-like type, but exclusively for the
dovecot package. The documentation refers to these fields as accepting a
package, and the default value is the dovecot package. I assumed the code
predates packages as a distinct type so I ran a git blame -- and it turns out
that type is used so inferior packages can be used as packages. I've changed the
type of the package field to reflect this new information, though I've left the
documentation referring to the correct type.

Speaking of documentation and correct types, I changed the documentation for
this code to refer to the expected type without the maybe even if that's not the
name used in the code itself. I also added default values that match the effect
of not providing anything to the field and made a note that the defaults all
match the upstream documentation. That way users know what they're getting
without having to check outside the Guix docs, and they don't need to know about
implementation details.

While doing that, I decided that I should actually ensure the list of IP
addresses is a list of IP addresses, so a defined a predicate to check for that
and setup a new type to use it.

I also noticed that my section serializers could just use the field name as the
section header instead of passing that manually. I've modified that code reflect
this insight.

As a last note not directly related to this review, I found out about
mkdir-p/perms and rewrote radicale-activation to use it. That's a handy little
tidbit that probably ought to be documented -- although it seems it can
introduce some security vulnerability or other which may be why it's not.

Toggle quote (2 lines)
> You should not need to quote symbols here.

I agree. Unfortunately, the experience of running the code does not ;) Someone
in the Matrix also suggested this situation was normal for g-expressions. This
may be a bug, but that's outside of knowledge domain.

Toggle quote (9 lines)
> Note the mismatch between documented and stored type: Your type is
> "maybe-symbol", but you store strings. Instead, you can a) move the the logic
> to uglify the name to radicale-serialize-symbol or define a
> radicale-serialize-uglified-symbol variant if you need the normal
> radicale-serialize-symbol, or b) document that you actually take strings. Of
> course, you can also think of other solutions.
>
> Hint: I'd personally prefer solution a).

I've decided to convert the uglified symbols back into actual symbols before
returning them from the sanitizer. This seems to resolve the issue I was having
with serializers not being called. For some reason both of the other options you
propose bother me for largely stylistic reasons. I don't want to accept strings
because arbitrary strings aren't acceptable; only a concrete set of values. I
associate those semantics with symbols. Similarly, defining a separate
serializer just for these symbols feels wrong because the other symbols are
similarly not arbitrary and thus there is no meaningful semantic distinction. I
would define those other symbols (the encoding fields) as delimited if I knew
what the valid options for them were, but Radicale doesn't document the
available options.

I've run another set of tests on this code to make sure all of it works as
expected -- including the HTTP headers this time :)

Thanks again,
Juli

* doc/guix.texi (radicale-configuration): Update documentation to reflect new
configuration, add new symbols.
* gnu/services/mail.scm (%default-radicale-config-file): Delete.
(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?): New symbol.
(radicale-configuration, radicale-configuration?): Use define-configuration.
(radicale-activation, radicale-shepherd-service): Update for new
configuration format.
(radicale-activation): Use user-defined values for service files.
(radicale-service-type): Capitalize "Radicale" in description.

Change-Id: Ic88b8ff2750e3d658f6c7cee02d33417aa8ee6d2
---
doc/guix.texi | 188 ++++++++++++++++++++-
gnu/services/mail.scm | 368 +++++++++++++++++++++++++++++++++++++-----
2 files changed, 511 insertions(+), 45 deletions(-)

Toggle diff (399 lines)
diff --git a/doc/guix.texi b/doc/guix.texi
index 858d5751bfe..3c6920ee569 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -28248,23 +28248,195 @@ Mail Services
@cindex CardDAV
@defvar radicale-service-type
-This is the type of the @uref{https://radicale.org, Radicale} CalDAV/CardDAV
-server whose value should be a @code{radicale-configuration}.
+This is the type of the @uref{https://radicale.org, Radicale}
+CalDAV/CardDAV server whose value should be a
+@code{radicale-configuration}. The default configuration matches the
+@uref{https://radicale.org/v3.html#configuration, upstream
+documentation}.
@end defvar
@deftp {Data Type} radicale-configuration
Data type representing the configuration of @command{radicale}.
+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} (default: @code{'()}) (type: radicale-auth-configuration)
+Configuration for auth-related variables.
+
+@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} (default: @code{'none}) (type: 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} (default: @code{"/etc/radicale/users"}) (type: file-name)
+Path to the htpasswd file. Use htpasswd or similar to generate this
+file.
+
+@item @code{htpasswd-encryption} (default: @code{'md5}) (type: symbol)
+Encryption method used in the htpasswd file. Options are @code{plain},
+@code{bcrypt}, and @code{md5}.
+
+@item @code{delay} (default: @code{1}) (type: non-negative-integer)
+Average delay after failed login attempts in seconds.
+
+@item @code{realm} (default: @code{"Radicale - Password Required"}) (type: string)
+Message displayed in the client when a password is needed.
+
+@end table
+
+@end deftp
+
+@item @code{encoding} (default: @code{'()}) (type: radicale-encoding-configuration)
+Configuration for encoding-related variables.
+
+@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} (default: @code{'utf-8}) (type: symbol)
+Encoding for responding requests.
+
+@item @code{stock} (default: @code{'utf-8}) (type: symbol)
+Encoding for storing local collections.
+
+@end table
+
+@end deftp
+
+@item @code{headers-file} (default: none) (type: file-like)
+Custom HTTP headers.
+
+@item @code{logging} (default: @code{'()}) (type: radicale-logging-configuration)
+Configuration for logging-related variables.
+
+@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} (default: @code{'warning}) (type: symbol)
+Set the logging level. One of @code{debug}, @code{info},
+@code{warning}, @code{error}, or @code{critical}.
+
+@item @code{mask-passwords?} (default: @code{#t}) (type: boolean)
+Whether to include passwords in logs.
+
+@end table
+
+@end deftp
+
+@item @code{rights} (default: @code{'()}) (type: radicale-rights-configuration)
+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} (default: @code{'owner-only}) (type: 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} (default: @code{""}) (type: file-name)
+File for the rights backend @code{from-file}.
+
+@end table
+
+@end deftp
+
+@item @code{server} (default: @code{'()}) (type: radicale-server-configuration)
+Configuration for server-related variables. Ignored if WSGI is used.
+
+@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} (default: @code{(list "localhost:5232")}) (type: list-of-ip-addresses)
+List of IP addresses that the server will bind to.
+
+@item @code{max-connections} (default: @code{8}) (type: non-negative-integer)
+Maximum number of parallel connections. Set to 0 to disable the limit.
+
+@item @code{max-content-length} (default: @code{100000000}) (type: non-negative-integer)
+Maximum size of the request body in bytes.
+
+@item @code{timeout} (default: @code{30}) (type: non-negative-integer)
+Socket timeout in seconds.
+
+@item @code{ssl?} (default: @code{#f}) (type: boolean)
+Whether to enable transport layer encryption.
+
+@item @code{certificate} (default: @code{"/etc/ssl/radicale.cert.pem"}) (type: file-name)
+Path of the SSL certificate.
+
+@item @code{key} (default: @code{"/etc/ssl/radicale.key.pem"}) (type: file-name)
+Path to the private key for SSL. Only effective if @code{ssl?} is
+@code{#t}.
+
+@item @code{certificate-authority} (default: @code{""}) (type: 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} (default: @code{'()}) (type: radicale-storage-configuration)
+Configuration for storage-related variables.
+
+@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} (default: @code{'multifilesystem}) (type: symbol)
+Backend used to store data. Options are @code{multifilesystem} and
+@code{multifilesystem-nolock}.
+
+@item @code{filesystem-folder} (default: @code{"/var/lib/radicale/collections"}) (type: file-name)
+Folder for storing local collections. Created if not present.
+
+@item @code{max-sync-token-age} (default: @code{2592000}) (type: non-negative-integer)
+Delete sync-tokens that are older than the specified time in seconds.
+
+@item @code{hook} (default: @code{""}) (type: string)
+Command run after changes to storage.
@end table
+
+@end deftp
+
+@item @code{web-interface?} (default: @code{#t}) (type: boolean)
+Whether to use Radicale's built-in web interface.
+
+@end table
+
@end deftp
@subsubheading Rspamd Service
diff --git a/gnu/services/mail.scm b/gnu/services/mail.scm
index afe1bb60169..9b4bfd360fc 100644
--- a/gnu/services/mail.scm
+++ b/gnu/services/mail.scm
@@ -7,6 +7,7 @@
;;; Copyright © 2020 Jonathan Brielmaier <jonathan.brielmaier@web.de>
;;; Copyright © 2023 Thomas Ieong <th.ieong@free.fr>
;;; Copyright © 2023 Saku Laesvuori <saku@laesvuori.fi>
+;;; Copyright © 2024 Juliana Sims <juli@incana.org>
;;;
;;; This file is part of GNU Guix.
;;;
@@ -38,10 +39,12 @@ (define-module (gnu services mail)
#:use-module (gnu packages dav)
#:use-module (gnu packages tls)
#:use-module (guix deprecation)
+ #:use-module ((guix diagnostics) #:select (source-properties->location))
#: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 +82,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 +1943,258 @@ (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 (comma-separated-ip-list? lst)
+ (every (lambda (s)
+ (or (string-prefix? "localhost" s)
+ ((@@ (gnu services vpn) ipv4-address?) s)
+ ((@@ (gnu services vpn) ipv6-address?) s)))
+ lst))
-[server]
-hosts = localhost:5232"))
+(define-maybe boolean (prefix radicale-))
+(define-maybe comma-separated-ip-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-))
+
+;; 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-ip-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)
+ (string->symbol (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)))
+ (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)))
+ (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)))
+ (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)))
+ (file
+ maybe-file-name
+ "File for the rights backend @code{from-file}.")
+ (prefix radicale-))
+
+(define-confi
This message was truncated. Download the full message here.
?
Your comment

Commenting via the web interface is currently disabled.

To comment on this conversation send an email to 69719@debbugs.gnu.org

To respond to this issue using the mumi CLI, first switch to it
mumi current 69719
Then, you may apply the latest patchset in this issue (with sign off)
mumi am -- -s
Or, compose a reply to this issue
mumi compose
Or, send patches to this issue
mumi send-email *.patch