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