This commit is contained in:
Ray Andrew 2025-12-03 20:09:09 -06:00
parent 07b68562d3
commit cfd5093d74
Signed by: rayandrew
SSH key fingerprint: SHA256:XYrYrxF0Z3A72n8P/p6mqPRNQZT22F88XcLsG+kX4xw
24 changed files with 712 additions and 65 deletions

4
bin/cb
View file

@ -55,11 +55,11 @@ stdout_is_a_tty() {
} }
requested_open_ended() { requested_open_ended() {
[[ "${args[0]:-}" == "-" ]] [[ ${args[0]:-} == "-" ]]
} }
requested_test_suite() { requested_test_suite() {
[[ "${args[0]:-}" == "--test" ]] [[ ${args[0]:-} == "--test" ]]
} }
enable_tee_like_chaining() { enable_tee_like_chaining() {

35
bin/gpg-add-uid Executable file
View file

@ -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" <<EOF
adduid
$NAME
$EMAIL
O
save
EOF
echo "Done! New UID added. Verify with: gpg --list-keys $KEY_ID"

44
bin/gpg-backup-key Executable file
View file

@ -0,0 +1,44 @@
#!/bin/bash
# Backup GPG key (both private and public) to a directory
# Usage: gpg-backup-key [output-dir] [key-id or email]
set -e
OUTPUT_DIR="${1:-$HOME}"
KEY_ID="${2:-}"
# 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
# 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"

52
bin/gpg-delete-key Executable file
View file

@ -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."

56
bin/gpg-private-key Executable file
View file

@ -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

51
bin/gpg-public-key Executable file
View file

@ -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

58
bin/gpg-restore-key Executable file
View file

@ -0,0 +1,58 @@
#!/bin/bash
# Restore GPG key from backup file
# Usage: gpg-restore-key <private-key-file> [public-key-file]
set -e
if [[ $# -lt 1 ]]; then
echo "Usage: gpg-restore-key <private-key-file> [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"

93
bin/gpg-setup Executable file
View file

@ -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 <<EOF
Key-Type: RSA
Key-Length: 4096
Subkey-Type: RSA
Subkey-Length: 4096
Name-Real: $PRIMARY_NAME
Name-Email: $PRIMARY_EMAIL
Expire-Date: 2y
%ask-passphrase
%commit
EOF
echo ""
echo "Primary key created!"
fi
# Get key ID
KEY_ID=$(gpg --list-secret-keys --keyid-format LONG "$PRIMARY_EMAIL" 2>/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"

View file

@ -4,12 +4,12 @@
export PATH="/etc/profiles/per-user/$USER/bin:/run/current-system/sw/bin:$PATH" export PATH="/etc/profiles/per-user/$USER/bin:/run/current-system/sw/bin:$PATH"
# Handle piped input or file argument # Handle piped input or file argument
if [[ -n "$1" ]]; then if [[ -n $1 ]]; then
file="$1" file="$1"
else else
# Read from stdin to temp file # Read from stdin to temp file
tmpfile=$(mktemp) tmpfile=$(mktemp)
cat > "$tmpfile" cat >"$tmpfile"
mime=$(file --mime-type -b "$tmpfile") mime=$(file --mime-type -b "$tmpfile")
# Add extension based on mime type # Add extension based on mime type
@ -23,7 +23,7 @@ else
*) ext="" ;; *) ext="" ;;
esac esac
if [[ -n "$ext" ]]; then if [[ -n $ext ]]; then
mv "$tmpfile" "${tmpfile}${ext}" mv "$tmpfile" "${tmpfile}${ext}"
file="${tmpfile}${ext}" file="${tmpfile}${ext}"
else else
@ -38,17 +38,17 @@ viewers=()
viewers+=("open (default app)") viewers+=("open (default app)")
case "$mime" in case "$mime" in
application/pdf) application/pdf)
viewers+=("zathura") viewers+=("zathura")
;; ;;
image/*) image/*)
viewers+=("chafa (terminal)") viewers+=("chafa (terminal)")
;; ;;
text/html) text/html)
viewers+=("w3m (browser)") viewers+=("w3m (browser)")
viewers+=("less (text)") viewers+=("less (text)")
;; ;;
text/*) text/*)
viewers+=("less") viewers+=("less")
;; ;;
esac esac
@ -57,9 +57,12 @@ esac
selected=$(printf '%s\n' "${viewers[@]}" | fzf --prompt="Open with: " --height=10) selected=$(printf '%s\n' "${viewers[@]}" | fzf --prompt="Open with: " --height=10)
case "$selected" in case "$selected" in
"open (default app)") open "$file" ;; "open (default app)") open "$file" ;;
"chafa (terminal)") chafa "$file"; read -n 1 -s -r -p "Press any key..." ;; "chafa (terminal)")
"zathura") zathura "$file" ;; chafa "$file"
"w3m (browser)") w3m -T text/html "$file" ;; read -n 1 -s -r -p "Press any key..."
"less"*) less "$file" ;; ;;
"zathura") zathura "$file" ;;
"w3m (browser)") w3m -T text/html "$file" ;;
"less"*) less "$file" ;;
esac esac

View file

@ -2,28 +2,28 @@
# Open attachment with correct extension based on mime type # Open attachment with correct extension based on mime type
tmpfile=$(mktemp) tmpfile=$(mktemp)
cat > "$tmpfile" cat >"$tmpfile"
mime=$(file --mime-type -b "$tmpfile") mime=$(file --mime-type -b "$tmpfile")
case "$mime" in case "$mime" in
application/pdf) ext=".pdf" ;; application/pdf) ext=".pdf" ;;
image/png) ext=".png" ;; image/png) ext=".png" ;;
image/jpeg) ext=".jpg" ;; image/jpeg) ext=".jpg" ;;
image/gif) ext=".gif" ;; image/gif) ext=".gif" ;;
text/html) ext=".html" ;; text/html) ext=".html" ;;
text/plain) ext=".txt" ;; text/plain) ext=".txt" ;;
application/zip) ext=".zip" ;; application/zip) ext=".zip" ;;
application/msword) ext=".doc" ;; application/msword) ext=".doc" ;;
application/vnd.openxmlformats-officedocument.wordprocessingml.document) ext=".docx" ;; application/vnd.openxmlformats-officedocument.wordprocessingml.document) ext=".docx" ;;
application/vnd.ms-excel) ext=".xls" ;; application/vnd.ms-excel) ext=".xls" ;;
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet) ext=".xlsx" ;; application/vnd.openxmlformats-officedocument.spreadsheetml.sheet) ext=".xlsx" ;;
application/vnd.ms-powerpoint) ext=".ppt" ;; application/vnd.ms-powerpoint) ext=".ppt" ;;
application/vnd.openxmlformats-officedocument.presentationml.presentation) ext=".pptx" ;; application/vnd.openxmlformats-officedocument.presentationml.presentation) ext=".pptx" ;;
*) ext="" ;; *) ext="" ;;
esac esac
if [[ -n "$ext" ]]; then if [[ -n $ext ]]; then
mv "$tmpfile" "${tmpfile}${ext}" mv "$tmpfile" "${tmpfile}${ext}"
tmpfile="${tmpfile}${ext}" tmpfile="${tmpfile}${ext}"
fi fi

View file

@ -6,7 +6,7 @@ if [[ "$(uname)" == "Darwin" ]]; then
aerospace workspace 8 aerospace workspace 8
else else
aerospace workspace 8 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 fi
else else
# Linux (i3/sway) # Linux (i3/sway)

View file

@ -4,6 +4,7 @@
# Or: path-shim "command with args" # Or: path-shim "command with args"
export PATH="/etc/profiles/per-user/$USER/bin:/run/current-system/sw/bin:$PATH" 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 if [[ $# -eq 1 ]]; then
# Single argument - run it through bash to handle complex commands # Single argument - run it through bash to handle complex commands

12
bin/wb
View file

@ -6,7 +6,7 @@ BOOKMARKS_FILE="${DOTFILES}/secrets/wb.txt"
# Decrypt bookmarks from sops # Decrypt bookmarks from sops
getBookmarks() { getBookmarks() {
if [[ -f "${BOOKMARKS_FILE}" ]]; then if [[ -f ${BOOKMARKS_FILE} ]]; then
# Unencrypted file (for development/testing) # Unencrypted file (for development/testing)
cat "${BOOKMARKS_FILE}" cat "${BOOKMARKS_FILE}"
elif [[ -f "${BOOKMARKS_FILE%.txt}.enc" ]]; then 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='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) --preview-window=down:12:wrap)
if [[ -n "$selected_line" ]]; then if [[ -n $selected_line ]]; then
IFS=$'\t' read -r key url _ <<<"$selected_line" IFS=$'\t' read -r key url _ <<<"$selected_line"
execute "$url" execute "$url"
else else
@ -225,7 +225,7 @@ fi
if [[ $editMode == 1 ]]; then if [[ $editMode == 1 ]]; then
encFile="${BOOKMARKS_FILE%.txt}.enc" encFile="${BOOKMARKS_FILE%.txt}.enc"
if [[ -f "$encFile" ]]; then if [[ -f $encFile ]]; then
echo "Editing encrypted bookmarks file" echo "Editing encrypted bookmarks file"
# Decrypt to the .txt file (matches .sops.yaml path_regex), edit, re-encrypt # Decrypt to the .txt file (matches .sops.yaml path_regex), edit, re-encrypt
trap "rm -f '${BOOKMARKS_FILE}'" EXIT 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" sops --encrypt --input-type binary --output-type binary "${BOOKMARKS_FILE}" >"$encFile"
rm -f "${BOOKMARKS_FILE}" rm -f "${BOOKMARKS_FILE}"
echo "Saved and re-encrypted" echo "Saved and re-encrypted"
elif [[ -f "${BOOKMARKS_FILE}" ]]; then elif [[ -f ${BOOKMARKS_FILE} ]]; then
echo "Editing ${BOOKMARKS_FILE}" echo "Editing ${BOOKMARKS_FILE}"
${EDITOR:-vim} "${BOOKMARKS_FILE}" ${EDITOR:-vim} "${BOOKMARKS_FILE}"
else else
@ -245,7 +245,7 @@ if [[ $editMode == 1 ]]; then
fi fi
if [[ $queryMode == 1 ]]; then if [[ $queryMode == 1 ]]; then
if [[ -z "$q" ]]; then if [[ -z $q ]]; then
bs=$(bks) bs=$(bks)
else else
bs=$(bks | grep -i "$q") bs=$(bks | grep -i "$q")
@ -297,7 +297,7 @@ fi
if [ $hitCount -gt 1 ]; then if [ $hitCount -gt 1 ]; then
exactHit=0 exactHit=0
while read -r key url misc; do while read -r key url misc; do
if [[ "$key" == "$q" ]]; then if [[ $key == "$q" ]]; then
exactHit=1 exactHit=1
echo "$key $url" >"$tempFile" echo "$key $url" >"$tempFile"
break break

View file

@ -10,9 +10,10 @@ logfile ~/.local/state/msmtp.log
# Personal Gmail account # Personal Gmail account
account personal account personal
host smtp.gmail.com host smtp.gmail.com
port 465
from raydreww@gmail.com from raydreww@gmail.com
user 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 tls_starttls off
# UChicago account (via DavMail) # UChicago account (via DavMail)
@ -21,7 +22,7 @@ host 127.0.0.1
port 1025 port 1025
from rayandrew@uchicago.edu from rayandrew@uchicago.edu
user 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 auth plain
tls off tls off
tls_starttls off tls_starttls off

View file

@ -23,7 +23,7 @@ set trash = '+Trash'
# PGP settings # PGP settings
set use_from = yes set use_from = yes
set pgp_verify_sig = yes set pgp_verify_sig = yes
set pgp_sign_as = 0x07AA5254804C009F set pgp_sign_as = 0xBAA368F02F486080
set pgp_timeout = 3600 set pgp_timeout = 3600
# Mailboxes # Mailboxes
@ -34,5 +34,9 @@ named-mailboxes "p/important" =Important
named-mailboxes "p/trash" =Trash named-mailboxes "p/trash" =Trash
named-mailboxes "p/archive" =Archive 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 # Signature
set signature = "~/.config/neomutt/signatures/personal" set signature = "~/.config/neomutt/signatures/personal"

View file

@ -22,7 +22,7 @@ set trash = '+Trash'
# PGP settings # PGP settings
set use_from = yes set use_from = yes
set pgp_sign_as = 0xEEF04CFFE9DFE5FC set pgp_sign_as = 0xBAA368F02F486080
set pgp_verify_sig = yes set pgp_verify_sig = yes
set pgp_timeout = 3600 set pgp_timeout = 3600
@ -35,5 +35,9 @@ named-mailboxes "u/trash" =Trash
named-mailboxes "u/archive" =Archive named-mailboxes "u/archive" =Archive
named-mailboxes "u/teaching" =Teaching 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 # Signature
set signature = "~/.config/neomutt/signatures/uchicago" set signature = "~/.config/neomutt/signatures/uchicago"

View file

@ -72,10 +72,10 @@ color index_date '#475e6c' default
color index_size '#475e6c' default color index_size '#475e6c' default
color index_flags '#49e9a6' default '.*' color index_flags '#49e9a6' default '.*'
# New mail - highlighted with bg_highlight # New mail - gold with subtle background
color index '#e4b781' '#0c3f5f' "~N" color index '#e4b781' '#041520' "~N"
color index_author '#df769b' '#0c3f5f' "~N" color index_author '#df769b' '#041520' "~N"
color index_subject '#49d6e9' '#0c3f5f' "~N" color index_subject '#e4b781' '#041520' "~N"
# Flagged mail # Flagged mail
color index '#e66533' default "~F" color index '#e66533' default "~F"

View file

@ -68,3 +68,22 @@ macro attach o "<enter-command>unset wait_key<enter><pipe-entry>~/dotfiles/bin/o
macro attach O "<enter-command>unset wait_key<enter><pipe-entry>~/dotfiles/bin/mailcap-open<enter>" "Open with fzf picker" macro attach O "<enter-command>unset wait_key<enter><pipe-entry>~/dotfiles/bin/mailcap-open<enter>" "Open with fzf picker"
macro attach,pager p "|git apply<enter>" "Apply git patch" macro attach,pager p "|git apply<enter>" "Apply git patch"
macro attach,pager P "|git-apply-patch<enter>" "Apply git patch (interactive)" macro attach,pager P "|git-apply-patch<enter>" "Apply git patch (interactive)"
# Notmuch search
bind index,pager \\ vfolder-from-query
macro index,pager ga "<vfolder-from-query>date:30d..<enter>" "View recent mail (30 days)"
macro index,pager gA "<vfolder-from-query>*<enter>" "View all mail"
macro index,pager gn "<vfolder-from-query>tag:unread<enter>" "View unread mail"
macro index,pager gr "<vfolder-from-query>date:7d..<enter>" "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 "<enter-command>unset mark_old<enter><toggle-new><sync-mailbox>" "Mark as unread"
macro index,pager mr "<clear-flag>N<sync-mailbox>" "Mark as read"
macro index ma "<tag-pattern>.<enter><tag-prefix><clear-flag>N<untag-pattern>.<enter><sync-mailbox>" "Mark all as read"

View file

@ -4,14 +4,18 @@
set header_cache = "~/.cache/neomutt/headers/" set header_cache = "~/.cache/neomutt/headers/"
set message_cachedir = "~/.cache/neomutt/messages/" set message_cachedir = "~/.cache/neomutt/messages/"
# Shell
set shell = "/bin/bash -l"
# Editor # Editor
set editor = "emacs -nw" set editor = "nvim"
set edit_headers = yes set edit_headers = yes
# General settings # General settings
set color_directcolor = yes set color_directcolor = yes
set implicit_autoview = yes set implicit_autoview = yes
set crypt_use_gpgme = yes set crypt_use_gpgme = yes
unset mark_old
alternative_order text/enriched text/plain text alternative_order text/enriched text/plain text
set delete = yes set delete = yes
set abort_key = "<Esc>" set abort_key = "<Esc>"
@ -25,8 +29,8 @@ set mail_check_stats
set status_chars = " *%A" 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 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 date_format = "%d.%m.%Y %H:%M"
set sort = threads set sort = date
set sort_aux = reverse-last-date-received set sort_aux = date
set uncollapse_jump set uncollapse_jump
set sort_re set sort_re
set index_format = "%4C %Z %{%b %d} %-15.15L %?E?(%E)&? %s" 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" named-mailboxes "u" "~/mail/uchicago/Inbox"
folder-hook ~/mail/uchicago/ "source ~/.config/neomutt/accounts/uchicago" folder-hook ~/mail/uchicago/ "source ~/.config/neomutt/accounts/uchicago"
# Jump to last (newest) message when opening folders
folder-hook . "push <last-entry>"
# 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 primary account (personal)
source ~/.config/neomutt/accounts/personal source ~/.config/neomutt/accounts/personal

17
config/notmuch/config Normal file
View file

@ -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

177
docs/gpg-setup.md Normal file
View file

@ -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 <raydreww@gmail.com>
uid [ultimate] Ray Andrew Sinurat <rayandrew@uchicago.edu>
uid [ultimate] Ray Andrew <raydreww@gmail.com>
uid [ultimate] Ray Andrew <rayandrew@uchicago.edu>
uid [ultimate] Ray A. O. Sinurat <raydreww@gmail.com>
uid [ultimate] Ray A. O. Sinurat <rayandrew@uchicago.edu>
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-<KEY_ID>.asc` (chmod 600)
- `gpg-public-key-<KEY_ID>.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
```

View file

@ -99,6 +99,8 @@
programs.nixfmt.enable = true; programs.nixfmt.enable = true;
programs.stylua.enable = true; programs.stylua.enable = true;
programs.shfmt.enable = true; programs.shfmt.enable = true;
programs.shfmt.includes = [ "bin/*" ];
programs.shfmt.indent_size = 4;
programs.fish_indent.enable = true; programs.fish_indent.enable = true;
programs.shellcheck.enable = true; programs.shellcheck.enable = true;
settings.global.excludes = [ "flake.lock" ]; settings.global.excludes = [ "flake.lock" ];

View file

@ -13,6 +13,7 @@
davmail = mkEnableOption "Enable DavMail"; davmail = mkEnableOption "Enable DavMail";
mbsync = mkEnableOption "Enable Mbsync"; mbsync = mkEnableOption "Enable Mbsync";
neomutt = mkEnableOption "Enable NeoMutt"; neomutt = mkEnableOption "Enable NeoMutt";
notmuch = mkEnableOption "Enable notmuch";
mailcap = mkEnableOption "Enable mailcap"; mailcap = mkEnableOption "Enable mailcap";
}; };
@ -28,9 +29,13 @@
enable = config.custom.email.mbsync; enable = config.custom.email.mbsync;
configFile = "${dots}/config/mbsync/mbsyncrc"; configFile = "${dots}/config/mbsync/mbsyncrc";
frequency = "*:0/1"; 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 = { environment = {
SOPS_AGE_KEY_FILE = "${xdg-config-dir}/sops/age/keys.txt"; 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 "$@" exec env TERM=xterm-direct ${neomutt}/bin/neomutt "$@"
'') '')
] ]
++ lib.optionals config.custom.email.notmuch [
notmuch
]
++ lib.optionals config.custom.email.mailcap [ ++ lib.optionals config.custom.email.mailcap [
mailcap mailcap
w3m # HTML rendering w3m # HTML rendering
@ -61,6 +69,9 @@
"msmtp".source = config.lib.file.mkOutOfStoreSymlink "${dots}/config/msmtp"; "msmtp".source = config.lib.file.mkOutOfStoreSymlink "${dots}/config/msmtp";
"neomutt".source = config.lib.file.mkOutOfStoreSymlink "${dots}/config/neomutt"; "neomutt".source = config.lib.file.mkOutOfStoreSymlink "${dots}/config/neomutt";
"isyncrc".source = config.lib.file.mkOutOfStoreSymlink "${dots}/config/mbsync/mbsyncrc"; "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 # mailcap symlink
@ -76,5 +87,10 @@
mkdir -p ~/.cache/neomutt/messages mkdir -p ~/.cache/neomutt/messages
mkdir -p ~/.local/state 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";
};
}; };
} }

View file

@ -105,6 +105,7 @@
neomutt = true; neomutt = true;
mbsync = true; mbsync = true;
mailcap = true; mailcap = true;
notmuch = true;
}; };
}; };
} }