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