This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

superconfig / tools / mutt / flake-module.nix
14 kB 347 lines
1{ self, inputs, ... }: 2{ 3 perSystem = 4 { 5 pkgs, 6 lib, 7 system, 8 ... 9 }: 10 { 11 packages.mutt = 12 let 13 notmuchConfig = self.packages.${system}.notmuch.passthru.configuration.configFile.path; 14 15 inherit (pkgs) elinks; 16 17 # Generate virtual-mailboxes from Maildir folder structure at build time 18 # This discovers sieve folders dynamically — no hardcoded list needed 19 genVirtualMailboxes = pkgs.writeShellScript "gen-virtual-mailboxes" '' 20 maildir="$HOME/Maildir" 21 [ -d "$maildir" ] || exit 0 22 for dir in "$maildir"/.*; do 23 [ -d "$dir/cur" ] || continue 24 dotname=$(basename "$dir") 25 # Display name without leading dot 26 name=''${dotname#.} 27 echo "virtual-mailboxes \"$name\" \"notmuch://?query=folder:$dotname\"" 28 done | sort 29 ''; 30 31 mailcap = pkgs.writeText "mailcap" '' 32 text/html; ${elinks}/bin/elinks -dump ; copiousoutput; 33 ''; 34 35 # Script to view HTML emails in browser with embedded images 36 viewHtmlBrowser = pkgs.writeShellScriptBin "view-html-browser" '' 37 set -euo pipefail 38 tmpfile=$(mktemp --suffix=.html) 39 trap 'rm -f "$tmpfile"' EXIT 40 41 ${pkgs.python3}/bin/python3 -c ' 42 import email, sys, base64 43 44 msg = email.message_from_file(sys.stdin) 45 46 # Build map of Content-ID -> base64 data URI 47 cid_map = {} 48 for part in msg.walk(): 49 cid = part.get("Content-ID", "").strip("<>") 50 if cid and part.get_content_maintype() == "image": 51 data = part.get_payload(decode=True) 52 if data: 53 mime = part.get_content_type() 54 cid_map[cid] = f"data:{mime};base64,{base64.b64encode(data).decode()}" 55 56 # Find HTML part 57 html = None 58 for part in msg.walk(): 59 if part.get_content_type() == "text/html": 60 payload = part.get_payload(decode=True) 61 charset = part.get_content_charset() or "utf-8" 62 html = payload.decode(charset, errors="replace") 63 break 64 65 if html: 66 for cid, data_uri in cid_map.items(): 67 html = html.replace(f"cid:{cid}", data_uri) 68 print(html) 69 else: 70 print("<html><body><p>No HTML content found.</p></body></html>") 71 ' > "$tmpfile" 72 73 ${if pkgs.stdenv.isDarwin then "open" else "xdg-open"} "$tmpfile" 74 sleep 2 75 ''; 76 77 msmtprc = pkgs.writeText "msmtprc" '' 78 defaults 79 logfile ~/.msmtp.log 80 timeout 10 81 account neoprism 82 host neoprism.r 83 account c-base 84 from lassulus@c-base.org 85 host c-mail.c-base.org 86 port 465 87 tls on 88 tls_starttls off 89 auth on 90 user lassulus 91 passwordeval rbw get --folder c-base shell.c-base.org --field password 92 account clan-infra 93 from infra@clan.lol 94 host mail.clan.lol 95 port 587 96 tls on 97 tls_starttls on 98 auth on 99 user infra@clan.lol 100 passwordeval SOPS_AGE_KEY=$(rbw get --folder clan.lol clan-infra --field age-key) nix develop git+https://git.clan.lol/clan/clan-infra -c clan vars get --flake git+https://git.clan.lol/clan/clan-infra web01 infra-mail/infra-password 101 account dedede 102 from mail@dedede.org 103 host mail.privateemail.com 104 port 587 105 tls on 106 tls_starttls on 107 auth on 108 user mail@dedede.org 109 passwordeval rbw get --folder dedede mail@dedede.org 110 account default: neoprism 111 ''; 112 113 msmtp = pkgs.writeShellScriptBin "msmtp" '' 114 export NOTMUCH_CONFIG="${notmuchConfig}" 115 116 # Save message to temporary file first 117 tmpfile=$(mktemp) 118 trap 'rm -f "$tmpfile"' EXIT 119 cat > "$tmpfile" 120 121 # Detect which account to use based on From: header in the email 122 account="" 123 from_line=$(grep -i "^From:" "$tmpfile" | head -1) 124 if [[ "$from_line" =~ @c-base\.org ]]; then 125 account="c-base" 126 elif [[ "$from_line" =~ @clan\.lol ]]; then 127 account="clan-infra" 128 elif [[ "$from_line" =~ @dedede\.org ]]; then 129 account="dedede" 130 fi 131 132 # For external accounts (c-base, clan-infra, dedede), always send directly 133 if [ -n "$account" ]; then 134 if ${pkgs.msmtp}/bin/msmtp -C ${msmtprc} -a "$account" "$@" < "$tmpfile"; then 135 ${pkgs.notmuch}/bin/notmuch insert +sent < "$tmpfile" 136 exit 0 137 else 138 exit 1 139 fi 140 fi 141 142 # For default account, try neoprism.r first, fallback to SSH 143 if ${pkgs.msmtp}/bin/msmtp -C ${msmtprc} "$@" < "$tmpfile" 2>/dev/null; then 144 ${pkgs.notmuch}/bin/notmuch insert +sent < "$tmpfile" 145 exit 0 146 fi 147 148 # If direct connection failed, fall back to SSH tunnel 149 echo "Direct connection failed, sending via SSH..." >&2 150 if ${pkgs.openssh}/bin/ssh -o ConnectTimeout=10 -p 45621 lass@neoprism.lassul.us 'msmtp -C /dev/null --host=localhost --port=25 --read-envelope-from --read-recipients' < "$tmpfile"; then 151 ${pkgs.notmuch}/bin/notmuch insert +sent < "$tmpfile" 152 exit 0 153 else 154 echo "Failed to send email" >&2 155 exit 1 156 fi 157 ''; 158 159 muttrc = pkgs.writeText "muttrc" '' 160 # read html mails 161 auto_view text/html 162 set mailcap_path = ${mailcap} 163 164 # notmuch 165 set folder="$HOME/Maildir" 166 set nm_default_uri = "notmuch://$HOME/Maildir" 167 set nm_record = yes 168 set nm_record_tags = "-inbox me archive" 169 set virtual_spoolfile 170 set spoolfile = "notmuch://?query=folder:\"\" AND NOT tag:archive" 171 172 set mail_check_stats 173 set confirmappend = no 174 set confirmcreate = no 175 set mbox_type = Maildir 176 set delete = yes 177 set sendmail="${msmtp}/bin/msmtp" 178 set from="lassulus@lassul.us" 179 alternates ^.*@lassul\.us$ ^lassulus@c-base\.org$ 180 unset envelope_from_address 181 set use_envelope_from 182 set reverse_name 183 set markers=no 184 185 # Folder hooks: set alternates per-mailbox so identities don't overlap 186 folder-hook . "unalternates *; alternates ^.*@lassul\\.us$ ^lassulus@c-base\\.org$; set from=lassulus@lassul.us" 187 folder-hook "imaps://.*mail\\.clan\\.lol" "unalternates *; alternates ^.*@clan\\.lol$; set from=infra@clan.lol" 188 folder-hook "imaps://.*mail\\.privateemail\\.com" "unalternates *; alternates ^.*@dedede\\.org$; set from=mail@dedede.org" 189 190 # Automatically use c-base email when sending/replying to c-base addresses 191 send-hook "." "unmy_hdr From" 192 send-hook "~C .*@c-base\.org" "my_hdr From: lassulus@c-base.org" 193 194 set sort=threads 195 196 set index_format="%4C %Z %?GI?%GI& ? %[%y-%m-%d] %-20.20a %?M?(%3M)& ? %s %> %r %g" 197 198 virtual-mailboxes "INBOX" "notmuch://?query=folder:\"\" AND NOT tag:archive" 199 virtual-mailboxes "Unread" "notmuch://?query=tag:unread" 200 source "${genVirtualMailboxes}|" 201 virtual-mailboxes "TODO" "notmuch://?query=tag:TODO" 202 virtual-mailboxes "Starred" "notmuch://?query=tag:*" 203 virtual-mailboxes "Archive" "notmuch://?query=tag:archive" 204 virtual-mailboxes "Sent" "notmuch://?query=tag:sent" 205 virtual-mailboxes "Junk" "notmuch://?query=tag:junk" 206 virtual-mailboxes "All" "notmuch://?query=*" 207 208 # clan.lol IMAP - no mailboxes declaration to prevent any startup connection 209 # Select the separator entry in sidebar, folder-hook will redirect to the IMAP server 210 virtual-mailboxes "clan-infra" "notmuch://?query=tag:nonexistent-separator-tag" 211 account-hook imaps://mail.clan.lol "set imap_user = 'infra@clan.lol'; set imap_pass = \"\`SOPS_AGE_KEY=\$(rbw get --folder clan.lol clan-infra --field age-key) nix develop git+https://git.clan.lol/clan/clan-infra -c clan vars get --flake git+https://git.clan.lol/clan/clan-infra web01 infra-mail/infra-password\`\"" 212 folder-hook "notmuch://\\?query=tag:nonexistent-separator-tag" "push '<change-folder>imaps://mail.clan.lol<enter>'" 213 214 # dedede.org IMAP (Namecheap Private Email) 215 virtual-mailboxes "dedede" "notmuch://?query=tag:nonexistent-dedede-tag" 216 account-hook imaps://mail.privateemail.com "set imap_user = 'mail@dedede.org'; set imap_pass = \"\`rbw get --folder dedede mail@dedede.org\`\"" 217 folder-hook "notmuch://\\?query=tag:nonexistent-dedede-tag" "push '<change-folder>imaps://mail.privateemail.com<enter>'" 218 219 tag-transforms "junk" "k" \ 220 "unread" "u" \ 221 "replied" "↻" \ 222 "TODO" "T" \ 223 224 # notmuch bindings 225 macro index \\\\ "<vfolder-from-query>" 226 macro index + "<modify-labels>+*\n<sync-mailbox>" 227 macro index - "<modify-labels>-*\n<sync-mailbox>" 228 229 # muchsync 230 bind index \Cr noop 231 macro index \Cr \ 232 "<enter-command>unset wait_key<enter> \ 233 <sync-mailbox> \ 234 <shell-escape>${self.packages.${pkgs.system}.muchsync}/bin/muchsync -vv<enter> \ 235 <sync-mailbox> 236 237 #killed 238 bind index d noop 239 bind pager d noop 240 241 bind index S noop 242 bind index s noop 243 bind pager S noop 244 bind pager s noop 245 macro index S "<modify-labels-then-hide>-inbox -unread +junk\n" 246 macro index s "<modify-labels>-junk\n" 247 macro pager S "<modify-labels-then-hide>-inbox -unread +junk\n" 248 macro pager s "<modify-labels>-junk\n" 249 250 bind index A noop 251 bind index a noop 252 bind pager A noop 253 bind pager a noop 254 macro index A "<modify-labels>+archive -unread -inbox\n" "Archive message" 255 macro pager A "<modify-labels>+archive -unread -inbox\n" "Archive message" 256 257 bind index U noop 258 bind index u noop 259 bind pager U noop 260 bind pager u noop 261 macro index U "<modify-labels>+unread\n" 262 macro index u "<modify-labels>-unread\n" 263 macro pager U "<modify-labels>+unread\n" 264 macro pager u "<modify-labels>-unread\n" 265 266 bind index t noop 267 bind pager t noop 268 macro index t "<modify-labels>" 269 270 # top index bar in email view 271 set pager_index_lines=7 272 # top_index_bar toggle 273 macro pager ,@1 "<enter-command> set pager_index_lines=0; macro pager ] ,@2 'Toggle indexbar<Enter>" 274 macro pager ,@2 "<enter-command> set pager_index_lines=3; macro pager ] ,@3 'Toggle indexbar<Enter>" 275 macro pager ,@3 "<enter-command> set pager_index_lines=7; macro pager ] ,@1 'Toggle indexbar<Enter>" 276 macro pager ] ,@1 'Toggle indexbar 277 278 # scan urls from emails 279 macro index,pager \cb "<pipe-message> ${lib.getExe pkgs.urlscan}<Enter>" 280 macro attach,compose \cb "<pipe-entry> ${lib.getExe pkgs.urlscan}<Enter>" 281 282 # view HTML in graphical browser 283 macro index,pager H "<pipe-message>${viewHtmlBrowser}/bin/view-html-browser<Enter>" "View HTML in browser" 284 285 # forward invoice to sevdesk 286 send-hook "~t autobox@sevdesk.email" "my_hdr From: sevdesk@lassul.us" 287 macro index,pager I "<forward-message>autobox@sevdesk.email<enter>" "Forward invoice to sevdesk" 288 289 # sidebar 290 set sidebar_divider_char = '│' 291 set sidebar_delim_chars = "/" 292 set sidebar_short_path 293 set sidebar_folder_indent 294 set sidebar_visible = yes 295 set sidebar_format = '%D%?F? [%F]?%* %?N?%N/? %?S?%S?' 296 set sidebar_width = 30 297 color sidebar_new yellow red 298 299 # sidebar bindings 300 bind index <left> sidebar-prev 301 bind index <right> sidebar-next 302 bind index <space> sidebar-open 303 # sidebar toggle 304 macro index,pager ,@) "<enter-command> set sidebar_visible=no; macro index,pager [ ,@( 'Toggle sidebar'<Enter>" 305 macro index,pager ,@( "<enter-command> set sidebar_visible=yes; macro index,pager [ ,@) 'Toggle sidebar'<Enter>" 306 macro index,pager [ ,@( 'Toggle sidebar' 307 308 # forward with attachment 309 set mime_forward = no 310 set forward_attachments = yes 311 312 # better handling of long URLs 313 set wrap = 0 # don't wrap lines in the pager 314 set markers = no # don't show + markers for wrapped lines 315 set smart_wrap = yes # wrap at word boundaries when displaying 316 set pager_stop = yes # don't jump to next message at end of pager 317 ''; 318 319 in 320 (inputs.wrappers.lib.wrapPackage { 321 pkgs = pkgs; 322 package = pkgs.neomutt; 323 aliases = [ "mutt" ]; 324 runtimeInputs = [ 325 elinks 326 pkgs.msmtp 327 pkgs.notmuch 328 pkgs.openssh 329 pkgs.urlscan 330 ] 331 ++ lib.optionals pkgs.stdenv.isLinux [ 332 pkgs.iputils 333 ]; 334 env = { 335 NOTMUCH_CONFIG = notmuchConfig; 336 }; 337 flags = { 338 "-F" = "${muttrc}"; 339 }; 340 }).overrideAttrs 341 (old: { 342 passthru = (old.passthru or { }) // { 343 inherit msmtp; 344 }; 345 }); 346 }; 347}