1
0
Fork 0
mirror of https://github.com/nix-community/home-manager.git synced 2025-12-02 15:11:03 +01:00
home-manager/modules/programs/neomutt/default.nix
Adam Dinwoodie dbfcd3292d accounts.email: add option to disable an account
Allow a user to disable an email account by setting
`accounts.email.accounts.<name>.enable = false`.  This is useful if
someone wants to configure email accounts globally but only use them in
certain circumstances.

Everywhere email account configuration is used, check if the account is
enabled before checking any attributes of the account.
2025-08-07 14:08:46 -05:00

559 lines
15 KiB
Nix

{
config,
lib,
pkgs,
...
}:
let
inherit (lib)
attrValues
concatStringsSep
concatMapStringsSep
filter
filterAttrs
isString
mkIf
mkOption
optionalString
types
;
cfg = config.programs.neomutt;
neomuttAccountsCfg = filterAttrs (
n: a: a.enable && a.neomutt.enable
) config.accounts.email.accounts;
neomuttAccounts = attrValues neomuttAccountsCfg;
accountCommandNeeded = lib.any (
a:
a.neomutt.mailboxType == "imap"
|| (lib.any (m: !isString m && m.type == "imap") a.neomutt.extraMailboxes)
) neomuttAccounts;
accountCommand =
let
imapAccounts = filter (
a: a.imap.host != null && a.userName != null && a.passwordCommand != null
) neomuttAccounts;
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 = lib.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 = "%D%?F? [%F]?%* %?N?%N/?%S";
description = ''
Sidebar format. Check neomutt documentation for details.
'';
};
};
};
sortOptions = [
"date"
"date-received"
"from"
"mailbox-order"
"score"
"size"
"spam"
"subject"
"threads"
"to"
];
bindModule = types.submodule {
options = {
map = mkOption {
type =
let
menus = [
"alias"
"attach"
"browser"
"compose"
"editor"
"generic"
"index"
"mix"
"pager"
"pgp"
"postpone"
"query"
"smime"
];
in
with types;
either (enum menus) (listOf (enum menus));
default = [ "index" ];
description = "Select the menu to bind the command to.";
};
key = mkOption {
type = types.str;
example = "<left>";
description = ''
The key to bind.
If you want to bind '\Cp' for example, which would be Ctrl + 'p', you need to escape twice: '\\Cp'!
'';
};
action = mkOption {
type = types.str;
example = "<enter-command>toggle sidebar_visible<enter><refresh>";
description = "Specify the action to take.";
};
};
};
mkNotmuchVirtualboxes =
virtualMailboxes:
"${concatStringsSep "\n" (
map (
{
name,
query,
limit,
type,
}:
''virtual-mailboxes "${name}" "notmuch://?query=${lib.escapeURL query}${
optionalString (limit != null) "&limit=${toString limit}"
}${optionalString (type != null) "&type=${type}"}"''
) virtualMailboxes
)}";
setOption = n: v: if v == null then "unset ${n}" else "set ${n}=${v}";
escape = lib.replaceStrings [ "%" ] [ "%25" ];
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}'";
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 if passwordCommand != null then
let
smtpProto = if smtp.tls.enable && !smtp.tls.useStartTls then "smtps" else "smtp";
smtpPort = if smtp.port != null then ":${toString smtp.port}" else "";
smtpBaseUrl = "${smtpProto}://${escape userName}@${smtp.host}${smtpPort}";
in
{
smtp_url = "'${smtpBaseUrl}'";
smtp_pass = ''"`${passCmd}`"'';
}
else
{ };
genAccountConfig =
account:
let
folderHook = lib.mapAttrsToList setOption (
genCommonFolderHooks account
// lib.optionalAttrs cfg.changeFolderWhenSourcingAccount {
folder = "'${accountRoot account}'";
}
);
in
''
${concatStringsSep "\n" folderHook}
'';
registerAccount =
account:
let
mailboxes =
if account.neomutt.mailboxName == null then
"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:
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 "${mailboxroot}/${extra.mailbox}"''
else
''named-mailboxes "${extra.name}" "${mailboxroot}/${extra.mailbox}"''
) account.neomutt.extraMailboxes;
in
[ "## register account ${account.name}" ]
++ lib.optional account.neomutt.showDefaultMailbox ''${mailboxes} "${mailroot}/${account.folders.inbox}"''
++ [
extraMailboxes
''
${hookName} ${mailroot}/ " \
source ${accountFilename account} "
''
];
mraSection =
account:
if account.imap.host != null || account.maildir != null then
genAccountConfig account
else
throw "Only maildir and IMAP is supported at the moment";
optionsStr = attrs: concatStringsSep "\n" (lib.mapAttrsToList setOption attrs);
sidebarSection = ''
# Sidebar
set sidebar_visible = yes
set sidebar_short_path = ${lib.hm.booleans.yesNo cfg.sidebar.shortPath}
set sidebar_width = ${toString cfg.sidebar.width}
set sidebar_format = '${cfg.sidebar.format}'
'';
genBindMapper =
bindType:
concatMapStringsSep "\n" (
bind: ''${bindType} ${concatStringsSep "," (lib.toList bind.map)} ${bind.key} "${bind.action}"''
);
bindSection = (genBindMapper "bind") cfg.binds;
macroSection = (genBindMapper "macro") cfg.macros;
mailCheckSection = ''
set mail_check_stats
set mail_check_stats_interval = ${toString cfg.checkStatsInterval}
'';
notmuchSection =
account:
let
virtualMailboxes = account.notmuch.neomutt.virtualMailboxes;
in
''
# notmuch section
set nm_default_uri = "notmuch://${config.accounts.email.maildirBasePath}"
${optionalString (account.notmuch.neomutt.enable && builtins.length virtualMailboxes > 0) (
mkNotmuchVirtualboxes virtualMailboxes
)}
'';
accountStr =
account:
with account;
let
signature =
if account.signature.showSignature == "none" then
"unset signature"
else if account.signature.command != null then
''set signature = "${account.signature.command}|"''
else
"set signature = ${pkgs.writeText "signature.txt" account.signature.text}";
in
concatStringsSep "\n" (
[
''
# Generated by Home Manager.${optionalString cfg.unmailboxes ''
unmailboxes *
''}
set ssl_force_tls = ${lib.hm.booleans.yesNo (imap.tls.enable || imap.tls.useStartTls)}
set certificate_file=${toString config.accounts.email.certificatesFile}
# GPG section
set crypt_autosign = ${lib.hm.booleans.yesNo (gpg.signByDefault or false)}
set crypt_opportunistic_encrypt = ${lib.hm.booleans.yesNo (gpg.encryptByDefault 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)}
''
]
++ (lib.optional (cfg.checkStatsInterval != null) mailCheckSection)
++ (lib.optional cfg.sidebar.enable sidebarSection)
++ [
''
# MRA section
${mraSection account}
# Extra configuration
${account.neomutt.extraConfig}
${signature}
''
]
++ lib.optional (account.notmuch.enable && account.notmuch.neomutt.enable) (notmuchSection account)
);
in
{
options = {
programs.neomutt = {
enable = lib.mkEnableOption "the NeoMutt mail client";
package = lib.mkPackageOption pkgs "neomutt" { };
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 {
# Allow users to choose any option from sortOptions, or any option prefixed with "reverse-"
type = types.enum (
builtins.concatMap (_pre: map (_opt: _pre + _opt) sortOptions) [
""
"reverse-"
"last-"
"reverse-last-"
]
);
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.";
};
changeFolderWhenSourcingAccount =
lib.mkEnableOption "changing the folder when sourcing an account"
// {
default = true;
};
sourcePrimaryAccount = lib.mkEnableOption "source the primary account by default" // {
default = true;
};
unmailboxes = mkOption {
type = types.bool;
default = false;
description = ''
Set `unmailboxes *` at the start of account configurations.
It removes previous sidebar mailboxes when sourcing an account configuration.
See <http://www.mutt.org/doc/manual/#mailboxes> for more information.
'';
};
extraConfig = mkOption {
type = types.lines;
default = "";
description = "Extra configuration appended to the end.";
};
};
accounts.email.accounts = mkOption {
type = with types; attrsOf (submodule (import ./accounts.nix));
};
};
config = mkIf cfg.enable {
home.packages = [ cfg.package ];
home.file =
let
rcFile = account: {
"${accountFilename account}".text = accountStr account;
};
in
lib.foldl' (a: b: a // b) { } (map rcFile neomuttAccounts);
xdg.configFile."neomutt/neomuttrc" = mkIf (neomuttAccounts != [ ]) {
text =
let
# Find the primary account, if it has neomutt enabled;
# otherwise use the first neomutt account as primary.
primary = lib.head (filter (a: a.primary) neomuttAccounts ++ neomuttAccounts);
in
concatStringsSep "\n" (
[
"# 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"
"set crypt_use_gpgme = yes"
"alternative_order text/enriched text/plain text"
"set delete = yes"
(optionalString cfg.vimKeys "source ${cfg.package}/share/doc/neomutt/vim-keys/vim-keys.rc")
]
++ (lib.optionals (cfg.binds != [ ]) [
''
# Binds''
bindSection
])
++ [
''
# Macros''
macroSection
"# Register accounts"
(optionalString accountCommandNeeded ''
set account_command = '${accountCommand}/bin/account-command.sh'
'')
]
++ (lib.flatten (map registerAccount neomuttAccounts))
++ [
(optionalString cfg.sourcePrimaryAccount ''
# Source primary account
source ${accountFilename primary}
'')
"# Extra configuration"
(optionsStr cfg.settings)
cfg.extraConfig
]
);
};
assertions = [
{
assertion = ((filter (b: (lib.length (lib.toList b.map)) == 0) (cfg.binds ++ cfg.macros)) == [ ]);
message = "The 'programs.neomutt.(binds|macros).map' list must contain at least one element.";
}
];
warnings =
let
hasOldBinds = binds: (filter (b: !(lib.isList b.map)) binds) != [ ];
in
mkIf (hasOldBinds (cfg.binds ++ cfg.macros)) [
"Specifying 'programs.neomutt.(binds|macros).map' as a string is deprecated, use a list of strings instead. See https://github.com/nix-community/home-manager/pull/1885."
];
};
}