From patchwork Tue Mar 12 01:14:06 2024 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: Juliana Sims X-Patchwork-Id: 61677 Return-Path: X-Original-To: patchwork@mira.cbaines.net Delivered-To: patchwork@mira.cbaines.net Received: by mira.cbaines.net (Postfix, from userid 113) id 807BE27BBEA; Tue, 12 Mar 2024 01:23:54 +0000 (GMT) X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on mira.cbaines.net X-Spam-Level: X-Spam-Status: No, score=-2.7 required=5.0 tests=BAYES_00,DKIM_INVALID, DKIM_SIGNED,MAILING_LIST_MULTI,SPF_HELO_PASS,URIBL_BLOCKED autolearn=ham autolearn_force=no version=3.4.6 Received: from lists.gnu.org (lists.gnu.org [209.51.188.17]) by mira.cbaines.net (Postfix) with ESMTPS id 75B2227BBE2 for ; Tue, 12 Mar 2024 01:23:50 +0000 (GMT) Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1rjqra-0000ib-Kx; Mon, 11 Mar 2024 21:23:30 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1rjqrY-0000i7-6G for guix-patches@gnu.org; Mon, 11 Mar 2024 21:23:28 -0400 Received: from debbugs.gnu.org ([2001:470:142:5::43]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_128_GCM_SHA256:128) (Exim 4.90_1) (envelope-from ) id 1rjqrX-00069Z-UH for guix-patches@gnu.org; Mon, 11 Mar 2024 21:23:27 -0400 Received: from Debian-debbugs by debbugs.gnu.org with local (Exim 4.84_2) (envelope-from ) id 1rjqs5-0007NG-O8 for guix-patches@gnu.org; Mon, 11 Mar 2024 21:24:01 -0400 X-Loop: help-debbugs@gnu.org Subject: [bug#69719] [PATCH v2] services: radicale: Use define-configuration. Resent-From: Juliana Sims Original-Sender: "Debbugs-submit" Resent-CC: guix-patches@gnu.org Resent-Date: Tue, 12 Mar 2024 01:24:01 +0000 Resent-Message-ID: Resent-Sender: help-debbugs@gnu.org X-GNU-PR-Message: followup 69719 X-GNU-PR-Package: guix-patches X-GNU-PR-Keywords: patch To: liliana.prikler@gmail.com Cc: Juliana Sims , 69719@debbugs.gnu.org Received: via spool by 69719-submit@debbugs.gnu.org id=B69719.171020662028311 (code B ref 69719); Tue, 12 Mar 2024 01:24:01 +0000 Received: (at 69719) by debbugs.gnu.org; 12 Mar 2024 01:23:40 +0000 Received: from localhost ([127.0.0.1]:41499 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1rjqri-0007MY-Kf for submit@debbugs.gnu.org; Mon, 11 Mar 2024 21:23:40 -0400 Received: from out-183.mta1.migadu.com ([95.215.58.183]:23329) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1rjqre-0007M3-4z for 69719@debbugs.gnu.org; Mon, 11 Mar 2024 21:23:36 -0400 X-Report-Abuse: Please report any abuse attempt to abuse@migadu.com and include these headers. DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=incana.org; s=key1; t=1710206573; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=zdJVTxhmc8UNK6v5d0wDfhrYUB6CW3T6QWdlDpUh/QE=; b=3KYOyyLELBUHC4MyGidJhztFeE4TRMmvmP6254gEoxIC3Op0hHbk+S97+RKBE5XyL+94WI N0+qxWhXw/Vp42OEKVv6KmO74A4Plo69VI9g7+TsPJ2aiwDVc5p/P1l541E3sLgrz0hIrd jI2Wu5bL6toDS1GjF3KAJlREne0SHMuyQxfgSRBIMGfM8Q+SViGLmEewWnvPF3lslNJEnK YLQZFNSnOTF/vcrywakD9a/tvhFiQlsci9EUxNw/4wFwf/Tw/1q0i5CXNbWmIpYCOQOzyE S4FCF5WlXs0lrvb4wyFhvvw5BW+YcSULlEiBs8l38ARbRkhYlZNSu5kNnXBGQQ== Date: Mon, 11 Mar 2024 21:14:06 -0400 Message-ID: <31d0eb8638da64260378c9756f00bd8a062b04ca.1710206046.git.juli@incana.org> In-Reply-To: <525f302593642aba838cb3ee966482794302cffb.camel@gmail.com> References: <525f302593642aba838cb3ee966482794302cffb.camel@gmail.com> MIME-Version: 1.0 X-Migadu-Flow: FLOW_OUT X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list X-BeenThere: guix-patches@gnu.org List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-to: Juliana Sims X-ACL-Warn: , Juliana Sims via Guix-patches X-Patchwork-Original-From: Juliana Sims via Guix-patches via From: Juliana Sims Errors-To: guix-patches-bounces+patchwork=mira.cbaines.net@gnu.org Sender: guix-patches-bounces+patchwork=mira.cbaines.net@gnu.org X-getmail-retrieved-from-mailbox: Patches 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. > 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. > 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(-) base-commit: 4003c60abf7a6e59e47cc2deb9eef2f104ebb994 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 ;;; Copyright © 2023 Thomas Ieong ;;; Copyright © 2023 Saku Laesvuori +;;; Copyright © 2024 Juliana Sims ;;; ;;; 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 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-configuration radicale-server-configuration + (hosts + maybe-comma-separated-ip-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))) + (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 headers-file? file-like?) +(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 headers-file) +(define-maybe logging-config) +(define-maybe rights-config) +(define-maybe server-config) +(define-maybe storage-config) + +(define ((serialize-radicale-section fields) name cfg) + #~(format #f "[~a]\n~a\n" '#$name #$(serialize-configuration cfg fields))) + +(define serialize-auth-config + (serialize-radicale-section radicale-auth-configuration-fields)) +(define serialize-encoding-config + (serialize-radicale-section radicale-encoding-configuration-fields)) +(define serialize-logging-config + (serialize-radicale-section radicale-logging-configuration-fields)) +(define serialize-rights-config + (serialize-radicale-section radicale-rights-configuration-fields)) +(define serialize-server-config + (serialize-radicale-section radicale-server-configuration-fields)) +(define serialize-storage-config + (serialize-radicale-section 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 + (file-like radicale) + "Package that provides @command{radicale}.") + (auth + maybe-auth-config + "Configuration for auth-related variables.") + (encoding + maybe-encoding-config + "Configuration for encoding-related variables.") + (headers-file + maybe-headers-file + "Custom HTTP headers." + (serializer + (lambda (field-name value) + #~(begin + (use-modules (ice-9 rdelim)) + (format #f "[headers]\n~a\n\n" + (with-input-from-file #$value read-string)))))) + (logging + maybe-logging-config + "Configuration for logging-related variables.") + (rights + maybe-rights-config + "Configuration for rights-related variables.") + (server + maybe-server-config + "Configuration for server-related variables. Ignored if WSGI is used.") + (storage + maybe-storage-config + "Configuration for storage-related variables.") + (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 +2208,88 @@ (define %radicale-accounts (home-directory "/var/empty") (shell (file-append shadow "/sbin/nologin"))))) -(define radicale-shepherd-service - (match-lambda - (($ 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 - (($ package config-file) + (($ _ 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 + (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))))))) + (let ((user (getpwnam "radicale"))) + ;; Collections directory perms + (mkdir-p/perms #$collections-dir user #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/perms #$password-file-dir user #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. ;;;