diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 463671241..fe4a326e1 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -163,6 +163,9 @@ /modules/programs/mu.nix @KarlJoad +/modules/programs/mujmap.nix @elizagamedev +/tests/modules/programs/mujmap @elizagamedev + /modules/programs/navi.nix @marsam /modules/programs/ncmpcpp.nix @olmokramer diff --git a/modules/misc/news.nix b/modules/misc/news.nix index ea06be5f0..98bfa9828 100644 --- a/modules/misc/news.nix +++ b/modules/misc/news.nix @@ -554,6 +554,13 @@ in A new module is available: 'services.mopidy'. ''; } + + { + time = "2022-06-21T22:29:37+00:00"; + message = '' + A new module is available: 'programs.mujmap'. + ''; + } ]; }; } diff --git a/modules/modules.nix b/modules/modules.nix index 7789be99b..d494d63b7 100644 --- a/modules/modules.nix +++ b/modules/modules.nix @@ -109,6 +109,7 @@ let ./programs/mpv.nix ./programs/msmtp.nix ./programs/mu.nix + ./programs/mujmap.nix ./programs/navi.nix ./programs/ncmpcpp.nix ./programs/ncspot.nix diff --git a/modules/programs/mujmap.nix b/modules/programs/mujmap.nix new file mode 100644 index 000000000..9d290fefb --- /dev/null +++ b/modules/programs/mujmap.nix @@ -0,0 +1,315 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.programs.mujmap; + + mujmapAccounts = + filter (a: a.mujmap.enable) (attrValues config.accounts.email.accounts); + + missingNotmuchAccounts = map (a: a.name) + (filter (a: !a.notmuch.enable && a.mujmap.notmuchSetupWarning) + mujmapAccounts); + + notmuchConfigHelp = + map (name: "accounts.email.accounts.${name}.notmuch.enable = true;") + missingNotmuchAccounts; + + settingsFormat = pkgs.formats.toml { }; + + filterNull = attrs: attrsets.filterAttrs (n: v: v != null) attrs; + + configFile = account: + let + settings'' = if (account.jmap == null) then + { } + else + filterNull { + fqdn = account.jmap.host; + session_url = account.jmap.sessionUrl; + }; + + settings' = settings'' // { + username = account.userName; + password_command = escapeShellArgs account.passwordCommand; + } // filterNull account.mujmap.settings; + + settings = if (hasAttr "fqdn" settings') then + (removeAttrs settings' [ "session_url" ]) + else + settings'; + in { + name = "${account.maildir.absPath}/mujmap.toml"; + value.source = settingsFormat.generate + "mujmap-${lib.replaceStrings [ "@" ] [ "_at_" ] account.address}.toml" + settings; + }; + + tagsOpts = { + lowercase = mkOption { + type = types.bool; + default = false; + description = '' + If true, translate all mailboxes to lowercase names when mapping to notmuch + tags. + ''; + }; + + directory_separator = mkOption { + type = types.str; + default = "/"; + example = "."; + description = '' + Directory separator for mapping notmuch tags to maildirs. + ''; + }; + + inbox = mkOption { + type = types.str; + default = "inbox"; + description = '' + Tag for notmuch to use for messages stored in the mailbox labeled with the + Inbox name attribute. + + If set to an empty string, this mailbox and its child + mailboxes are not synchronized with a tag. + ''; + }; + + deleted = mkOption { + type = types.str; + default = "deleted"; + description = '' + Tag for notmuch to use for messages stored in the mailbox labeled with the + Trash name attribute. + + If set to an empty string, this mailbox and its child + mailboxes are not synchronized with a tag. + ''; + }; + + sent = mkOption { + type = types.str; + default = "sent"; + description = '' + Tag for notmuch to use for messages stored in the mailbox labeled with the + Sent name attribute. + + If set to an empty string, this mailbox and its child + mailboxes are not synchronized with a tag. + ''; + }; + + spam = mkOption { + type = types.str; + default = "spam"; + description = '' + Tag for notmuch to use for messages stored in the mailbox labeled with the + Junk name attribute and/or with the $Junk keyword, + except for messages with the $NotJunk keyword. + + If set to an empty string, this mailbox, its child + mailboxes, and these keywords are not synchronized with a tag. + ''; + }; + + important = mkOption { + type = types.str; + default = "important"; + description = '' + Tag for notmuch to use for messages stored in the mailbox labeled with the + Important name attribute and/or with the $Important + keyword. + + If set to an empty string, this mailbox, its child + mailboxes, and these keywords are not synchronized with a tag. + ''; + }; + + phishing = mkOption { + type = types.str; + default = "phishing"; + description = '' + Tag for notmuch to use for the IANA $Phishing keyword. + + If set to an empty string, this keyword is not synchronized with a tag. + ''; + }; + }; + + rootOpts = { + username = mkOption { + type = types.nullOr types.str; + default = null; + example = "alice@example.com"; + description = '' + Username for basic HTTP authentication. + + If null, defaults to + . + ''; + }; + + password_command = mkOption { + type = types.nullOr (types.either types.str (types.listOf types.str)); + default = null; + apply = p: if isList p then escapeShellArgs p else p; + example = "pass alice@example.com"; + description = '' + Shell command which will print a password to stdout for basic HTTP + authentication. + + If null, defaults to + . + ''; + }; + + fqdn = mkOption { + type = types.nullOr types.str; + default = null; + example = "example.com"; + description = '' + Fully qualified domain name of the JMAP service. + + mujmap looks up the JMAP SRV record for this host to determine the JMAP session + URL. Mutually exclusive with + . + + If null, defaults to + . + ''; + }; + + session_url = mkOption { + type = types.nullOr types.str; + default = null; + example = "https://jmap.example.com/.well-known/jmap"; + description = '' + Sesion URL to connect to. + + Mutually exclusive with + . + + If null, defaults to + . + ''; + }; + + auto_create_new_mailboxes = mkOption { + type = types.bool; + default = true; + description = '' + Whether to create new mailboxes automatically on the server from notmuch + tags. + ''; + }; + + cache_dir = mkOption { + type = types.nullOr types.str; + default = null; + description = '' + The cache directory in which to store mail files while they are being + downloaded. The default is operating-system specific. + ''; + }; + + tags = mkOption { + type = types.submodule { + freeformType = settingsFormat.type; + options = tagsOpts; + }; + default = { }; + description = '' + Tag configuration. + + Beware that there are quirks that require manual consideration if changing the + values of these files; please see + + for more details. + ''; + }; + }; + + mujmapOpts = { + enable = mkEnableOption "mujmap JMAP synchronization for notmuch"; + + notmuchSetupWarning = mkOption { + type = types.bool; + default = true; + description = '' + Warn if Notmuch is not also enabled for this account. + + This can safely be disabled if mujmap.toml is managed + outside of Home Manager. + ''; + }; + + settings = mkOption { + type = types.submodule { + freeformType = settingsFormat.type; + options = rootOpts; + }; + default = { }; + description = '' + Settings which are applied to mujmap.toml + for the account. + + See the mujmap project + for documentation of settings not explicitly covered by this module. + ''; + }; + }; + + mujmapModule = types.submodule { options = { mujmap = mujmapOpts; }; }; +in { + meta.maintainers = with maintainers; [ elizagamedev ]; + + options = { + programs.mujmap = { + enable = mkEnableOption "mujmap Gmail synchronization for notmuch"; + + package = mkOption { + type = types.package; + default = pkgs.mujmap; + defaultText = "pkgs.mujmap"; + description = '' + mujmap package to use. + ''; + }; + }; + + accounts.email.accounts = + mkOption { type = with types; attrsOf mujmapModule; }; + }; + + config = mkIf cfg.enable (mkMerge [ + (mkIf (missingNotmuchAccounts != [ ]) { + warnings = ['' + mujmap is enabled for the following email accounts, but notmuch is not: + + ${concatStringsSep "\n " missingNotmuchAccounts} + + Notmuch can be enabled with: + + ${concatStringsSep "\n " notmuchConfigHelp} + + If you have configured notmuch outside of Home Manager, you can suppress this + warning with: + + programs.mujmap.notmuchSetupWarning = false; + '']; + }) + + { + warnings = flatten (map (account: account.warnings) mujmapAccounts); + + home.packages = [ cfg.package ]; + + # Notmuch should ignore non-mail files created by mujmap. + programs.notmuch.new.ignore = [ "/.*[.](toml|json|lock)$/" ]; + + home.file = listToAttrs (map configFile mujmapAccounts); + } + ]); +} diff --git a/tests/default.nix b/tests/default.nix index 919b142bb..a0093f087 100644 --- a/tests/default.nix +++ b/tests/default.nix @@ -77,6 +77,7 @@ import nmt { ./modules/programs/mbsync ./modules/programs/mpv ./modules/programs/mu + ./modules/programs/mujmap ./modules/programs/ncmpcpp ./modules/programs/ne ./modules/programs/neomutt diff --git a/tests/modules/programs/mujmap/default.nix b/tests/modules/programs/mujmap/default.nix new file mode 100644 index 000000000..8de0e917c --- /dev/null +++ b/tests/modules/programs/mujmap/default.nix @@ -0,0 +1,5 @@ +{ + mujmap-defaults = ./mujmap-defaults.nix; + mujmap-fqdn-and-session-url-specified = + ./mujmap-fqdn-and-session-url-specified.nix; +} diff --git a/tests/modules/programs/mujmap/mujmap-defaults-expected.toml b/tests/modules/programs/mujmap/mujmap-defaults-expected.toml new file mode 100644 index 000000000..87b5bf402 --- /dev/null +++ b/tests/modules/programs/mujmap/mujmap-defaults-expected.toml @@ -0,0 +1,14 @@ +auto_create_new_mailboxes = true +fqdn = "example.com" +password_command = "'password-command'" +username = "home.manager" + +[tags] +deleted = "deleted" +directory_separator = "/" +important = "important" +inbox = "inbox" +lowercase = false +phishing = "phishing" +sent = "sent" +spam = "spam" diff --git a/tests/modules/programs/mujmap/mujmap-defaults.nix b/tests/modules/programs/mujmap/mujmap-defaults.nix new file mode 100644 index 000000000..704978997 --- /dev/null +++ b/tests/modules/programs/mujmap/mujmap-defaults.nix @@ -0,0 +1,25 @@ +{ config, lib, pkgs, ... }: + +with lib; + +{ + imports = [ ../../accounts/email-test-accounts.nix ]; + + config = { + programs.mujmap.enable = true; + programs.mujmap.package = config.lib.test.mkStubPackage { }; + + accounts.email.accounts."hm@example.com" = { + jmap.host = "example.com"; + mujmap.enable = true; + notmuch.enable = true; + }; + + nmt.script = '' + assertFileExists home-files/Mail/hm@example.com/mujmap.toml + assertFileContent home-files/Mail/hm@example.com/mujmap.toml ${ + ./mujmap-defaults-expected.toml + } + ''; + }; +} diff --git a/tests/modules/programs/mujmap/mujmap-fqdn-and-session-url-specified.nix b/tests/modules/programs/mujmap/mujmap-fqdn-and-session-url-specified.nix new file mode 100644 index 000000000..03c3f542e --- /dev/null +++ b/tests/modules/programs/mujmap/mujmap-fqdn-and-session-url-specified.nix @@ -0,0 +1,26 @@ +{ config, lib, pkgs, ... }: + +with lib; + +{ + imports = [ ../../accounts/email-test-accounts.nix ]; + + config = { + programs.mujmap.enable = true; + programs.mujmap.package = config.lib.test.mkStubPackage { }; + + accounts.email.accounts."hm@example.com" = { + jmap.host = "example.com"; + jmap.sessionUrl = "https://jmap.example.com/"; + mujmap.enable = true; + notmuch.enable = true; + }; + + nmt.script = '' + assertFileExists home-files/Mail/hm@example.com/mujmap.toml + assertFileContent home-files/Mail/hm@example.com/mujmap.toml ${ + ./mujmap-defaults-expected.toml + } + ''; + }; +}