From cfd5093d74a921ae467745fc093b1ea0af06d0e2 Mon Sep 17 00:00:00 2001 From: Ray Andrew Date: Wed, 3 Dec 2025 20:09:09 -0600 Subject: [PATCH] email ux --- bin/cb | 4 +- bin/gpg-add-uid | 35 ++++++ bin/gpg-backup-key | 44 ++++++++ bin/gpg-delete-key | 52 +++++++++ bin/gpg-private-key | 56 ++++++++++ bin/gpg-public-key | 51 +++++++++ bin/gpg-restore-key | 58 ++++++++++ bin/gpg-setup | 93 ++++++++++++++++ bin/mailcap-open | 59 ++++++----- bin/open-attachment | 32 +++--- bin/open-mail | 2 +- bin/path-shim | 1 + bin/wb | 12 +-- config/msmtp/config | 5 +- config/neomutt/accounts/personal | 6 +- config/neomutt/accounts/uchicago | 6 +- config/neomutt/colors | 8 +- config/neomutt/keybinds | 19 ++++ config/neomutt/neomuttrc | 19 +++- config/notmuch/config | 17 +++ docs/gpg-setup.md | 177 +++++++++++++++++++++++++++++++ flake.nix | 2 + home/email/default.nix | 18 +++- hosts/dango/default.nix | 1 + 24 files changed, 712 insertions(+), 65 deletions(-) create mode 100755 bin/gpg-add-uid create mode 100755 bin/gpg-backup-key create mode 100755 bin/gpg-delete-key create mode 100755 bin/gpg-private-key create mode 100755 bin/gpg-public-key create mode 100755 bin/gpg-restore-key create mode 100755 bin/gpg-setup create mode 100644 config/notmuch/config create mode 100644 docs/gpg-setup.md diff --git a/bin/cb b/bin/cb index c73e1c6..8a23b91 100755 --- a/bin/cb +++ b/bin/cb @@ -55,11 +55,11 @@ stdout_is_a_tty() { } requested_open_ended() { - [[ "${args[0]:-}" == "-" ]] + [[ ${args[0]:-} == "-" ]] } requested_test_suite() { - [[ "${args[0]:-}" == "--test" ]] + [[ ${args[0]:-} == "--test" ]] } enable_tee_like_chaining() { diff --git a/bin/gpg-add-uid b/bin/gpg-add-uid new file mode 100755 index 0000000..7e0763c --- /dev/null +++ b/bin/gpg-add-uid @@ -0,0 +1,35 @@ +#!/bin/bash +# Add a new UID to an existing GPG key +# Usage: gpg-add-uid "Name" "email@example.com" [key-id] + +set -e + +if [[ $# -lt 2 ]]; then + echo 'Usage: gpg-add-uid "Name" "email@example.com" [key-id]' + echo "" + echo "If key-id is not provided, uses the first secret key found." + exit 1 +fi + +NAME="$1" +EMAIL="$2" +KEY_ID="${3:-$(gpg --list-secret-keys --keyid-format LONG 2>/dev/null | grep '^sec' | head -1 | sed 's/.*\/\([A-F0-9]*\) .*/\1/')}" + +if [[ -z $KEY_ID ]]; then + echo "Error: No GPG secret key found. Create one first with: gpg --full-generate-key" + exit 1 +fi + +echo "Adding UID '$NAME <$EMAIL>' to key $KEY_ID" + +# Use expect-like input via gpg --command-fd +gpg --batch --command-fd 0 --edit-key "$KEY_ID" </dev/null | grep '^sec' | head -1 | sed 's/.*\/\([A-F0-9]*\) .*/\1/') +fi + +if [[ -z $KEY_ID ]]; then + echo "Error: No GPG key found" + exit 1 +fi + +# Create output directory if needed +mkdir -p "$OUTPUT_DIR" + +PRIVATE_KEY="$OUTPUT_DIR/gpg-private-key-$KEY_ID.asc" +PUBLIC_KEY="$OUTPUT_DIR/gpg-public-key-$KEY_ID.asc" + +echo "Backing up GPG key $KEY_ID" +echo "" + +echo "Exporting private key to $PRIVATE_KEY..." +gpg --armor --export-secret-keys "$KEY_ID" >"$PRIVATE_KEY" +chmod 600 "$PRIVATE_KEY" + +echo "Exporting public key to $PUBLIC_KEY..." +gpg --armor --export "$KEY_ID" >"$PUBLIC_KEY" + +echo "" +echo "Backup complete!" +echo " Private key: $PRIVATE_KEY" +echo " Public key: $PUBLIC_KEY" +echo "" +echo "WARNING: Keep your private key safe and never share it!" +echo "" +echo "To restore, run:" +echo " gpg-restore-key $PRIVATE_KEY" diff --git a/bin/gpg-delete-key b/bin/gpg-delete-key new file mode 100755 index 0000000..abb6674 --- /dev/null +++ b/bin/gpg-delete-key @@ -0,0 +1,52 @@ +#!/bin/bash +# Delete a GPG key (both secret and public) +# Usage: gpg-delete-key [key-id or email] + +set -e + +KEY_ID="${1:-}" + +# If no key specified, show available keys and prompt +if [[ -z $KEY_ID ]]; then + echo "Available GPG keys:" + echo "" + gpg --list-secret-keys --keyid-format LONG 2>/dev/null || echo "No keys found" + echo "" + read -p "Enter key ID or email to delete: " KEY_ID + + if [[ -z $KEY_ID ]]; then + echo "No key specified. Aborting." + exit 1 + fi +fi + +# Get the full key fingerprint +FINGERPRINT=$(gpg --list-secret-keys --with-colons "$KEY_ID" 2>/dev/null | grep '^fpr' | head -1 | cut -d: -f10) + +if [[ -z $FINGERPRINT ]]; then + echo "Error: Key not found: $KEY_ID" + exit 1 +fi + +echo "Key to delete:" +echo "" +gpg --list-keys --keyid-format LONG "$KEY_ID" +echo "" + +echo "WARNING: This will permanently delete the secret and public key!" +read -p "Are you sure? Type 'yes' to confirm: " CONFIRM + +if [[ $CONFIRM != "yes" ]]; then + echo "Aborting." + exit 1 +fi + +echo "" +echo "Deleting secret key..." +gpg --batch --yes --delete-secret-keys "$FINGERPRINT" + +echo "Deleting public key..." +gpg --batch --yes --delete-keys "$FINGERPRINT" + +echo "" +echo "Key deleted successfully." diff --git a/bin/gpg-private-key b/bin/gpg-private-key new file mode 100755 index 0000000..39a249a --- /dev/null +++ b/bin/gpg-private-key @@ -0,0 +1,56 @@ +#!/bin/bash +# Export GPG private key (BE CAREFUL - keep this safe!) +# Usage: gpg-private-key [-c] [key-id or email] +# -c Copy to clipboard instead of printing + +set -e + +COPY=false +KEY_ID="" + +# Parse arguments +while [[ $# -gt 0 ]]; do + case "$1" in + -c | --copy) + COPY=true + shift + ;; + *) + KEY_ID="$1" + shift + ;; + esac +done + +# If no key specified, use first secret key +if [[ -z $KEY_ID ]]; then + KEY_ID=$(gpg --list-secret-keys --keyid-format LONG 2>/dev/null | grep '^sec' | head -1 | sed 's/.*\/\([A-F0-9]*\) .*/\1/') +fi + +if [[ -z $KEY_ID ]]; then + echo "Error: No GPG key found" + exit 1 +fi + +echo "WARNING: You are exporting your PRIVATE key!" >&2 +echo "Keep this safe and never share it publicly!" >&2 +echo "" >&2 + +if $COPY; then + if [[ "$(uname)" == "Darwin" ]]; then + gpg --armor --export-secret-keys "$KEY_ID" | pbcopy + echo "Private key copied to clipboard" + elif command -v xclip &>/dev/null; then + gpg --armor --export-secret-keys "$KEY_ID" | xclip -selection clipboard + echo "Private key copied to clipboard" + elif command -v wl-copy &>/dev/null; then + gpg --armor --export-secret-keys "$KEY_ID" | wl-copy + echo "Private key copied to clipboard" + else + echo "Error: No clipboard tool found (pbcopy, xclip, or wl-copy)" + exit 1 + fi + echo "Remember to clear your clipboard after use!" >&2 +else + gpg --armor --export-secret-keys "$KEY_ID" +fi diff --git a/bin/gpg-public-key b/bin/gpg-public-key new file mode 100755 index 0000000..a52ee2d --- /dev/null +++ b/bin/gpg-public-key @@ -0,0 +1,51 @@ +#!/bin/bash +# Export GPG public key +# Usage: gpg-public-key [-c] [key-id or email] +# -c Copy to clipboard instead of printing + +set -e + +COPY=false +KEY_ID="" + +# Parse arguments +while [[ $# -gt 0 ]]; do + case "$1" in + -c | --copy) + COPY=true + shift + ;; + *) + KEY_ID="$1" + shift + ;; + esac +done + +# If no key specified, use first secret key +if [[ -z $KEY_ID ]]; then + KEY_ID=$(gpg --list-secret-keys --keyid-format LONG 2>/dev/null | grep '^sec' | head -1 | sed 's/.*\/\([A-F0-9]*\) .*/\1/') +fi + +if [[ -z $KEY_ID ]]; then + echo "Error: No GPG key found" + exit 1 +fi + +if $COPY; then + if [[ "$(uname)" == "Darwin" ]]; then + gpg --armor --export "$KEY_ID" | pbcopy + echo "Public key copied to clipboard" + elif command -v xclip &>/dev/null; then + gpg --armor --export "$KEY_ID" | xclip -selection clipboard + echo "Public key copied to clipboard" + elif command -v wl-copy &>/dev/null; then + gpg --armor --export "$KEY_ID" | wl-copy + echo "Public key copied to clipboard" + else + echo "Error: No clipboard tool found (pbcopy, xclip, or wl-copy)" + exit 1 + fi +else + gpg --armor --export "$KEY_ID" +fi diff --git a/bin/gpg-restore-key b/bin/gpg-restore-key new file mode 100755 index 0000000..7350bbf --- /dev/null +++ b/bin/gpg-restore-key @@ -0,0 +1,58 @@ +#!/bin/bash +# Restore GPG key from backup file +# Usage: gpg-restore-key [public-key-file] + +set -e + +if [[ $# -lt 1 ]]; then + echo "Usage: gpg-restore-key [public-key-file]" + echo "" + echo "Examples:" + echo " gpg-restore-key ~/private-key-backup.asc" + echo " gpg-restore-key ~/private-key.asc ~/public-key.asc" + exit 1 +fi + +PRIVATE_KEY="$1" +PUBLIC_KEY="${2:-}" + +if [[ ! -f $PRIVATE_KEY ]]; then + echo "Error: File not found: $PRIVATE_KEY" + exit 1 +fi + +echo "Importing private key from $PRIVATE_KEY..." +gpg --import "$PRIVATE_KEY" + +if [[ -n $PUBLIC_KEY && -f $PUBLIC_KEY ]]; then + echo "" + echo "Importing public key from $PUBLIC_KEY..." + gpg --import "$PUBLIC_KEY" +fi + +# Get the key ID that was just imported +KEY_ID=$(gpg --list-secret-keys --keyid-format LONG 2>/dev/null | grep '^sec' | head -1 | sed 's/.*\/\([A-F0-9]*\) .*/\1/') + +if [[ -z $KEY_ID ]]; then + echo "Error: Could not find imported key" + exit 1 +fi + +echo "" +echo "Key imported successfully!" +echo "" +gpg --list-keys --keyid-format LONG "$KEY_ID" + +echo "" +read -p "Do you want to trust this key ultimately? [y/N] " -n 1 -r +echo +if [[ $REPLY =~ ^[Yy]$ ]]; then + echo "Setting ultimate trust..." + echo -e "5\ny\n" | gpg --command-fd 0 --edit-key "$KEY_ID" trust 2>/dev/null + echo "Done!" +fi + +echo "" +echo "Key ID: $KEY_ID" +echo "Update your neomutt config with:" +echo " set pgp_sign_as = 0x$KEY_ID" diff --git a/bin/gpg-setup b/bin/gpg-setup new file mode 100755 index 0000000..a16dadd --- /dev/null +++ b/bin/gpg-setup @@ -0,0 +1,93 @@ +#!/bin/bash +# Setup GPG key with all email identities +# Usage: gpg-setup + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +# Configuration +PRIMARY_NAME="Ray Andrew Sinurat" +PRIMARY_EMAIL="raydreww@gmail.com" + +# Additional UIDs to add (name|email) +ADDITIONAL_UIDS=( + "Ray Andrew Sinurat|rayandrew@uchicago.edu" + "Ray Andrew|raydreww@gmail.com" + "Ray Andrew|rayandrew@uchicago.edu" + "Ray A. O. Sinurat|raydreww@gmail.com" + "Ray A. O. Sinurat|rayandrew@uchicago.edu" + "Ray Andrew Obaja Sinurat|raydreww@gmail.com" + "Ray Andrew Obaja Sinurat|rayandrew@uchicago.edu" +) + +# Check if key already exists +if gpg --list-secret-keys "$PRIMARY_EMAIL" &>/dev/null; then + echo "GPG key for $PRIMARY_EMAIL already exists." + echo "" + gpg --list-secret-keys --keyid-format LONG "$PRIMARY_EMAIL" + echo "" + read -p "Do you want to add missing UIDs to this key? [y/N] " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + exit 0 + fi +else + echo "Creating new GPG key for $PRIMARY_NAME <$PRIMARY_EMAIL>" + echo "" + echo "You will be prompted for a passphrase." + echo "" + + gpg --full-generate-key --batch </dev/null | grep '^sec' | head -1 | sed 's/.*\/\([A-F0-9]*\) .*/\1/') + +if [[ -z $KEY_ID ]]; then + echo "Error: Could not find key ID" + exit 1 +fi + +echo "" +echo "Key ID: $KEY_ID" +echo "" +echo "Adding additional UIDs..." + +# Get existing UIDs +EXISTING_UIDS=$(gpg --list-keys "$KEY_ID" 2>/dev/null | grep '^uid' | sed 's/.*] //') + +for uid in "${ADDITIONAL_UIDS[@]}"; do + NAME="${uid%|*}" + EMAIL="${uid#*|}" + UID_STRING="$NAME <$EMAIL>" + + if echo "$EXISTING_UIDS" | grep -qF "$UID_STRING"; then + echo " [skip] $UID_STRING (already exists)" + else + echo " [add] $UID_STRING" + "$SCRIPT_DIR/gpg-add-uid" "$NAME" "$EMAIL" "$KEY_ID" 2>/dev/null || true + fi +done + +echo "" +echo "Done! Final key:" +echo "" +gpg --list-keys --keyid-format LONG "$KEY_ID" + +echo "" +echo "Update your neomutt config with:" +echo " set pgp_sign_as = 0x$KEY_ID" diff --git a/bin/mailcap-open b/bin/mailcap-open index fbf6eaf..54d80e9 100755 --- a/bin/mailcap-open +++ b/bin/mailcap-open @@ -4,26 +4,26 @@ export PATH="/etc/profiles/per-user/$USER/bin:/run/current-system/sw/bin:$PATH" # Handle piped input or file argument -if [[ -n "$1" ]]; then +if [[ -n $1 ]]; then file="$1" else # Read from stdin to temp file tmpfile=$(mktemp) - cat > "$tmpfile" + cat >"$tmpfile" mime=$(file --mime-type -b "$tmpfile") # Add extension based on mime type case "$mime" in - application/pdf) ext=".pdf" ;; - image/png) ext=".png" ;; - image/jpeg) ext=".jpg" ;; - image/gif) ext=".gif" ;; - text/html) ext=".html" ;; - text/plain) ext=".txt" ;; - *) ext="" ;; + application/pdf) ext=".pdf" ;; + image/png) ext=".png" ;; + image/jpeg) ext=".jpg" ;; + image/gif) ext=".gif" ;; + text/html) ext=".html" ;; + text/plain) ext=".txt" ;; + *) ext="" ;; esac - if [[ -n "$ext" ]]; then + if [[ -n $ext ]]; then mv "$tmpfile" "${tmpfile}${ext}" file="${tmpfile}${ext}" else @@ -38,28 +38,31 @@ viewers=() viewers+=("open (default app)") case "$mime" in - application/pdf) - viewers+=("zathura") - ;; - image/*) - viewers+=("chafa (terminal)") - ;; - text/html) - viewers+=("w3m (browser)") - viewers+=("less (text)") - ;; - text/*) - viewers+=("less") - ;; +application/pdf) + viewers+=("zathura") + ;; +image/*) + viewers+=("chafa (terminal)") + ;; +text/html) + viewers+=("w3m (browser)") + viewers+=("less (text)") + ;; +text/*) + viewers+=("less") + ;; esac # Select with fzf selected=$(printf '%s\n' "${viewers[@]}" | fzf --prompt="Open with: " --height=10) case "$selected" in - "open (default app)") open "$file" ;; - "chafa (terminal)") chafa "$file"; read -n 1 -s -r -p "Press any key..." ;; - "zathura") zathura "$file" ;; - "w3m (browser)") w3m -T text/html "$file" ;; - "less"*) less "$file" ;; +"open (default app)") open "$file" ;; +"chafa (terminal)") + chafa "$file" + read -n 1 -s -r -p "Press any key..." + ;; +"zathura") zathura "$file" ;; +"w3m (browser)") w3m -T text/html "$file" ;; +"less"*) less "$file" ;; esac diff --git a/bin/open-attachment b/bin/open-attachment index 2c0e10e..0315a9a 100755 --- a/bin/open-attachment +++ b/bin/open-attachment @@ -2,28 +2,28 @@ # Open attachment with correct extension based on mime type tmpfile=$(mktemp) -cat > "$tmpfile" +cat >"$tmpfile" mime=$(file --mime-type -b "$tmpfile") case "$mime" in - application/pdf) ext=".pdf" ;; - image/png) ext=".png" ;; - image/jpeg) ext=".jpg" ;; - image/gif) ext=".gif" ;; - text/html) ext=".html" ;; - text/plain) ext=".txt" ;; - application/zip) ext=".zip" ;; - application/msword) ext=".doc" ;; - application/vnd.openxmlformats-officedocument.wordprocessingml.document) ext=".docx" ;; - application/vnd.ms-excel) ext=".xls" ;; - application/vnd.openxmlformats-officedocument.spreadsheetml.sheet) ext=".xlsx" ;; - application/vnd.ms-powerpoint) ext=".ppt" ;; - application/vnd.openxmlformats-officedocument.presentationml.presentation) ext=".pptx" ;; - *) ext="" ;; +application/pdf) ext=".pdf" ;; +image/png) ext=".png" ;; +image/jpeg) ext=".jpg" ;; +image/gif) ext=".gif" ;; +text/html) ext=".html" ;; +text/plain) ext=".txt" ;; +application/zip) ext=".zip" ;; +application/msword) ext=".doc" ;; +application/vnd.openxmlformats-officedocument.wordprocessingml.document) ext=".docx" ;; +application/vnd.ms-excel) ext=".xls" ;; +application/vnd.openxmlformats-officedocument.spreadsheetml.sheet) ext=".xlsx" ;; +application/vnd.ms-powerpoint) ext=".ppt" ;; +application/vnd.openxmlformats-officedocument.presentationml.presentation) ext=".pptx" ;; +*) ext="" ;; esac -if [[ -n "$ext" ]]; then +if [[ -n $ext ]]; then mv "$tmpfile" "${tmpfile}${ext}" tmpfile="${tmpfile}${ext}" fi diff --git a/bin/open-mail b/bin/open-mail index 0521d48..5384d70 100755 --- a/bin/open-mail +++ b/bin/open-mail @@ -6,7 +6,7 @@ if [[ "$(uname)" == "Darwin" ]]; then aerospace workspace 8 else aerospace workspace 8 - open -na Ghostty --args --title="Mail" -e /etc/profiles/per-user/rayandrew/bin/neomutt + open -na Ghostty --args --title="Mail" -e ~/dotfiles/bin/path-shim neomutt fi else # Linux (i3/sway) diff --git a/bin/path-shim b/bin/path-shim index 62181de..66fc0b4 100755 --- a/bin/path-shim +++ b/bin/path-shim @@ -4,6 +4,7 @@ # Or: path-shim "command with args" export PATH="/etc/profiles/per-user/$USER/bin:/run/current-system/sw/bin:$PATH" +export SOPS_AGE_KEY_FILE="$HOME/.config/sops/age/keys.txt" if [[ $# -eq 1 ]]; then # Single argument - run it through bash to handle complex commands diff --git a/bin/wb b/bin/wb index d72d163..5718633 100755 --- a/bin/wb +++ b/bin/wb @@ -6,7 +6,7 @@ BOOKMARKS_FILE="${DOTFILES}/secrets/wb.txt" # Decrypt bookmarks from sops getBookmarks() { - if [[ -f "${BOOKMARKS_FILE}" ]]; then + if [[ -f ${BOOKMARKS_FILE} ]]; then # Unencrypted file (for development/testing) cat "${BOOKMARKS_FILE}" elif [[ -f "${BOOKMARKS_FILE%.txt}.enc" ]]; then @@ -144,7 +144,7 @@ fuzzy() { --preview='bash -c "url=\$(echo {} | cut -f2); desc=\$(echo {} | cut -f3); tags=\$(echo {} | cut -f4); echo -e \"\$url\n\nDesc: \$desc\n\nTags: \$tags\""' \ --preview-window=down:12:wrap) - if [[ -n "$selected_line" ]]; then + if [[ -n $selected_line ]]; then IFS=$'\t' read -r key url _ <<<"$selected_line" execute "$url" else @@ -225,7 +225,7 @@ fi if [[ $editMode == 1 ]]; then encFile="${BOOKMARKS_FILE%.txt}.enc" - if [[ -f "$encFile" ]]; then + if [[ -f $encFile ]]; then echo "Editing encrypted bookmarks file" # Decrypt to the .txt file (matches .sops.yaml path_regex), edit, re-encrypt trap "rm -f '${BOOKMARKS_FILE}'" EXIT @@ -234,7 +234,7 @@ if [[ $editMode == 1 ]]; then sops --encrypt --input-type binary --output-type binary "${BOOKMARKS_FILE}" >"$encFile" rm -f "${BOOKMARKS_FILE}" echo "Saved and re-encrypted" - elif [[ -f "${BOOKMARKS_FILE}" ]]; then + elif [[ -f ${BOOKMARKS_FILE} ]]; then echo "Editing ${BOOKMARKS_FILE}" ${EDITOR:-vim} "${BOOKMARKS_FILE}" else @@ -245,7 +245,7 @@ if [[ $editMode == 1 ]]; then fi if [[ $queryMode == 1 ]]; then - if [[ -z "$q" ]]; then + if [[ -z $q ]]; then bs=$(bks) else bs=$(bks | grep -i "$q") @@ -297,7 +297,7 @@ fi if [ $hitCount -gt 1 ]; then exactHit=0 while read -r key url misc; do - if [[ "$key" == "$q" ]]; then + if [[ $key == "$q" ]]; then exactHit=1 echo "$key $url" >"$tempFile" break diff --git a/config/msmtp/config b/config/msmtp/config index a01acc5..9b5fcb6 100644 --- a/config/msmtp/config +++ b/config/msmtp/config @@ -10,9 +10,10 @@ logfile ~/.local/state/msmtp.log # Personal Gmail account account personal host smtp.gmail.com +port 465 from raydreww@gmail.com user raydreww@gmail.com -passwordeval "sops -d --extract '[\"personal\"]' ~/dotfiles/home/email/secrets.yaml" +passwordeval sops -d --extract '["personal"]' ~/dotfiles/home/email/secrets.yaml tls_starttls off # UChicago account (via DavMail) @@ -21,7 +22,7 @@ host 127.0.0.1 port 1025 from rayandrew@uchicago.edu user rayandrew@uchicago.edu -passwordeval "sops -d --extract '[\"uchicago\"]' ~/dotfiles/home/email/secrets.yaml" +passwordeval sops -d --extract '["uchicago"]' ~/dotfiles/home/email/secrets.yaml auth plain tls off tls_starttls off diff --git a/config/neomutt/accounts/personal b/config/neomutt/accounts/personal index 0e2cac2..3064e97 100644 --- a/config/neomutt/accounts/personal +++ b/config/neomutt/accounts/personal @@ -23,7 +23,7 @@ set trash = '+Trash' # PGP settings set use_from = yes set pgp_verify_sig = yes -set pgp_sign_as = 0x07AA5254804C009F +set pgp_sign_as = 0xBAA368F02F486080 set pgp_timeout = 3600 # Mailboxes @@ -34,5 +34,9 @@ named-mailboxes "p/important" =Important named-mailboxes "p/trash" =Trash named-mailboxes "p/archive" =Archive +# Virtual mailboxes (notmuch) +virtual-mailboxes "All Mail" "notmuch://?query=folder:personal/** AND date:30d.." +virtual-mailboxes "Unread" "notmuch://?query=folder:personal/** AND tag:unread" + # Signature set signature = "~/.config/neomutt/signatures/personal" diff --git a/config/neomutt/accounts/uchicago b/config/neomutt/accounts/uchicago index 225badb..acb16cb 100644 --- a/config/neomutt/accounts/uchicago +++ b/config/neomutt/accounts/uchicago @@ -22,7 +22,7 @@ set trash = '+Trash' # PGP settings set use_from = yes -set pgp_sign_as = 0xEEF04CFFE9DFE5FC +set pgp_sign_as = 0xBAA368F02F486080 set pgp_verify_sig = yes set pgp_timeout = 3600 @@ -35,5 +35,9 @@ named-mailboxes "u/trash" =Trash named-mailboxes "u/archive" =Archive named-mailboxes "u/teaching" =Teaching +# Virtual mailboxes (notmuch) +virtual-mailboxes "All Mail" "notmuch://?query=folder:uchicago/** AND date:30d.." +virtual-mailboxes "Unread" "notmuch://?query=folder:uchicago/** AND tag:unread" + # Signature set signature = "~/.config/neomutt/signatures/uchicago" diff --git a/config/neomutt/colors b/config/neomutt/colors index aeaf110..c83637a 100644 --- a/config/neomutt/colors +++ b/config/neomutt/colors @@ -72,10 +72,10 @@ color index_date '#475e6c' default color index_size '#475e6c' default color index_flags '#49e9a6' default '.*' -# New mail - highlighted with bg_highlight -color index '#e4b781' '#0c3f5f' "~N" -color index_author '#df769b' '#0c3f5f' "~N" -color index_subject '#49d6e9' '#0c3f5f' "~N" +# New mail - gold with subtle background +color index '#e4b781' '#041520' "~N" +color index_author '#df769b' '#041520' "~N" +color index_subject '#e4b781' '#041520' "~N" # Flagged mail color index '#e66533' default "~F" diff --git a/config/neomutt/keybinds b/config/neomutt/keybinds index 714968f..526355f 100644 --- a/config/neomutt/keybinds +++ b/config/neomutt/keybinds @@ -68,3 +68,22 @@ macro attach o "unset wait_key~/dotfiles/bin/o macro attach O "unset wait_key~/dotfiles/bin/mailcap-open" "Open with fzf picker" macro attach,pager p "|git apply" "Apply git patch" macro attach,pager P "|git-apply-patch" "Apply git patch (interactive)" + +# Notmuch search +bind index,pager \\ vfolder-from-query +macro index,pager ga "date:30d.." "View recent mail (30 days)" +macro index,pager gA "*" "View all mail" +macro index,pager gn "tag:unread" "View unread mail" +macro index,pager gr "date:7d.." "View recent mail (7 days)" + +# Compose +bind index c mail + +# Compose menu - PGP shortcuts +bind compose S pgp-menu + +# Mark messages +bind index,pager m noop +macro index,pager mu "unset mark_old" "Mark as unread" +macro index,pager mr "N" "Mark as read" +macro index ma ".N." "Mark all as read" diff --git a/config/neomutt/neomuttrc b/config/neomutt/neomuttrc index 638a28f..6ca6321 100644 --- a/config/neomutt/neomuttrc +++ b/config/neomutt/neomuttrc @@ -4,14 +4,18 @@ set header_cache = "~/.cache/neomutt/headers/" set message_cachedir = "~/.cache/neomutt/messages/" +# Shell +set shell = "/bin/bash -l" + # Editor -set editor = "emacs -nw" +set editor = "nvim" set edit_headers = yes # General settings set color_directcolor = yes set implicit_autoview = yes set crypt_use_gpgme = yes +unset mark_old alternative_order text/enriched text/plain text set delete = yes set abort_key = "" @@ -25,8 +29,8 @@ set mail_check_stats set status_chars = " *%A" set status_format = "[ Folder: %D ] [%r%m messages%?n? (%n new)?%?d? (%d to delete)?%?t? (%t tagged)? ]%>─%?p?( %p postponed )?" set date_format = "%d.%m.%Y %H:%M" -set sort = threads -set sort_aux = reverse-last-date-received +set sort = date +set sort_aux = date set uncollapse_jump set sort_re set index_format = "%4C %Z %{%b %d} %-15.15L %?E?(%E)&? %s" @@ -62,5 +66,14 @@ folder-hook ~/mail/personal/ "source ~/.config/neomutt/accounts/personal" named-mailboxes "u" "~/mail/uchicago/Inbox" folder-hook ~/mail/uchicago/ "source ~/.config/neomutt/accounts/uchicago" +# Jump to last (newest) message when opening folders +folder-hook . "push " + +# Notmuch virtual mailboxes (search across all mail) +set nm_config_file = `echo "$HOME/.config/notmuch/config"` +set nm_default_url = `echo "notmuch://$HOME/mail"` +set nm_query_type = messages +set nm_record_tags = "-inbox,sent" + # Source primary account (personal) source ~/.config/neomutt/accounts/personal diff --git a/config/notmuch/config b/config/notmuch/config new file mode 100644 index 0000000..460ec33 --- /dev/null +++ b/config/notmuch/config @@ -0,0 +1,17 @@ +[database] +path=mail + +[user] +name=Ray Andrew +primary_email=raydreww@gmail.com +other_email=rayandrew@uchicago.edu + +[new] +tags=unread;inbox +ignore=.mbsyncstate;.strstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstrstr + +[search] +exclude_tags=deleted;spam + +[maildir] +synchronize_flags=true diff --git a/docs/gpg-setup.md b/docs/gpg-setup.md new file mode 100644 index 0000000..5108e6a --- /dev/null +++ b/docs/gpg-setup.md @@ -0,0 +1,177 @@ +# GPG Setup for Email Signing + +## Quick Setup (Automated) + +Run the setup script to create a GPG key with all email identities: + +```bash +gpg-setup +``` + +This will: +1. Create a 4096-bit RSA key (expires in 2 years) +2. Add all name/email variations as UIDs +3. Print the key ID to use in neomutt config + +## Manual Setup + +### Step 1: Create the primary key + +```bash +gpg --full-generate-key +``` + +When prompted: +1. Select `(1) RSA and RSA` +2. Key size: `4096` +3. Expiration: `2y` (or your preference) +4. Real name: `Ray Andrew Sinurat` (use your most formal name) +5. Email: `raydreww@gmail.com` (primary email) +6. Comment: (leave empty) +7. Enter a passphrase + +### Step 2: Add additional UIDs + +Add more email addresses and name variations to the same key: + +```bash +gpg-add-uid "Ray Andrew Sinurat" "rayandrew@uchicago.edu" +gpg-add-uid "Ray Andrew" "raydreww@gmail.com" +gpg-add-uid "Ray Andrew" "rayandrew@uchicago.edu" +gpg-add-uid "Ray A. O. Sinurat" "raydreww@gmail.com" +gpg-add-uid "Ray A. O. Sinurat" "rayandrew@uchicago.edu" +``` + +### Example final key structure + +``` +sec rsa4096/ABCD1234EFGH5678 2024-01-01 [SC] [expires: 2026-01-01] +uid [ultimate] Ray Andrew Sinurat +uid [ultimate] Ray Andrew Sinurat +uid [ultimate] Ray Andrew +uid [ultimate] Ray Andrew +uid [ultimate] Ray A. O. Sinurat +uid [ultimate] Ray A. O. Sinurat +ssb rsa4096/1234567890ABCDEF 2024-01-01 [E] [expires: 2026-01-01] +``` + +## Get Key ID + +```bash +gpg --list-secret-keys --keyid-format LONG +``` + +The key ID is the part after `rsa4096/` (e.g., `ABCD1234EFGH5678`). + +## Update NeoMutt Config + +Use the **same key ID** for both accounts: + +### Personal (`config/neomutt/accounts/personal`) +``` +set pgp_sign_as = 0xYOUR_KEY_ID +``` + +### UChicago (`config/neomutt/accounts/uchicago`) +``` +set pgp_sign_as = 0xYOUR_KEY_ID +``` + +## Export Public Key (for sharing) + +```bash +# Print to stdout +gpg-public-key + +# Copy to clipboard (works on macOS, Linux with xclip or wl-copy) +gpg-public-key -c + +# Export specific key +gpg-public-key raydreww@gmail.com + +# Export to file +gpg-public-key > ~/public-key.asc +``` + +## Import Existing Keys + +If you have backed up keys: + +```bash +# Restore from backup (imports and sets trust) +gpg-restore-key ~/private-key-backup.asc + +# Or with public key too +gpg-restore-key ~/private-key.asc ~/public-key.asc +``` + +## Backup Keys + +```bash +# Backup both keys to home directory +gpg-backup-key + +# Backup to specific directory +gpg-backup-key ~/secure-backup + +# Backup specific key +gpg-backup-key ~/backup raydreww@gmail.com +``` + +This creates: +- `gpg-private-key-.asc` (chmod 600) +- `gpg-public-key-.asc` + +### Manual export + +```bash +# Export private key (keep this safe!) +gpg-private-key > ~/private-key-backup.asc + +# Copy private key to clipboard +gpg-private-key -c + +# Export public key +gpg-public-key > ~/public-key-backup.asc +``` + +## GPG Agent + +Make sure gpg-agent is running. It's enabled in home-manager config: + +```nix +services.gpg-agent = { + enable = true; +}; +``` + +To manually start: +```bash +gpgconf --launch gpg-agent +``` + +## Troubleshooting + +### "secret key not found" +- Check key ID matches: `gpg --list-secret-keys` +- Ensure gpg-agent is running: `gpgconf --launch gpg-agent` +- Reload agent: `gpg-connect-agent reloadagent /bye` + +### Disable signing temporarily +In neomutt account file, set: +``` +set crypt_autosign = no +``` + +## Delete Keys + +To delete a GPG key (e.g., when leaving an organization): + +```bash +# Delete by key ID or email +gpg-delete-key 7C19EB1AF0BD68BF +gpg-delete-key raydreww@gmail.com + +# Interactive mode (shows keys and prompts) +gpg-delete-key +``` diff --git a/flake.nix b/flake.nix index 4b7b9d4..a1655b8 100644 --- a/flake.nix +++ b/flake.nix @@ -99,6 +99,8 @@ programs.nixfmt.enable = true; programs.stylua.enable = true; programs.shfmt.enable = true; + programs.shfmt.includes = [ "bin/*" ]; + programs.shfmt.indent_size = 4; programs.fish_indent.enable = true; programs.shellcheck.enable = true; settings.global.excludes = [ "flake.lock" ]; diff --git a/home/email/default.nix b/home/email/default.nix index 8820324..82a1be4 100644 --- a/home/email/default.nix +++ b/home/email/default.nix @@ -13,6 +13,7 @@ davmail = mkEnableOption "Enable DavMail"; mbsync = mkEnableOption "Enable Mbsync"; neomutt = mkEnableOption "Enable NeoMutt"; + notmuch = mkEnableOption "Enable notmuch"; mailcap = mkEnableOption "Enable mailcap"; }; @@ -28,9 +29,13 @@ enable = config.custom.email.mbsync; configFile = "${dots}/config/mbsync/mbsyncrc"; frequency = "*:0/1"; - extraPackages = with pkgs; [ sops ]; + extraPackages = with pkgs; [ sops ] ++ lib.optionals config.custom.email.notmuch [ notmuch ]; + postExec = lib.mkIf config.custom.email.notmuch "${pkgs.notmuch}/bin/notmuch new"; environment = { SOPS_AGE_KEY_FILE = "${xdg-config-dir}/sops/age/keys.txt"; + } + // lib.optionalAttrs config.custom.email.notmuch { + NOTMUCH_CONFIG = "${xdg-config-dir}/notmuch/config"; }; }; @@ -49,6 +54,9 @@ exec env TERM=xterm-direct ${neomutt}/bin/neomutt "$@" '') ] + ++ lib.optionals config.custom.email.notmuch [ + notmuch + ] ++ lib.optionals config.custom.email.mailcap [ mailcap w3m # HTML rendering @@ -61,6 +69,9 @@ "msmtp".source = config.lib.file.mkOutOfStoreSymlink "${dots}/config/msmtp"; "neomutt".source = config.lib.file.mkOutOfStoreSymlink "${dots}/config/neomutt"; "isyncrc".source = config.lib.file.mkOutOfStoreSymlink "${dots}/config/mbsync/mbsyncrc"; + } + // lib.optionalAttrs config.custom.email.notmuch { + "notmuch/config".source = config.lib.file.mkOutOfStoreSymlink "${dots}/config/notmuch/config"; }; # mailcap symlink @@ -76,5 +87,10 @@ mkdir -p ~/.cache/neomutt/messages mkdir -p ~/.local/state ''; + + # Set NOTMUCH_CONFIG environment variable + custom.environment.variables = lib.mkIf config.custom.email.notmuch { + NOTMUCH_CONFIG = "${xdg-config-dir}/notmuch/config"; + }; }; } diff --git a/hosts/dango/default.nix b/hosts/dango/default.nix index 2202b15..583438d 100644 --- a/hosts/dango/default.nix +++ b/hosts/dango/default.nix @@ -105,6 +105,7 @@ neomutt = true; mbsync = true; mailcap = true; + notmuch = true; }; }; }