aboutsummaryrefslogtreecommitdiffstats
path: root/modules/communication/neomutt
diff options
context:
space:
mode:
authorArpit Chakladar <arpitchakladar+git@gmail.com>2026-07-15 21:55:29 +0530
committerArpit Chakladar <arpitchakladar+git@gmail.com>2026-07-15 21:55:29 +0530
commit6b5020927fbb0a041f54c2224d4734e97cf78097 (patch)
tree9abd3f51a75de1896ae9ecd0d04d01063b95cfc5 /modules/communication/neomutt
parent4dc0cd05ea21815cb6d10486706e5a84a327f2e3 (diff)
downloadhome-manager-config-6b5020927fbb0a041f54c2224d4734e97cf78097.tar.gz
home-manager-config-6b5020927fbb0a041f54c2224d4734e97cf78097.zip
feat: reworked the structure for communication/neomutt
Using neomutt as the module for email instead of just "email" as used previously. "email" was ambigious in that it was too vague on what it was supposed to do with email, now neomutt clearly specifies that we plan to use neomutt as our email client. However mbsync and notmuch are still used under the hood that is not reflected in the module name "neomutt". This is fine as they only serve the purpose of downloading and querying the emails which may be considered a background feature.
Diffstat (limited to 'modules/communication/neomutt')
-rw-r--r--modules/communication/neomutt/account/default.nix106
-rw-r--r--modules/communication/neomutt/account/flavors/gmail.nix45
-rw-r--r--modules/communication/neomutt/account/options.nix328
-rw-r--r--modules/communication/neomutt/assertions.nix7
-rw-r--r--modules/communication/neomutt/default.nix429
-rw-r--r--modules/communication/neomutt/neomutt.nix423
-rw-r--r--modules/communication/neomutt/sync/default.nix8
-rw-r--r--modules/communication/neomutt/sync/mbsync.nix9
-rw-r--r--modules/communication/neomutt/sync/notmuch.nix9
-rw-r--r--modules/communication/neomutt/theme.nix4
10 files changed, 939 insertions, 429 deletions
diff --git a/modules/communication/neomutt/account/default.nix b/modules/communication/neomutt/account/default.nix
new file mode 100644
index 0000000..183e5ff
--- /dev/null
+++ b/modules/communication/neomutt/account/default.nix
@@ -0,0 +1,106 @@
+{ config, lib, ... }:
+
+with lib;
+
+let
+ inherit (lib) types;
+in
+{
+ options.communication.neomutt.accounts = mkOption {
+ type = types.attrsOf (
+ types.submodule {
+ imports = [
+ ./options.nix
+ ./flavors/gmail.nix
+ ];
+ }
+ );
+ default = { };
+ description = "Specification of email accounts.";
+ };
+
+ config = mkIf config.communication.neomutt.enable {
+ accounts.email.accounts = mapAttrs (
+ name: account:
+ let
+ a = account;
+ in
+ {
+ inherit (a) realName address primary;
+ userName = if a.userName != null then a.userName else a.address;
+
+ passwordCommand =
+ if a.passwordGopassSecret != null then
+ "${lib.getExe config.security.gopass.package} -o ${a.passwordGopassSecret}"
+ else
+ null;
+
+ flavor = a.flavor;
+ inherit (a) aliases;
+
+ folders = {
+ inherit (a.folders) inbox trash;
+ }
+ // optionalAttrs (a.folders.drafts != null) { drafts = a.folders.drafts; }
+ // optionalAttrs (a.folders.sent != null) { sent = a.folders.sent; };
+
+ neomutt = {
+ enable = true;
+ mailboxType = a.neomutt.mailboxType;
+ extraConfig = a.neomutt.extraConfig;
+ };
+
+ mbsync = {
+ enable = true;
+ create = a.mbsync.create;
+ patterns = a.mbsync.patterns;
+ flatten = a.mbsync.flatten;
+ extraConfig = {
+ account = a.mbsync.extraConfig.account;
+ channel = a.mbsync.extraConfig.channel;
+ local = a.mbsync.extraConfig.local;
+ remote = a.mbsync.extraConfig.remote;
+ };
+ };
+
+ notmuch = {
+ enable = true;
+ neomutt = {
+ enable = true;
+ virtualMailboxes = a.notmuch.neomutt.virtualMailboxes;
+ };
+ };
+
+ imap = optionalAttrs (a.imap.host != null) {
+ host = a.imap.host;
+ port = a.imap.port;
+ tls = {
+ enable = a.imap.tls.enable;
+ useStartTls = a.imap.tls.useStartTls;
+ };
+ };
+
+ smtp = optionalAttrs (a.smtp.host != null) {
+ host = a.smtp.host;
+ port = a.smtp.port;
+ tls = {
+ enable = a.smtp.tls.enable;
+ useStartTls = a.smtp.tls.useStartTls;
+ };
+ };
+
+ gpg = optionalAttrs (a.gpg.key != null) {
+ key = a.gpg.key;
+ signByDefault = a.gpg.signByDefault;
+ encryptByDefault = a.gpg.encryptByDefault;
+ };
+
+ signature = optionalAttrs (a.signature.text != null || a.signature.command != null) {
+ text = a.signature.text;
+ command = a.signature.command;
+ showSignature = a.signature.showSignature;
+ };
+ }
+ ) (filterAttrs (n: a: a.enable) config.communication.neomutt.accounts);
+ };
+}
diff --git a/modules/communication/neomutt/account/flavors/gmail.nix b/modules/communication/neomutt/account/flavors/gmail.nix
new file mode 100644
index 0000000..5ccceed
--- /dev/null
+++ b/modules/communication/neomutt/account/flavors/gmail.nix
@@ -0,0 +1,45 @@
+{
+ config,
+ lib,
+ name,
+ ...
+}:
+
+{
+ config = lib.mkIf (config.flavor == "gmail.com") {
+ folders = {
+ inbox = lib.mkDefault "Inbox";
+ drafts = lib.mkDefault "[Gmail]/Drafts";
+ sent = lib.mkDefault "[Gmail]/Sent Mail";
+ trash = lib.mkDefault "[Gmail]/Trash";
+ };
+
+ mbsync = {
+ create = lib.mkDefault "maildir";
+ patterns = lib.mkDefault [
+ "INBOX"
+ "[Gmail]/Drafts"
+ "[Gmail]/Sent Mail"
+ "[Gmail]/Spam"
+ "[Gmail]/Trash"
+ ];
+ extraConfig.channel = {
+ Sync = lib.mkDefault "All";
+ CopyArrivalDate = lib.mkDefault "yes";
+ Expunge = lib.mkDefault "Both";
+ SyncState = lib.mkDefault "*";
+ };
+ };
+
+ neomutt.extraConfig = lib.mkDefault ''
+ mailboxes =Inbox ="[Gmail]/Drafts" ="[Gmail]/Sent Mail" ="[Gmail]/Spam" ="[Gmail]/Trash"
+ '';
+
+ notmuch.neomutt.virtualMailboxes = lib.mkDefault [
+ {
+ name = "All Mail";
+ query = "folder:${name}/Inbox or folder:\"${name}/[Gmail]/Sent Mail\"";
+ }
+ ];
+ };
+}
diff --git a/modules/communication/neomutt/account/options.nix b/modules/communication/neomutt/account/options.nix
new file mode 100644
index 0000000..bb53231
--- /dev/null
+++ b/modules/communication/neomutt/account/options.nix
@@ -0,0 +1,328 @@
+{ lib, ... }:
+
+{
+ options = {
+ enable = lib.mkEnableOption "this email account" // {
+ default = true;
+ };
+
+ realName = lib.mkOption {
+ type = lib.types.str;
+ description = "Name displayed when sending mails.";
+ };
+
+ address = lib.mkOption {
+ type = lib.types.str;
+ description = "Email address of this account.";
+ };
+
+ userName = lib.mkOption {
+ type = lib.types.nullOr lib.types.str;
+ default = null;
+ description = "Server username. Defaults to address if null.";
+ };
+
+ passwordGopassSecret = lib.mkOption {
+ type = lib.types.nullOr lib.types.str;
+ default = null;
+ description = "Gopass secret path (e.g. mail/user@gmail.com). Constructs passwordCommand automatically.";
+ };
+
+ primary = lib.mkOption {
+ type = lib.types.bool;
+ default = false;
+ description = "Whether this is the primary account.";
+ };
+
+ flavor = lib.mkOption {
+ type = lib.types.nullOr (
+ lib.types.enum [
+ "gmail.com"
+ "fastmail.com"
+ "outlook.office365.com"
+ "outlook.office365.com-ews"
+ "mailbox.org"
+ "migadu.com"
+ "posteo.de"
+ "runbox.com"
+ "yandex.com"
+ "davmail"
+ "plain"
+ ]
+ );
+ default = null;
+ description = "Email provider flavor for automatic configuration.";
+ };
+
+ folders = {
+ inbox = lib.mkOption {
+ type = lib.types.str;
+ default = "Inbox";
+ description = "Inbox folder.";
+ };
+
+ drafts = lib.mkOption {
+ type = lib.types.nullOr lib.types.str;
+ default = null;
+ description = "Drafts folder.";
+ };
+
+ sent = lib.mkOption {
+ type = lib.types.nullOr lib.types.str;
+ default = null;
+ description = "Sent mail folder.";
+ };
+
+ trash = lib.mkOption {
+ type = lib.types.str;
+ default = "Trash";
+ description = "Trash folder.";
+ };
+ };
+
+ neomutt = {
+ mailboxType = lib.mkOption {
+ type = lib.types.enum [
+ "maildir"
+ "imap"
+ ];
+ default = "maildir";
+ description = "Whether this account uses maildir folders or IMAP mailboxes.";
+ };
+
+ extraConfig = lib.mkOption {
+ type = lib.types.lines;
+ default = "";
+ description = "Extra neomutt config lines for this account.";
+ };
+ };
+
+ mbsync = {
+ create = lib.mkOption {
+ type = lib.types.enum [
+ "none"
+ "maildir"
+ "imap"
+ "both"
+ ];
+ default = "none";
+ description = "Automatically create missing mailboxes.";
+ };
+
+ patterns = lib.mkOption {
+ type = lib.types.listOf lib.types.str;
+ default = [ "INBOX" ];
+ description = "Mailbox patterns to synchronize.";
+ };
+
+ flatten = lib.mkOption {
+ type = lib.types.nullOr lib.types.str;
+ default = null;
+ description = "Flatten hierarchy delimiter.";
+ };
+
+ extraConfig = {
+ account = lib.mkOption {
+ type = lib.types.attrsOf (
+ lib.types.oneOf [
+ lib.types.str
+ lib.types.int
+ lib.types.bool
+ (lib.types.listOf lib.types.str)
+ ]
+ );
+ default = { };
+ description = "Account section extra configuration.";
+ };
+
+ channel = lib.mkOption {
+ type = lib.types.attrsOf (
+ lib.types.oneOf [
+ lib.types.str
+ lib.types.int
+ lib.types.bool
+ (lib.types.listOf lib.types.str)
+ ]
+ );
+ default = { };
+ description = "Channel extra configuration.";
+ };
+
+ local = lib.mkOption {
+ type = lib.types.attrsOf (
+ lib.types.oneOf [
+ lib.types.str
+ lib.types.int
+ lib.types.bool
+ (lib.types.listOf lib.types.str)
+ ]
+ );
+ default = { };
+ description = "Local store extra configuration.";
+ };
+
+ remote = lib.mkOption {
+ type = lib.types.attrsOf (
+ lib.types.oneOf [
+ lib.types.str
+ lib.types.int
+ lib.types.bool
+ (lib.types.listOf lib.types.str)
+ ]
+ );
+ default = { };
+ description = "Remote store extra configuration.";
+ };
+ };
+ };
+
+ notmuch = {
+ neomutt = {
+ virtualMailboxes = lib.mkOption {
+ type = lib.types.listOf (
+ lib.types.submodule {
+ options = {
+ name = lib.mkOption {
+ type = lib.types.str;
+ description = "Display name.";
+ };
+
+ query = lib.mkOption {
+ type = lib.types.str;
+ description = "Notmuch query.";
+ };
+
+ limit = lib.mkOption {
+ type = lib.types.nullOr lib.types.int;
+ default = null;
+ description = "Result limit.";
+ };
+
+ type = lib.mkOption {
+ type = lib.types.nullOr (
+ lib.types.enum [
+ "threads"
+ "messages"
+ ]
+ );
+ default = null;
+ description = "Result type.";
+ };
+ };
+ }
+ );
+ default = [ ];
+ description = "Virtual mailboxes from notmuch queries.";
+ };
+ };
+ };
+
+ imap = {
+ host = lib.mkOption {
+ type = lib.types.nullOr lib.types.str;
+ default = null;
+ description = "IMAP server hostname.";
+ };
+
+ port = lib.mkOption {
+ type = lib.types.nullOr lib.types.port;
+ default = null;
+ description = "IMAP server port.";
+ };
+
+ tls = {
+ enable = lib.mkOption {
+ type = lib.types.bool;
+ default = true;
+ description = "Enable TLS.";
+ };
+
+ useStartTls = lib.mkOption {
+ type = lib.types.bool;
+ default = false;
+ description = "Use STARTTLS.";
+ };
+ };
+ };
+
+ smtp = {
+ host = lib.mkOption {
+ type = lib.types.nullOr lib.types.str;
+ default = null;
+ description = "SMTP server hostname.";
+ };
+
+ port = lib.mkOption {
+ type = lib.types.nullOr lib.types.port;
+ default = null;
+ description = "SMTP server port.";
+ };
+
+ tls = {
+ enable = lib.mkOption {
+ type = lib.types.bool;
+ default = true;
+ description = "Enable TLS.";
+ };
+
+ useStartTls = lib.mkOption {
+ type = lib.types.bool;
+ default = false;
+ description = "Use STARTTLS.";
+ };
+ };
+ };
+
+ gpg = {
+ key = lib.mkOption {
+ type = lib.types.nullOr lib.types.str;
+ default = null;
+ description = "GPG key to use.";
+ };
+
+ signByDefault = lib.mkOption {
+ type = lib.types.bool;
+ default = false;
+ description = "Sign messages by default.";
+ };
+
+ encryptByDefault = lib.mkOption {
+ type = lib.types.bool;
+ default = false;
+ description = "Encrypt messages by default.";
+ };
+ };
+
+ signature = {
+ text = lib.mkOption {
+ type = lib.types.nullOr lib.types.str;
+ default = null;
+ description = "Signature text.";
+ };
+
+ command = lib.mkOption {
+ type = lib.types.nullOr lib.types.path;
+ default = null;
+ description = "Command that generates a signature.";
+ };
+
+ showSignature = lib.mkOption {
+ type = lib.types.nullOr (
+ lib.types.enum [
+ "append"
+ "attach"
+ "none"
+ ]
+ );
+ default = null;
+ description = "Signature display method.";
+ };
+ };
+
+ aliases = lib.mkOption {
+ type = lib.types.listOf lib.types.str;
+ default = [ ];
+ description = "Alternative email addresses for this account.";
+ };
+ };
+}
diff --git a/modules/communication/neomutt/assertions.nix b/modules/communication/neomutt/assertions.nix
index 22434a4..b85280e 100644
--- a/modules/communication/neomutt/assertions.nix
+++ b/modules/communication/neomutt/assertions.nix
@@ -17,13 +17,6 @@
'';
}
{
- assertion = !config.communication.neomutt.enable || config.communication.email.enable;
- message = ''
- communication.neomutt is enabled but communication.email.enable is not.
- neomutt requires the email module (mbsync + notmuch) to be enabled.
- '';
- }
- {
assertion = !config.communication.neomutt.enable || config.terminal.kitty.enable;
message = ''
communication.neomutt is enabled but terminal.kitty.enable is not.
diff --git a/modules/communication/neomutt/default.nix b/modules/communication/neomutt/default.nix
index be3045d..34147fd 100644
--- a/modules/communication/neomutt/default.nix
+++ b/modules/communication/neomutt/default.nix
@@ -1,431 +1,20 @@
-# neomutt - Terminal email client
-{
- config,
- lib,
- pkgs,
- ...
-}:
-let
- neomuttDesktopItem = pkgs.makeDesktopItem {
- name = "neomutt";
- desktopName = "NeoMutt";
- exec = "${lib.getExe config.programs.kitty.package} -e ${lib.getExe config.programs.neomutt.package}";
- icon = "kitty";
- categories = [
- "Network"
- "Email"
- ];
- comment = "Terminal email client";
- terminal = false;
- type = "Application";
- };
-in
+{ config, lib, ... }:
+
{
imports = [
+ ./account
+ ./sync
+ ./neomutt.nix
./assertions.nix
./theme.nix
];
+
options.communication.neomutt = {
- enable = lib.mkEnableOption "Enables neomutt.";
+ enable = lib.mkEnableOption "Email suite (neomutt + mbsync + notmuch)";
};
config = lib.mkIf config.communication.neomutt.enable {
- xdg.mimeApps.defaultApplications = {
- "x-scheme-handler/mailto" = "neomutt.desktop";
- };
-
- home.packages = [
- pkgs.urlscan
- pkgs.w3m
- neomuttDesktopItem
- ];
-
- xdg.configFile."neomutt/mailcap".text = ''
- text/html; w3m -dump -T text/html '%s'; copiousoutput; nametemplate=%s.html
- text/html; $BROWSER '%s'; nametemplate=%s.html
- image/*; xdg-open '%s' &
- application/pdf; xdg-open '%s'; nametemplate=%s.pdf
- video/*; xdg-open '%s' &
- audio/*; xdg-open '%s' &
- application/*; xdg-open '%s' &
- '';
-
- xdg.configFile."neomutt/flag_legend.txt".text = ''
- NeoMutt Index Flags
- ===================
-
- Message state ($flag_chars) -- unchanged defaults
- N New - unread, arrived since you last checked
- O Old - unread, but seen in a previous session
- D Deleted - marked for deletion (pending expunge)
- d Att-del - has attachments marked for deletion
- ! Flagged - important (toggle with F)
- * Tagged - selected for a bulk operation (toggle with t)
- r Replied - you've replied to this message
- (blank) - read, nothing else notable
-
- Cryptography ($crypt_chars)
-  Verified - signed, and the signature checks out
-  Encrypted - PGP/SMIME encrypted
-  Signed - PGP/SMIME signed (unverified)
-  PGP key - message contains a PGP public key
- (blank) - no cryptography info
-
- Addressing ($to_chars)
-  To you only - sent to you and only you
-  To (with others) - you're in the To: list, among others
-  Cc only - you're only in the Cc: list
-  From you - sent by you
-  Mailing list - sent to a list you're subscribed to
- (blank) - not in To/Cc (e.g. Bcc, unlisted)
-
- Press q to close.
- '';
-
- programs.neomutt = {
- sidebar.enable = true;
- sort = "reverse-threads";
- vimKeys = true;
- unmailboxes = true;
- checkStatsInterval = 20;
-
- extraConfig = ''
- set menu_scroll = yes
-
- # cancel any prompt/command-line entry with Esc, vim-style
- set abort_key = "<Esc>"
-
- # attachments: use our mailcap for dedicated openers,
- # and auto-render html/images inline where sensible
- set mailcap_path = "~/.config/neomutt/mailcap"
- auto_view text/html image/png image/jpeg image/gif application/pdf
- '';
-
- binds = [
- # --- sidebar navigation (vim: k=up, j=down) ---
- {
- map = [
- "index"
- "pager"
- ];
- key = "\\CK";
- action = "sidebar-prev";
- }
- {
- map = [
- "index"
- "pager"
- ];
- key = "\\CJ";
- action = "sidebar-next";
- }
- {
- # select the highlighted mailbox directly, no Ctrl+O needed
- map = [
- "index"
- "pager"
- ];
- key = "o";
- action = "sidebar-open";
- }
-
- # --- open / go deeper (vim: l = right/forward) ---
- {
- map = [ "index" ];
- key = "<Return>";
- action = "display-message";
- }
- {
- map = [ "index" ];
- key = "l";
- action = "display-message";
- }
- {
- map = [ "browser" ];
- key = "<Return>";
- action = "select-entry";
- }
- {
- map = [ "browser" ];
- key = "l";
- action = "select-entry";
- }
- {
- map = [ "attach" ];
- key = "<Return>";
- action = "view-attach";
- }
- {
- map = [ "attach" ];
- key = "l";
- action = "view-attach";
- }
- {
- map = [
- "alias"
- "query"
- ];
- key = "<Return>";
- action = "select-entry";
- }
-
- # --- back / exit (vim: h = left/back) ---
- {
- map = [ "pager" ];
- key = "h";
- action = "exit";
- }
- {
- map = [ "browser" ];
- key = "h";
- action = "exit";
- }
- {
- map = [ "attach" ];
- key = "h";
- action = "exit";
- }
- {
- map = [ "index" ];
- key = "h";
- action = "collapse-thread";
- }
-
- # --- pager line scrolling (vim: j/k move by line) ---
- {
- map = [ "pager" ];
- key = "j";
- action = "next-line";
- }
- {
- map = [ "pager" ];
- key = "k";
- action = "previous-line";
- }
- {
- # kept from before: jump straight to the next unread message,
- # extends the h/l "back/forward" metaphor already in use
- map = [ "pager" ];
- key = "l";
- action = "next-unread";
- }
-
- # --- top/bottom of buffer (vim: gg/G -- NOT 0/$, which mean
- # start/end of *line* in vim and would be a false friend here) ---
- {
- map = [ "index" ];
- key = "gg";
- action = "first-entry";
- }
- {
- map = [ "index" ];
- key = "G";
- action = "last-entry";
- }
- {
- map = [ "pager" ];
- key = "gg";
- action = "top";
- }
- {
- map = [ "pager" ];
- key = "G";
- action = "bottom";
- }
- {
- map = [ "browser" ];
- key = "gg";
- action = "first-entry";
- }
- {
- map = [ "browser" ];
- key = "G";
- action = "last-entry";
- }
-
- # --- page scrolling (vim: Ctrl-f/b full page, Ctrl-d/u half page) ---
- {
- map = [ "pager" ];
- key = "\\Cf";
- action = "next-page";
- }
- {
- map = [ "pager" ];
- key = "\\Cb";
- action = "previous-page";
- }
- {
- map = [ "pager" ];
- key = "\\Cd";
- action = "half-down";
- }
- {
- map = [ "pager" ];
- key = "\\Cu";
- action = "half-up";
- }
- {
- # index has no half-page concept, so Ctrl-d/u fall back to
- # the same full-page scroll as Ctrl-f/b
- map = [ "index" ];
- key = "\\Cf";
- action = "next-page";
- }
- {
- map = [ "index" ];
- key = "\\Cb";
- action = "previous-page";
- }
- {
- map = [ "index" ];
- key = "\\Cd";
- action = "half-down";
- }
- {
- map = [ "index" ];
- key = "\\Cu";
- action = "half-up";
- }
-
- # --- search (vim: / forward, ? backward, n/N repeat) ---
- {
- map = [
- "index"
- "pager"
- ];
- key = "/";
- action = "search";
- }
- {
- map = [
- "index"
- "pager"
- ];
- key = "?";
- action = "search-reverse";
- }
- {
- map = [
- "index"
- "pager"
- ];
- key = "n";
- action = "search-next";
- }
- {
- map = [
- "index"
- "pager"
- ];
- key = "N";
- action = "search-opposite";
- }
-
- {
- map = [
- "index"
- "pager"
- ];
- key = "u";
- action = "undelete-message";
- }
-
- # --- copy (vim: yy yank -- non-destructive, unlike save-message
- # which moves/deletes the original) ---
- {
- map = [ "index" ];
- key = "y";
- action = "noop";
- }
- {
- map = [ "index" ];
- key = "yy";
- action = "copy-message";
- }
-
- # --- tagging (vim: v/V visual select single/whole block) ---
- # NOTE: this overrides neomutt's default "v" (view-attachments).
- # Attachments are still reachable from the pager/index via the
- # mailcap auto-view or the attach menu, but if you want plain
- # "v" back for view-attachments, just delete these two binds.
- {
- map = [ "index" ];
- key = "v";
- action = "tag-entry";
- }
- {
- map = [ "index" ];
- key = "V";
- action = "tag-thread";
- }
-
- # --- mailbox switching (vim: gt = next tab) ---
- {
- map = [ "index" ];
- key = "gt";
- action = "next-unread-mailbox";
- }
-
- # --- filter/limit (vim: gf to 'find' or 'filter') ---
- {
- map = [ "index" ];
- key = "gf";
- action = "limit";
- }
-
- # --- filter/limit (vim: gf to 'find' or 'filter') ---
- {
- map = [ "index" ];
- key = "gF";
- action = "limit";
- }
-
- # --- help message to show functions and keybindings ---
- {
- map = [
- "index"
- "pager"
- "browser"
- "attach"
- ];
- key = "g?";
- action = "help";
- }
- ];
-
- macros = [
- {
- map = [
- "index"
- "pager"
- ];
- key = "O";
- action = "<sync-mailbox><shell-escape>mbsync -a<enter><sync-mailbox>";
- }
- {
- # gx: extract links from the current message and open one, vim-netrw style
- map = [
- "index"
- "pager"
- ];
- key = "gx";
- action = "<pipe-message>urlscan<Enter>";
- }
- {
- map = [
- "index"
- "pager"
- ];
- key = "gl";
- action = "<shell-escape>less ~/.config/neomutt/flag_legend.txt<enter>";
- }
- {
- # ZZ: save and quit, vim-style (sync mailbox, then quit)
- map = [ "index" ];
- key = "ZZ";
- action = "<sync-mailbox><quit>";
- }
- ];
- };
+ accounts.email.maildirBasePath = "${config.home.homeDirectory}/.local/share/mail";
+ home.sessionVariables.MAILDIR = config.accounts.email.maildirBasePath;
};
}
diff --git a/modules/communication/neomutt/neomutt.nix b/modules/communication/neomutt/neomutt.nix
new file mode 100644
index 0000000..84afdf7
--- /dev/null
+++ b/modules/communication/neomutt/neomutt.nix
@@ -0,0 +1,423 @@
+# neomutt - Terminal email client
+{
+ config,
+ lib,
+ pkgs,
+ ...
+}:
+let
+ neomuttDesktopItem = pkgs.makeDesktopItem {
+ name = "neomutt";
+ desktopName = "NeoMutt";
+ exec = "${lib.getExe config.programs.kitty.package} -e ${lib.getExe config.programs.neomutt.package}";
+ icon = "kitty";
+ categories = [
+ "Network"
+ "Email"
+ ];
+ comment = "Terminal email client";
+ terminal = false;
+ type = "Application";
+ };
+in
+{
+ config = lib.mkIf config.communication.neomutt.enable {
+ xdg.mimeApps.defaultApplications = {
+ "x-scheme-handler/mailto" = "neomutt.desktop";
+ };
+
+ home.packages = [
+ pkgs.urlscan
+ pkgs.w3m
+ neomuttDesktopItem
+ ];
+
+ xdg.configFile."neomutt/mailcap".text = ''
+ text/html; w3m -dump -T text/html '%s'; copiousoutput; nametemplate=%s.html
+ text/html; $BROWSER '%s'; nametemplate=%s.html
+ image/*; xdg-open '%s' &
+ application/pdf; xdg-open '%s'; nametemplate=%s.pdf
+ video/*; xdg-open '%s' &
+ audio/*; xdg-open '%s' &
+ application/*; xdg-open '%s' &
+ '';
+
+ xdg.configFile."neomutt/flag_legend.txt".text = ''
+ NeoMutt Index Flags
+ ===================
+
+ Message state ($flag_chars) -- unchanged defaults
+ N New - unread, arrived since you last checked
+ O Old - unread, but seen in a previous session
+ D Deleted - marked for deletion (pending expunge)
+ d Att-del - has attachments marked for deletion
+ ! Flagged - important (toggle with F)
+ * Tagged - selected for a bulk operation (toggle with t)
+ r Replied - you've replied to this message
+ (blank) - read, nothing else notable
+
+ Cryptography ($crypt_chars)
+  Verified - signed, and the signature checks out
+  Encrypted - PGP/SMIME encrypted
+  Signed - PGP/SMIME signed (unverified)
+  PGP key - message contains a PGP public key
+ (blank) - no cryptography info
+
+ Addressing ($to_chars)
+  To you only - sent to you and only you
+  To (with others) - you're in the To: list, among others
+  Cc only - you're only in the Cc: list
+  From you - sent by you
+  Mailing list - sent to a list you're subscribed to
+ (blank) - not in To/Cc (e.g. Bcc, unlisted)
+
+ Press q to close.
+ '';
+
+ programs.neomutt = {
+ sidebar.enable = true;
+ sort = "reverse-threads";
+ vimKeys = true;
+ unmailboxes = true;
+ checkStatsInterval = 20;
+
+ extraConfig = ''
+ set menu_scroll = yes
+
+ # cancel any prompt/command-line entry with Esc, vim-style
+ set abort_key = "<Esc>"
+
+ # attachments: use our mailcap for dedicated openers,
+ # and auto-render html/images inline where sensible
+ set mailcap_path = "~/.config/neomutt/mailcap"
+ auto_view text/html image/png image/jpeg image/gif application/pdf
+ '';
+
+ binds = [
+ # --- sidebar navigation (vim: k=up, j=down) ---
+ {
+ map = [
+ "index"
+ "pager"
+ ];
+ key = "\\CK";
+ action = "sidebar-prev";
+ }
+ {
+ map = [
+ "index"
+ "pager"
+ ];
+ key = "\\CJ";
+ action = "sidebar-next";
+ }
+ {
+ # select the highlighted mailbox directly, no Ctrl+O needed
+ map = [
+ "index"
+ "pager"
+ ];
+ key = "o";
+ action = "sidebar-open";
+ }
+
+ # --- open / go deeper (vim: l = right/forward) ---
+ {
+ map = [ "index" ];
+ key = "<Return>";
+ action = "display-message";
+ }
+ {
+ map = [ "index" ];
+ key = "l";
+ action = "display-message";
+ }
+ {
+ map = [ "browser" ];
+ key = "<Return>";
+ action = "select-entry";
+ }
+ {
+ map = [ "browser" ];
+ key = "l";
+ action = "select-entry";
+ }
+ {
+ map = [ "attach" ];
+ key = "<Return>";
+ action = "view-attach";
+ }
+ {
+ map = [ "attach" ];
+ key = "l";
+ action = "view-attach";
+ }
+ {
+ map = [
+ "alias"
+ "query"
+ ];
+ key = "<Return>";
+ action = "select-entry";
+ }
+
+ # --- back / exit (vim: h = left/back) ---
+ {
+ map = [ "pager" ];
+ key = "h";
+ action = "exit";
+ }
+ {
+ map = [ "browser" ];
+ key = "h";
+ action = "exit";
+ }
+ {
+ map = [ "attach" ];
+ key = "h";
+ action = "exit";
+ }
+ {
+ map = [ "index" ];
+ key = "h";
+ action = "collapse-thread";
+ }
+
+ # --- pager line scrolling (vim: j/k move by line) ---
+ {
+ map = [ "pager" ];
+ key = "j";
+ action = "next-line";
+ }
+ {
+ map = [ "pager" ];
+ key = "k";
+ action = "previous-line";
+ }
+ {
+ # kept from before: jump straight to the next unread message,
+ # extends the h/l "back/forward" metaphor already in use
+ map = [ "pager" ];
+ key = "l";
+ action = "next-unread";
+ }
+
+ # --- top/bottom of buffer (vim: gg/G -- NOT 0/$, which mean
+ # start/end of *line* in vim and would be a false friend here) ---
+ {
+ map = [ "index" ];
+ key = "gg";
+ action = "first-entry";
+ }
+ {
+ map = [ "index" ];
+ key = "G";
+ action = "last-entry";
+ }
+ {
+ map = [ "pager" ];
+ key = "gg";
+ action = "top";
+ }
+ {
+ map = [ "pager" ];
+ key = "G";
+ action = "bottom";
+ }
+ {
+ map = [ "browser" ];
+ key = "gg";
+ action = "first-entry";
+ }
+ {
+ map = [ "browser" ];
+ key = "G";
+ action = "last-entry";
+ }
+
+ # --- page scrolling (vim: Ctrl-f/b full page, Ctrl-d/u half page) ---
+ {
+ map = [ "pager" ];
+ key = "\\Cf";
+ action = "next-page";
+ }
+ {
+ map = [ "pager" ];
+ key = "\\Cb";
+ action = "previous-page";
+ }
+ {
+ map = [ "pager" ];
+ key = "\\Cd";
+ action = "half-down";
+ }
+ {
+ map = [ "pager" ];
+ key = "\\Cu";
+ action = "half-up";
+ }
+ {
+ # index has no half-page concept, so Ctrl-d/u fall back to
+ # the same full-page scroll as Ctrl-f/b
+ map = [ "index" ];
+ key = "\\Cf";
+ action = "next-page";
+ }
+ {
+ map = [ "index" ];
+ key = "\\Cb";
+ action = "previous-page";
+ }
+ {
+ map = [ "index" ];
+ key = "\\Cd";
+ action = "half-down";
+ }
+ {
+ map = [ "index" ];
+ key = "\\Cu";
+ action = "half-up";
+ }
+
+ # --- search (vim: / forward, ? backward, n/N repeat) ---
+ {
+ map = [
+ "index"
+ "pager"
+ ];
+ key = "/";
+ action = "search";
+ }
+ {
+ map = [
+ "index"
+ "pager"
+ ];
+ key = "?";
+ action = "search-reverse";
+ }
+ {
+ map = [
+ "index"
+ "pager"
+ ];
+ key = "n";
+ action = "search-next";
+ }
+ {
+ map = [
+ "index"
+ "pager"
+ ];
+ key = "N";
+ action = "search-opposite";
+ }
+
+ {
+ map = [
+ "index"
+ "pager"
+ ];
+ key = "u";
+ action = "undelete-message";
+ }
+
+ # --- copy (vim: yy yank -- non-destructive, unlike save-message
+ # which moves/deletes the original) ---
+ {
+ map = [ "index" ];
+ key = "y";
+ action = "noop";
+ }
+ {
+ map = [ "index" ];
+ key = "yy";
+ action = "copy-message";
+ }
+
+ # --- tagging (vim: v/V visual select single/whole block) ---
+ # NOTE: this overrides neomutt's default "v" (view-attachments).
+ # Attachments are still reachable from the pager/index via the
+ # mailcap auto-view or the attach menu, but if you want plain
+ # "v" back for view-attachments, just delete these two binds.
+ {
+ map = [ "index" ];
+ key = "v";
+ action = "tag-entry";
+ }
+ {
+ map = [ "index" ];
+ key = "V";
+ action = "tag-thread";
+ }
+
+ # --- mailbox switching (vim: gt = next tab) ---
+ {
+ map = [ "index" ];
+ key = "gt";
+ action = "next-unread-mailbox";
+ }
+
+ # --- filter/limit (vim: gf to 'find' or 'filter') ---
+ {
+ map = [ "index" ];
+ key = "gf";
+ action = "limit";
+ }
+
+ # --- filter/limit (vim: gf to 'find' or 'filter') ---
+ {
+ map = [ "index" ];
+ key = "gF";
+ action = "limit";
+ }
+
+ # --- help message to show functions and keybindings ---
+ {
+ map = [
+ "index"
+ "pager"
+ "browser"
+ "attach"
+ ];
+ key = "g?";
+ action = "help";
+ }
+ ];
+
+ macros = [
+ {
+ map = [
+ "index"
+ "pager"
+ ];
+ key = "O";
+ action = "<sync-mailbox><shell-escape>mbsync -a<enter><sync-mailbox>";
+ }
+ {
+ # gx: extract links from the current message and open one, vim-netrw style
+ map = [
+ "index"
+ "pager"
+ ];
+ key = "gx";
+ action = "<pipe-message>urlscan<Enter>";
+ }
+ {
+ map = [
+ "index"
+ "pager"
+ ];
+ key = "gl";
+ action = "<shell-escape>less ~/.config/neomutt/flag_legend.txt<enter>";
+ }
+ {
+ # ZZ: save and quit, vim-style (sync mailbox, then quit)
+ map = [ "index" ];
+ key = "ZZ";
+ action = "<sync-mailbox><quit>";
+ }
+ ];
+ };
+ };
+}
diff --git a/modules/communication/neomutt/sync/default.nix b/modules/communication/neomutt/sync/default.nix
new file mode 100644
index 0000000..3fd9362
--- /dev/null
+++ b/modules/communication/neomutt/sync/default.nix
@@ -0,0 +1,8 @@
+{ ... }:
+
+{
+ imports = [
+ ./mbsync.nix
+ ./notmuch.nix
+ ];
+}
diff --git a/modules/communication/neomutt/sync/mbsync.nix b/modules/communication/neomutt/sync/mbsync.nix
new file mode 100644
index 0000000..6d5ad86
--- /dev/null
+++ b/modules/communication/neomutt/sync/mbsync.nix
@@ -0,0 +1,9 @@
+{ config, lib, ... }:
+
+with lib;
+
+{
+ config = mkIf config.communication.neomutt.enable {
+ programs.mbsync.enable = true;
+ };
+}
diff --git a/modules/communication/neomutt/sync/notmuch.nix b/modules/communication/neomutt/sync/notmuch.nix
new file mode 100644
index 0000000..9679bf4
--- /dev/null
+++ b/modules/communication/neomutt/sync/notmuch.nix
@@ -0,0 +1,9 @@
+{ config, lib, ... }:
+
+with lib;
+
+{
+ config = mkIf config.communication.neomutt.enable {
+ programs.notmuch.enable = true;
+ };
+}
diff --git a/modules/communication/neomutt/theme.nix b/modules/communication/neomutt/theme.nix
index 1d62c55..583b03f 100644
--- a/modules/communication/neomutt/theme.nix
+++ b/modules/communication/neomutt/theme.nix
@@ -58,7 +58,7 @@
color index_tags cyan default # 󰓼 base0C (Transformed tags, %g %J)
# --- Index Message States ---------------------------------------------
- # By targeting *only* `index_subject`, we preserve your multi-color
+ # By targeting *only* `index_subject`, you preserve your multi-color
# columns while still easily identifying unread or flagged emails.
color index_subject blue default "~U" # Unread: Blue (base0D)
color index_subject blue default "~N" # 󰄯 New: Blue (base0D)
@@ -117,7 +117,7 @@
color body brightblack default "^gpg: " #
# --- Sidebar (was completely unset despite sidebar.enable = true) -----
- set sidebar_format = " %D%* %?N? %N?%?F? %F? "
+ set sidebar_format = " %D%* %?N? %N%?F? %F? "
color sidebar_background default default # transparent, blends with terminal bg
color sidebar_divider white default # base03 (matches old tree color)
color sidebar_ordinary white default # base05 (normal mailbox entries)