482 lines
8.0 KiB
Bash
482 lines
8.0 KiB
Bash
#!/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 <<EOF
|
|
usage: ${BIN} [-di] FILE...
|
|
d dump message (default)
|
|
i inspect message tree
|
|
|
|
EOF
|
|
return
|
|
}
|
|
|
|
local F ACTION=dump
|
|
|
|
for F in "$@"
|
|
do
|
|
case "$F" in
|
|
-i)
|
|
ACTION=inspect
|
|
continue
|
|
;;
|
|
-d)
|
|
ACTION=dump
|
|
continue
|
|
;;
|
|
-*)
|
|
echo "error: unkown flag '$F'" >&2
|
|
return
|
|
;;
|
|
esac
|
|
|
|
$ACTION "$F"
|
|
done
|
|
}
|
|
|
|
readonly BIN=${0##*/}
|
|
readonly CR=$'\r'
|
|
|
|
mime "$@" |