From: Paul Fox Date: Fri, 9 Nov 2012 20:37:59 +0000 (-0600) Subject: Added ml script to docs/contrib. X-Git-Url: http://git.marmaro.de/?a=commitdiff_plain;h=fa194d6d12bd0d4d2196508cbb0e5bfddb6efe37;p=mmh Added ml script to docs/contrib. --- diff --git a/docs/contrib/ml b/docs/contrib/ml new file mode 100755 index 0000000..f5796c4 --- /dev/null +++ b/docs/contrib/ml @@ -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 <&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 </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 text is a bug in + # mhl. there's no reason to display this message when not + # actually pausing for to be pressed. + sed -e '1s/^(Message .*)$/---------/' \ + -e 's/Press 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 + +