You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
482 lines
8.0 KiB
482 lines
8.0 KiB
#!/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 "$@" |