From d059b9448a04100cdd231872254283dc278a4ea0 Mon Sep 17 00:00:00 2001
From: Eliza Velasquez <4576666+elizagamedev@users.noreply.github.com>
Date: Wed, 11 May 2022 22:11:22 -0700
Subject: [PATCH] mujmap: add module
mujmap is a tool that synchronizes mail between a mail server and
notmuch via JMAP. It's very similar to lieer, so I heavily based the
implementation of the notmuch module on lieer's. I did not include an
equivalent to lieer's periodic synchronization service, however,
because I plan to soon introduce a daemon mode to mujmap.
https://github.com/elizagamedev/mujmap
---
.github/CODEOWNERS | 3 +
modules/misc/news.nix | 7 +
modules/modules.nix | 1 +
modules/programs/mujmap.nix | 315 ++++++++++++++++++
tests/default.nix | 1 +
tests/modules/programs/mujmap/default.nix | 5 +
.../mujmap/mujmap-defaults-expected.toml | 14 +
.../programs/mujmap/mujmap-defaults.nix | 25 ++
.../mujmap-fqdn-and-session-url-specified.nix | 26 ++
9 files changed, 397 insertions(+)
create mode 100644 modules/programs/mujmap.nix
create mode 100644 tests/modules/programs/mujmap/default.nix
create mode 100644 tests/modules/programs/mujmap/mujmap-defaults-expected.toml
create mode 100644 tests/modules/programs/mujmap/mujmap-defaults.nix
create mode 100644 tests/modules/programs/mujmap/mujmap-fqdn-and-session-url-specified.nix
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
+ }
+ '';
+ };
+}