forked from
lass.pds.lassul.us/superconfig
This repository has no description
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}