#!/usr/bin/env bash # analysiert E-Maildateien und gibt sie geordnet aus # https://gist.github.com/markusfisch/2649043 ############################################################################## #### MIME interface ############################################################################## # Parse message in MIME format and create a temporary cache directory mime_parse() { MIME_CACHE=${MIME_CACHE:-`mktemp -d ${BIN}.XXXXXXXXXX`} local D=$MIME_CACHE local HEADER=1 local LAST= local BOUNDARY= while read do REPLY=${REPLY%$CR} [ "$REPLY" == '.' ] && break # in mime header if [ "$HEADER" ] then # header closed [ "$REPLY" ] || { HEADER= [ -r "$D/content-type" ] && { local VALUE value "`< "$D/content-type"`" \ '[Bb][Oo][Uu][Nn][Dd][Aa][Rr][Yy]=' [ "$VALUE" ] && { BOUNDARY=$VALUE echo "$BOUNDARY" > "$D/boundary" } } [ -r "$D/content-disposition" ] && { local VALUE value "`< $D/content-disposition`" \ '[Ff][Ii][Ll][Ee][Nn][Aa][Mm][Ee]=' [ "$VALUE" ] && { echo "$VALUE" >> "$MIME_CACHE/attachments" echo "$D" >> "$MIME_CACHE/attachments-paths" } } continue } local F if [[ "$REPLY" == [' '$'\t']* ]] then [ "$LAST" ] || continue F=$LAST else F=`lower "${REPLY%%:*}"` LAST=$F fi echo ${REPLY#*:} >> "$D/$F" continue elif [ "$BOUNDARY" ] && [ "${REPLY:0:2}" == '--' ] then [[ "$REPLY" == --$BOUNDARY* ]] && { [ "$D" == "$MIME_CACHE" ] || D=${D%/*} if [ "$REPLY" == "--$BOUNDARY--" ] then if [ -r "$D/boundary" ] then BOUNDARY=`< "$D/boundary"` else BOUNDARY= fi HEADER= else local PART=1 [ -r "$D/parts" ] && { PART=`< "$D/parts"` (( ++PART )) } echo $PART > "$D/parts" D="$D/part-$PART" mkdir "$D" || return 1 HEADER=1 fi } continue fi echo "$REPLY"$CR >> $D/body done } # Free MIME data structure mime_free() { echo $MIME_CACHE cp -r $MIME_CACHE/* /volume1/homes/admin/dump/ rm -rf $MIME_CACHE MIME_CACHE= } # Decode possibly encoded message text # # @param 1 - message directory mime_decode_message() { local F="$1/body" [ -r "$F" ] && { local T=`< "$1/content-type"` CS='cat' case "$T" in [Tt][Ee][Xx][Tt]/*) local VALUE value "$T" '[Cc][Hh][Aa][Rr][Ss][Ee][Tt]=' [ "$VALUE" ] && CS="iconv -f $VALUE -t utf-8" ;; esac case "`< "$1/content-transfer-encoding"`" in *[Qq][Uu][Oo][Tt][Ee][Dd]-[Pp][Rr][Ii][Nn][Tt][Aa][Bb][Ll][Ee]*) decode_quoted_printable ;; *[Bb][Aa][Ss][Ee]64*) base64 -d -i ;; *) cat ;; esac < "$F" | $CS } 2>/dev/null } # Display message with header information # # @param 1 - message directory mime_display_message() { # echo headers { local H HEADERS=${HEADERS:-from to subject date attachments} local M=0 # get length of longest header label { local L for H in $HEADERS do L=${#H} (( L > M )) && M=$L done } local W=$(( ${WIDTH:-80}-(M+2) )) for H in $HEADERS do local F="$1/$H" while ! [ -r "$F" ] do [ "$F" == "$MIME_CACHE/$H" ] && break F="$MIME_CACHE/$H" done [ -r "$F" ] || continue local S if [ "$H" == 'attachments' ] then S=`< "$F"` S=${S//$'\n'/ } else S=`decode_encoded_word < "$F"` fi local N L=${#S} LABEL=$H for (( N = 0; N < L; N += W )) do printf "%-${M}s %-${W}s\n" "$LABEL" "${S:$N:$W}" LABEL= done done [ "$H" ] && echo } mime_decode_message "$1" } # Returns true if content type is text # # @param 1 - file with content type mime_content_is_text() { case "`< "$1"`" in *[Tt][Ee][Xx][Tt]/[Pp][Ll][Aa][Ii][Nn]*|\ *[Tt][Ee][Xx][Tt]/[Hh][Tt][Mm][Ll]*) return 0 ;; esac 2>/dev/null return 1 } # Traverse message tree to find message text # # @param 1 - directory in MIME tree # @param 2 - callback function mime_find_message() { (( $# < 2 )) && return 1 local TYPE=0 case "`< "$1/content-type"`" in *[Mm][Uu][Ll][Tt][Ii][Pp][Aa][Rr][Tt]/[Aa][Ll][Tt][Ee][Rr][Nn][Aa][Tt][Ii][Vv][Ee]*) TYPE=1 ;; *[Mm][Uu][Ll][Tt][Ii][Pp][Aa][Rr][Tt]/[Dd][Ii][Gg][Ee][Ss][Tt]*) TYPE=2 ;; esac 2>/dev/null local N PARTS=`< "$1/parts"` for (( N=1; N < PARTS; ++N )) do local P="$1/part-$N" [ -r "$P/body" ] && mime_content_is_text "$P/content-type" && { $2 "$P" (( TYPE == 2 )) || return 0 } (( TYPE == 2 )) && return 0 [ -r "$P/parts" ] && mime_find_message "$P" "$2" && return 0 done return 1 } # Echo message from MIME data structure # # @param 1 - callback function (optional) mime_message() { local C=${1:-mime_display_message} [ -r "$MIME_CACHE/parts" ] && mime_find_message "$MIME_CACHE" $C && return [ -r "$MIME_CACHE/content-type" ] && { mime_content_is_text "$MIME_CACHE/content-type" || return } $C "$MIME_CACHE" } ############################################################################## #### Encoding/Decoding ############################################################################## # Decode quoted-printable-encoded stream decode_quoted_printable() { local C=0 EOF=0 while (( ! EOF )) do read -d '=' || EOF=1 (( C )) && if [[ $REPLY == [$'\r'$'\n']* ]] then REPLY=${REPLY:1} else printf \\x"${REPLY:0:2}" REPLY=${REPLY:2} fi echo -n "$REPLY" C=1 done } # Decode MIME encoded-word syntax decode_encoded_word() { while read do while [[ $REPLY == *'=?'* ]] do echo -n ${REPLY%%'=?'*} local A=${REPLY#*'?='} V=${REPLY#*'=?'} V=${V%%'?='*} local P=( ${V//\?/ } ) if (( ${#P[@]} == 3 )) then case "${P[1]}" in [Qq]) echo -n "${P[2]}" | decode_quoted_printable ;; [Bb]) echo -n "${P[2]}" | base64 -d -i ;; esac | iconv -f "${P[0]}" -t utf-8 else echo -n $V fi REPLY=$A done echo -n $REPLY done } which iconv &>/dev/null || iconv() { cat } which base64 &>/dev/null || { echo 'error: base64 not found!' >&2 echo 'Either install it or get this fallback implementation:' >&2 echo 'https://gist.github.com/2648733' >&2 exit 1 } ############################################################################## #### String auxiliaries ############################################################################## # Make string lower case # # @param 1 - some string if [ $BASH_VERSINFO ] && (( ${BASH_VERSINFO[0]} > 3 )) then lower() { echo "${1,,}" } else lower() { echo "$1" | tr '[:upper:]' '[:lower:]' } fi # Find a key/value pair in the given string and set VALUE accordingly # # @param 1 - string # @param 2 - pattern of key value() { [[ "$1" == *$2* ]] || { VALUE= return } VALUE=${1#*$2} local QUOTE="${VALUE:0:1}" case "$QUOTE" in '"'|"'") ;; *) QUOTE= ;; esac if [ "$QUOTE" ] then VALUE=${VALUE:1} VALUE=${VALUE%%$QUOTE*} else VALUE=${VALUE%% *} fi } ############################################################################## #### Features ############################################################################## # Manually check data structure # # @param 1 - message file inspect() { [ -r "$1" ] || { echo "error: file $1 not found" >&2 return 1 } echo "(unpacking \"$1\")" mime_parse < "$1" && cd "$MIME_CACHE" && \ ls && \ PS1='inspect> ' bash && \ cd .. mime_free } # Dump message text # # @param 1 - message file dump() { mime_parse < "$1" && mime_message mime_free } ############################################################################## #### Command processing ############################################################################## # Process arguments # # @param ... - arguments mime() { (( $# < 1 )) && { cat <&2 return ;; esac $ACTION "$F" done } readonly BIN=${0##*/} readonly CR=$'\r' mime "$@"