--- /dev/null
+#!/bin/bash
+
+# ml is a mail reading interface for mh(1). the design is that of
+# a thin wrapper (this script) which uses 'less' for message
+# display, and mh commands for doing the real work.
+#
+# this script was completely and utterly inspired by a message
+# posted by Ralph Corderoy to the nmh developer's list, describing
+# his similar, unpublished, script:
+# http://lists.nongnu.org/archive/html/nmh-workers/2012-02/msg00148.html
+#
+# see the usage() and help() functions, below, for more detail. (or
+# use 'ml -?' for usage, and '?' within ml for help.)
+#
+# ml creates its own lesskeys map file the first time you run it,
+# called ~/Mail/ml_lesskeymap.
+#
+# there are a number of places where i let ml invoke my own wrapper
+# scripts to do something mh-like. these wrappers do things like
+# provide safe(r) message deletion, select among repl formats, etc.
+# all of these can be easily changed -- see the do_xxxx() functions.
+# all are assumed to operate on mh-style message specifications, and
+# on 'cur' by default.
+#
+# this script uses the sequences 'ml', 'mldel', 'mlspam', 'mlunr',
+# 'mlkeep', and 'mlrepl'. it also manipulates the user's Unseen-sequence.
+#
+# paul fox, pgf@foxharp.boston.ma.us, february 2012
+# ------------
+
+
+create_lesskey_map()
+{
+ # the lesskey(1) bindings that cause less to work well with ml are:
+ lesskey -o $lesskeymap -- - <<-EOF
+ \^ quit \^
+ \? quit \?
+ E quit E
+ H quit H
+ J quit J
+ n quit n
+ K quit K
+ P quit P
+ p quit p
+ Q quit Q
+ q quit q
+ R quit R
+ S quit S
+ U quit U
+ V quit V
+ X quit X
+ d quit d
+ f quit f
+ r quit r
+ s quit s
+ u quit u
+ i quit i
+
+ # \40 maps the space char, to force the last page to start at
+ # the end of prev page, rather than lining up with bottom of
+ # screen.
+ \40 forw-screen-force
+EOF
+
+}
+
+
+#
+# the functions named do_xxxx() are the ones that are most ripe for
+# customization. feel free to nuke my personal preferences.
+#
+
+do_rmm()
+{
+ # d "$@" ; return # pgf's private alias
+ rmm "$@"
+}
+
+do_spamremove()
+{
+ # spam "$@" ; return # pgf's private alias
+ refile +spambucket "$@" # you're on your own
+}
+
+do_reply()
+{
+ # rf "$@" ; return # pgf's private alias
+ repl "$@"
+}
+
+do_replyall()
+{
+ # R "$@" ; return # pgf's private alias
+ repl -cc to -cc cc "$@"
+}
+
+do_forw()
+{
+ # f "$@" ; return # pgf's private alias
+ forw "$@"
+}
+
+do_edit()
+{
+ ${VISUAL:-${EDITOR:-vi}} $(mhpath cur)
+}
+
+do_urlview()
+{
+ urlview $(mhpath cur)
+}
+
+do_viewhtml()
+{
+ echo 'mhshow-show-text/html: ' \
+ ' %p/usr/bin/lynx -force_html '%F' -dump | less' \
+ > /tmp/ml-mhshow-html$$
+
+ MHSHOW=/tmp/ml-mhshow-html$$ \
+ MM_CHARSET=us-ascii \
+ mhshow -type text/html "$@"
+
+ rm -f /tmp/ml-mhshow-html$$
+}
+
+do_sort()
+{
+ # the intent is to apply some sort of thread/date ordering.
+ # be sure no sequences have been started
+ verify_empty "Sorting requires starting over." mldel || return
+ verify_empty "Sorting requires starting over." mlspam || return
+ verify_empty "Sorting requires starting over." mlunr || return
+
+ # sort by date, then by subject, to get, to get subject-major,
+ # date-minor ordering
+ sortm ml
+ sortm -textfield subject ml
+}
+
+
+
+usage()
+{
+ cat <<EOF >&2
+usage: $me [ msgs | -s | -a ]
+ $me will present the specified 'msgs' (any valid MH message
+ specification). With no arguments, messages will come from
+ the '$ml_unseen_seq' sequence.
+ Use "$me -s" to get the status of sequences used internally by $me, or
+ "$me -a" to apply previous results (shouldn't usually be needed).
+ Use ? when in less to display help for '$me'.
+EOF
+ exit 1
+}
+
+help()
+{
+
+ less -c <<EOF
+
+
+
+ "ml" takes an MH message specification as argument.
+ If none is specified, ml will operate on the sequence named "$ml_unseen_seq".
+
+ Messages are repeatedly displayed using 'less', which mostly
+ behaves as usual. less is configured with some special key
+ bindings which cause it to quit with special exit codes. These
+ in turn cause ml to execute distinct commands: they might cause
+ ml to display the next message, to mark the current message as
+ spam, to quit, etc.
+
+ The special key bindings within less are:
+
+ ? display this help (in a separate 'less' invocation)
+
+ ^ show first message
+ n,J show next message
+ p,P,K show previous message
+
+ d mark message for later deletion, by adding to sequence 'mldel'.
+ s mark message for later spam training, by adding to sequence 'mlspam'.
+ u mark message to remain "unread", by adding to sequence 'mlunr'.
+ U undo, i.e., remove it from any of 'mldel', 'mlspam', and 'mlunr'.
+
+ r compose a reply
+ R compose a reply to all message recipients
+ f forward the current message
+
+ S sort the messages, by subject and date
+ H render html from the message
+ V run 'urlview' on the message
+ E edit the raw message file
+
+ q quit. The 'mlunr' sequence will be added back to '$ml_unseen_seq',
+ messages in the 'mldel' are deleted, and those in 'mlspam'
+ are dealt with accordingly. Any messages that were read,
+ but not deleted or marked as spam will be left in the
+ 'mlkeep' sequence. If ml dies unexpectedly (or the 'Q'
+ command is used instead of 'q'), "ml -a" (see below) can
+ be used to apply the changes that would have been made.
+
+ Q,X exit. Useful if you want to "start over". The '$ml_unseen_seq'
+ sequence will be restored to its previous state, and the
+ current message list is preserved to 'mlprev'. No other
+ message processing is done.
+
+ Any other command which causes less to quit will simply display
+ the next message. ('q', for instance)
+
+ ml recognizes three special commandline arguments:
+ "ml -s" will report the status of the sequences ml uses, which is
+ handy after quitting with 'X', for example.
+ "ml -a" will apply the changes indicated by the user -- messages
+ in the 'mldel' sequence are deleted, messages in the
+ 'mlspam' sequence are trained and marked as spam, and
+ the 'mlunr' sequence is added to the '$ml_unseen_seq'
+ sequence.
+ "ml -k" will recreate the ml_lesskey file used by ml when running
+ less. ml will usually handle this automatically.
+
+EOF
+}
+
+normal_quit()
+{
+ apply_changes
+ mark -sequence ml -delete all 2>/dev/null
+ exit
+}
+
+ask()
+{
+ immed=;
+
+ if [ "$1" = -i ]
+ then
+ immed="-n 1"
+ shift
+ fi
+ echo -n "${1}? [N/y] "
+ read $immed a
+ #read a
+ case $a in
+ [Yy]*) return 0 ;;
+ *) return 1 ;;
+ esac
+
+}
+
+# ensure the given sequence is empty
+verify_empty()
+{
+ pre="$1"
+ seq=$2
+ if pick $seq:first >/dev/null 2>&1
+ then
+ echo $pre
+ if ask "Non-empty '$seq' sequence found, okay to continue"
+ then
+ mark -sequence $seq -delete all 2>/dev/null
+ else
+ return 1
+ fi
+ fi
+ return 0
+}
+
+# safely return the (non-zero) length of given sequence, with error if empty
+seq_count()
+{
+ msgs=$(pick $1 2>/dev/null) || return 1
+ echo "$msgs" | wc -l
+}
+
+# move 'ml' to 'mlprev'
+preserve_ml_seq()
+{
+ mark -sequence mlprev -zero -add ml 2>/dev/null
+ mark -sequence ml -delete all 2>/dev/null
+}
+
+# restore the unseen sequence to its value on entry
+restore_unseen()
+{
+ mark -sequence $ml_unseen_seq -add saveunseen 2>/dev/null
+}
+
+# add the message to just one of the special sequences.
+markit()
+{
+ case $1 in
+ mlkeep) # this is really an undo, since it restores default action
+ mark -add -sequence mlkeep cur
+ mark -delete -sequence mlspam cur 2>/dev/null
+ mark -delete -sequence mldel cur 2>/dev/null
+ mark -delete -sequence mlunr cur 2>/dev/null
+ ;;
+ mlspam)
+ mark -delete -sequence mlkeep cur 2>/dev/null
+ mark -add -sequence mlspam cur
+ mark -delete -sequence mldel cur 2>/dev/null
+ mark -delete -sequence mlunr cur 2>/dev/null
+ ;;
+ mldel)
+ mark -delete -sequence mlkeep cur 2>/dev/null
+ mark -delete -sequence mlspam cur 2>/dev/null
+ mark -add -sequence mldel cur
+ mark -delete -sequence mlunr cur 2>/dev/null
+ ;;
+ mlunr)
+ mark -delete -sequence mlkeep cur 2>/dev/null
+ mark -delete -sequence mlspam cur 2>/dev/null
+ mark -delete -sequence mldel cur 2>/dev/null
+ mark -add -sequence mlunr cur
+ ;;
+ mlrepl) # this sequence only affects the displayed header of the message.
+ mark -add -sequence mlrepl cur
+ ;;
+ esac
+}
+
+# emit an informational header at the top of each message.
+header()
+{
+ local msg=$1
+
+ this_mess="${BOLD}Message $folder:$msg${NORMAL}"
+
+ # get index of current message
+ mindex=$(echo "$ml_contents" | grep -xn $msg)
+ mindex=${mindex%:*}
+
+ # are we on the first or last or only messages?
+ if [ $ml_len != 1 ]
+ then
+ if [ $mindex = 1 ]
+ then
+ mindex="${BOLD}FIRST${NORMAL}"
+ elif [ $mindex = $ml_len ]
+ then
+ mindex="${BOLD}LAST${NORMAL}"
+ fi
+ fi
+ position="($mindex of $ml_len)"
+
+ # have we done anything to this message?
+ r=; s=;
+ if pick mlrepl 2>/dev/null | grep -qx $msg
+ then
+ r="${BLUE}Replied ${NORMAL}"
+ fi
+ if pick mldel 2>/dev/null | grep -qx $msg
+ then
+ s="${RED}Deleted ${NORMAL}"
+ elif pick mlspam 2>/dev/null | grep -qx $msg
+ then
+ s="${RED}Spam ${NORMAL}"
+ elif pick mlunr 2>/dev/null | grep -qx $msg
+ then
+ s="${RED}Unread ${NORMAL}"
+ fi
+ status=${r}${s}
+
+ # show progress for whole ml run (how many deleted, etc.)
+ scnt=$(seq_count mlspam)
+ dcnt=$(seq_count mldel)
+ ucnt=$(seq_count mlunr)
+ others="${scnt:+$scnt spam }${dcnt:+$dcnt deleted }${ucnt:+$ucnt marked unread}"
+ others="${others:+[$others]}"
+
+ statusline="$this_mess $position $status $others"
+
+ echo $statusline
+
+}
+
+# emit the header again
+footer()
+{
+ echo "-----------"
+ echo "$statusline"
+}
+
+# make the Subject: and From: headers stand out
+colorize()
+{
+ sed \
+ -e 's/^\(Subject: *\)\(.*\)/\1'"$RED"'\2'"$NORMAL"'/' \
+ -e 's/^\(From: *\)\(.*\)/\1'"$BLUE"'\2'"$NORMAL"'/' # 2>/dev/null
+}
+
+cleanup()
+{
+ # the first replacement gets rid of the default header that
+ # show emits with every message -- we provide our own.
+ # for the second: i think the 'Press <return> text is a bug in
+ # mhl. there's no reason to display this message when not
+ # actually pausing for <return> to be pressed.
+ sed -e '1s/^(Message .*)$/---------/' \
+ -e 's/Press <return> to show content\.\.\.//'
+}
+
+# this is the where the message is displayed, using less
+show_msg()
+{
+ local nmsg
+ local which=$1
+
+
+ # only (re)set $msg if pick succeeds
+ if nmsg=$(pick ml:$which 2>/dev/null)
+ then
+ msg=$nmsg
+ viewcount=0
+ else
+ # do we keep hitting the same message?
+ : $(( viewcount += 1 ))
+ if [ $viewcount -gt 2 ]
+ then
+ if ask -i "See message $msg yet again"
+ then
+ viewcount=0
+ else
+ normal_quit
+ fi
+ fi
+ fi
+
+ (
+ header $msg
+ Mail=$(mhpath +)
+ export NMH_NON_INTERACTIVE=1
+ export MHSHOW=$Mail/mhn.noshow
+ mhshow $msg |
+ cleanup |
+ colorize
+ footer
+ ) | LESS=miXcR less $lesskeyfileopt
+ return $? # return less' exit code
+}
+
+# bad things would happen if we were to keep going after the current
+# folder has been changed from another shell.
+check_current_folder()
+{
+ curfold=$(folder -fast)
+ if [ "$curfold" != "$folder" ] # danger, will robinson!!
+ then
+ echo "Current folder has changed to '$curfold'!"
+ echo "Answering 'no' will discard changes, and exit."
+ if ask "Switch back to '$folder'"
+ then
+ folder +$folder
+ else
+ restore_unseen
+ preserve_ml_seq
+ exit
+ fi
+ fi
+}
+
+loop()
+{
+ local nextmsg
+
+ nextmsg=first
+ while :
+ do
+ check_current_folder
+
+ show_msg $nextmsg
+ cmd=$? # save the less exit code
+
+ check_current_folder
+
+ # by default, stay on the same message
+ nextmsg=cur
+
+ case $cmd in
+
+ # help
+ $_ques) help
+ ;;
+
+ # dispatch
+ $_d) markit mldel
+ ##nextmsg=next
+ ;;
+ $_s) markit mlspam
+ ##nextmsg=next
+ ;;
+ $_u) markit mlunr
+ ##nextmsg=next
+ ;;
+ $_U) markit mlkeep
+ ##nextmsg=next
+ ;;
+
+ # send mail
+ $_r) do_reply
+ markit mlrepl
+ #nextmsg=cur
+ ;;
+ $_R) do_replyall
+ markit mlrepl
+ #nextmsg=cur
+ ;;
+ $_f) do_forw
+ markit mlrepl
+ #nextmsg=cur
+ ;;
+
+ # special viewers
+ $_H) do_viewhtml
+ #nextmsg=cur
+ ;;
+ $_V) do_urlview
+ #nextmsg=cur
+ ;;
+ $_E) do_edit
+ #nextmsg=cur
+ ;;
+ $_i) show_status | less -c
+ #nextmsg=cur
+ ;;
+
+ # quitting
+ $_q) normal_quit
+ ;;
+
+ $_X|$_Q) restore_unseen
+ preserve_ml_seq
+ exit
+ ;;
+
+ # other
+ $_S) do_sort
+ nextmsg=first
+ ;;
+
+ # navigation
+ $_up) nextmsg=first
+ ;;
+
+ $_K) nextmsg=prev
+ ;;
+ $_p|$_P) nextmsg=prev
+ ;;
+ $_n|$_J) nextmsg=next
+ ;;
+ *) nextmsg=next
+ ;;
+
+ esac
+ done
+}
+
+# summarize ml's internal sequences, for "ml -s"
+show_status()
+{
+ echo Folder: $folder
+ for s in mlspam mldel mlrepl mlunr
+ do
+ #pick $s:first >/dev/null 2>&1 || continue
+ case $s in
+ mlrepl) echo "Have attempted a reply: (sequence $s)" ;;
+ mldel) echo "Will delete: (sequence $s)" ;;
+ mlspam) echo "Will mark as spam: (sequence $s)" ;;
+ mlunr) echo "Will mark as unseen: (sequence $s)" ;;
+ # mlkeep) echo "Will leave as seen: (sequence $s)" ;;
+ esac
+ scan $s 2>/dev/null || echo ' none'
+ done
+}
+
+apply_changes()
+{
+ if cnt=$(seq_count mlspam)
+ then
+ echo "Marking $cnt messages as spam."
+ do_spamremove mlspam
+ fi
+
+ if cnt=$(seq_count mldel)
+ then
+ echo "Removing $cnt messages."
+ do_rmm mldel
+ fi
+
+ if cnt=$(seq_count mlunr)
+ then
+ echo "Marking $cnt messages unread."
+ mark -add -sequence $ml_unseen_seq mlunr 2>/dev/null
+ mark -sequence mlunr -delete all
+ fi
+
+ if cnt=$(seq_count mlkeep)
+ then
+ echo "Keeping $cnt messages in sequence 'mlkeep':"
+ scan mlkeep
+ fi
+}
+
+# decimal to character mappings. lesskeys lets you specify exit codes
+# from less as ascii characters, but the shell really wants them to be
+# numeric, in decimal. these definitions let you do "quit S" in
+# lesskeys, and then check against $_S here in the shell.
+char_init()
+{
+ _A=65; _B=66; _C=67; _D=68; _E=69; _F=70; _G=71; _H=72; _I=73;
+ _J=74; _K=75; _L=76; _M=77; _N=78; _O=79; _P=80; _Q=81; _R=82;
+ _S=83; _T=84; _U=85; _V=86; _W=87; _X=88; _Y=89; _Z=90;
+
+ _a=97; _b=98; _c=99; _d=100; _e=101; _f=102; _g=103; _h=104; _i=105;
+ _j=106; _k=107; _l=108; _m=109; _n=110; _o=111; _p=112; _q=113; _r=114;
+ _s=115; _t=116; _u=117; _v=118; _w=119; _x=120; _y=121; _z=122;
+
+ _up=94; _ques=63;
+}
+
+color_init()
+{
+ RED="$(printf \\033[1\;31m)"
+ GREEN="$(printf \\033[1\;32m)"
+ YELLOW="$(printf \\033[1\;33m)"
+ BLUE="$(printf \\033[1\;34m)"
+ PURPLE="$(printf \\033[1\;35m)"
+ CYAN="$(printf \\033[1\;36m)"
+ BOLD="$(printf \\033[1m)"
+ NORMAL="$(printf \\033[m)"
+ ESC="$(printf \\033)"
+}
+
+
+# in-line execution starts here
+
+set -u # be defensive
+
+me=${0##*/}
+
+folder=$(folder -fast)
+lesskeymap=$(mhpath +)/ml_lesskeymap
+lesskeyfileopt="--lesskey-file=$lesskeymap"
+
+if [ ! -f $lesskeymap -o $0 -nt $lesskeymap ]
+then
+ create_lesskey_map
+fi
+
+ml_unseen_seq=$(mhparam Unseen-Sequence)
+: ${ml_unseen_seq:=unseen} # default to "unseen"
+
+# check arguments
+case ${1:-} in
+ -s) show_status; exit ;; # "ml -s"
+ -a) apply_changes; exit ;; # "ml -a"
+ -k) create_lesskey_map; exit ;; # "ml -k" (should be automatic)
+ -*) usage ;; # "ml -?"
+ "") starting_seq=$ml_unseen_seq ;; # "ml"
+ *) starting_seq="$*" ;; # "ml picked ..."
+esac
+
+
+# if sequence ml isn't empty, another instance may be running
+verify_empty "Another instance of ml may be running." ml || exit
+
+# gather any user message specifications into the sequence 'ml'
+if ! mark -sequence ml -zero -add $starting_seq >/dev/null 2>&1
+then
+ echo "No messages (or message sequence) specified."
+ exit 1
+fi
+
+# uncomment for debug
+# exec 2>/tmp/ml.log; set -x
+
+# get the full list of messages, and count them
+ml_contents=$(pick ml)
+ml_len=$(echo "$ml_contents" | wc -l)
+
+# if these aren't empty, we might not have "ml a"pplied changes from
+# a previous invocation, so warn.
+verify_empty "You might want to run 'ml -a'." mldel || exit
+verify_empty "You might want to run 'ml -a'." mlspam || exit
+verify_empty "You might want to run 'ml -a'." mlunr || exit
+
+mark -sequence mlrepl -delete all 2>/dev/null
+
+# initialize 'mlkeep' to 'ml', since we assume all undeleted non-spam
+# messages will be kept.
+mark -zero -sequence mlkeep ml
+
+# save a copy of the unseen sequence, for restore if 'X' is used to quit.
+mark -zero -sequence saveunseen $ml_unseen_seq
+
+char_init
+color_init
+
+loop
+
+