From a09cfdbaf11c821340cff24d9ad1c264708ee12e Mon Sep 17 00:00:00 2001 From: Christian Dannie Storgaard Date: Sun, 11 Feb 2024 19:22:37 +0200 Subject: [PATCH] neomutt: Initial IMAP support (#4597) neomutt: Updated options and added tests neomutt: Added test for individual mailbox type neomutt: Formatted code neomutt: Enable ssl_force_tls based on IMAP instead of SMTP neomutt: Applied suggestions from @chayleaf neomutt: fix breaking tests --- modules/programs/neomutt-accounts.nix | 16 +++ modules/programs/neomutt.nix | 111 +++++++++++++++--- .../neomutt/account-command.sh-expected | 39 ++++++ tests/modules/programs/neomutt/default.nix | 2 + .../neomutt/hm-example.com-imap-expected.conf | 35 ++++++ .../neomutt/neomutt-with-imap-expected.conf | 34 ++++++ ...utt-with-imap-type-mailboxes-expected.conf | 36 ++++++ .../neomutt-with-imap-type-mailboxes.nix | 57 +++++++++ .../programs/neomutt/neomutt-with-imap.nix | 44 +++++++ 9 files changed, 360 insertions(+), 14 deletions(-) create mode 100644 tests/modules/programs/neomutt/account-command.sh-expected create mode 100644 tests/modules/programs/neomutt/hm-example.com-imap-expected.conf create mode 100644 tests/modules/programs/neomutt/neomutt-with-imap-expected.conf create mode 100644 tests/modules/programs/neomutt/neomutt-with-imap-type-mailboxes-expected.conf create mode 100644 tests/modules/programs/neomutt/neomutt-with-imap-type-mailboxes.nix create mode 100644 tests/modules/programs/neomutt/neomutt-with-imap.nix diff --git a/modules/programs/neomutt-accounts.nix b/modules/programs/neomutt-accounts.nix index 27e3b1229..a7ed7ec62 100644 --- a/modules/programs/neomutt-accounts.nix +++ b/modules/programs/neomutt-accounts.nix @@ -17,6 +17,14 @@ let default = null; description = "Name to display"; }; + + type = mkOption { + type = types.nullOr (types.enum [ "maildir" "imap" ]); + example = "imap"; + default = null; + description = + "Whether this mailbox is a maildir folder or an IMAP mailbox"; + }; }; }; @@ -75,6 +83,14 @@ in { description = "Use a different name as mailbox name"; }; + mailboxType = mkOption { + type = types.enum [ "maildir" "imap" ]; + default = "maildir"; + example = "imap"; + description = + "Whether this account uses maildir folders or IMAP mailboxes"; + }; + extraMailboxes = mkOption { type = with types; listOf (either str (submodule extraMailboxOptions)); default = [ ]; diff --git a/modules/programs/neomutt.nix b/modules/programs/neomutt.nix index fa9c2624a..1067bda1a 100644 --- a/modules/programs/neomutt.nix +++ b/modules/programs/neomutt.nix @@ -8,6 +8,59 @@ let neomuttAccounts = filter (a: a.neomutt.enable) (attrValues config.accounts.email.accounts); + accountCommandNeeded = any (a: + a.neomutt.enable && (a.neomutt.mailboxType == "imap" + || (any (m: !isString m && m.type == "imap") a.neomutt.extraMailboxes))) + (attrValues config.accounts.email.accounts); + + accountCommand = let + imapAccounts = filter (a: + a.neomutt.enable && a.imap.host != null && a.userName != null + && a.passwordCommand != null) (attrValues config.accounts.email.accounts); + accountCase = account: + let passwordCmd = toString account.passwordCommand; + in '' + ${account.userName}@${account.imap.host}) + found=1 + username="${account.userName}" + password="$(${passwordCmd})" + ;;''; + in pkgs.writeShellScriptBin "account-command.sh" '' + # Automatically set login variables based on the current account. + # This requires NeoMutt >= 2022-05-16 + + while [ ! -z "$1" ]; do + case "$1" in + --hostname) + shift + hostname="$1" + ;; + --username) + shift + username="$1@" + ;; + --type) + shift + type="$1" + ;; + *) + exit 1 + ;; + esac + shift + done + + found= + case "''${username}''${hostname}" in + ${concatMapStringsSep "\n" accountCase imapAccounts} + esac + + if [ -n "$found" ]; then + echo "username: $username" + echo "password: $password" + fi + ''; + sidebarModule = types.submodule { options = { enable = mkEnableOption "sidebar support"; @@ -101,6 +154,21 @@ let accountFilename = account: config.xdg.configHome + "/neomutt/" + account.name; + accountRootIMAP = account: + let + userName = + lib.optionalString (account.userName != null) "${account.userName}@"; + port = lib.optionalString (account.imap.port != null) + ":${toString account.imap.port}"; + protocol = if account.imap.tls.enable then "imaps" else "imap"; + in "${protocol}://${userName}${account.imap.host}${port}"; + + accountRoot = account: + if account.neomutt.mailboxType == "imap" then + accountRootIMAP account + else + account.maildir.absPath; + genCommonFolderHooks = account: with account; { from = "'${address}'"; @@ -128,12 +196,12 @@ let smtp_pass = ''"`${passCmd}`"''; }; - genMaildirAccountConfig = account: + genAccountConfig = account: with account; let folderHook = mapAttrsToList setOption (genCommonFolderHooks account // optionalAttrs cfg.changeFolderWhenSourcingAccount { - folder = "'${account.maildir.absPath}'"; + folder = "'${accountRoot account}'"; }); in '' ${concatStringsSep "\n" folderHook} @@ -145,29 +213,40 @@ let "mailboxes" else ''named-mailboxes "${account.neomutt.mailboxName}"''; + mailroot = accountRoot account; + hookName = if account.neomutt.mailboxType == "imap" then + "account-hook" + else + "folder-hook"; extraMailboxes = concatMapStringsSep "\n" (extra: - if isString extra then - ''mailboxes "${account.maildir.absPath}/${extra}"'' + let + mailboxroot = if !isString extra && extra.type == "imap" then + accountRootIMAP account + else if !isString extra && extra.type == "maildir" then + account.maildir.absPath + else + mailroot; + in if isString extra then + ''mailboxes "${mailboxroot}/${extra}"'' else if extra.name == null then - ''mailboxes "${account.maildir.absPath}/${extra.mailbox}"'' + ''mailboxes "${mailboxroot}/${extra.mailbox}"'' else - '' - named-mailboxes "${extra.name}" "${account.maildir.absPath}/${extra.mailbox}"'') + ''named-mailboxes "${extra.name}" "${mailboxroot}/${extra.mailbox}"'') account.neomutt.extraMailboxes; in with account; '' # register account ${name} - ${mailboxes} "${maildir.absPath}/${folders.inbox}" + ${mailboxes} "${mailroot}/${folders.inbox}" ${extraMailboxes} - folder-hook ${maildir.absPath}/ " \ + ${hookName} ${mailroot}/ " \ source ${accountFilename account} " ''; mraSection = account: with account; - if account.maildir != null then - genMaildirAccountConfig account + if account.imap.host != null || account.maildir != null then + genAccountConfig account else - throw "Only maildir is supported at the moment"; + throw "Only maildir and IMAP is supported at the moment"; optionsStr = attrs: concatStringsSep "\n" (mapAttrsToList setOption attrs); @@ -219,7 +298,7 @@ let in '' # Generated by Home Manager. set ssl_force_tls = ${ - lib.hm.booleans.yesNo (smtp.tls.enable || smtp.tls.useStartTls) + lib.hm.booleans.yesNo (imap.tls.enable || imap.tls.useStartTls) } set certificate_file=${toString config.accounts.email.certificatesFile} @@ -366,7 +445,11 @@ in { "source ${pkgs.neomutt}/share/doc/neomutt/vim-keys/vim-keys.rc"} # Register accounts - ${concatMapStringsSep "\n" registerAccount neomuttAccounts} + ${ + optionalString (accountCommandNeeded) '' + set account_command = '${accountCommand}/bin/account-command.sh' + '' + }${concatMapStringsSep "\n" registerAccount neomuttAccounts} # Source primary account source ${accountFilename primary} diff --git a/tests/modules/programs/neomutt/account-command.sh-expected b/tests/modules/programs/neomutt/account-command.sh-expected new file mode 100644 index 000000000..5698cf99c --- /dev/null +++ b/tests/modules/programs/neomutt/account-command.sh-expected @@ -0,0 +1,39 @@ +#!/nix/store/00000000000000000000000000000000-bash/bin/bash +# Automatically set login variables based on the current account. +# This requires NeoMutt >= 2022-05-16 + +while [ ! -z "$1" ]; do + case "$1" in + --hostname) + shift + hostname="$1" + ;; + --username) + shift + username="$1@" + ;; + --type) + shift + type="$1" + ;; + *) + exit 1 + ;; + esac +shift +done + +found= +case "${username}${hostname}" in + home.manager@imap.example.com) + found=1 + username="home.manager" + password="$(password-command)" + ;; +esac + +if [ -n "$found" ]; then + echo "username: $username" + echo "password: $password" +fi + diff --git a/tests/modules/programs/neomutt/default.nix b/tests/modules/programs/neomutt/default.nix index 88e90c1d4..d13b870e5 100644 --- a/tests/modules/programs/neomutt/default.nix +++ b/tests/modules/programs/neomutt/default.nix @@ -1,6 +1,7 @@ { neomutt-simple = ./neomutt.nix; neomutt-with-msmtp = ./neomutt-with-msmtp.nix; + neomutt-with-imap = ./neomutt-with-imap.nix; neomutt-not-primary = ./neomutt-not-primary.nix; neomutt-with-binds = ./neomutt-with-binds.nix; neomutt-with-binds-with-warning = ./neomutt-with-binds-with-warning.nix; @@ -9,6 +10,7 @@ neomutt-with-gpg = ./neomutt-with-gpg.nix; neomutt-no-folder-change = ./neomutt-no-folder-change.nix; neomutt-with-named-mailboxes = ./neomutt-with-named-mailboxes.nix; + neomutt-with-imap-type-mailboxes = ./neomutt-with-imap-type-mailboxes.nix; neomutt-with-signature = ./neomutt-with-signature.nix; neomutt-with-signature-command = ./neomutt-with-signature-command.nix; neomutt-with-starttls = ./neomutt-with-starttls.nix; diff --git a/tests/modules/programs/neomutt/hm-example.com-imap-expected.conf b/tests/modules/programs/neomutt/hm-example.com-imap-expected.conf new file mode 100644 index 000000000..bfaeeab34 --- /dev/null +++ b/tests/modules/programs/neomutt/hm-example.com-imap-expected.conf @@ -0,0 +1,35 @@ +# 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 crypt_opportunistic_encrypt = 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='imaps://home.manager@imap.example.com:993' +set from='hm@example.com' +set postponed='+Drafts' +set realname='H. M. Test' +set record='+Sent' +set spoolfile='+Inbox' +set trash='+Trash' + + +# Extra configuration +color status cyan default + + +unset signature diff --git a/tests/modules/programs/neomutt/neomutt-with-imap-expected.conf b/tests/modules/programs/neomutt/neomutt-with-imap-expected.conf new file mode 100644 index 000000000..f3e3051e6 --- /dev/null +++ b/tests/modules/programs/neomutt/neomutt-with-imap-expected.conf @@ -0,0 +1,34 @@ +# 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 + + + + +# Register accounts +set account_command = '/nix/store/00000000000000000000000000000000-account-command.sh/bin/account-command.sh' +# register account hm@example.com +mailboxes "imaps://home.manager@imap.example.com:993/Inbox" + +account-hook imaps://home.manager@imap.example.com:993/ " \ + source /home/hm-user/.config/neomutt/hm@example.com " + + +# Source primary account +source /home/hm-user/.config/neomutt/hm@example.com + +# Extra configuration + + + diff --git a/tests/modules/programs/neomutt/neomutt-with-imap-type-mailboxes-expected.conf b/tests/modules/programs/neomutt/neomutt-with-imap-type-mailboxes-expected.conf new file mode 100644 index 000000000..f8acd9fcc --- /dev/null +++ b/tests/modules/programs/neomutt/neomutt-with-imap-type-mailboxes-expected.conf @@ -0,0 +1,36 @@ +# 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 + + + + +# Register accounts +set account_command = '/nix/store/00000000000000000000000000000000-account-command.sh/bin/account-command.sh' +# register account hm@example.com +named-mailboxes "someCustomName" "/home/hm-user/Mail/hm@example.com/Inbox" +mailboxes "/home/hm-user/Mail/hm@example.com/Sent" +named-mailboxes "Spam" "imaps://home.manager@imap.example.com:993/Junk Email" +mailboxes "/home/hm-user/Mail/hm@example.com/Trash" +folder-hook /home/hm-user/Mail/hm@example.com/ " \ + source /home/hm-user/.config/neomutt/hm@example.com " + + +# Source primary account +source /home/hm-user/.config/neomutt/hm@example.com + +# Extra configuration + + + diff --git a/tests/modules/programs/neomutt/neomutt-with-imap-type-mailboxes.nix b/tests/modules/programs/neomutt/neomutt-with-imap-type-mailboxes.nix new file mode 100644 index 000000000..10dcdbed6 --- /dev/null +++ b/tests/modules/programs/neomutt/neomutt-with-imap-type-mailboxes.nix @@ -0,0 +1,57 @@ +{ config, lib, pkgs, ... }: + +with lib; + +{ + imports = [ ../../accounts/email-test-accounts.nix ]; + + config = { + accounts.email.accounts = { + "hm@example.com" = { + notmuch.enable = true; + neomutt = { + enable = true; + extraConfig = '' + color status cyan default + ''; + mailboxName = "someCustomName"; + extraMailboxes = [ + "Sent" + { + mailbox = "Junk Email"; + name = "Spam"; + type = "imap"; + } + { mailbox = "Trash"; } + ]; + }; + imap.port = 993; + }; + }; + + programs.neomutt = { + enable = true; + vimKeys = false; + }; + + test.stubs.neomutt = { }; + + nmt.script = '' + assertFileExists home-files/.config/neomutt/neomuttrc + assertFileExists home-files/.config/neomutt/hm@example.com + assertFileContent $(normalizeStorePaths home-files/.config/neomutt/neomuttrc) ${ + ./neomutt-with-imap-type-mailboxes-expected.conf + } + assertFileContent home-files/.config/neomutt/hm@example.com ${ + ./hm-example.com-expected + } + + confFile=$(grep -o \ + '/nix/store/.*-account-command.sh/bin/account-command.sh' \ + $TESTED/home-files/.config/neomutt/neomuttrc) + assertFileContent "$(normalizeStorePaths "$confFile")" ${ + ./account-command.sh-expected + } + ''; + }; +} diff --git a/tests/modules/programs/neomutt/neomutt-with-imap.nix b/tests/modules/programs/neomutt/neomutt-with-imap.nix new file mode 100644 index 000000000..3f234fde7 --- /dev/null +++ b/tests/modules/programs/neomutt/neomutt-with-imap.nix @@ -0,0 +1,44 @@ +{ config, lib, pkgs, ... }: + +with lib; + +{ + imports = [ ../../accounts/email-test-accounts.nix ]; + + config = { + accounts.email.accounts = { + "hm@example.com" = { + neomutt = { + enable = true; + mailboxType = "imap"; + extraConfig = '' + color status cyan default + ''; + }; + imap.port = 993; + }; + }; + + programs.neomutt.enable = true; + + test.stubs.neomutt = { }; + + nmt.script = '' + assertFileExists home-files/.config/neomutt/neomuttrc + assertFileExists home-files/.config/neomutt/hm@example.com + assertFileContent $(normalizeStorePaths home-files/.config/neomutt/neomuttrc) ${ + ./neomutt-with-imap-expected.conf + } + assertFileContent home-files/.config/neomutt/hm@example.com ${ + ./hm-example.com-imap-expected.conf + } + + confFile=$(grep -o \ + '/nix/store/.*-account-command.sh/bin/account-command.sh' \ + $TESTED/home-files/.config/neomutt/neomuttrc) + assertFileContent "$(normalizeStorePaths "$confFile")" ${ + ./account-command.sh-expected + } + ''; + }; +}