-
-
Save dubiouscript/5771c71ad658d007d7a936688d458cfb to your computer and use it in GitHub Desktop.
bash functions to dump and inspect a message in MIME format
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bash | |
############################################################################## | |
#### 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() | |
{ | |
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 "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment