diff --git a/modules/lib/maintainers.nix b/modules/lib/maintainers.nix index 19fa96b10..b91ba715a 100644 --- a/modules/lib/maintainers.nix +++ b/modules/lib/maintainers.nix @@ -363,6 +363,15 @@ github = "lukasngl"; githubId = 69244516; }; + soywod = { + name = "Clément DOUIN"; + email = "clement.douin@posteo.net"; + matrix = "@soywod:matrix.org"; + github = "soywod"; + githubId = 10437171; + keys = + [{ fingerprint = "75F0 AB7C FE01 D077 AEE6 CAFD 353E 4A18 EE0F AB72"; }]; + }; toastal = { email = "toastal+nix@posteo.net"; matrix = "@toastal:matrix.org"; diff --git a/modules/programs/himalaya.nix b/modules/programs/himalaya.nix index 92328153f..54f8d1ecc 100644 --- a/modules/programs/himalaya.nix +++ b/modules/programs/himalaya.nix @@ -1,140 +1,253 @@ { config, lib, pkgs, ... }: + let - cfg = config.programs.himalaya; - - enabledAccounts = - lib.filterAttrs (_: a: a.himalaya.enable) (config.accounts.email.accounts); - + # aliases + inherit (config.programs) himalaya; tomlFormat = pkgs.formats.toml { }; - himalayaConfig = let - toHimalayaConfig = account: - { + # attrs util that removes entries containing a null value + compactAttrs = lib.filterAttrs (_: val: !isNull val); + + # make a himalaya config from a home-manager email account config + mkAccountConfig = _: account: + let + globalConfig = { email = account.address; display-name = account.realName; default = account.primary; - - mailboxes = { + folder-aliases = { inbox = account.folders.inbox; sent = account.folders.sent; - draft = account.folders.drafts; - # NOTE: himalaya does not support configuring the name of the trash folder + drafts = account.folders.drafts; + trash = account.folders.trash; }; - } // (lib.optionalAttrs (account.signature.showSignature == "append") { - # FIXME: signature cannot be attached - signature = account.signature.text; - signature-delim = account.signature.delimiter; - }) // (if account.himalaya.backend == null then { - backend = "none"; - } else if account.himalaya.backend == "imap" then { - # FIXME: does not support disabling TLS altogether - # NOTE: does not accept sequence of strings for password commands - backend = account.himalaya.backend; - imap-login = account.userName; - imap-passwd-cmd = lib.escapeShellArgs account.passwordCommand; + }; + + signatureConfig = + lib.optionalAttrs (account.signature.showSignature == "append") { + # TODO: signature cannot be attached yet + # https://todo.sr.ht/~soywod/himalaya/27 + signature = account.signature.text; + signature-delim = account.signature.delimiter; + }; + + imapConfig = lib.optionalAttrs (!isNull account.imap) (compactAttrs { + backend = "imap"; imap-host = account.imap.host; imap-port = account.imap.port; + imap-ssl = account.imap.tls.enable; imap-starttls = account.imap.tls.useStartTls; - } else if account.himalaya.backend == "maildir" then { - backend = account.himalaya.backend; - maildir-root-dir = account.maildirBasePath; - } else - throw "Unsupported backend: ${account.himalaya.backend}") - // (if account.himalaya.sender == null then { - sender = "none"; - } else if account.himalaya.sender == "smtp" then { - sender = account.himalaya.sender; - smtp-login = account.userName; - smtp-passwd-cmd = lib.escapeShellArgs account.passwordCommand; + imap-login = account.userName; + imap-passwd-cmd = builtins.concatStringsSep " " account.passwordCommand; + }); + + maildirConfig = + lib.optionalAttrs (isNull account.imap && !isNull account.maildir) + (compactAttrs { + backend = "maildir"; + maildir-root-dir = account.maildir.absPath; + }); + + smtpConfig = lib.optionalAttrs (!isNull account.smtp) (compactAttrs { + sender = "smtp"; smtp-host = account.smtp.host; smtp-port = account.smtp.port; + smtp-ssl = account.smtp.tls.enable; smtp-starttls = account.smtp.tls.useStartTls; - } else if account.himalaya.sender == "sendmail" then { - sender = account.himalaya.sender; - } else - throw "Unsupported sender: ${account.himalaya.sender}") - // account.himalaya.settings; - in { - # NOTE: will not start without this configured, but each account overrides it - display-name = ""; - } // cfg.settings // (lib.mapAttrs (_: toHimalayaConfig) enabledAccounts); -in { - meta.maintainers = with lib.hm.maintainers; [ toastal ]; + smtp-login = account.userName; + smtp-passwd-cmd = builtins.concatStringsSep " " account.passwordCommand; + }); - options = with lib; { - programs.himalaya = { - enable = mkEnableOption "himalaya mail client"; + sendmailConfig = + lib.optionalAttrs (isNull account.smtp) { sender = "sendmail"; }; - package = mkOption { - type = types.package; - default = pkgs.himalaya; - defaultText = literalExpression "pkgs.himalaya"; - description = '' - Package providing the himalaya mail client. - ''; + config = globalConfig // signatureConfig // imapConfig // maildirConfig + // smtpConfig // sendmailConfig; + + in lib.recursiveUpdate config account.himalaya.settings; + + # make a systemd service config from a name and a description + mkServiceConfig = name: desc: + let + inherit (config.services."himalaya-${name}") enable environment settings; + optionalArg = key: + if (key ? settings && !isNull settings."${key}") then + [ "--${key} ${settings."${key}"}" ] + else + [ ]; + in { + "himalaya-${name}" = lib.mkIf enable { + Unit = { + Description = desc; + After = [ "network.target" ]; + }; + Install = { WantedBy = [ "default.target" ]; }; + Service = { + ExecStart = lib.concatStringsSep " " + ([ "${himalaya.package}/bin/himalaya" ] ++ optionalArg "account" + ++ [ name ] ++ optionalArg "keepalive"); + ExecSearchPath = "/bin"; + Environment = + lib.mapAttrsToList (key: val: "${key}=${val}") environment; + Restart = "always"; + RestartSec = 10; + }; }; + }; - settings = mkOption { - type = tomlFormat.type; +in { + meta.maintainers = with lib.hm.maintainers; [ soywod toastal ]; + + options = { + programs.himalaya = { + enable = lib.mkEnableOption "Enable the Himalaya email client."; + package = lib.mkPackageOption pkgs "himalaya" { }; + settings = lib.mkOption { + type = lib.types.submodule { freeformType = tomlFormat.type; }; default = { }; - example = lib.literalExpression '' - { - email-listing-page-size = 50; - watch-cmds = [ "mbsync -a" ] - } - ''; description = '' - Global himalaya configuration values. + Himalaya global configuration. + See for supported values. ''; }; }; - accounts.email.accounts = mkOption { - type = with types; - attrsOf (submodule { - options.himalaya = { - enable = mkEnableOption '' - the himalaya mail client for this account + services = { + himalaya-notify = { + enable = + lib.mkEnableOption "Enable the Himalaya new emails notifier service."; + + environment = lib.mkOption { + type = with lib.types; attrsOf str; + default = { }; + example = lib.literalExpression '' + { + "PASSWORD_STORE_DIR" = "~/.password-store"; + } + ''; + description = '' + Extra environment variables to be exported in the service. + ''; + }; + + settings = { + account = lib.mkOption { + type = with lib.types; nullOr str; + default = null; + example = "gmail"; + description = '' + Name of the account the notifier should be started for. If + no account is given, the default one is used. ''; - - backend = mkOption { - # TODO: “notmuch” (requires compile flag for himalaya, libnotmuch) - type = types.nullOr (types.enum [ "imap" "maildir" ]); - description = '' - The method for which himalaya will fetch, store, - etc. mail. - ''; - }; - - sender = mkOption { - type = types.nullOr (types.enum [ "smtp" "sendmail" ]); - description = '' - The method for which himalaya will send mail. - ''; - }; - - settings = mkOption { - type = tomlFormat.type; - default = { }; - example = lib.literalExpression '' - { - default-page-size = 50; - } - ''; - description = '' - Extra settings to add to this himalaya - account configuration. - ''; - }; }; - }); + + keepalive = lib.mkOption { + type = with lib.types; nullOr int; + default = null; + example = "500"; + description = '' + Notifier lifetime of the IDLE session (in seconds). + ''; + }; + }; + }; + + himalaya-watch = { + enable = lib.mkEnableOption + "Enable the Himalaya folder changes watcher service."; + + environment = lib.mkOption { + type = with lib.types; attrsOf str; + default = { }; + example = lib.literalExpression '' + { + "PASSWORD_STORE_DIR" = "~/.password-store"; + } + ''; + description = '' + Extra environment variables to be exported in the service. + ''; + }; + + settings = { + account = lib.mkOption { + type = with lib.types; nullOr str; + default = null; + example = "gmail"; + description = '' + Name of the account the watcher should be started for. If + no account is given, the default one is used. + ''; + }; + + keepalive = lib.mkOption { + type = with lib.types; nullOr int; + default = null; + example = "500"; + description = '' + Watcher lifetime of the IDLE session (in seconds). + ''; + }; + }; + }; + }; + + accounts.email.accounts = lib.mkOption { + type = lib.types.attrsOf (lib.types.submodule { + options.himalaya = { + enable = lib.mkEnableOption "Enable Himalaya for this email account."; + + # TODO: remove me for the next release + backend = lib.mkOption { + type = with lib.types; nullOr str; + default = null; + description = '' + Specifying 'accounts.email.accounts.*.himalaya.backend' is deprecated, + set 'accounts.email.accounts.*.himalaya.settings.backend' instead. + ''; + }; + + # TODO: remove me for the next release + sender = lib.mkOption { + type = with lib.types; nullOr str; + description = '' + Specifying 'accounts.email.accounts.*.himalaya.sender' is deprecated, + set 'accounts.email.accounts.*.himalaya.settings.sender' instead. + ''; + }; + + settings = lib.mkOption { + type = lib.types.submodule { freeformType = tomlFormat.type; }; + default = { }; + description = '' + Himalaya configuration for this email account. + See for supported values. + ''; + }; + }; + }); }; }; - config = lib.mkIf cfg.enable { - home.packages = [ cfg.package ]; + config = lib.mkIf himalaya.enable { + home.packages = [ himalaya.package ]; - xdg.configFile."himalaya/config.toml".source = - tomlFormat.generate "himalaya-config.toml" himalayaConfig; + xdg.configFile."himalaya/config.toml".source = let + enabledAccounts = lib.filterAttrs (_: account: account.himalaya.enable) + config.accounts.email.accounts; + accountsConfig = lib.mapAttrs mkAccountConfig enabledAccounts; + globalConfig = compactAttrs himalaya.settings; + allConfig = globalConfig // accountsConfig; + in tomlFormat.generate "himalaya-config.toml" allConfig; + + systemd.user.services = { } + // mkServiceConfig "notify" "Himalaya new emails notifier service" + // mkServiceConfig "watch" "Himalaya folder changes watcher service"; + + # TODO: remove me for the next release + warnings = (lib.optional ("backend" ? himalaya && !isNull himalaya.backend) + "Specifying 'accounts.email.accounts.*.himalaya.backend' is deprecated, set 'accounts.email.accounts.*.himalaya.settings.backend' instead") + ++ (lib.optional ("sender" ? himalaya && !isNull himalaya.sender) + "Specifying 'accounts.email.accounts.*.himalaya.sender' is deprecated, set 'accounts.email.accounts.*.himalaya.settings.sender' instead."); }; } diff --git a/tests/modules/programs/himalaya/himalaya-expected.toml b/tests/modules/programs/himalaya/basic-expected.toml similarity index 54% rename from tests/modules/programs/himalaya/himalaya-expected.toml rename to tests/modules/programs/himalaya/basic-expected.toml index ca7ada66d..abda4a13a 100644 --- a/tests/modules/programs/himalaya/himalaya-expected.toml +++ b/tests/modules/programs/himalaya/basic-expected.toml @@ -1,25 +1,24 @@ -display-name = "" -downloads-dir = "/data/download" - ["hm@example.com"] backend = "imap" default = true display-name = "H. M. Test" email = "hm@example.com" -email-listing-page-size = 50 imap-host = "imap.example.com" imap-login = "home.manager" -imap-passwd-cmd = "'password-command'" -imap-port = 995 +imap-passwd-cmd = "password-command" +imap-port = 993 +imap-ssl = true imap-starttls = false sender = "smtp" smtp-host = "smtp.example.com" smtp-login = "home.manager" -smtp-passwd-cmd = "'password-command'" +smtp-passwd-cmd = "password-command" smtp-port = 465 +smtp-ssl = true smtp-starttls = false -["hm@example.com".mailboxes] -draft = "Drafts" -inbox = "In" -sent = "Out" +["hm@example.com".folder-aliases] +drafts = "Drafts" +inbox = "Inbox" +sent = "Sent" +trash = "Trash" diff --git a/tests/modules/programs/himalaya/basic.nix b/tests/modules/programs/himalaya/basic.nix new file mode 100644 index 000000000..e2a8fa83c --- /dev/null +++ b/tests/modules/programs/himalaya/basic.nix @@ -0,0 +1,28 @@ +{ config, lib, pkgs, ... }: + +with lib; + +{ + imports = [ ../../accounts/email-test-accounts.nix ]; + + accounts.email.accounts = { + "hm@example.com" = { + imap.port = 993; + smtp.port = 465; + himalaya.enable = true; + himalaya.backend = "deprecated"; + himalaya.sender = "deprecated"; + }; + }; + + programs.himalaya = { enable = true; }; + + test.stubs.himalaya = { }; + + nmt.script = '' + assertFileExists home-files/.config/himalaya/config.toml + assertFileContent home-files/.config/himalaya/config.toml ${ + ./basic-expected.toml + } + ''; +} diff --git a/tests/modules/programs/himalaya/default.nix b/tests/modules/programs/himalaya/default.nix index 54c3978d8..e5473344b 100644 --- a/tests/modules/programs/himalaya/default.nix +++ b/tests/modules/programs/himalaya/default.nix @@ -1 +1,5 @@ -{ himalaya = ./himalaya.nix; } +{ + himalaya-basic = ./basic.nix; + himalaya-imap-smtp = ./imap-smtp.nix; + himalaya-maildir-sendmail = ./maildir-sendmail.nix; +} diff --git a/tests/modules/programs/himalaya/imap-smtp-expected.toml b/tests/modules/programs/himalaya/imap-smtp-expected.toml new file mode 100644 index 000000000..bcfee047e --- /dev/null +++ b/tests/modules/programs/himalaya/imap-smtp-expected.toml @@ -0,0 +1,29 @@ +email-listing-page-size = 40 + +["hm@example.com"] +backend = "imap" +default = true +display-name = "H. M. Test" +email = "hm@example.com" +email-listing-page-size = 50 +folder-listing-page-size = 50 +imap-host = "imap.example.com" +imap-login = "home.manager" +imap-passwd-cmd = "password-command" +imap-port = 143 +imap-ssl = false +imap-starttls = false +sender = "smtp" +smtp-host = "smtp.example.com" +smtp-login = "home.manager" +smtp-passwd-cmd = "password-command" +smtp-port = 465 +smtp-ssl = true +smtp-starttls = true + +["hm@example.com".folder-aliases] +custom = "Custom" +drafts = "D" +inbox = "In2" +sent = "Out" +trash = "Trash" diff --git a/tests/modules/programs/himalaya/imap-smtp.nix b/tests/modules/programs/himalaya/imap-smtp.nix new file mode 100644 index 000000000..c1347d23c --- /dev/null +++ b/tests/modules/programs/himalaya/imap-smtp.nix @@ -0,0 +1,58 @@ +{ config, lib, pkgs, ... }: + +with lib; + +{ + accounts.email.accounts = { + "hm@example.com" = { + primary = true; + address = "hm@example.com"; + userName = "home.manager"; + realName = "H. M. Test"; + passwordCommand = "password-command"; + imap = { + host = "imap.example.com"; + port = 143; + tls = { enable = false; }; + }; + smtp = { + host = "smtp.example.com"; + port = 465; + tls = { + enable = true; + useStartTls = true; + }; + }; + folders = { + inbox = "In"; + sent = "Out"; + drafts = "D"; + }; + himalaya = { + enable = true; + settings = { + folder-listing-page-size = 50; + email-listing-page-size = 50; + folder-aliases = { + inbox = "In2"; + custom = "Custom"; + }; + }; + }; + }; + }; + + programs.himalaya = { + enable = true; + settings = { email-listing-page-size = 40; }; + }; + + test.stubs.himalaya = { }; + + nmt.script = '' + assertFileExists home-files/.config/himalaya/config.toml + assertFileContent home-files/.config/himalaya/config.toml ${ + ./imap-smtp-expected.toml + } + ''; +} diff --git a/tests/modules/programs/himalaya/maildir-sendmail-expected.toml b/tests/modules/programs/himalaya/maildir-sendmail-expected.toml new file mode 100644 index 000000000..61ae940de --- /dev/null +++ b/tests/modules/programs/himalaya/maildir-sendmail-expected.toml @@ -0,0 +1,16 @@ +email-listing-page-size = 50 + +["hm@example.com"] +backend = "maildir" +default = true +display-name = "H. M. Test" +email = "hm@example.com" +maildir-root-dir = "/home/hm-user/Maildir/hm@example.com" +sender = "sendmail" +sendmail-cmd = "msmtp" + +["hm@example.com".folder-aliases] +drafts = "Drafts" +inbox = "Inbox" +sent = "Sent" +trash = "Deleted" diff --git a/tests/modules/programs/himalaya/himalaya.nix b/tests/modules/programs/himalaya/maildir-sendmail.nix similarity index 50% rename from tests/modules/programs/himalaya/himalaya.nix rename to tests/modules/programs/himalaya/maildir-sendmail.nix index 4d556ff86..a9d15d224 100644 --- a/tests/modules/programs/himalaya/himalaya.nix +++ b/tests/modules/programs/himalaya/maildir-sendmail.nix @@ -3,32 +3,27 @@ with lib; { - imports = [ ../../accounts/email-test-accounts.nix ]; - accounts.email.accounts = { "hm@example.com" = { + primary = true; + address = "hm@example.com"; + userName = "home.manager"; + realName = "H. M. Test"; + passwordCommand = "password-command"; + folders = { trash = "Deleted"; }; himalaya = { enable = true; - - backend = "imap"; - sender = "smtp"; - settings = { email-listing-page-size = 50; }; + settings = { + sender = "sendmail"; + sendmail-cmd = "msmtp"; + }; }; - - folders = { - inbox = "In"; - sent = "Out"; - drafts = "Drafts"; - }; - - imap.port = 995; - smtp.port = 465; }; }; programs.himalaya = { enable = true; - settings = { downloads-dir = "/data/download"; }; + settings = { email-listing-page-size = 50; }; }; test.stubs.himalaya = { }; @@ -36,7 +31,7 @@ with lib; nmt.script = '' assertFileExists home-files/.config/himalaya/config.toml assertFileContent home-files/.config/himalaya/config.toml ${ - ./himalaya-expected.toml + ./maildir-sendmail-expected.toml } ''; }