diff mbox series

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

Message ID c9d8798670448a18779e3c24b9b8a88902942936.1697378478.git.nils@landt.email
State New
Headers show
Series [bug#66557] home: services: Add goimapnotify service. | expand

Commit Message

Nils Landt Oct. 15, 2023, 2:01 p.m. UTC
From: Nils Landt <nils.landt@nlsoft.de>

* 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(-)


base-commit: d2923babf3ac44cb6faa88317f77c98f3016820d
--
2.41.0

Comments

Ricardo Wurmus Nov. 2, 2023, 8:55 a.m. UTC | #1
Hi Nils,

thank you for this service!  I had been looking for a goimapnotify
service just a few days ago, so this will definitely come in handy.

I don’t have the time for a comprehensive review, but I’ll comment on a
few things that stuck out to me.

> +                                    (password-cmd "pass my-private-email-account")
> +                                    (on-new-mail
> +                                      (file-append mbsync "/bin/mbsync private-account"))

It seems wrong to me to compose a command as a single string.  Usually
we separate the executable from the arguments.  People who want to run
the command in a shell can still do that by using “/bin/sh” “-c”
“command string”.

So I think it would be better to let these fields accept command lists.
FILE-APPEND should only join the package value with the file name of the
executable, but not include any arguments.

I suppose these values end up in the generated configuration file as
plain strings anyway, so perhaps it doesn’t matter much.  In that case
please also use FILE-APPEND to embed a reference to PASS.

> +(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))

Could this be (cons (goimapnotify-format-field field-name) value)
instead?  I don’t think we need this wrapping and unwrapping with G-exp
syntax.

> +(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)))

This looks a little too convoluted to me.  It’s the IF with the
*UNSPECIFIED* as the second branch (followed by the filter) that doesn’t
strike me as nice.  Perhaps you simplify this with a fold?

In any case, the filter predicate could be (negate unspecified?) instead
of the lambda expression.

> ; See https://gitlab.com/shackra/goimapnotify/-/blob/master/config.go?ref_type=heads#L46-62

Please don’t use “master” here, because it’s a moving target, so the
line anchors will become out of date.  Please use an arbitrary recent
commit instead.  Since this is a line comment please use “;;” instead of
“;” (which is used for comments in the margin).

> +(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))

The indentation for the MATCH clauses is too deep.  There are other
instances of oddly deep indentation, such as the body of
“(define-configuration/no-serialization home-goimapnotify-configuration
…)”.  Are you using Emacs?

Since you’re throwing away the first element you can reduce this to:

  (every (compose goimapnotify-account? second) lst)

I haven’t looked at the documentation much, but the capitalization of
“Stdin” and the spurious changes to seemingly empty lines in existing
documentation stood out to me.  It would be better to undo the changes
to unrelated documentation in the same file.
Bruno Victal Nov. 20, 2023, 5:16 p.m. UTC | #2
Hi Nils,

On 2023-10-15 15:01, Nils Landt wrote:
> This patch adds a home service for generating goimapnotify JSON
> configuration files.

[…]

> +@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"}.

Not a goimapnotify user but this looks like a daemon application.
I don't like this design much, I think goimapnotify should be launched and managed
using shepherd instead of simply exposing these files to the user.

> +@item @code{tls} (type: maybe-boolean)
> +
> +Use TLS?

Boolean fields are generally suffixed with '?'. (e.g. 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.

I'd prefer to write it as “stdin” (lowercase) or expand it to “standard input”.

> +@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}.

Same remark as 'tls' option.

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

“An executable or script to run after @code{on-new-mail} has ran.”

> +@table @asis
> +@item @code{reject-unauthorized} (type: maybe-boolean)
> +Skip verifying CA server identify?

Same remark as 'tls' option.

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

These are unrelated changes, can you drop these hunks?

> +(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))

This isn't needed, fields whose maybe-values are unset don't call the
serializing procedures.

> +(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)))

You can use 'serialize-configuration' instead which accounts for the unset maybe-values.

> +
> +(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-))

You can omit the parentheses around the field-type.

> +(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))

You can replace this with:
--8<---------------cut here---------------start------------->8---
(define list-of-goimapnotify-accounts?
  (list-of goimapnotify-account?))
--8<---------------cut here---------------end--------------->8---

> +(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))))))

This looks misindented, the .dir-locals file automatically handles
this if you are using emacs.

> +
> +(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.")))

Stylistically, I'd indent this as:

--8<---------------cut here---------------start------------->8---
(define home-goimapnotify-service-type
  (service-type
   (name 'home-goimapnotify-service)
   (extensions …
--8<---------------cut here---------------end--------------->8---


My 2¢!
Nils Landt Nov. 21, 2023, 3:25 p.m. UTC | #3
Thank you for the feedback, I'll work on implementing it, hopefully on the weekend. 

I've already added a few comments / questions below though.

> Bruno Victal <mirai@makinata.eu> hat am 20.11.2023 18:16 CET ls,
> 
> On 2023-10-15 15:01, Nils Landt wrote:
> > This patch adds a home service for generating goimapnotify JSON
> > configuration files.
> 
> […]
> 
> Not a goimapnotify user but this looks like a daemon application.
> I don't like this design much, I think goimapnotify should be launched and managed
> using shepherd instead of simply exposing these files to the user.

These options can not be given as command line options, they need to be in a config file.
Personally I don't use shepherd, so I won't be contributing shepherd services :)

> > +@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.
> 
> I'd prefer to write it as “stdin” (lowercase) or expand it to “standard input”.

I think this a good change, and I'm completely fine making it. I just want to mention that I copied these docstrings directly from the project's readme. But I see no reason why the author would capitalize it.

> > +(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))
> 
> You can replace this with:
> --8<---------------cut here---------------start------------->8---
> (define list-of-goimapnotify-accounts?
>   (list-of goimapnotify-account?))
> --8<---------------cut here---------------end--------------->8---

Wouldn't that fail because it expects '(goimapnotify-account goimapnotify-account[...])?

> This looks misindented, the .dir-locals file automatically handles
> this if you are using emacs.

I'm using vim. The code is indented with autoindent, as mentioned on https://guix.gnu.org/manual/devel/en/html_node/Vim-and-NeoVim.html . If this doesn't match the expected indentation as implemented in Emacs, I'd be grateful if you could indent this file before merging.
Nils Landt Nov. 21, 2023, 3:30 p.m. UTC | #4
Thank you for your feedback Ricardo. I did not ignore you, but I did not receive a notification email, so I didn't see your message until I received a notification for Brunos reply and checked the website.

I'll (try to) incorporate your feedback as well.
Nils Landt Nov. 26, 2023, 11:14 a.m. UTC | #5
Hello,

I have pushed a new version.
Compared to the version you reviewed, I made the following changes:
- docs: Stdin -> stdin
- docs: use file-append for "pass" example
- docs: re-add trailing whitespace
- rework filter + map to use fold instead
- update link to upstream config documentation from master to current commit
- ignore first element in list-of-goimapnotify-accounts?
- add question mark suffix to boolean fields (e.g. tls -> tls?)
- fix "on-new-mail" option in docstring
- remove parentheses around configuration field types

Lastly, some comments on review requests I was unable to implement.

Ricardo:
> So I think it would be better to let these fields accept command lists.
> FILE-APPEND should only join the package value with the file name of the
> executable, but not include any arguments.

I did not understand what this meant. Do you have any examples? I implemented your second suggestion of using file-append for pass instead.

> Could this be (cons (goimapnotify-format-field field-name) value)
> instead?  I don’t think we need this wrapping and unwrapping with G-exp
> syntax.

This results in e.g. ("boxes" . #("INBOX")) , leading to a syntax error in the generated guile script. I'm open to suggestions here.

Bruno:
> This isn't needed, fields whose maybe-values are unset don't call the
> serializing procedures.

combined with

> You can use 'serialize-configuration' instead which accounts for the unset maybe-values.

I put some time into this, but I don't see how serialize-configuration, which returns a gexp including string-append, could be used to turn a configuration record into the format required by guile-json.
Bruno Victal Nov. 28, 2023, 8:37 p.m. UTC | #6
Hi Nils,

On 2023-11-21 15:25, Nils Landt wrote:
>> Bruno Victal <mirai@makinata.eu> hat am 20.11.2023 18:16 CET ls,
>> On 2023-10-15 15:01, Nils Landt wrote:
>>> +(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))
>>
>> You can replace this with:
>> --8<---------------cut here---------------start------------->8---
>> (define list-of-goimapnotify-accounts?
>>   (list-of goimapnotify-account?))
>> --8<---------------cut here---------------end--------------->8---
> 
> Wouldn't that fail because it expects '(goimapnotify-account goimapnotify-account[...])?

Right, it should be something like:

--8<---------------cut here---------------start------------->8---
(define list-of-goimapnotify-accounts?
  (list-of (match-lambda ((? string?) (? goimapnotify-account?)))))
--8<---------------cut here---------------end--------------->8---
Bruno Victal Nov. 29, 2023, 5:20 p.m. UTC | #7
Hi Nils,

On 2023-11-26 11:14, Nils Landt wrote:
> Bruno:
>> This isn't needed, fields whose maybe-values are unset don't call the
>> serializing procedures.
> 
> combined with
> 
>> You can use 'serialize-configuration' instead which accounts for the unset maybe-values.
> 
> I put some time into this, but I don't see how serialize-configuration, which returns a gexp including string-append, could be used to turn a configuration record into the format required by guile-json.

Right, I missed that you are synthesizing a list for scm->json,
you will need to make use of the lower-level transducers in
(gnu services configuration).
The fstrim-service-type in (gnu services linux) provides a simple
example of its use though for your case you might be looking at
something like:

--8<---------------cut here---------------start------------->8---
;; note: untested snippet

(define (goimapnotify-files config)
  (match-record config <home-goimapnotify-configuration>
                (accounts)
    (map (match-lambda
           ((path account)
            (list path
                  (computed-file
                   (string-append "mail-imapnotify-config-"
                                  (goimapnotify-account-host account))
                   (with-extensions (list guile-json-4)
                     #~(begin
                         (use-modules (json builder))
                         (with-output-to-file #$output
                           (lambda ()
                             (scm->json
                              (list #$@(list-transduce
                                        (base-transducer account)
                                        rcons
                                        goimapnotify-account-fields)))
                             #:pretty #t))))))))
         accounts)))
--8<---------------cut here---------------end--------------->8---
Nils Landt Dec. 3, 2023, 3:56 p.m. UTC | #8
Hey Bruno,

I've just pushed a new version, please have another look.

> Bruno Victal <mirai@makinata.eu> hat am 29.11.2023 18:20 CET geschrieben:
> 
>  
> Hi Nils,
> 
> On 2023-11-26 11:14, Nils Landt wrote:
> > Bruno:
> >> This isn't needed, fields whose maybe-values are unset don't call the
> >> serializing procedures.

Took me a while to understand I needed to prefix the maybe-values. This then generates (goimapnotify-serialize-maybe-boolean).

> you will need to make use of the lower-level transducers in
> (gnu services configuration).
> The fstrim-service-type in (gnu services linux) provides a simple
> example of its use though for your case you might be looking at
> something like:

Thanks, I was able to get it work with a minor change! Don't really understand transducers yet, but I've starting reading up on them.

Nils
diff mbox series

Patch

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.")))