diff --git a/modules/accounts/email.nix b/modules/accounts/email.nix index cc9d0dc2..b3a9db94 100644 --- a/modules/accounts/email.nix +++ b/modules/accounts/email.nix @@ -391,6 +391,7 @@ in (import ../programs/getmail-accounts.nix) (import ../programs/mbsync-accounts.nix) (import ../programs/msmtp-accounts.nix) + (import ../programs/neomutt-accounts.nix) (import ../programs/notmuch-accounts.nix) (import ../programs/offlineimap-accounts.nix) ]); diff --git a/modules/misc/news.nix b/modules/misc/news.nix index 63c865b6..6b01617f 100644 --- a/modules/misc/news.nix +++ b/modules/misc/news.nix @@ -1311,6 +1311,13 @@ in A new module is available: 'services.grobi'. ''; } + + { + time = "2020-01-26T19:37:57+00:00"; + message = '' + A new module is available: 'programs.neomutt'. + ''; + } ]; }; } diff --git a/modules/modules.nix b/modules/modules.nix index dbb3f1df..64418dba 100644 --- a/modules/modules.nix +++ b/modules/modules.nix @@ -78,6 +78,7 @@ let (loadModule ./programs/mercurial.nix { }) (loadModule ./programs/mpv.nix { }) (loadModule ./programs/msmtp.nix { }) + (loadModule ./programs/neomutt.nix { }) (loadModule ./programs/neovim.nix { }) (loadModule ./programs/newsboat.nix { }) (loadModule ./programs/noti.nix { }) diff --git a/modules/programs/neomutt-accounts.nix b/modules/programs/neomutt-accounts.nix new file mode 100644 index 00000000..a374f116 --- /dev/null +++ b/modules/programs/neomutt-accounts.nix @@ -0,0 +1,35 @@ +{ config, lib, ... }: + +with lib; + +{ + options.neomutt = { + enable = mkEnableOption "NeoMutt"; + + sendMailCommand = mkOption { + type = types.nullOr types.str; + default = null; + example = "msmtpq --read-envelope-from --read-recipients"; + description = '' + Command to send a mail. If not set, neomutt will be in charge of sending mails. + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + example = "color status cyan default"; + description = '' + Extra lines to add to the folder hook for this account. + ''; + }; + }; + + config = mkIf config.neomutt.enable { + neomutt.sendMailCommand = mkOptionDefault ( + if config.msmtp.enable + then "msmtpq --read-envelope-from --read-recipients" + else null + ); + }; +} diff --git a/modules/programs/neomutt.nix b/modules/programs/neomutt.nix new file mode 100644 index 00000000..1f7022e0 --- /dev/null +++ b/modules/programs/neomutt.nix @@ -0,0 +1,319 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.programs.neomutt; + + neomuttAccounts = filter (a: a.neomutt.enable) + (attrValues config.accounts.email.accounts); + + sidebarModule = types.submodule { + options = { + enable = mkEnableOption "sidebar support"; + + width = mkOption { + type = types.int; + default = 22; + description = "Width of the sidebar"; + }; + + shortPath = mkOption { + type = types.bool; + default = true; + description = '' + By default sidebar shows the full path of the mailbox, but + with this enabled only the relative name is shown. + ''; + }; + + format = mkOption { + type = types.str; + default = "%B%?F? [%F]?%* %?N?%N/?%S"; + description = '' + Sidebar format. Check neomutt documentation for details. + ''; + }; + }; + }; + + bindModule = types.submodule { + options = { + map = mkOption { + type = types.enum [ + "alias" + "attach" + "browser" + "compose" + "editor" + "generic" + "index" + "mix" + "pager" + "pgp" + "postpone" + "query" + "smime" + ]; + default = "index"; + description = "Select the menu to bind the command to."; + }; + + key = mkOption { + type = types.str; + example = ""; + description = "The key to bind."; + }; + + action = mkOption { + type = types.str; + example = "toggle sidebar_visible"; + description = "Specify the action to take."; + }; + }; + }; + + yesno = x: if x then "yes" else "no"; + setOption = n: v: if v == null then "unset ${n}" else "set ${n}=${v}"; + escape = replaceStrings ["%"] ["%25"]; + + accountFilename = account: + config.xdg.configHome + "/neomutt/" + account.name; + + genCommonFolderHooks = account: with account; + { + from = "'${address}'"; + realname = "'${realName}'"; + spoolfile = "'+${folders.inbox}'"; + record = if folders.sent == null then null else "'+${folders.sent}'"; + postponed = "'+${folders.drafts}'"; + trash = "'+${folders.trash}'"; + }; + + mtaSection = account: with account; + let + passCmd = concatStringsSep " " passwordCommand; + in + if neomutt.sendMailCommand != null then { + sendmail = "'${neomutt.sendMailCommand}'"; + } else let + smtpProto = if smtp.tls.enable then "smtps" else "smtp"; + smtpBaseUrl = "${smtpProto}://${escape userName}@${smtp.host}"; + in { + smtp_url = "'${smtpBaseUrl}'"; + smtp_pass = "'`${passCmd}`'"; + }; + + genMaildirAccountConfig = account: with account; + let + folderHook = + mapAttrsToList setOption ( + genCommonFolderHooks account + // { folder = "'${account.maildir.absPath}'"; } + ) + ++ optional (neomutt.extraConfig != "") neomutt.extraConfig; + in + '' + ${concatStringsSep "\n" folderHook} + ''; + + registerAccount = account: with account; + '' + # register account ${name} + mailboxes "${account.maildir.absPath}/${folders.inbox}" + folder-hook ${account.maildir.absPath}/ " \ + source ${accountFilename account} " + ''; + + mraSection = account: with account; + if account.maildir != null + then genMaildirAccountConfig account + else throw "Only maildir is supported at the moment"; + + optionsStr = attrs: + concatStringsSep "\n" (mapAttrsToList setOption attrs); + + + sidebarSection = '' + # Sidebar + set sidebar_visible = yes + set sidebar_short_path = ${yesno cfg.sidebar.shortPath} + set sidebar_width = ${toString cfg.sidebar.width} + set sidebar_format = '${cfg.sidebar.format}' + ''; + + bindSection = + concatMapStringsSep + "\n" + (bind: "bind ${bind.map} ${bind.key} \"${bind.action}\"") + cfg.binds; + + macroSection = + concatMapStringsSep + "\n" + (bind: "macro ${bind.map} ${bind.key} \"${bind.action}\"") + cfg.macros; + + mailCheckSection = '' + set mail_check_stats + set mail_check_stats_interval = ${toString cfg.checkStatsInterval} + ''; + + notmuchSection = account: with account; '' + # notmuch section + set nm_default_uri = "notmuch://${config.accounts.email.maildirBasePath}" + virtual-mailboxes "My INBOX" "notmuch://?query=tag:inbox" + ''; + + accountStr = account: with account; '' + # Generated by Home Manager. + set ssl_force_tls = yes + set certificate_file=${config.accounts.email.certificatesFile} + + # GPG section + set crypt_use_gpgme = yes + set crypt_autosign = ${yesno (gpg.signByDefault or false)} + set pgp_use_gpg_agent = yes + set mbox_type = ${if maildir != null then "Maildir" else "mbox"} + set sort = "${cfg.sort}" + + # MTA section + ${optionsStr (mtaSection account)} + + ${optionalString (cfg.checkStatsInterval != null) mailCheckSection} + + ${optionalString cfg.sidebar.enable sidebarSection} + + # MRA section + ${mraSection account} + + # Extra configuration + ${account.neomutt.extraConfig} + '' + + optionalString (account.signature.showSignature != "none") '' + set signature = ${pkgs.writeText "signature.txt" account.signature.text} + '' + + optionalString account.notmuch.enable (notmuchSection account); + +in + +{ + options = { + programs.neomutt = { + enable = mkEnableOption "the NeoMutt mail client"; + + sidebar = mkOption { + type = sidebarModule; + default = {}; + description = "Options related to the sidebar."; + }; + + binds = mkOption { + type = types.listOf bindModule; + default = []; + description = "List of keybindings."; + }; + + macros = mkOption { + type = types.listOf bindModule; + default = []; + description = "List of macros."; + }; + + sort = mkOption { + type = types.enum [ + "date" + "date-received" + "from" + "mailbox-order" + "score" + "size" + "spam" + "subject" + "threads" + "to" + ]; + default = "threads"; + description = "Sorting method on messages."; + }; + + vimKeys = mkOption { + type = types.bool; + default = false; + description = "Enable vim-like bindings."; + }; + + checkStatsInterval = mkOption { + type = types.nullOr types.int; + default = null; + example = 60; + description = "Enable and set the interval of automatic mail check."; + }; + + editor = mkOption { + type = types.str; + default = "$EDITOR"; + description = "Select the editor used for writing mail."; + }; + + settings = mkOption { + type = types.attrsOf types.str; + default = {}; + description = "Extra configuration appended to the end."; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = "Extra configuration appended to the end."; + }; + }; + }; + + config = mkIf cfg.enable { + home.packages = [ pkgs.neomutt ]; + home.file = let + rcFile = account: { + "${accountFilename account}".text = accountStr account; + }; + in + foldl' (a: b: a // b) {} (map rcFile neomuttAccounts); + + xdg.configFile."neomutt/neomuttrc" = mkIf (neomuttAccounts != []) { + text = + let + primary = filter (a: a.primary) neomuttAccounts; + in + '' + # Generated by Home Manager. + set header_cache = "${config.xdg.cacheHome}/neomutt/headers/" + set message_cachedir = "${config.xdg.cacheHome}/neomutt/messages/" + set editor = "${cfg.editor}" + set implicit_autoview = yes + + alternative_order text/enriched text/plain text + + set delete = yes + + # Binds + ${bindSection} + + # Macros + ${macroSection} + + ${optionalString cfg.vimKeys "source ${pkgs.neomutt}/share/doc/neomutt/vim-keys/vim-keys.rc"} + + # Extra configuration + ${optionsStr cfg.settings} + + ${cfg.extraConfig} + '' + + + concatMapStringsSep "\n" registerAccount neomuttAccounts + + + # source primary account + "source ${accountFilename (builtins.head primary)}"; + }; + }; +} diff --git a/tests/default.nix b/tests/default.nix index ab5e4a00..01b316cd 100644 --- a/tests/default.nix +++ b/tests/default.nix @@ -33,6 +33,7 @@ import nmt { // import ./modules/misc/xdg // import ./modules/misc/xsession // import ./modules/programs/firefox + // import ./modules/programs/neomutt // import ./modules/programs/rofi // import ./modules/services/sxhkd // import ./modules/systemd diff --git a/tests/modules/programs/neomutt/default.nix b/tests/modules/programs/neomutt/default.nix new file mode 100644 index 00000000..3c778e9e --- /dev/null +++ b/tests/modules/programs/neomutt/default.nix @@ -0,0 +1,3 @@ +{ + neomutt-simple = ./neomutt.nix; +} diff --git a/tests/modules/programs/neomutt/hm-example.com-expected b/tests/modules/programs/neomutt/hm-example.com-expected new file mode 100644 index 00000000..430509c3 --- /dev/null +++ b/tests/modules/programs/neomutt/hm-example.com-expected @@ -0,0 +1,37 @@ +# Generated by Home Manager. +set ssl_force_tls = yes +set certificate_file=/etc/ssl/certs/ca-certificates.crt + +# GPG section +set crypt_use_gpgme = yes +set crypt_autosign = no +set pgp_use_gpg_agent = yes +set mbox_type = Maildir +set sort = "threads" + +# MTA section +set smtp_pass='`password-command`' +set smtp_url='smtps://home.manager@smtp.example.com' + + + + + +# MRA section +set folder='/home/hm-user/Mail/hm@example.com' +set from='hm@example.com' +set postponed='+Drafts' +set realname='H. M. Test' +set record='+Sent' +set spoolfile='+Inbox' +set trash='+Trash' +color status cyan default + + + +# Extra configuration +color status cyan default + +# notmuch section +set nm_default_uri = "notmuch:///home/hm-user/Mail" +virtual-mailboxes "My INBOX" "notmuch://?query=tag:inbox" diff --git a/tests/modules/programs/neomutt/neomutt-expected.conf b/tests/modules/programs/neomutt/neomutt-expected.conf new file mode 100644 index 00000000..7711aa5a --- /dev/null +++ b/tests/modules/programs/neomutt/neomutt-expected.conf @@ -0,0 +1,27 @@ +# Generated by Home Manager. +set header_cache = "/home/hm-user/.cache/neomutt/headers/" +set message_cachedir = "/home/hm-user/.cache/neomutt/messages/" +set editor = "$EDITOR" +set implicit_autoview = yes + +alternative_order text/enriched text/plain text + +set delete = yes + +# Binds + + +# Macros + + + + +# Extra configuration + + + +# register account hm@example.com +mailboxes "/home/hm-user/Mail/hm@example.com/Inbox" +folder-hook /home/hm-user/Mail/hm@example.com/ " \ + source /home/hm-user/.config/neomutt/hm@example.com " +source /home/hm-user/.config/neomutt/hm@example.com \ No newline at end of file diff --git a/tests/modules/programs/neomutt/neomutt.nix b/tests/modules/programs/neomutt/neomutt.nix new file mode 100644 index 00000000..d7ffadd8 --- /dev/null +++ b/tests/modules/programs/neomutt/neomutt.nix @@ -0,0 +1,46 @@ +{ config, lib, pkgs, ... }: + +with lib; + +{ + imports = [ ../../accounts/email-test-accounts.nix ]; + + config = { + home.username = "hm-user"; + home.homeDirectory = "/home/hm-user"; + xdg.configHome = mkForce "/home/hm-user/.config"; + xdg.cacheHome = mkForce "/home/hm-user/.cache"; + + accounts.email.accounts = { + "hm@example.com" = { + primary = true; + notmuch.enable = true; + neomutt = { + enable = true; + extraConfig = '' + color status cyan default + ''; + }; + imap.port = 993; + }; + }; + + programs.neomutt = { + enable = true; + vimKeys = false; + }; + + nixpkgs.overlays = [ + (self: super: { + neomutt = pkgs.writeScriptBin "dummy-neomutt" ""; + }) + ]; + + nmt.script = '' + assertFileExists home-files/.config/neomutt/neomuttrc + assertFileExists home-files/.config/neomutt/hm@example.com + assertFileContent home-files/.config/neomutt/neomuttrc ${./neomutt-expected.conf} + assertFileContent home-files/.config/neomutt/hm@example.com ${./hm-example.com-expected} + ''; + }; +}