diff options
| -rw-r--r-- | modules/programs/aerc/default.nix | 54 | ||||
| -rw-r--r-- | modules/programs/default.nix | 4 | ||||
| -rw-r--r-- | modules/programs/git/default.nix | 41 | ||||
| -rw-r--r-- | modules/programs/gpg/default.nix | 18 | ||||
| -rw-r--r-- | modules/programs/less/default.nix | 15 | ||||
| -rw-r--r-- | modules/programs/pass/default.nix | 69 | ||||
| -rw-r--r-- | modules/programs/zsh/default.nix | 3 | ||||
| -rw-r--r-- | modules/scripts/aerc-mail-setup.sh | 333 | ||||
| -rw-r--r-- | modules/scripts/aerc-sync-mail.sh | 15 | ||||
| -rw-r--r-- | modules/scripts/default.nix | 28 | ||||
| -rw-r--r-- | users/arpit.nix | 4 |
11 files changed, 570 insertions, 14 deletions
diff --git a/modules/programs/aerc/default.nix b/modules/programs/aerc/default.nix new file mode 100644 index 0000000..92b1cd6 --- /dev/null +++ b/modules/programs/aerc/default.nix @@ -0,0 +1,54 @@ +{ + config, + lib, + ... +}: +{ + config = lib.mkIf config.programs.aerc.enable { + programs.aerc = { + extraConfig = { + general = { + unsafe-accounts-conf = false; + default-save-path = "${config.home.homeDirectory}/Downloads"; + use-terminal-pinentry = false; + }; + + ui = { + index-columns = "flags:4,name<32%,subject,date>="; + column-name = "{{with index .From 0}}{{.Address}}{{if .Name}} ({{.Name}}){{end}}{{end}}"; + timestamp-format = "2006-01-02 15:04"; + this-day-time-format = "15:04"; + this-week-time-format = "Mon 15:04"; + sidebar-width = 24; + }; + + viewer = { + pager = lib.getExe config.programs.less.package; + }; + + filters = '' + text/plain = wrap -w 100 | colorize + text/html = ! html + text/* = ${lib.getExe config.programs.bat.package} -fP --file-name="''${AERC_FILENAME:-message.txt}" --style=plain + message/delivery-status = colorize + .headers = colorize + ''; + + hooks = { + mail-added = "aerc-sync-mail \"$AERC_ACCOUNT\""; + mail-deleted = "aerc-sync-mail \"$AERC_ACCOUNT\""; + flag-changed = "aerc-sync-mail \"$AERC_ACCOUNT\""; + }; + }; + }; + + home.file.".config/aerc/notmuch-query-map".text = '' + Inbox=tag:inbox and not tag:deleted + Unread=tag:unread and not tag:deleted + Flagged=tag:flagged and not tag:deleted + Sent=folder:sent or folder:Sent or folder:"[Gmail]/Sent Mail" + Archive=not tag:inbox and not tag:deleted + All=* + ''; + }; +} diff --git a/modules/programs/default.nix b/modules/programs/default.nix index 5f6b49a..ed54f24 100644 --- a/modules/programs/default.nix +++ b/modules/programs/default.nix @@ -3,6 +3,7 @@ # Tools - Collection of tool configurations (browsers, editors, utilities, etc.) { imports = [ + ./aerc ./aria2 ./bat ./bluetui @@ -15,10 +16,12 @@ ./ffmpeg ./fzf ./git + ./gpg ./heroic ./htop ./impala ./kitty + ./less ./lf ./lsd ./maim @@ -29,6 +32,7 @@ ./openvpn ./ouch ./pamixer + ./pass ./playerctl ./qemu ./slop diff --git a/modules/programs/git/default.nix b/modules/programs/git/default.nix index 24d63c7..cfba093 100644 --- a/modules/programs/git/default.nix +++ b/modules/programs/git/default.nix @@ -1,6 +1,15 @@ -{ lib, config, ... }: +{ + config, + lib, + pkgs, + ... +}: # Git - Distributed version control system +let + passCfg = config.programs.pass; + passGitHelper = lib.getExe pkgs.pass-git-helper; +in { options.programs.git = { username = lib.mkOption { @@ -15,19 +24,23 @@ config = lib.mkIf config.programs.git.enable { programs.git = { - settings = { - user = { - name = config.programs.git.username; - email = config.programs.git.email; - }; - credential.helper = "store --file ${config.xdg.cacheHome}/git/credential"; - core.askPass = ""; - }; + settings = + lib.recursiveUpdate + { + user = { + name = config.programs.git.username; + email = config.programs.git.email; + }; + core.askPass = ""; + } + ( + lib.optionalAttrs passCfg.enable { + credential."https://github.com" = { + username = passCfg.github.username; + helper = passGitHelper; + }; + } + ); }; - - # The base directory of the credential file must exist - home.activation.createGitCacheDirectory = lib.hm.dag.entryAfter [ - "writeBoundary" - ] "mkdir -p ${config.xdg.cacheHome}/git"; }; } diff --git a/modules/programs/gpg/default.nix b/modules/programs/gpg/default.nix new file mode 100644 index 0000000..f6bce6e --- /dev/null +++ b/modules/programs/gpg/default.nix @@ -0,0 +1,18 @@ +{ + config, + lib, + pkgs, + ... +}: + +{ + config = lib.mkIf config.programs.gpg.enable { + services.gpg-agent = { + enable = true; + enableZshIntegration = true; + defaultCacheTtl = 3600; + maxCacheTtl = 86400; + pinentry.package = pkgs.pinentry-gtk2; + }; + }; +} diff --git a/modules/programs/less/default.nix b/modules/programs/less/default.nix new file mode 100644 index 0000000..516377d --- /dev/null +++ b/modules/programs/less/default.nix @@ -0,0 +1,15 @@ +{ config, lib, ... }: + +# less - terminal pager +{ + config = lib.mkIf config.programs.less.enable { + programs.less = { + options = [ + "--RAW-CONTROL-CHARS" + "--quit-if-one-screen" + "--no-init" + "--ignore-case" + ]; + }; + }; +} diff --git a/modules/programs/pass/default.nix b/modules/programs/pass/default.nix new file mode 100644 index 0000000..0d11ab7 --- /dev/null +++ b/modules/programs/pass/default.nix @@ -0,0 +1,69 @@ +{ + config, + lib, + pkgs, + ... +}: +let + cfg = config.programs.pass; + githubMapping = '' + [github.com] + target = github/token + username = ${cfg.github.username} + username_extractor = static + + [github.com/*] + target = github/token + username = ${cfg.github.username} + username_extractor = static + + [*.github.com] + target = github/token + username = ${cfg.github.username} + username_extractor = static + + [*.github.com/*] + target = github/token + username = ${cfg.github.username} + username_extractor = static + ''; +in +{ + options.programs.pass = { + enable = lib.mkEnableOption "pass password manager"; + + package = lib.mkPackageOption pkgs "pass" { }; + + storeDir = lib.mkOption { + type = lib.types.str; + default = "${config.home.homeDirectory}/.password-store"; + description = "Path to the pass password store."; + }; + + github = { + username = lib.mkOption { + type = lib.types.str; + default = "x-access-token"; + description = "Username returned to Git for GitHub HTTPS credentials."; + }; + + tokenEntry = lib.mkOption { + type = lib.types.str; + default = "github/token"; + description = "Pass entry containing the GitHub token on its first line."; + }; + }; + }; + + config = lib.mkIf cfg.enable { + programs.password-store = { + enable = true; + package = cfg.package; + settings = { + PASSWORD_STORE_DIR = cfg.storeDir; + }; + }; + + home.file.".config/pass-git-helper/git-pass-mapping.ini".text = githubMapping; + }; +} diff --git a/modules/programs/zsh/default.nix b/modules/programs/zsh/default.nix index 482c201..f3bdbb1 100644 --- a/modules/programs/zsh/default.nix +++ b/modules/programs/zsh/default.nix @@ -22,6 +22,9 @@ autoload -U colors && colors + export GPG_TTY="$(tty)" + gpg-connect-agent updatestartuptty /bye >/dev/null 2>&1 + bindkey "^[[3~" delete-char bindkey "^?" backward-delete-char ${nixCommandWrappers} diff --git a/modules/scripts/aerc-mail-setup.sh b/modules/scripts/aerc-mail-setup.sh new file mode 100644 index 0000000..e1e2448 --- /dev/null +++ b/modules/scripts/aerc-mail-setup.sh @@ -0,0 +1,333 @@ +#!/usr/bin/env bash +set -euo pipefail + +config_home="${XDG_CONFIG_HOME:-$HOME/.config}" +data_home="${XDG_DATA_HOME:-$HOME/.local/share}" +state_home="${XDG_STATE_HOME:-$HOME/.local/state}" +password_store_dir="${PASSWORD_STORE_DIR:-$HOME/.password-store}" + +mail_root="${data_home}/mail" +isync_config="${config_home}/isyncrc" +aerc_accounts="${config_home}/aerc/accounts.conf" +notmuch_config="${config_home}/notmuch/default/config" +query_map="${config_home}/aerc/notmuch-query-map" + +usage() { + cat <<'USAGE' +Usage: + aerc-mail-setup add Add or update a Gmail, Yahoo, or Outlook account + aerc-mail-setup remove Remove generated aerc/isync blocks for an account + aerc-mail-setup list List generated accounts + aerc-mail-setup sync Run mbsync and notmuch indexing + +Secrets are stored through pass(1). Generated private config is written under +~/.config/isyncrc, ~/.config/aerc/accounts.conf, and ~/.config/notmuch/default/config. +Removing an account only removes generated config blocks; it does not delete mail or passwords. +USAGE +} + +prompt() { + local label="$1" + local default="${2:-}" + local value + + if [[ -n "$default" ]]; then + printf '%s [%s]: ' "$label" "$default" >&2 + read -r value + printf '%s\n' "${value:-$default}" + else + printf '%s: ' "$label" >&2 + read -r value + printf '%s\n' "$value" + fi +} + +sanitize_account() { + printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9_.-]/-/g' +} + +url_encode_user() { + printf '%s' "$1" | sed 's/%/%25/g; s/@/%40/g; s/+/%2B/g' +} + +ensure_query_map() { + mkdir -p "$(dirname "$query_map")" + if [[ ! -e "$query_map" ]]; then + cat >"$query_map" <<'EOF' +Inbox=tag:inbox and not tag:deleted +Unread=tag:unread and not tag:deleted +Flagged=tag:flagged and not tag:deleted +Sent=folder:sent or folder:Sent or folder:"[Gmail]/Sent Mail" +Archive=not tag:inbox and not tag:deleted +All=* +EOF + fi +} + +write_notmuch_config() { + local name="$1" + local email="$2" + + mkdir -p "$(dirname "$notmuch_config")" "$mail_root" "$state_home/isync" + if [[ ! -e "$notmuch_config" ]]; then + cat >"$notmuch_config" <<EOF +[database] +path=${mail_root} +mail_root=${mail_root} + +[user] +name=${name} +primary_email=${email} + +[new] +tags=unread;inbox; +ignore=.uidvalidity;.mbsyncstate + +[search] +exclude_tags=deleted;spam; + +[maildir] +synchronize_flags=true +EOF + fi +} + +replace_block() { + local file="$1" + local block_name="$2" + local tmp + + mkdir -p "$(dirname "$file")" + tmp="$(mktemp)" + if [[ -e "$file" ]]; then + awk -v start="# BEGIN ${block_name}" -v end="# END ${block_name}" ' + $0 == start { skip = 1; next } + $0 == end { skip = 0; next } + !skip { print } + ' "$file" >"$tmp" + fi + + { + sed '/^[[:space:]]*$/N;/^\n$/D' "$tmp" 2>/dev/null || true + printf '\n# BEGIN %s\n' "$block_name" + cat + printf '# END %s\n' "$block_name" + } >"$file" + rm -f "$tmp" + chmod 600 "$file" +} + +remove_block() { + local file="$1" + local block_name="$2" + local tmp + + [[ -e "$file" ]] || return + tmp="$(mktemp)" + awk -v start="# BEGIN ${block_name}" -v end="# END ${block_name}" ' + $0 == start { skip = 1; next } + $0 == end { skip = 0; next } + !skip { print } + ' "$file" >"$tmp" + cat "$tmp" >"$file" + rm -f "$tmp" + chmod 600 "$file" +} + +pass_insert_if_requested() { + local pass_entry="$1" + + if pass show "$pass_entry" >/dev/null 2>&1; then + return + fi + + printf 'No pass entry found at %s.\n' "$pass_entry" + if [[ ! -f "${password_store_dir}/.gpg-id" ]]; then + cat >&2 <<EOF +pass is not initialized yet. + +Create or choose a GPG key, then initialize pass with: + gpg --list-secret-keys --keyid-format=long + pass init <gpg-key-id-or-email> + +After that, rerun: + aerc-mail-setup add +EOF + exit 1 + fi + + printf 'Store the password there now? [y/N]: ' + read -r answer + case "$answer" in + y|Y|yes|YES) + pass insert "$pass_entry" + ;; + *) + printf 'Create it later with: pass insert %s\n' "$pass_entry" + ;; + esac +} + +provider_defaults() { + local provider="$1" + case "$provider" in + gmail) + imap_host="imap.gmail.com" + imap_port="993" + imap_tls="IMAPS" + imap_auth="LOGIN" + smtp_host="smtp.gmail.com" + smtp_port="587" + smtp_scheme="smtp+login" + ;; + yahoo) + imap_host="imap.mail.yahoo.com" + imap_port="993" + imap_tls="IMAPS" + imap_auth="LOGIN" + smtp_host="smtp.mail.yahoo.com" + smtp_port="587" + smtp_scheme="smtp+login" + ;; + outlook) + imap_host="outlook.office365.com" + imap_port="993" + imap_tls="IMAPS" + imap_auth="LOGIN" + smtp_host="smtp.office365.com" + smtp_port="587" + smtp_scheme="smtp+login" + ;; + *) + printf 'Unsupported provider: %s\n' "$provider" >&2 + exit 1 + ;; + esac +} + +add_account() { + local provider account email name user pass_entry sync_patterns + local imap_host imap_port imap_tls imap_auth smtp_host smtp_port smtp_scheme + local encoded_user + + provider="$(prompt "Provider (gmail/yahoo/outlook)")" + provider="$(printf '%s' "$provider" | tr '[:upper:]' '[:lower:]')" + provider_defaults "$provider" + sync_patterns="*" + + email="$(prompt "Email address")" + account="$(prompt "Account id" "$(sanitize_account "$email")")" + account="$(sanitize_account "$account")" + name="$(prompt "Display name")" + + printf 'Use an app password stored in pass; OAuth is not configured here.\n' + user="$(prompt "IMAP/SMTP username" "$email")" + + imap_host="$(prompt "IMAP host" "$imap_host")" + imap_port="$(prompt "IMAP port" "$imap_port")" + imap_tls="$(prompt "mbsync IMAP TLS mode (IMAPS/STARTTLS/None)" "$imap_tls")" + imap_auth="$(prompt "mbsync IMAP auth mechanism" "$imap_auth")" + smtp_host="$(prompt "SMTP host" "$smtp_host")" + smtp_port="$(prompt "SMTP port" "$smtp_port")" + smtp_scheme="$(prompt "aerc SMTP scheme" "$smtp_scheme")" + + pass_entry="$(prompt "pass entry for this account password" "mail/${account}")" + pass_insert_if_requested "$pass_entry" + + mkdir -p "${mail_root}/${account}/INBOX/cur" "${mail_root}/${account}/INBOX/new" "${mail_root}/${account}/INBOX/tmp" + ensure_query_map + write_notmuch_config "$name" "$email" + + replace_block "$isync_config" "aerc-mail:${account}" <<EOF +IMAPAccount ${account}-remote +Host ${imap_host} +Port ${imap_port} +User ${user} +PassCmd "pass show ${pass_entry}" +TLSType ${imap_tls} +AuthMechs ${imap_auth} + +IMAPStore ${account}-remote +Account ${account}-remote + +MaildirStore ${account}-local +SubFolders Verbatim +Path ${mail_root}/${account}/ +Inbox ${mail_root}/${account}/INBOX + +Channel ${account} +Far :${account}-remote: +Near :${account}-local: +Patterns ${sync_patterns} +Create Both +Remove Both +Expunge Both +Sync Full +SyncState * +EOF + + encoded_user="$(url_encode_user "$user")" + replace_block "$aerc_accounts" "aerc-mail:${account}" <<EOF +[${account}] +source = notmuch://${mail_root} +maildir-store = ${mail_root} +maildir-account-path = ${account} +multi-file-strategy = act-all +query-map = ${query_map} +from = ${name} <${email}> +outgoing = ${smtp_scheme}://${encoded_user}@${smtp_host}:${smtp_port} +outgoing-cred-cmd = pass show ${pass_entry} +check-mail = 5m +check-mail-cmd = aerc-sync-mail ${account} +check-mail-timeout = 5m +default = Inbox +copy-to = Sent +archive = Archive +postpone = Drafts +EOF + + printf 'Configured %s. Run this once for the initial download:\n' "$account" + printf ' aerc-sync-mail %s\n' "$account" +} + +list_accounts() { + if [[ ! -e "$aerc_accounts" ]]; then + printf 'No generated accounts found.\n' + return + fi + sed -n 's/^# BEGIN aerc-mail:\(.*\)$/\1/p' "$aerc_accounts" +} + +remove_account() { + local account + + account="$(prompt "Account id to remove")" + account="$(sanitize_account "$account")" + + remove_block "$isync_config" "aerc-mail:${account}" + remove_block "$aerc_accounts" "aerc-mail:${account}" + printf 'Removed generated config blocks for %s.\n' "$account" +} + +case "${1:-}" in + add) + add_account + ;; + remove) + remove_account + ;; + list) + list_accounts + ;; + sync) + shift + aerc-sync-mail "$@" + ;; + -h|--help|help|"") + usage + ;; + *) + usage >&2 + exit 1 + ;; +esac diff --git a/modules/scripts/aerc-sync-mail.sh b/modules/scripts/aerc-sync-mail.sh new file mode 100644 index 0000000..0a69346 --- /dev/null +++ b/modules/scripts/aerc-sync-mail.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -euo pipefail + +target="${1:--a}" +mail_root="${XDG_DATA_HOME:-$HOME/.local/share}/mail" +mkdir -p "$mail_root" + +if [[ "$target" == "all" ]]; then + target="-a" +else + mkdir -p "${mail_root}/${target}/INBOX/cur" "${mail_root}/${target}/INBOX/new" "${mail_root}/${target}/INBOX/tmp" +fi + +mbsync "$target" +notmuch new diff --git a/modules/scripts/default.nix b/modules/scripts/default.nix index 9d29192..c2423bc 100644 --- a/modules/scripts/default.nix +++ b/modules/scripts/default.nix @@ -37,6 +37,34 @@ let # Script definitions: { path, env?, deps?, conditions? } # condition: attrset of { option (string path), value (expected value) } scriptDefs = { + aerc-mail-setup = { + path = ./aerc-mail-setup.sh; + deps = [ + pkgs.gnupg + pkgs.isync + pkgs.notmuch + pkgs.pass + ]; + conditions = [ + { + option = "programs.aerc.enable"; + value = true; + } + ]; + }; + aerc-sync-mail = { + path = ./aerc-sync-mail.sh; + deps = [ + pkgs.isync + pkgs.notmuch + ]; + conditions = [ + { + option = "programs.aerc.enable"; + value = true; + } + ]; + }; aria2-run = { path = ./aria2-run.sh; deps = [ diff --git a/users/arpit.nix b/users/arpit.nix index dea8dbb..cbc29e4 100644 --- a/users/arpit.nix +++ b/users/arpit.nix @@ -11,6 +11,7 @@ desktop.enable = true; # Tools + programs.aerc.enable = true; programs.aria2.enable = true; programs.bat.enable = true; programs.bluetui.enable = true; @@ -25,10 +26,12 @@ programs.git.enable = true; programs.git.username = "Arpit Chakladar"; programs.git.email = "arpitchakladar@proton.me"; + programs.gpg.enable = true; programs.heroic.enable = true; programs.htop.enable = true; programs.impala.enable = true; programs.kitty.enable = true; + programs.less.enable = true; programs.lf.enable = true; programs.lsd.enable = true; programs.maim.enable = true; @@ -39,6 +42,7 @@ programs.openvpn.enable = true; programs.ouch.enable = true; programs.pamixer.enable = true; + programs.pass.enable = true; programs.playerctl.enable = true; programs.qemu.enable = true; programs.slop.enable = true; |
