paul vixie wrote:
 > Ken Hornstein wrote:
 > >> Well, it depends on the message.  Sometimes I get a message with 20 photos
 > >> attached.  I just want to be able to easily go from one to the next 
 > >> without
 > >> having to type their part number.
 > >
 > > But ... what's wrong with doing something like:
 > >
 > > mhstore -type image
 > >
 > > Then you can deal with each of those photos as a seperate file.
 > 
 > ui wise, i don't want files. i want to act on the attachments (view 
 > them, in this case) and then leave them stored as attachments (without 
 > having to delete any temporary files.)
 > 
 > i don't know exactly how to match mime to the simplicity of show(1), and 
 > i've been violently repulsed any time i tried to use mhshow(1), but i 
 > think what i want would look a bit like whatnow(1) is to send(1).
 > (for many years i used "send -wa" because my sendmail.cf was a hack, 
 > then i used "push" when i stopped being fascinated by SMTP traces.)
 > 
 > the command set i'd suggest for whatnowmime(1) would include things like:
 > 
 > ls | dir
 >      shows the full message structure (like in ftp)
 > cd x.y.z
 >      go to part x subpart y subsubpart z
 > next
 >      go to next attachment at same level (like in gdb)
 > step
 >      go to next attachment at or below the current one (like gdb)
 > prev
 >      opposite of next
 > back
 >      opposite of step
 > up
 >      go up one level
 > view
 >      launch an external viewer suitable for this mimetype
 > show
 >      do the best you can in inline ascii for this mimetype
 > save [-r] [x.y.z]
 >      save the current part (default) or part x subpart y subsubpart z, maybe 
 > recursively (-r)
 > quit
 >      exit from mhshow(1)

i wrote a script for myself (and my wife) that does much of this (but
by no means all).  the script i use to actually read a mail message
invokes mhshow for the main parts, and then invokes my "mhdisp" script.

mhdisp does the work of wandering through the parts of the message in
order (as well as letting the user select a specific part), and either
does the obvious thing (i.e., displays a text part) or asks the user
what to do (as in the case of a pdf -- i use different readers at
different times), or lets me save a part as a file, and prompts me
with a default filename.  it's made life with MH and MIME much more
pleasant in our household.

there's a bit of a chewing-gum-and-string aspect to it all -- for
instance, to force some actions from mhshow i create custom profiles
on the fly for it to use for that invocation.  it was easier to do
that than create a single profile that always did the right thing in
all cases.

other than being customized mainly for the mail i get (i never receive
audio files, so there's no custom handler for those, for instance),
the biggest missing piece currently is a fully recursive handler for
message/rfc822 parts.  i end up having to save those to a file, and
then run mhdisp on them separately.

i don't see anything terribly difficult about converting what i've
done as a looping "what do you want to do next?" shell into separate
commandline commands, except for the preservation of state between
each such command, and the necessary reparsing of the original message
for each one.  all of the currently hard-coded actions would need to
be sensibly configurable, too, probably with something different than
the current mhn.defaults.  so in principle what paul is asking for is
SMOP (simple matter of programming), but it's definitely a project.

(i just reread paul's message, above, and realize he wasn't describing
separate commandline commands, but the sub-commands in a whatnowmime
shell.  that's more similar to what i've done than i realized.)

i've attached mhdisp in case someone wants to see it.  it's never been
out in the wild before, so it might depend horribly on my environment --
no promises.

paul



 > 
 > -- 
 > P Vixie
 > 
 > _______________________________________________
 > Nmh-workers mailing list
 > Nmh-workers@nongnu.org
 > https://lists.nongnu.org/mailman/listinfo/nmh-workers


=----------------------
 paul fox, p...@foxharp.boston.ma.us (arlington, ma, where it's 38.8 degrees)
#!/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, p...@foxharp.boston.ma.us, spring 2014

me=${0##*/}

oursttysettings=$(stty -g)

usage()
{
    exec >&2
    echo "usage: $me [options] <message-spec>"
    echo " -f|-file <pathname>"
    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 <partno>[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 "${args[@]}"
    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 "$@"
_______________________________________________
Nmh-workers mailing list
Nmh-workers@nongnu.org
https://lists.nongnu.org/mailman/listinfo/nmh-workers

Reply via email to