]> diplodocus.org Git - nmh/blob - docs/contrib/ml
Merge branch 'newlock'
[nmh] / docs / contrib / ml
1 #!/bin/bash
2
3 # ml is a mail reading interface for mh(1). the design is that of
4 # a thin wrapper (this script) which uses 'less' for message
5 # display, and mh commands for doing the real work.
6 #
7 # this script was completely and utterly inspired by a message
8 # posted by Ralph Corderoy to the nmh developer's list, describing
9 # his similar, unpublished, script:
10 # http://lists.nongnu.org/archive/html/nmh-workers/2012-02/msg00148.html
11 #
12 # see the usage() and help() functions, below, for more detail. (or
13 # use 'ml -?' for usage, and '?' within ml for help.)
14 #
15 # ml creates its own lesskeys map file the first time you run it,
16 # called ~/Mail/ml_lesskeymap.
17 #
18 # there are a number of places where i let ml invoke my own wrapper
19 # scripts to do something mh-like. these wrappers do things like
20 # provide safe(r) message deletion, select among repl formats, etc.
21 # all of these can be easily changed -- see the do_xxxx() functions.
22 # all are assumed to operate on mh-style message specifications, and
23 # on 'cur' by default.
24 #
25 # this script uses the sequences 'ml', 'mldel', 'mlspam', 'mlunr',
26 # 'mlkeep', and 'mlrepl'. it also manipulates the user's Unseen-sequence.
27 #
28 # paul fox, pgf@foxharp.boston.ma.us, february 2012
29 # ------------
30
31
32 create_lesskey_map()
33 {
34 # the lesskey(1) bindings that cause less to work well with ml are:
35 lesskey -o $lesskeymap -- - <<-EOF
36 \^ quit \^
37 \? quit \?
38 E quit E
39 H quit H
40 J quit J
41 n quit n
42 K quit K
43 P quit P
44 p quit p
45 Q quit Q
46 q quit q
47 R quit R
48 S quit S
49 U quit U
50 V quit V
51 X quit X
52 d quit d
53 f quit f
54 r quit r
55 s quit s
56 u quit u
57 i quit i
58
59 # \40 maps the space char, to force the last page to start at
60 # the end of prev page, rather than lining up with bottom of
61 # screen.
62 \40 forw-screen-force
63 EOF
64
65 }
66
67
68 #
69 # the functions named do_xxxx() are the ones that are most ripe for
70 # customization. feel free to nuke my personal preferences.
71 #
72
73 do_rmm()
74 {
75 # d "$@" ; return # pgf's private alias
76 rmm "$@"
77 }
78
79 do_spamremove()
80 {
81 # spam "$@" ; return # pgf's private alias
82 refile +spambucket "$@" # you're on your own
83 }
84
85 do_reply()
86 {
87 # rf "$@" ; return # pgf's private alias
88 repl "$@"
89 }
90
91 do_replyall()
92 {
93 # R "$@" ; return # pgf's private alias
94 repl -cc to -cc cc "$@"
95 }
96
97 do_forw()
98 {
99 # f "$@" ; return # pgf's private alias
100 forw "$@"
101 }
102
103 do_edit()
104 {
105 ${VISUAL:-${EDITOR:-vi}} $(mhpath cur)
106 }
107
108 do_urlview()
109 {
110 urlview $(mhpath cur)
111 }
112
113 do_viewhtml()
114 {
115 echo 'mhshow-show-text/html: ' \
116 ' %p/usr/bin/lynx -force_html '%F' -dump | less' \
117 > /tmp/ml-mhshow-html$$
118
119 MHSHOW=/tmp/ml-mhshow-html$$ \
120 MM_CHARSET=us-ascii \
121 mhshow -type text/html "$@"
122
123 rm -f /tmp/ml-mhshow-html$$
124 }
125
126 do_sort()
127 {
128 # the intent is to apply some sort of thread/date ordering.
129 # be sure no sequences have been started
130 verify_empty "Sorting requires starting over." mldel || return
131 verify_empty "Sorting requires starting over." mlspam || return
132 verify_empty "Sorting requires starting over." mlunr || return
133
134 # sort by date, then by subject, to get, to get subject-major,
135 # date-minor ordering
136 sortm ml
137 sortm -textfield subject ml
138 }
139
140
141
142 usage()
143 {
144 cat <<EOF >&2
145 usage: $me [ msgs | -s | -a ]
146 $me will present the specified 'msgs' (any valid MH message
147 specification). With no arguments, messages will come from
148 the '$ml_unseen_seq' sequence.
149 Use "$me -s" to get the status of sequences used internally by $me, or
150 "$me -a" to apply previous results (shouldn't usually be needed).
151 Use ? when in less to display help for '$me'.
152 EOF
153 exit 1
154 }
155
156 help()
157 {
158
159 less -c <<EOF
160
161
162
163 "ml" takes an MH message specification as argument.
164 If none is specified, ml will operate on the sequence named "$ml_unseen_seq".
165
166 Messages are repeatedly displayed using 'less', which mostly
167 behaves as usual. less is configured with some special key
168 bindings which cause it to quit with special exit codes. These
169 in turn cause ml to execute distinct commands: they might cause
170 ml to display the next message, to mark the current message as
171 spam, to quit, etc.
172
173 The special key bindings within less are:
174
175 ? display this help (in a separate 'less' invocation)
176
177 ^ show first message
178 n,J show next message
179 p,P,K show previous message
180
181 d mark message for later deletion, by adding to sequence 'mldel'.
182 s mark message for later spam training, by adding to sequence 'mlspam'.
183 u mark message to remain "unread", by adding to sequence 'mlunr'.
184 U undo, i.e., remove it from any of 'mldel', 'mlspam', and 'mlunr'.
185
186 r compose a reply
187 R compose a reply to all message recipients
188 f forward the current message
189
190 S sort the messages, by subject and date
191 H render html from the message
192 V run 'urlview' on the message
193 E edit the raw message file
194
195 q quit. The 'mlunr' sequence will be added back to '$ml_unseen_seq',
196 messages in the 'mldel' are deleted, and those in 'mlspam'
197 are dealt with accordingly. Any messages that were read,
198 but not deleted or marked as spam will be left in the
199 'mlkeep' sequence. If ml dies unexpectedly (or the 'Q'
200 command is used instead of 'q'), "ml -a" (see below) can
201 be used to apply the changes that would have been made.
202
203 Q,X exit. Useful if you want to "start over". The '$ml_unseen_seq'
204 sequence will be restored to its previous state, and the
205 current message list is preserved to 'mlprev'. No other
206 message processing is done.
207
208 Any other command which causes less to quit will simply display
209 the next message. ('q', for instance)
210
211 ml recognizes three special commandline arguments:
212 "ml -s" will report the status of the sequences ml uses, which is
213 handy after quitting with 'X', for example.
214 "ml -a" will apply the changes indicated by the user -- messages
215 in the 'mldel' sequence are deleted, messages in the
216 'mlspam' sequence are trained and marked as spam, and
217 the 'mlunr' sequence is added to the '$ml_unseen_seq'
218 sequence.
219 "ml -k" will recreate the ml_lesskey file used by ml when running
220 less. ml will usually handle this automatically.
221
222 EOF
223 }
224
225 normal_quit()
226 {
227 apply_changes
228 mark -sequence ml -delete all 2>/dev/null
229 exit
230 }
231
232 ask()
233 {
234 immed=;
235
236 if [ "$1" = -i ]
237 then
238 immed="-n 1"
239 shift
240 fi
241 echo -n "${1}? [N/y] "
242 read $immed a
243 #read a
244 case $a in
245 [Yy]*) return 0 ;;
246 *) return 1 ;;
247 esac
248
249 }
250
251 # ensure the given sequence is empty
252 verify_empty()
253 {
254 pre="$1"
255 seq=$2
256 if pick $seq:first >/dev/null 2>&1
257 then
258 echo $pre
259 if ask "Non-empty '$seq' sequence found, okay to continue"
260 then
261 mark -sequence $seq -delete all 2>/dev/null
262 else
263 return 1
264 fi
265 fi
266 return 0
267 }
268
269 # safely return the (non-zero) length of given sequence, with error if empty
270 seq_count()
271 {
272 msgs=$(pick $1 2>/dev/null) || return 1
273 echo "$msgs" | wc -l
274 }
275
276 # move 'ml' to 'mlprev'
277 preserve_ml_seq()
278 {
279 mark -sequence mlprev -zero -add ml 2>/dev/null
280 mark -sequence ml -delete all 2>/dev/null
281 }
282
283 # restore the unseen sequence to its value on entry
284 restore_unseen()
285 {
286 mark -sequence $ml_unseen_seq -add saveunseen 2>/dev/null
287 }
288
289 # add the message to just one of the special sequences.
290 markit()
291 {
292 case $1 in
293 mlkeep) # this is really an undo, since it restores default action
294 mark -add -sequence mlkeep cur
295 mark -delete -sequence mlspam cur 2>/dev/null
296 mark -delete -sequence mldel cur 2>/dev/null
297 mark -delete -sequence mlunr cur 2>/dev/null
298 ;;
299 mlspam)
300 mark -delete -sequence mlkeep cur 2>/dev/null
301 mark -add -sequence mlspam cur
302 mark -delete -sequence mldel cur 2>/dev/null
303 mark -delete -sequence mlunr cur 2>/dev/null
304 ;;
305 mldel)
306 mark -delete -sequence mlkeep cur 2>/dev/null
307 mark -delete -sequence mlspam cur 2>/dev/null
308 mark -add -sequence mldel cur
309 mark -delete -sequence mlunr cur 2>/dev/null
310 ;;
311 mlunr)
312 mark -delete -sequence mlkeep cur 2>/dev/null
313 mark -delete -sequence mlspam cur 2>/dev/null
314 mark -delete -sequence mldel cur 2>/dev/null
315 mark -add -sequence mlunr cur
316 ;;
317 mlrepl) # this sequence only affects the displayed header of the message.
318 mark -add -sequence mlrepl cur
319 ;;
320 esac
321 }
322
323 # emit an informational header at the top of each message.
324 header()
325 {
326 local msg=$1
327
328 this_mess="${BOLD}Message $folder:$msg${NORMAL}"
329
330 # get index of current message
331 mindex=$(echo "$ml_contents" | grep -xn $msg)
332 mindex=${mindex%:*}
333
334 # are we on the first or last or only messages?
335 if [ $ml_len != 1 ]
336 then
337 if [ $mindex = 1 ]
338 then
339 mindex="${BOLD}FIRST${NORMAL}"
340 elif [ $mindex = $ml_len ]
341 then
342 mindex="${BOLD}LAST${NORMAL}"
343 fi
344 fi
345 position="($mindex of $ml_len)"
346
347 # have we done anything to this message?
348 r=; s=;
349 if pick mlrepl 2>/dev/null | grep -qx $msg
350 then
351 r="${BLUE}Replied ${NORMAL}"
352 fi
353 if pick mldel 2>/dev/null | grep -qx $msg
354 then
355 s="${RED}Deleted ${NORMAL}"
356 elif pick mlspam 2>/dev/null | grep -qx $msg
357 then
358 s="${RED}Spam ${NORMAL}"
359 elif pick mlunr 2>/dev/null | grep -qx $msg
360 then
361 s="${RED}Unread ${NORMAL}"
362 fi
363 status=${r}${s}
364
365 # show progress for whole ml run (how many deleted, etc.)
366 scnt=$(seq_count mlspam)
367 dcnt=$(seq_count mldel)
368 ucnt=$(seq_count mlunr)
369 others="${scnt:+$scnt spam }${dcnt:+$dcnt deleted }${ucnt:+$ucnt marked unread}"
370 others="${others:+[$others]}"
371
372 statusline="$this_mess $position $status $others"
373
374 echo $statusline
375
376 }
377
378 # emit the header again
379 footer()
380 {
381 echo "-----------"
382 echo "$statusline"
383 }
384
385 # make the Subject: and From: headers stand out
386 colorize()
387 {
388 sed \
389 -e 's/^\(Subject: *\)\(.*\)/\1'"$RED"'\2'"$NORMAL"'/' \
390 -e 's/^\(From: *\)\(.*\)/\1'"$BLUE"'\2'"$NORMAL"'/' # 2>/dev/null
391 }
392
393 cleanup()
394 {
395 # the first replacement gets rid of the default header that
396 # show emits with every message -- we provide our own.
397 # for the second: i think the 'Press <return> text is a bug in
398 # mhl. there's no reason to display this message when not
399 # actually pausing for <return> to be pressed.
400 sed -e '1s/^(Message .*)$/---------/' \
401 -e 's/Press <return> to show content\.\.\.//'
402 }
403
404 # this is the where the message is displayed, using less
405 show_msg()
406 {
407 local nmsg
408 local which=$1
409
410
411 # only (re)set $msg if pick succeeds
412 if nmsg=$(pick ml:$which 2>/dev/null)
413 then
414 msg=$nmsg
415 viewcount=0
416 else
417 # do we keep hitting the same message?
418 : $(( viewcount += 1 ))
419 if [ $viewcount -gt 2 ]
420 then
421 if ask -i "See message $msg yet again"
422 then
423 viewcount=0
424 else
425 normal_quit
426 fi
427 fi
428 fi
429
430 (
431 header $msg
432 Mail=$(mhpath +)
433 export NMH_NON_INTERACTIVE=1
434 export MHSHOW=$Mail/mhn.noshow
435 mhshow $msg |
436 cleanup |
437 colorize
438 footer
439 ) | LESS=miXcR less $lesskeyfileopt
440 return $? # return less' exit code
441 }
442
443 # bad things would happen if we were to keep going after the current
444 # folder has been changed from another shell.
445 check_current_folder()
446 {
447 curfold=$(folder -fast)
448 if [ "$curfold" != "$folder" ] # danger, will robinson!!
449 then
450 echo "Current folder has changed to '$curfold'!"
451 echo "Answering 'no' will discard changes, and exit."
452 if ask "Switch back to '$folder'"
453 then
454 folder +$folder
455 else
456 restore_unseen
457 preserve_ml_seq
458 exit
459 fi
460 fi
461 }
462
463 loop()
464 {
465 local nextmsg
466
467 nextmsg=first
468 while :
469 do
470 check_current_folder
471
472 show_msg $nextmsg
473 cmd=$? # save the less exit code
474
475 check_current_folder
476
477 # by default, stay on the same message
478 nextmsg=cur
479
480 case $cmd in
481
482 # help
483 $_ques) help
484 ;;
485
486 # dispatch
487 $_d) markit mldel
488 ##nextmsg=next
489 ;;
490 $_s) markit mlspam
491 ##nextmsg=next
492 ;;
493 $_u) markit mlunr
494 ##nextmsg=next
495 ;;
496 $_U) markit mlkeep
497 ##nextmsg=next
498 ;;
499
500 # send mail
501 $_r) do_reply
502 markit mlrepl
503 #nextmsg=cur
504 ;;
505 $_R) do_replyall
506 markit mlrepl
507 #nextmsg=cur
508 ;;
509 $_f) do_forw
510 markit mlrepl
511 #nextmsg=cur
512 ;;
513
514 # special viewers
515 $_H) do_viewhtml
516 #nextmsg=cur
517 ;;
518 $_V) do_urlview
519 #nextmsg=cur
520 ;;
521 $_E) do_edit
522 #nextmsg=cur
523 ;;
524 $_i) show_status | less -c
525 #nextmsg=cur
526 ;;
527
528 # quitting
529 $_q) normal_quit
530 ;;
531
532 $_X|$_Q) restore_unseen
533 preserve_ml_seq
534 exit
535 ;;
536
537 # other
538 $_S) do_sort
539 nextmsg=first
540 ;;
541
542 # navigation
543 $_up) nextmsg=first
544 ;;
545
546 $_K) nextmsg=prev
547 ;;
548 $_p|$_P) nextmsg=prev
549 ;;
550 $_n|$_J) nextmsg=next
551 ;;
552 *) nextmsg=next
553 ;;
554
555 esac
556 done
557 }
558
559 # summarize ml's internal sequences, for "ml -s"
560 show_status()
561 {
562 echo Folder: $folder
563 for s in mlspam mldel mlrepl mlunr
564 do
565 #pick $s:first >/dev/null 2>&1 || continue
566 case $s in
567 mlrepl) echo "Have attempted a reply: (sequence $s)" ;;
568 mldel) echo "Will delete: (sequence $s)" ;;
569 mlspam) echo "Will mark as spam: (sequence $s)" ;;
570 mlunr) echo "Will mark as unseen: (sequence $s)" ;;
571 # mlkeep) echo "Will leave as seen: (sequence $s)" ;;
572 esac
573 scan $s 2>/dev/null || echo ' none'
574 done
575 }
576
577 apply_changes()
578 {
579 if cnt=$(seq_count mlspam)
580 then
581 echo "Marking $cnt messages as spam."
582 do_spamremove mlspam
583 fi
584
585 if cnt=$(seq_count mldel)
586 then
587 echo "Removing $cnt messages."
588 do_rmm mldel
589 fi
590
591 if cnt=$(seq_count mlunr)
592 then
593 echo "Marking $cnt messages unread."
594 mark -add -sequence $ml_unseen_seq mlunr 2>/dev/null
595 mark -sequence mlunr -delete all
596 fi
597
598 if cnt=$(seq_count mlkeep)
599 then
600 echo "Keeping $cnt messages in sequence 'mlkeep':"
601 scan mlkeep
602 fi
603 }
604
605 # decimal to character mappings. lesskeys lets you specify exit codes
606 # from less as ascii characters, but the shell really wants them to be
607 # numeric, in decimal. these definitions let you do "quit S" in
608 # lesskeys, and then check against $_S here in the shell.
609 char_init()
610 {
611 _A=65; _B=66; _C=67; _D=68; _E=69; _F=70; _G=71; _H=72; _I=73;
612 _J=74; _K=75; _L=76; _M=77; _N=78; _O=79; _P=80; _Q=81; _R=82;
613 _S=83; _T=84; _U=85; _V=86; _W=87; _X=88; _Y=89; _Z=90;
614
615 _a=97; _b=98; _c=99; _d=100; _e=101; _f=102; _g=103; _h=104; _i=105;
616 _j=106; _k=107; _l=108; _m=109; _n=110; _o=111; _p=112; _q=113; _r=114;
617 _s=115; _t=116; _u=117; _v=118; _w=119; _x=120; _y=121; _z=122;
618
619 _up=94; _ques=63;
620 }
621
622 color_init()
623 {
624 RED="$(printf \\033[1\;31m)"
625 GREEN="$(printf \\033[1\;32m)"
626 YELLOW="$(printf \\033[1\;33m)"
627 BLUE="$(printf \\033[1\;34m)"
628 PURPLE="$(printf \\033[1\;35m)"
629 CYAN="$(printf \\033[1\;36m)"
630 BOLD="$(printf \\033[1m)"
631 NORMAL="$(printf \\033[m)"
632 ESC="$(printf \\033)"
633 }
634
635
636 # in-line execution starts here
637
638 set -u # be defensive
639
640 me=${0##*/}
641
642 folder=$(folder -fast)
643 lesskeymap=$(mhpath +)/ml_lesskeymap
644 lesskeyfileopt="--lesskey-file=$lesskeymap"
645
646 if [ ! -f $lesskeymap -o $0 -nt $lesskeymap ]
647 then
648 create_lesskey_map
649 fi
650
651 ml_unseen_seq=$(mhparam Unseen-Sequence)
652 : ${ml_unseen_seq:=unseen} # default to "unseen"
653
654 # check arguments
655 case ${1:-} in
656 -s) show_status; exit ;; # "ml -s"
657 -a) apply_changes; exit ;; # "ml -a"
658 -k) create_lesskey_map; exit ;; # "ml -k" (should be automatic)
659 -*) usage ;; # "ml -?"
660 "") starting_seq=$ml_unseen_seq ;; # "ml"
661 *) starting_seq="$*" ;; # "ml picked ..."
662 esac
663
664
665 # if sequence ml isn't empty, another instance may be running
666 verify_empty "Another instance of ml may be running." ml || exit
667
668 # gather any user message specifications into the sequence 'ml'
669 if ! mark -sequence ml -zero -add $starting_seq >/dev/null 2>&1
670 then
671 echo "No messages (or message sequence) specified."
672 exit 1
673 fi
674
675 # uncomment for debug
676 # exec 2>/tmp/ml.log; set -x
677
678 # get the full list of messages, and count them
679 ml_contents=$(pick ml)
680 ml_len=$(echo "$ml_contents" | wc -l)
681
682 # if these aren't empty, we might not have "ml a"pplied changes from
683 # a previous invocation, so warn.
684 verify_empty "You might want to run 'ml -a'." mldel || exit
685 verify_empty "You might want to run 'ml -a'." mlspam || exit
686 verify_empty "You might want to run 'ml -a'." mlunr || exit
687
688 mark -sequence mlrepl -delete all 2>/dev/null
689
690 # initialize 'mlkeep' to 'ml', since we assume all undeleted non-spam
691 # messages will be kept.
692 mark -zero -sequence mlkeep ml
693
694 # save a copy of the unseen sequence, for restore if 'X' is used to quit.
695 mark -zero -sequence saveunseen $ml_unseen_seq
696
697 char_init
698 color_init
699
700 loop
701
702