Subject: [bug#66557] [PATCH] home: services: Add goimapnotify service.

From: Nils Landt <[email-masked]>
Date: Sun, 15 Oct 2023 16:01:18 +0200

From: Nils Landt <[email-masked]>

* gnu/home/services/mail.scm: (home-goimapnotify-configuration,
home-goimapnotify-service-type, goimapnotify-account,
goimapnotify-tls-options): New variables.
(goimapnotify-format-field, goimapnotify-serialize-field,  goimapnotify-serialize-goimapnotify-tls-options): New procedures.
* doc/guix.texi (Mail Home Services): New node.
---
This patch adds a home service for generating goimapnotify JSON
configuration files.

I was unable to get generate-documentation working with
sub-documentation, so the configurations are documented separately.

 doc/guix.texi              | 209 +++++++++++++++++++++++++++------
 gnu/home/services/mail.scm | 234 ++++++++++++++++++++++++++++++++++++-
 2 files changed, 406 insertions(+), 37 deletions(-)

diff --git a/doc/guix.texi b/doc/guix.texi
index 3517c95251..fba13d4a43 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -44703,25 +44703,162 @@ Sound Home Services

 @node Mail Home Services
 @subsection Mail Home Services
-
+
 The @code{(gnu home services mail)} module provides services that help
 you set up the tools to work with emails in your home environment.
-
+
+@cindex goimapnotify
+@uref{https://gitlab.com/shackra/goimapnotify, goimapnotify} watches your
+mailbox(es) and executes a script on (new / deleted / updated) messages.
+
+Using @code{home-goimapnotify-configuration}, you can generate a config file
+for each account you want to watch (file name relative to @code{$HOME}), e.g.:
+
+@lisp
+(simple-service 'mail-imapnotify-config-examples
+                home-goimapnotify-service-type
+                (home-goimapnotify-configuration
+                  (accounts (list
+                              `(".config/goimapnotify/private-account.conf"
+                                ,(goimapnotify-account
+                                    (host "imap.example.org")
+                                    (port 993)
+                                    (tls #t)
+                                    (username "example")
+                                    (password-cmd "pass my-private-email-account")
+                                    (on-new-mail
+                                      (file-append mbsync "/bin/mbsync private-account"))
+                                    (on-new-mail-post
+                                      (file-append mu "/bin/mu index"))
+                                    (boxes '("INBOX"))))
+                              `(".config/goimapnotify/work-account.conf"
+                                ,(goimapnotify-account
+                                    (host "imap.work.example.org")
+                                    (port 993)
+                                    (tls #t)
+                                    (username "example")
+                                    (password "12345")
+                                    (on-new-mail
+                                      (file-append mbsync "/bin/mbsync work-account"))
+                                    (on-new-mail-post
+                                      "notify-send 'New mail'")
+                                    (boxes '("INBOX"
+                                            "On Call")))))))))
+@end lisp
+
+Note: to utilize the config files, you need to start a separate goimapnotify
+process for each one.  Continuing the example above:
+@code{goimapnotify -conf "$HOME/.config/goimapnotify/private-account.conf"} and
+@code{goimapnotify -conf "$HOME/.config/goimapnotify/work-account.conf"}.
+
+@c %start of fragment
+@deftp {Data Type} home-goimapnotify-configuration
+Available @code{home-goimapnotify-configuration} fields are:
+
+@table @asis
+@item @code{accounts} (default: @code{()}) (type: list-of-goimapnotify-accounts)
+List of accounts that goimapnotify should watch.  For each account, a
+separate configuration file will be generated.
+@end table
+
+@end deftp
+@c %end of fragment
+
+@c %start of fragment
+
+@deftp {Data Type} goimapnotify-account
+Available @code{goimapnotify-account} fields are:
+
+@table @asis
+@item @code{host} (type: maybe-string)
+Address of the IMAP server to connect to.
+
+@item @code{host-cmd} (type: maybe-string-or-file-like)
+An executable or script that retrieves your host from somewhere, we
+cannot pass arguments to this command from Stdin.
+
+@item @code{port} (type: maybe-integer)
+Port of the IMAP server to connect to.
+
+@item @code{tls} (type: maybe-boolean)
+
+Use TLS?
+
+@item @code{tls-options} (type: maybe-goimapnotify-tls-options)
+Option(s) for the TLS connection.  Currently, only one option is
+supported.
+
+@item @code{username} (type: maybe-string)
+Username for authentication.
+
+@item @code{username-cmd} (type: maybe-string-or-file-like)
+An executable or script that retrieves your username from
+somewhere, we cannot pass arguments to this command from Stdin.
+
+@item @code{password} (type: maybe-string)
+Password for authentication.
+
+@item @code{password-cmd} (type:
+ maybe-string-or-file-like)
+An executable or script that retrieves your password from somewhere, we
+cannot pass arguments to this command from Stdin.
+
+@item @code{xoauth2}
+(type: maybe-boolean)
+You can also use xoauth2 instead of password based authentication by
+setting the xoauth2 option to true and the output of a tool which can
+provide xoauth2 encoded tokens in passwordCmd.  Examples:
+@uref{https://github.com/google/oauth2l,Google oauth2l} or
+@uref{https://github.com/harishkrupo/oauth2ms,xoauth2 fetcher for O36
+5}.
+
+@item @code{on-new-mail} (type: maybe-string-or-file-like)
+An executable or script to run when new mail has arrived.
+
+@item @code{on-new-mail-post} (type: maybe-string-or-file-like)
+An executable or script to run after onNewMail has ran.
+
+@item @code{wait} (type: maybe-integer)
+The delay in seconds before the mail syncing is triggered.
+
+@item @code{boxes} (type: maybe-list-of-strings)
+Mailboxes to watch.
+
+@end table
+
+@end deftp
+
+@c %end of fragment
+
+@c %start of fragment
+
+@deftp {Data Type} goimapnotify-tls-options
+Available @code{goimapnotify-tls-options} fields are:
+
+@table @asis
+@item @code{reject-unauthorized} (type: maybe-boolean)
+Skip verifying CA server identify?
+
+@end table
+
+@end deftp
+@c %end of fragment
+
 @cindex msmtp
 @uref{https://marlam.de/msmtp, MSMTP} is a @acronym{SMTP, Simple Mail
 Transfer Protocol} client.  It sends mail to a predefined SMTP server
 that takes care of proper delivery.
-
+
 The service reference is given below.
-
+
 @defvar home-msmtp-service-type
 This is the service type for @command{msmtp}.  Its value must be a
 @code{home-msmtp-configuration}, as shown below.  It provides the
 @file{~/.config/msmtp/config} file.
-
+
 As an example, here is how you would configure @code{msmtp} for a single
 account:
-
+
 @lisp
 (service home-msmtp-service-type
          (home-msmtp-configuration
@@ -44739,101 +44876,101 @@ Mail Home Services
 @end defvar

 @c %start of fragment
-
+
 @deftp {Data Type} home-msmtp-configuration
 Available @code{home-msmtp-configuration} fields are:
-
+
 @table @asis
 @item @code{defaults} (type: msmtp-configuration)
 The configuration that will be set as default for all accounts.
-
+
 @item @code{accounts} (default: @code{'()}) (type: list-of-msmtp-accounts)
 A list of @code{msmtp-account} records which contain information about
 all your accounts.
-
+
 @item @code{default-account} (type: maybe-string)
 Set the default account.
-
+
 @item @code{extra-content} (default: @code{""}) (type: string)
 Extra content appended as-is to the configuration file.  Run
 @command{man msmtp} for more information about the configuration file
 format.
-
+
 @end table
-
+
 @end deftp
-
+
 @c %end of fragment
-
+
 @c %start of fragment
-
+
 @deftp {Data Type} msmtp-account
 Available @code{msmtp-account} fields are:
-
+
 @table @asis
 @item @code{name} (type: string)
 The unique name of the account.
-
+
 @item @code{configuration} (type: msmtp-configuration)
 The configuration for this given account.
-
+
 @end table
-
+
 @end deftp
-
+
 @c %end of fragment

 @c %start of fragment
-
+
 @deftp {Data Type} msmtp-configuration
 Available @code{msmtp-configuration} fields are:
-
+
 @table @asis
 @item @code{auth?} (type: maybe-boolean)
 Enable or disable authentication.
-
+
 @item @code{tls?} (type: maybe-boolean)
 Enable or disable TLS (also known as SSL) for secured connections.
-
+
 @item @code{tls-starttls?} (type: maybe-boolean)
 Choose the TLS variant: start TLS from within the session (‘on’,
 default), or tunnel the session through TLS (‘off’).
-
+
 @item @code{tls-trust-file} (type: maybe-string)
 Activate server certificate verification using a list of trusted
 Certification Authorities (CAs).
-
+
 @item @code{log-file} (type: maybe-string)
 Enable logging to the specified file.  An empty argument disables
 logging.  The file name ‘-’ directs the log information to standard
 output.
-
+
 @item @code{host} (type: maybe-string)
 The SMTP server to send the mail to.
-
+
 @item @code{port} (type: maybe-integer)
 The port that the SMTP server listens on.  The default is 25 ("smtp"),
 unless TLS without STARTTLS is used, in which case it is 465 ("smtps").
-
+
 @item @code{user} (type: maybe-string)
 Set the user name for authentication.
-
+
 @item @code{from} (type: maybe-string)
 Set the envelope-from address.
-
+
 @item @code{password-eval} (type: maybe-string)
 Set the password for authentication to the output (stdout) of the
 command cmd.
-
+
 @item @code{extra-content} (default: @code{""}) (type: string)
 Extra content appended as-is to the configuration block.  Run
 @command{man msmtp} for more information about the configuration file
 format.
-
+
 @end table
-
+
 @end deftp
-
+
 @c %end of fragment

 @node Messaging Home Services
diff --git a/gnu/home/services/mail.scm b/gnu/home/services/mail.scm
index 5445c82c67..923867ca66 100644
--- a/gnu/home/services/mail.scm
+++ b/gnu/home/services/mail.scm
@@ -18,15 +18,44 @@

 (define-module (gnu home services mail)
   #:use-module (guix gexp)
+  #:use-module (guix records)
   #:use-module (gnu services)
   #:use-module (gnu services configuration)
   #:use-module (gnu home services)
   #:use-module (gnu home services shepherd)
+  #:use-module (gnu home services utils)
   #:use-module (gnu packages mail)
+  #:use-module (gnu packages guile)
+  #:use-module (ice-9 match)
   #:use-module (ice-9 string-fun)
   #:use-module (srfi srfi-1)
   #:use-module (srfi srfi-26)
-  #:export (home-msmtp-configuration
+  #:export (home-goimapnotify-configuration
+            home-goimapnotify-configuration-fields
+            home-goimapnotify-configuration?
+            home-goimapnotify-configuration-accounts
+            home-goimapnotify-service-type
+            goimapnotify-account
+            goimapnotify-account-fields
+            goimapnotify-account-host
+            goimapnotify-account-host-cmd
+            goimapnotify-account-port
+            goimapnotify-account-tls
+            goimapnotify-account-tls-options
+            goimapnotify-account-username
+            goimapnotify-account-username-cmd
+            goimapnotify-account-password
+            goimapnotify-account-password-cmd
+            goimapnotify-account-xoauth2
+            goimapnotify-account-on-new-mail
+            goimapnotify-account-on-new-mail-post
+            goimapnotify-account-wait
+            goimapnotify-account-boxes
+            goimapnotify-tls-options
+            goimapnotify-tls-options-fields
+            goimapnotify-tls-options-reject-unauthorized
+
+            home-msmtp-configuration
             home-msmtp-configuration?
             home-msmtp-configuration-defaults
             home-msmtp-configuration-accounts
@@ -220,3 +249,206 @@ (define home-msmtp-service-type
                 (description "Configure msmtp, a simple
 @acronym{SMTP, Simple Mail Transfer Protocol} client that can relay email
 to SMTP servers.")))
+
+; Configuration for goimapnotify from (gnu packages mail)
+
+(define-maybe string)
+(define-maybe integer)
+(define-maybe boolean)
+(define-maybe list-of-strings)
+(define-maybe string-or-file-like)
+
+(define (string-or-file-like? value)
+  (or (string? value)
+      (file-like? value)))
+
+(define (goimapnotify-format-field field-name)
+  (object->camel-case-string field-name))
+
+(define (goimapnotify-serialize-field field-name value)
+  "This is converted to JSON later, so we don't return a string here"
+  #~(#$(goimapnotify-format-field field-name) . #$value))
+
+(define (goimapnotify-serialize-string-or-file-like field-name value)
+  (goimapnotify-serialize-string field-name value))
+
+(define (goimapnotify-maybe-serialize field-name value serialization-function)
+  (if (maybe-value-set? value)
+    (serialization-function field-name value)
+    ""))
+
+(define (goimapnotify-serialize-maybe-string-or-file-like field-name value)
+ (goimapnotify-maybe-serialize field-name value
+                               goimapnotify-serialize-string-or-file-like))
+
+(define goimapnotify-serialize-string goimapnotify-serialize-field)
+(define (goimapnotify-serialize-maybe-string field-name value)
+ (goimapnotify-maybe-serialize field-name value goimapnotify-serialize-string))
+
+(define (goimapnotify-serialize-maybe-integer field-name value)
+ (goimapnotify-maybe-serialize field-name value goimapnotify-serialize-integer))
+(define goimapnotify-serialize-integer goimapnotify-serialize-field)
+
+(define (goimapnotify-serialize-maybe-boolean field-name value)
+  (goimapnotify-maybe-serialize field-name value goimapnotify-serialize-boolean))
+(define goimapnotify-serialize-boolean goimapnotify-serialize-field)
+
+(define (goimapnotify-serialize-maybe-list-of-strings field-name value)
+  (goimapnotify-maybe-serialize field-name value goimapnotify-serialize-list-of-strings))
+(define (goimapnotify-serialize-list-of-strings field-name value)
+  (goimapnotify-serialize-field field-name (list->array 1 value)))
+
+(define (goimapnotify-serialize-maybe-goimapnotify-tls-options field-name config)
+  (goimapnotify-maybe-serialize field-name config
+                                goimapnotify-serialize-goimapnotify-tls-options))
+
+(define (goimapnotify-serialize-goimapnotify-tls-options field-name config)
+  (goimapnotify-serialize-field
+    field-name
+    (prepare-configuration-for-json config goimapnotify-tls-options-fields)))
+
+(define (prepare-configuration-for-json config fields)
+  "Convert the configuration to the format expected by guile-json.
+  Unset maybe-values do not appear in the configuration file."
+  (filter
+    (lambda (val)
+      (not (unspecified? val)))
+    (map
+      (lambda (field)
+        (let ((value ((configuration-field-getter field) config)))
+          (if (maybe-value-set? value)
+            ((configuration-field-serializer field)
+             (configuration-field-name field)
+             value)
+            *unspecified*)))
+      fields)))
+
+(define-configuration goimapnotify-tls-options
+                      (reject-unauthorized
+                        (maybe-boolean)
+                        "Skip verifying CA server identify?")
+                      (prefix goimapnotify-))
+
+(define-maybe goimapnotify-tls-options)
+
+; See https://gitlab.com/shackra/goimapnotify/-/blob/master/config.go?ref_type=heads#L46-62
+(define-configuration goimapnotify-account
+                      (host
+                        (maybe-string)
+                        "Address of the IMAP server to connect to.")
+                      (host-cmd
+                        (maybe-string-or-file-like)
+                        "An executable or script that retrieves your host from somewhere,
+                        we cannot pass arguments to this command from Stdin.")
+                      (port
+                        (maybe-integer)
+                        "Port of the IMAP server to connect to.")
+                      (tls
+                        (maybe-boolean)
+                        "Use TLS?")
+                      (tls-options
+                        (maybe-goimapnotify-tls-options)
+                        "Option(s) for the TLS connection. Currently, only one option is
+                        supported.")
+                      (username
+                        (maybe-string)
+                        "Username for authentication.")
+                      (username-cmd
+                        (maybe-string-or-file-like)
+                        "An executable or script that retrieves your username from
+                        somewhere, we cannot pass arguments to this command from Stdin.")
+                      (password
+                        (maybe-string)
+                        "Password for authentication.")
+                      (password-cmd
+                        (maybe-string-or-file-like)
+                        "An executable or script that retrieves your password from
+                        somewhere, we cannot pass arguments to this command from Stdin.")
+                      (xoauth2
+                        (maybe-boolean)
+                        "You can also use xoauth2 instead of password based authentication
+                        by setting the xoauth2 option to true and the output of a tool
+                        which can provide xoauth2 encoded tokens in passwordCmd.
+                        Examples: @url{https://github.com/google/oauth2l, Google oauth2l}
+                        or
+                        @url{https://github.com/harishkrupo/oauth2ms, xoauth2 fetcher for O365}.")
+                      (on-new-mail
+                        (maybe-string-or-file-like)
+                        "An executable or script to run when new mail has arrived.")
+                      (on-new-mail-post
+                        (maybe-string-or-file-like)
+                        "An executable or script to run after onNewMail has ran.")
+                      (wait
+                        (maybe-integer)
+                        "The delay in seconds before the mail syncing is triggered.")
+                      (boxes
+                        (maybe-list-of-strings)
+                        "Mailboxes to watch.")
+                      (prefix goimapnotify-))
+
+(define (list-of-goimapnotify-accounts? lst)
+  "List is in the form of '((file-name file-like))"
+  (every (lambda (element)
+           (match element
+                  ((string ($ <goimapnotify-account>))
+                   #t)
+                  (_ #f)))
+         lst))
+
+(define-configuration/no-serialization home-goimapnotify-configuration
+                                       (accounts
+                                         (list-of-goimapnotify-accounts '())
+                                         "List of accounts that goimapnotify should watch.
+                                         For each account, a separate configuration file
+                                         will be generated."))
+
+(define (home-goimapnotify-extension old-config extensions)
+  (match-record old-config <home-goimapnotify-configuration>
+                (accounts)
+                (home-goimapnotify-configuration
+                  (inherit old-config)
+                  (accounts (append accounts
+                                    (append-map
+                                      home-goimapnotify-configuration-accounts
+                                      extensions))))))
+
+(define (goimapnotify-files config)
+  (define* (account->json account-config-and-path)
+    (match
+      account-config-and-path
+      ((path account-config)
+       (let ((prepared-config
+               (prepare-configuration-for-json
+                 account-config
+                 goimapnotify-account-fields)))
+         `((,path
+            ,(computed-file
+               (string-append
+                 "mail-imapnotify-config-"
+                 (goimapnotify-account-host account-config))
+               (with-extensions (list guile-json-4)
+                   #~(begin
+                       (use-modules (json builder))
+
+                       (with-output-to-file #$output
+                         (lambda ()
+                           (scm->json '(#$@prepared-config)
+                                      #:pretty #t))))))))))))
+
+  (match-record config <home-goimapnotify-configuration>
+                (accounts)
+                (append-map
+                  (cut account->json <>)
+                  accounts)))
+
+(define home-goimapnotify-service-type
+  (service-type (name 'home-goimapnotify-service)
+                (extensions
+                  (list (service-extension
+                          home-files-service-type
+                          goimapnotify-files)))
+                (compose identity)
+                (extend home-goimapnotify-extension)
+                (default-value (home-goimapnotify-configuration))
+                (description "Configure goimapnotify to execute scripts on IMAP
+                             mailbox changes.")))

base-commit: d2923babf3ac44cb6faa88317f77c98f3016820d
--
2.41.0