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.
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
12 # see the usage() and help() functions, below, for more detail. (or
13 # use 'ml -?' for usage, and '?' within ml for help.)
15 # ml creates its own lesskeys map file the first time you run it,
16 # called ~/Mail/ml_lesskeymap.
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.
25 # this script uses the sequences 'ml', 'mldel', 'mlspam', 'mlunr',
26 # 'mlkeep', and 'mlrepl'. it also manipulates the user's Unseen-sequence.
28 # paul fox, pgf@foxharp.boston.ma.us, february 2012
34 # the lesskey(1) bindings that cause less to work well with ml are:
35 lesskey -o $lesskeymap -- - <<-EOF
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
69 # the functions named do_xxxx() are the ones that are most ripe for
70 # customization. feel free to nuke my personal preferences.
75 # d "$@" ; return # pgf's private alias
81 # spam "$@" ; return # pgf's private alias
82 refile +spambucket "$@" # you're on your own
87 # rf "$@" ; return # pgf's private alias
93 # R "$@" ; return # pgf's private alias
94 repl -cc to -cc cc "$@"
99 # f "$@" ; return # pgf's private alias
105 ${VISUAL:-${EDITOR:-vi}} $(mhpath cur)
110 urlview $(mhpath cur)
115 echo 'mhshow-show-text/html: ' \
116 ' %p/usr/bin/lynx -force_html '%F' -dump | less' \
117 > /tmp/ml-mhshow-html$$
119 MHSHOW=/tmp/ml-mhshow-html$$ \
120 MM_CHARSET=us-ascii \
121 mhshow -type text/html "$@"
123 rm -f /tmp/ml-mhshow-html$$
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
134 # sort by date, then by subject, to get, to get subject-major,
135 # date-minor ordering
137 sortm -textfield subject ml
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'.
163 "ml" takes an MH message specification as argument.
164 If none is specified, ml will operate on the sequence named "$ml_unseen_seq".
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
173 The special key bindings within less are:
175 ? display this help (in a separate 'less' invocation)
178 n,J show next message
179 p,P,K show previous message
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'.
187 R compose a reply to all message recipients
188 f forward the current message
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
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.
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.
208 Any other command which causes less to quit will simply display
209 the next message. ('q', for instance)
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'
219 "ml -k" will recreate the ml_lesskey file used by ml when running
220 less. ml will usually handle this automatically.
228 mark -sequence ml -delete all 2>/dev/null
241 echo -n "${1}? [N/y] "
251 # ensure the given sequence is empty
256 if pick $seq:first >/dev/null 2>&1
259 if ask "Non-empty '$seq' sequence found, okay to continue"
261 mark -sequence $seq -delete all 2>/dev/null
269 # safely return the (non-zero) length of given sequence, with error if empty
272 msgs=$(pick $1 2>/dev/null) || return 1
276 # move 'ml' to 'mlprev'
279 mark -sequence mlprev -zero -add ml 2>/dev/null
280 mark -sequence ml -delete all 2>/dev/null
283 # restore the unseen sequence to its value on entry
286 mark -sequence $ml_unseen_seq -add saveunseen 2>/dev/null
289 # add the message to just one of the special sequences.
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
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
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
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
317 mlrepl) # this sequence only affects the displayed header of the message.
318 mark -add -sequence mlrepl cur
323 # emit an informational header at the top of each message.
328 this_mess="${BOLD}Message $folder:$msg${NORMAL}"
330 # get index of current message
331 mindex=$(echo "$ml_contents" | grep -xn $msg)
334 # are we on the first or last or only messages?
339 mindex="${BOLD}FIRST${NORMAL}"
340 elif [ $mindex = $ml_len ]
342 mindex="${BOLD}LAST${NORMAL}"
345 position="($mindex of $ml_len)"
347 # have we done anything to this message?
349 if pick mlrepl 2>/dev/null | grep -qx $msg
351 r="${BLUE}Replied ${NORMAL}"
353 if pick mldel 2>/dev/null | grep -qx $msg
355 s="${RED}Deleted ${NORMAL}"
356 elif pick mlspam 2>/dev/null | grep -qx $msg
358 s="${RED}Spam ${NORMAL}"
359 elif pick mlunr 2>/dev/null | grep -qx $msg
361 s="${RED}Unread ${NORMAL}"
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]}"
372 statusline="$this_mess $position $status $others"
378 # emit the header again
385 # make the Subject: and From: headers stand out
389 -e 's/^\(Subject: *\)\(.*\)/\1'"$RED"'\2'"$NORMAL"'/' \
390 -e 's/^\(From: *\)\(.*\)/\1'"$BLUE"'\2'"$NORMAL"'/' # 2>/dev/null
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\.\.\.//'
404 # this is the where the message is displayed, using less
411 # only (re)set $msg if pick succeeds
412 if nmsg=$(pick ml:$which 2>/dev/null)
417 # do we keep hitting the same message?
418 : $(( viewcount += 1 ))
419 if [ $viewcount -gt 2 ]
421 if ask -i "See message $msg yet again"
433 export NMH_NON_INTERACTIVE=1
434 export MHSHOW=$Mail/mhn.noshow
439 ) | LESS=miXcR less $lesskeyfileopt
440 return $? # return less' exit code
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()
447 curfold=$(folder -fast)
448 if [ "$curfold" != "$folder" ] # danger, will robinson!!
450 echo "Current folder has changed to '$curfold'!"
451 echo "Answering 'no' will discard changes, and exit."
452 if ask "Switch back to '$folder'"
473 cmd=$? # save the less exit code
477 # by default, stay on the same message
524 $_i) show_status | less -c
532 $_X|$_Q) restore_unseen
548 $_p|$_P) nextmsg=prev
550 $_n|$_J) nextmsg=next
559 # summarize ml's internal sequences, for "ml -s"
563 for s in mlspam mldel mlrepl mlunr
565 #pick $s:first >/dev/null 2>&1 || continue
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)" ;;
573 scan $s 2>/dev/null || echo ' none'
579 if cnt=$(seq_count mlspam)
581 echo "Marking $cnt messages as spam."
585 if cnt=$(seq_count mldel)
587 echo "Removing $cnt messages."
591 if cnt=$(seq_count mlunr)
593 echo "Marking $cnt messages unread."
594 mark -add -sequence $ml_unseen_seq mlunr 2>/dev/null
595 mark -sequence mlunr -delete all
598 if cnt=$(seq_count mlkeep)
600 echo "Keeping $cnt messages in sequence 'mlkeep':"
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.
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;
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;
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)"
636 # in-line execution starts here
638 set -u # be defensive
642 folder=$(folder -fast)
643 lesskeymap=$(mhpath +)/ml_lesskeymap
644 lesskeyfileopt="--lesskey-file=$lesskeymap"
646 if [ ! -f $lesskeymap -o $0 -nt $lesskeymap ]
651 ml_unseen_seq=$(mhparam Unseen-Sequence)
652 : ${ml_unseen_seq:=unseen} # default to "unseen"
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 ..."
665 # if sequence ml isn't empty, another instance may be running
666 verify_empty "Another instance of ml may be running." ml || exit
668 # gather any user message specifications into the sequence 'ml'
669 if ! mark -sequence ml -zero -add $starting_seq >/dev/null 2>&1
671 echo "No messages (or message sequence) specified."
675 # uncomment for debug
676 # exec 2>/tmp/ml.log; set -x
678 # get the full list of messages, and count them
679 ml_contents=$(pick ml)
680 ml_len=$(echo "$ml_contents" | wc -l)
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
688 mark -sequence mlrepl -delete all 2>/dev/null
690 # initialize 'mlkeep' to 'ml', since we assume all undeleted non-spam
691 # messages will be kept.
692 mark -zero -sequence mlkeep ml
694 # save a copy of the unseen sequence, for restore if 'X' is used to quit.
695 mark -zero -sequence saveunseen $ml_unseen_seq