Added ml script to docs/contrib.
authorPaul Fox <pgf@foxharp.boston.ma.us>
Fri, 9 Nov 2012 20:37:59 +0000 (14:37 -0600)
committerDavid Levine <levinedl@acm.org>
Fri, 9 Nov 2012 20:37:59 +0000 (14:37 -0600)
docs/contrib/ml [new file with mode: 0755]

diff --git a/docs/contrib/ml b/docs/contrib/ml
new file mode 100755 (executable)
index 0000000..f5796c4
--- /dev/null
@@ -0,0 +1,702 @@
+#!/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
+
+