@@ -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
@@ -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.
;;;