diff --git a/bin/path-shim b/bin/path-shim index 66fc0b4..405626e 100755 --- a/bin/path-shim +++ b/bin/path-shim @@ -5,6 +5,7 @@ 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" +export NOTMUCH_CONFIG="$HOME/.config/notmuch/config" if [[ $# -eq 1 ]]; then # Single argument - run it through bash to handle complex commands diff --git a/bin/view-email-html b/bin/view-email-html new file mode 100755 index 0000000..33a8502 --- /dev/null +++ b/bin/view-email-html @@ -0,0 +1,191 @@ +#!/bin/bash +# Convert email to HTML and open in browser with images + +emlfile="/tmp/neomutt-email-$$.eml" +tmpdir="/tmp/neomutt-email-$$" +tmpfile="$tmpdir/email.html" + +# Save email from stdin +mkdir -p "$tmpdir" +cat >"$emlfile" + +# Extract MIME parts using ripmime if available +if command -v ripmime &>/dev/null; then + ripmime -i "$emlfile" -d "$tmpdir" --no-nameless 2>/dev/null +elif command -v munpack &>/dev/null; then + cd "$tmpdir" && munpack -q "$emlfile" 2>/dev/null +fi + +# Check if there's an HTML part extracted +html_part=$(find "$tmpdir" -maxdepth 1 -name "*.html" -o -name "*.htm" 2>/dev/null | head -1) + +if [[ -n $html_part && -s $html_part ]]; then + # Use extracted HTML directly + cp "$html_part" "$tmpfile" +else + # Generate HTML from text + { + echo "" + echo '' + echo "" + echo "" + echo '' + echo "" + + # Extract and display headers (handles multi-line headers) + echo '
' + awk 'BEGIN{IGNORECASE=1; show=0; header=""; value=""} + function decode_qp(str, result, i, hex, char) { + result = str + # Decode =XX hex sequences + while (match(result, /=[0-9A-Fa-f][0-9A-Fa-f]/)) { + hex = substr(result, RSTART+1, 2) + cmd = "printf \"\\x" hex "\"" + cmd | getline char + close(cmd) + result = substr(result, 1, RSTART-1) char substr(result, RSTART+3) + } + gsub(/_/, " ", result) + return result + } + function decode_mime(str, result, before, after, encoded, charset, encoding, text) { + result = str + # Decode =?charset?Q?text?= or =?charset?B?text?= + while (match(result, /=\?[^?]+\?[QqBb]\?[^?]+\?=/)) { + before = substr(result, 1, RSTART-1) + encoded = substr(result, RSTART, RLENGTH) + after = substr(result, RSTART+RLENGTH) + # Remove =?...?Q? and ?= + gsub(/=\?[^?]+\?[Qq]\?/, "", encoded) + gsub(/\?=$/, "", encoded) + text = decode_qp(encoded) + result = before text after + } + return result + } + function split_addresses(str, arr, n, i, c, inquote, current) { + n = 0; inquote = 0; current = "" + for (i = 1; i <= length(str); i++) { + c = substr(str, i, 1) + if (c == "\"") inquote = !inquote + if (c == "," && !inquote) { + gsub(/^[ \t]+|[ \t]+$/, "", current) + if (current != "") arr[++n] = current + current = "" + } else { + current = current c + } + } + gsub(/^[ \t]+|[ \t]+$/, "", current) + if (current != "") arr[++n] = current + return n + } + function output() { + if (header != "" && value != "") { + value = decode_mime(value) + gsub(//, "\\>", value) + # Only split To, CC, Bcc addresses (not Date, Subject, From) + if (tolower(header) ~ /^(to|cc|bcc)$/) { + n = split_addresses(value, addrs) + if (n > 1) { + print "

" header ":

" + } else { + print "

" header ": " value "

" + } + } else { + print "

" header ": " value "

" + } + } + header = ""; value = "" + } + /^$/{output(); exit} + /^(From|To|CC|Bcc|Reply-To|Subject|Date):/{ + output() + show=1 + match($0, /^[^:]+/) + header = substr($0, 1, RLENGTH) + value = substr($0, RLENGTH+2) + gsub(/^[ \t]+|[ \t]+$/, "", value) + next + } + /^[A-Za-z0-9-]+:/ && !/^(From|To|CC|Bcc|Reply-To|Subject|Date):/{output(); show=0} + show && /^[ \t]/{ + gsub(/^[ \t]+/, "", $0) + value = value " " $0 + } + END{output()}' "$emlfile" + echo "
" + + # Display body - extract text part and format code blocks + echo '
' + # Try to find extracted text file first + text_part=$(find "$tmpdir" -maxdepth 1 -name "textfile*" -o -name "*.txt" 2>/dev/null | head -1) + if [[ -n $text_part && -s $text_part ]]; then + body_content=$(cat "$text_part") + elif command -v mshow &>/dev/null; then + body_content=$(mshow -N "$emlfile" 2>&1 | grep -v "no filter or default handler" | grep -viE "^(From|To|Cc|Bcc|Reply-To|Subject|Date):") + else + body_content=$(sed '1,/^$/d' "$emlfile") + fi + # Process body: escape HTML, handle code blocks and inline code + echo "$body_content" | awk ' + BEGIN { incode=0 } + /^```/ { + if (incode) { print ""; incode=0 } + else { print "
"; incode=1 }
+                next
+            }
+            {
+                gsub(//, "\\>")
+                if (!incode) {
+                    # Handle inline code `code`
+                    while (match($0, /`[^`]+`/)) {
+                        before = substr($0, 1, RSTART-1)
+                        code = substr($0, RSTART+1, RLENGTH-2)
+                        after = substr($0, RSTART+RLENGTH)
+                        $0 = before "" code "" after
+                    }
+                }
+                print
+            }
+            END { if (incode) print "
" } + ' + echo "
" + + # Show extracted images + find "$tmpdir" -maxdepth 1 -type f \( -iname "*.png" -o -iname "*.jpg" -o -iname "*.jpeg" -o -iname "*.gif" \) 2>/dev/null | while read -r img; do + echo "

" + done + + echo "" + } >"$tmpfile" +fi + +open "$tmpfile" +rm -f "$emlfile" +# Clean up after 60 seconds in background +(sleep 60 && rm -rf "$tmpdir") & diff --git a/bin/yazi-pick-file b/bin/yazi-pick-file new file mode 100755 index 0000000..20fb835 --- /dev/null +++ b/bin/yazi-pick-file @@ -0,0 +1,13 @@ +#!/bin/bash +# Pick file using yazi for neomutt attachment + +tmpfile=/tmp/neomutt-yazi-pick + +if [ -z "$1" ]; then + cd ~ + yazi --chooser-file="$tmpfile" && + awk 'BEGIN {printf "%s", "push "} {printf "%s", "\"" $0 "\""}' "$tmpfile" >"${tmpfile}.cmd" && + mv "${tmpfile}.cmd" "$tmpfile" +elif [ "$1" == "clean" ]; then + rm -f "$tmpfile" +fi diff --git a/config/neomutt/accounts/uchicago b/config/neomutt/accounts/uchicago index 9252055..5320773 100644 --- a/config/neomutt/accounts/uchicago +++ b/config/neomutt/accounts/uchicago @@ -16,7 +16,8 @@ set from = 'rayandrew@uchicago.edu' set realname = 'Ray Andrew' set spoolfile = '+Inbox' set postponed = '+Drafts' -set record = '+Sent' +# set record = '+Sent' # Server saves sent mail automatically +unset record set trash = '+Trash' # PGP settings diff --git a/config/neomutt/keybinds b/config/neomutt/keybinds index cb0ad99..fbb1a2d 100644 --- a/config/neomutt/keybinds +++ b/config/neomutt/keybinds @@ -11,6 +11,7 @@ bind editor ^T complete # Pager bind index,pager V edit-raw-message +macro index,pager H "~/dotfiles/bin/view-email-html" "View email in browser" bind pager c imap-fetch-mail bind pager j next-line bind pager k previous-line @@ -77,6 +78,8 @@ bind index,pager E recall-message # Compose menu - PGP shortcuts bind compose S pgp-menu +macro compose a "~/dotfiles/bin/yazi-pick-filesource /tmp/neomutt-yazi-pick~/dotfiles/bin/yazi-pick-file clean" "Attach file with yazi" +bind compose d detach-file # Mark messages bind index,pager m noop diff --git a/config/neomutt/neomuttrc b/config/neomutt/neomuttrc index affbfd5..801ad04 100644 --- a/config/neomutt/neomuttrc +++ b/config/neomutt/neomuttrc @@ -10,6 +10,8 @@ set shell = "/bin/bash -l" # Editor set editor = "nvim" set edit_headers = yes +set query_command = "notmuch address '%s'" +set attach_save_dir = "~/" # General settings set color_directcolor = yes diff --git a/home/email/default.nix b/home/email/default.nix index 6351046..0c59ae4 100644 --- a/home/email/default.nix +++ b/home/email/default.nix @@ -28,7 +28,7 @@ services.mbsync = { enable = config.custom.email.mbsync; configFile = "${dots}/config/mbsync/mbsyncrc"; - frequency = "*:0/5"; + frequency = "*:0/3"; extraPackages = with pkgs; [ sops ] ++ lib.optionals config.custom.email.notmuch [ notmuch ]; postExec = lib.mkIf config.custom.email.notmuch "${pkgs.notmuch}/bin/notmuch new"; environment = { @@ -63,6 +63,8 @@ zathura # PDF viewer chafa # terminal image viewer bat # text viewer with syntax highlighting + mblaze # mshow for email viewing + ripmime # extract MIME parts from emails ]; # Symlink config files