aboutsummaryrefslogtreecommitdiffstats
path: root/modules
diff options
context:
space:
mode:
Diffstat (limited to 'modules')
-rw-r--r--modules/programs/aerc/default.nix54
-rw-r--r--modules/programs/default.nix4
-rw-r--r--modules/programs/git/default.nix41
-rw-r--r--modules/programs/gpg/default.nix18
-rw-r--r--modules/programs/less/default.nix15
-rw-r--r--modules/programs/pass/default.nix69
-rw-r--r--modules/programs/zsh/default.nix3
-rw-r--r--modules/scripts/aerc-mail-setup.sh333
-rw-r--r--modules/scripts/aerc-sync-mail.sh15
-rw-r--r--modules/scripts/default.nix28
10 files changed, 566 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 = [