Skip to content

Instantly share code, notes, and snippets.

@dubiouscript
Forked from markusfisch/mime.sh
Created June 10, 2019 16:00
Show Gist options
  • Save dubiouscript/5771c71ad658d007d7a936688d458cfb to your computer and use it in GitHub Desktop.
Save dubiouscript/5771c71ad658d007d7a936688d458cfb to your computer and use it in GitHub Desktop.
bash functions to dump and inspect a message in MIME format
#!/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