#!/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