aboutsummaryrefslogtreecommitdiff
path: root/home-manager/modules/programs/neomutt.nix
{ 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.
        '';
      };
    };
  };

  sortOptions = [
    "date"
    "date-received"
    "from"
    "mailbox-order"
    "score"
    "size"
    "spam"
    "subject"
    "threads"
    "to"
  ];

  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 = "<left>";
        description = "The key to bind.";
      };

      action = mkOption {
        type = types.str;
        example = "<enter-command>toggle sidebar_visible<enter><refresh>";
        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";
        smtpPort = if smtp.port != null then ":${toString smtp.port}" else "";
        smtpBaseUrl =
          "${smtpProto}://${escape userName}@${smtp.host}${smtpPort}";
      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 {
        # allow users to choose any option from sortOptions, or any option prefixed with "reverse-"
        type = types.enum
          (sortOptions ++ (map (option: "reverse-" + option) sortOptions));
        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.";
      };
    };

    accounts.email.accounts = mkOption {
      type = with types; attrsOf (submodule (import ./neomutt-accounts.nix));
    };
  };

  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)}";
    };
  };
}