Added ml script to docs/contrib.
[mmh] / 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