#!/bin/bash # mhdisp (for 'dispatch', 'display', 'dispense', take your pick) # examines the mime attachments in an MH mail message, lists them for # the user, and lets one view or save the images/documents/whatever, # in some cases allowing selection of the application to use # (xpdf/okular/acroread), and allowing editing of the filename to be # used if saving. # # on invocation, text parts can be excluded from the list (on the # assumption that they've already be seen via a default invocation of # mhshow), just "interesting" attachments can be included, or all # attachments. ("uninteresting" attachments are currently just # various digital signatures.) # # the default response to the prompt asking for which part to # display attempts to be clever, and will auto-increment through the # available parts as each is dealt with. # # the default response to all prompts is editable using readline. # # paul fox, address@hidden, spring 2014 me=${0##*/} oursttysettings=$(stty -g) usage() { exec >&2 echo "usage: $me [options] " echo " -f|-file " echo " will process the given file instead of using message-spec." echo " -s|-show {nontext,most,all}" echo " 'nontext' will only list interesting non-text parts" echo " 'most' adds parts of type text to the list (this the default)" echo " 'all' shows all parts (but not multipart wrappers)" exit 1 } #exec 2>/tmp/mhdisp.log #chmod a+rw /tmp/mhdisp.log #set -x Mail=$(mhpath +) parse_message() { local msg part_id therest local j partmap=( ) j=0 while read msg part_id therest do : debug: LINE: $msg $part_id $f1 $f2 $therest if [ $msg != '-' ] then doing_msg=$msg part_ids[$j]=0 split_ct_size_descrip partmap+=( [$part_id]=$j ) elif [ $part_id != '-' ] # have a new part to process then (( j++ )) : debug: j is $j, part_id is $part_id # and start gathering info on the new part part_ids[$j]=$part_id partmap+=( [$part_id]=$j ) split_ct_size_descrip else k="${therest%%=*}" v="${therest#*=}" case $k in name) name[$j]="$v" ;; filename) filename[$j]="$v" ;; esac fi done (( j++ )) numparts=$j } p_printf() { printf -v pt "$@" part_text+="$pt" } listparts() { part_text=; shown_parts=0 skipped_always_parts=0 skipped_text_parts=0 i=0 while (( i < numparts )) # i++ at bottom of loop do : debug: $showmode-${c_t[$i]} shown[$i]=; case $showmode-${c_t[$i]} in *-multipart/*) ;; show*-application/*-signature*) (( skipped_always_parts++ )) ;; show_non_text-text/*) (( skipped_text_parts++ )) ;; *) (( shown_parts++ )) shown[$i]=1 p_printf "%4s)" "${part_ids[$i]}" ct=${c_t[$i]} case $ct in image/*) ct=image ;; audio/*) ct=audio ;; application/octet-stream) ct=data ;; application/*) ct=${ct#*/} ;; text/html) ct=html ;; text/plain) ct=text ;; esac p_printf "\t%s" $ct if [ ! "${size[$i]}" ] then p_printf "\tMALFORMED-SIZE" elif [ "${size[$i]}" = 0 ] then p_printf "\tempty" else p_printf "\t%s" ${size[$i]} fi test "${name[$i]}" && p_printf "\t%s" \""${name[$i]}"\" if [ "${filename[$i]}" != "${name[$i]}" ] then test "${filename[$i]}" && p_printf " %s" "${filename[$i]}" fi if [ "${description[$i]}" != "${name[$i]}" -a \ "${description[$i]}" != "${filename[$i]}" ] then test "${description[$i]}" && p_printf " %s" "${description[$i]}" fi # record first text part, in case nothing's more interesting : debug: set first_part to $i ${first_part:=$i} case ${c_t[$i]} in show*-application/*-signature) ;; show_non_text-text/*) ;; *) case ${c_t[$i]} in text/*) ;; *) # default part will be first non-text part : debug: def_part becomes ${def_part:=$i} ;; esac ;; esac p_printf "\n" esac (( i++ )) done # if no non-text part, use first text part as default : debug: def_part becomes ${def_part:=$first_part} test "$part_text" || return 1 echo : $skipped_always_parts-$skipped_text_parts case $skipped_always_parts-$skipped_text_parts in 0-0) echo "All parts of message $doing_msg:" ;; 0-*) echo "Non-text parts of message $doing_msg:" ;; *-0) echo "Interesting parts of message $doing_msg:" ;; *-*) echo "Interesting non-text parts of message $doing_msg:" ;; esac printf "%s" "$part_text" } ask() { immed=; if [ "$1" = -i ] then immed="-n 1" shift fi echo -n "${1}? [N/y] " read $immed a case $a in [Yy]*) return 0 ;; *) return 1 ;; esac } savepart() { local i="$1" local part_id="$2" local msg="$3" savename="${filename[$i]}" # try filename : ${savename:="${name[$i]}"} # if still null, try name : ${savename:="${description[$i]}"} # if still null, try description if [ "$savename" ] then # reduce runs of, and convert, spaces savename="${savename// / }" savename="${savename// /_}" savename=/tmp/$savename else if [ "${c_t[$i]}" = "text/plain" ] then suffix=.txt elif [ "${c_t[$i]}" = "message/rfc822" ] then suffix=.mail else # pull a likely suffix out of mhn.defaults suffix=$(sed -n \ -e "s;mhshow-suffix-${c_t[$i]}: \(.*\);\1;p" \ $(mhparam etcdir)/mhn.defaults ) fi savename=/tmp/messagepart${suffix} fi while : do read -e -p "Filename: (leave empty, or 'q' to quit): " -i "$savename" savename 2>&1 case $savename in ""|q) echo No save; return ;; esac test -e "$savename" || break ask "Overwrite" && break done ct=${c_t[$i]} tmpmhn=/tmp/mhn.$(whoami).tmp echo "mhshow-show-$ct: cat '%f'" >$tmpmhn MHSHOW=$tmpmhn \ mhshow -noheader -form mhl.null -part $part_id $msg >$savename && echo -e '\nSaved.' rm -f $tmpmhn } showpdf() { local ct part_id msg choices prog ct="$1" part_id="$2" msg="$3" choices="xpdf, okular, or acroread? " read -e -p "$choices" ans 2>&1 case $ans in o*) prog=okular ;; a*) prog=acroread ;; *|x*) prog=xpdf ;; esac tmpmhn=/tmp/mhn.$(whoami).tmp echo "mhshow-show-$ct: $prog '%f'" >$tmpmhn MHSHOW=$tmpmhn \ mhshow -form mhl.null -part $part_id $msg | cat rm -f $tmpmhn } showimage() { local ct part_id msg choices prog ct="$1" part_id="$2" msg="$3" prog=xv #choices="xpdf, okular, or acroread? " #read -e -p "$choices" ans 2>&1 #case $ans in #o*) prog=okular ;; #a*) prog=acroread ;; #*|x*) prog=xpdf ;; #esac tmpmhn=/tmp/mhn.$(whoami).tmp echo "mhshow-show-$ct: $prog '%f'" >$tmpmhn MHSHOW=$tmpmhn \ mhshow -form mhl.null -part $part_id $msg | cat rm -f $tmpmhn } do_show() { local action="$1" local part_id="$2" local msg="$3" i=${partmap[$part_id]} case $action in next) ;; save) savepart $i $part_id "$msg" ;; view) # use trap to catch ^C. this lets viewer programs be killed # without dying ourselves. delay the trap until just before # invoking the viewer, however. ct=${c_t[$i]} case $ct in application/octet-stream|data) echo "Uh oh. Cannot determine what application to use to view" echo "the contents of part $part_id." filename="${filename[$i]}" : ${filename:="${name[$i]}"} # if still null, try name : ${filename:="${description[$i]}"} # if still null, try description case "$filename" in *.pdf|*.PDF) maybepdf=true ;; *.jpeg|*.jpg|*.PNG|*.png) maybeimage=true ;; esac if [ "$maybepdf" ] then echo read -e -i y -p "It may be a pdf. Try that? [Y/n] " ans 2>&1 case $ans in [yY]*) trap "" SIGINT showpdf "$ct" "$part_id" "$msg" trap - SIGINT return ;; esac fi if [ "$maybeimage" ] then echo read -e -i y -p "It may be an image. Try that? [Y/n] " ans 2>&1 case $ans in [yY]*) trap "" SIGINT showimage "$ct" "$part_id" "$msg" trap - SIGINT return ;; esac fi read -e -i y -p \ "Perhaps it's just text, and 'less' will work. Try that? [Y/n] "\ ans 2>&1 case $ans in [yY]*) less $part_id $filename tmpmhn=/tmp/mhn.$(whoami).tmp echo "mhshow-show-application/octet-stream: %pless '%f'" >$tmpmhn MHSHOW=$tmpmhn \ mhshow -form mhl.null -part $part_id $msg rm -f $tmpmhn return ;; esac ;; image/*|video/*|audio/*) trap "" SIGINT MHSHOW=$Mail/mhn.all \ mhshow -form mhl.null -part $part_id $msg | cat trap - SIGINT ;; application/pdf) trap "" SIGINT showpdf "$ct" "$part_id" "$msg" trap - SIGINT ;; application/msword) choices="antiword, or libreoffice? " read -e -p "$choices" ans 2>&1 case $ans in a*) prog=antiword; pager=less ;; l*) prog=libreoffice; pager=cat ;; esac tmpmhn=/tmp/mhn.$(whoami).tmp echo "mhshow-show-$ct: $prog '%f'" >$tmpmhn trap "" SIGINT MHSHOW=$tmpmhn \ mhshow -form mhl.null -part $part_id $msg | $pager trap - SIGINT rm -f $tmpmhn ;; message/rfc822) tmpmhn=/tmp/mhn.$(whoami).tmp echo "mhshow-show-$ct: mhshow -file '%f'" >$tmpmhn echo "mhshow-show-text/html: %p/usr/bin/elinks -force-html '%F' -dump" >>$tmpmhn trap "" SIGINT MHSHOW=$tmpmhn \ mhshow -form mhl.null -part $part_id $msg trap - SIGINT ;; *) #mhshow -form mhl.null -part $part_id $msg | less trap "" SIGINT mhshow -part $part_id $msg | less trap - SIGINT ;; esac ;; # untested below text) MHSHOW=$Mail/mhn.block_html \ mhshow -form mhl.null -part $part_id $msg | less ;; html) MHSHOW=$Mail/mhn.html_only \ mhshow -form mhl.null -part $part_id $msg ;; browser) #tmpfile=/tmp/$(whoami)-mh.html #echo "mhshow-show-text/html: cat '%f' >$tmpfile" >$Mail/mhn.tmp #MHSHOW=$Mail/mhn.tmp chromium-browser %s >/dev/null 2>&1 & #rm $Mail/mhn.tmp ;; graphical) ;; esac } prompt() { local act=v; # default to 's'tore for application/*, but 'v'iew for all others case "$def_part" in "") act=; ;; *) case ${c_t[$def_part]} in application/*) act=s ;; esac ;; esac echo # this trap wasn't necessary in the ubuntu 12.04 version of bash -- # ^C caused an exit by default. but bash 4.3.11(1) requires it. trap "stty $oursttysettings; exit" SIGINT txt="Enter [sv] or h/? or q: " default="${part_ids[${def_part}]}${act}" read -e -p "$txt" -i "$default" ans 2>&1 test "$ans" && partcmd="$ans" } interact_help() { cat <<-EOF Enter a part number followed by 's' or 'v' to save or view the part's content, or 'q' to quit. Only the last part number and command are considered, so there's no need to erase the answer offered by default. i.e.,: 1.2v will view part 1.2 1.2v2s will store part 2 1.2vq will quit 1.2v3 will view part 3 (because view is the default if there's no command present. EOF read -p "Enter to continue..." a } interact() { local part_id action i suspect local msg="$1" suspect=$(MHSHOW=$Mail/mhn.html_dump_wide \ mhshow -noconcat -type text/html "$@" | egrep ' [[:digit:]]+ of [[:digit:]]+ File\(s\)') if [ "$(mhlist -noheaders -type text/html)" ] then suspect="$suspect$(MHSHOW=$Mail/mhn.html_dump_wide \ mhshow -noconcat -type text/html "$@" | egrep '^ *Attachment.* from .*View')" fi if [ "$suspect" ] then echo echo '***WARNING!!! Message may contain web-based so-called "attachments"!!!'*** refno=$(MHSHOW=$Mail/mhn.html_dump_wide \ mhshow -noconcat -type text/html "$@" | sed -n -e 's/[[:space:]]*Attachment.* from .*\[\([[:digit:]]\)\]View .*/\1/p') refurl=$(MHSHOW=$Mail/mhn.html_dump_wide \ mhshow -noconcat -type text/html "$@" | sed -n -e "s/^[[:space:]]*${refno}\. \(.*\)/\1/p") if [ "$refurl" ] then echo echo Attachments might be found here: echo "$refurl" else echo "Hmmm... can't find url for the web-based attachments." fi fi while : do # listparts refers to the parsed info built by parse_message listparts || exit 0 # don't ask questions if stdout is redirected, just list. test -t 1 || return 0 # sets $response prompt $showmode action=view case $partcmd in *q) break ;; *a) showmode=show_most ;; *A) showmode=all ;; *h|*\?|*H) interact_help ;; [0-9]*) case $partcmd in *s) action=save ;; *v) action=view ;; *n) action=next ;; #*t) action=text ;; #*h) action=html ;; #*b) action=browser ;; #*g) action=graphical ;; esac re='([0-9.]+)[a-z]*$' # extract the last part number [[ "$partcmd" =~ $re ]] part_id=${BASH_REMATCH[1]} # first subexpression match do_show $action $part_id "$msg" # if we've just shown the only part, then quit if [ "$shown_parts" = 1 ] then break fi : debug: $part_id ${partmap[$part_id]} # loop through all the visible parts i=${partmap[$part_id]} while : do (( ++i >= numparts )) && i=1 test "${shown[$i]}" && break done def_part=$i : debug: def_part became $def_part ;; *) : big default; interact_help break ;; esac done } do_message() { local msg="$1" : ============ initial msg is "$msg" declare -A partmap declare -a part_ids name filename ct size description # parse_message reads processed mhlist info from stdin parse_message # but interact reads the original message from "$msg", # which might be "-file path" exec 0<&3 # restore the original stdin : ============ later msg is "$msg" interact "$msg" } massage_mhlist() { # use sed to make mhlist output more readily parseable: # - the first three expressions ensure the first two columns always # have some content (namely, at least a '-' character). # (the first takes care of an empty second column. the second # takes care of an empty first column, helped by the third, which # kicks in if the msg number is wider than 4 digits.) # - we don't care about the boundary marker or the disposition, # and disposition is malformed (no '=') anyway, so delete them. # - truncation by mhlist corrupts some content-types, so fix them. # - finally, we strip double-quotes from name=value parameters. expand | sed -e 's/^ / - /' \ -e 's/^\(....\) /\1 - /' \ -e 's/^\([0-9]\+\) /\1 - /' \ -e '/ boundary="/d' \ -e '/ disposition "/d' \ -e 's;application/pkcs7-signat;application/pkcs7-signature;' \ -e 's;application/pgp-signatur;application/pgp-signature;' \ -e 's;application/x-zip-compre;application/x-zip-compressed;' \ -e 's/\( [[:alnum:]]\+\)="\([^"]\+\)"$/\1=\2/' } main() { local arg usefile fname declare -a args showmode=show_most # choices: "show_non_text", "show_most", "all" while : do case $1 in -s|-show|--show) case $2 in n*|nontext) showmode=show_non_text ;; m*|most) showmode=show_most ;; a*|all) showmode=all ;; *) usage ;; esac shift 2 ;; -f|-file|--file) usefile=true fname="$2" : <"$fname" || exit 1 args[0]="-file $fname" shift 2 break ;; -*) usage ;; *) break ;; esac done # we need to be able to do user interaction from within do_message(), # which is getting stdin from a pipe. so save stdin on fd 3. exec 3<&0 if [ "$usefile" ] then : elif ! args=( $(pick -noseq "${@:-cur}" 2>/dev/null) ) then echo $me: no such message >&2 exit 0 fi for arg in "address@hidden" do mhlist -noheaders -verbose -disposition $arg | massage_mhlist | do_message "$arg" || exit done } # odd positioning of this routine in the file is purely because the # syntax colorizer in older versions of vile doesn't grok '<<<', and # the coloring is messed up for the rest of the edit buffer after it's # seen. split_ct_size_descrip() { # wow: doing it in one line, like this, is *hugely* expensive for # some reason: # read c_t[$j] size[$j] description[$j] <<< "$therest" # so split up the read from the array assignments: read c s d <<< "$therest" c_t[$j]="$c" size[$j]="$s" description[$j]="$d" } main "$@"