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