Updated 2015-02-12 21:08:55 by EMJ

EMJ An email is a tree of parts. Feed a file containing a raw email to the script below, it will build a tree, then use it to save all the parts separately as well as giving you the tree structure.

I used this to learn about Tcllib MIME, struct::tree, and sha1.

The tree you get is a file that looks like this:
    leaf 69a2faa59d {params {format flowed charset ISO-8859-1} content text/plain encoding 7bit}
    leaf 3210ba9e34 {params {charset ISO-8859-1} content text/html encoding 7bit}
    wrapper d0a17db3c7 {content multipart/alternative} 69a2faa59d 3210ba9e34
    leaf ce9a16e7d3 {params {name {Globe.pdf}} content application/pdf encoding base64}
    wrapper 12102a8c68 {content multipart/mixed} d0a17db3c7 ce9a16e7d3

This is the tree of parts, with the root at the bottom. Actually the structure of this example is:
       +--leaf 69a2faa59d
       +--leaf 3210ba9e34
    +--wrapper d0a17db3c7
    +--leaf ce9a16e7d3
    wrapper 12102a8c68

The root is always a "wrapper", with the main headers saved to a file. Other "wrappers" also have headers saved to a file (which may be empty). A "leaf" has headers (saved to a file) and a body (decoded and saved to a file). The name of a "leaf" (and its two files) is the 40-character (shortened to 10 above) of the data. The name of a wrapper and its header file (and the tree file for the root wrapper) is the hash of the list of hashes of its immediate children.

This is 7 or 8 years old, and I'm afraid there aren't many comments.
    #!/bin/sh
    # the next line restarts using tclsh \
    exec tclsh "$0" ${1+"[email protected]"}

    package require mime
    package require sha1
    package require struct::tree

    proc buildmimetree {token node} {
        set props [mime::getproperty $token -names]
        if {[lsearch $props parts] >= 0} {
            # this one has parts
            set partlist [mime::getproperty $token parts]
            foreach pt $partlist {
                # create a tree node for the part
                stree insert $node end $pt
                # process the part in its turn
                buildmimetree $pt $pt
            }
        }
    }

    proc mimeleaf {node} {
        set sha1tok [sha1::SHA1Init]
        set df [open $node.data w]
        # must be in binary mode
        fconfigure $df -translation binary
        set hash [mime::getbody $node -command [list bodypart $df $sha1tok]]
        close $df
        if [file exists $hash.data] {
            file delete $hash.data
        }
        file rename $node.data $hash.data
        stree set $node hash $hash
        savemimedata $hash $node
    }

    proc mimewrapper {node} {
        set childlist [list]
        foreach child [stree children $node] {
            lappend childlist [stree get $child hash]
        }
        set hash [sha1::sha1 $childlist]
        stree set $node hash $hash
        savemimedata $hash $node
    }

    proc savemimedata {hash node} {
        # write out headers
        set hf [open $hash.hdr w]
        fconfigure $hf -translation crlf
        writeheaders $hf $node
        close $hf
    }

    proc fixprops {node {type l}} {
        array set props [mime::getproperty $node]
        array unset props parts
        array unset props size
        if {[string length $props(encoding)] == 0} {
            array unset props encoding
        }
        if {[string equal $type w]} {
            array unset props encoding
        }
        array set pars $props(params)
        array unset pars boundary
        if {[llength [array names pars]] > 0} {
            set props(params) [array get pars]
        } else {
            array unset props params
        }
        return [list [array get props]]
    }

    proc writeheaders {channel node} {
        foreach header [::mime::getheader $node -names] {
            if {[string first "From " $header] == 0} {
                continue
            }
            # puts $channel "$header: [join [mime::getheader $node $header]]"
            # Keep repeats separate (good for Received:)
            foreach occ [mime::getheader $node $header] {
                puts $channel "$header: $occ"
            }
        }
    }

    proc bodypart {channel sha1token type {data ""}} {
        switch -exact -- $type {
            data {
                puts -nonewline $channel $data
                sha1::SHA1Update $sha1token $data
            }
            end {
                return [sha1::Hex [sha1::SHA1Final $sha1token]]
            }
        }
    }

    # -------- start of mainline code
    if {[llength $argv] != 1} {
        puts "Usage : mailbreak filename"
        exit 1
    }

    set fname [lindex $argv 0]
    if {![file readable $fname]} {
        puts "Can't open $fname"
        exit 1
    }

    # tokenize our file
    set token [mime::initialize -file $fname]

    # create a tree to use
    struct::tree stree
    # and rename the root after the main token
    stree rename root $token

    buildmimetree $token $token

    # walk the tree bottom-up collecting the pieces
    set toplevel [stree rootname]
    stree walk $toplevel -order post node {
        if [stree isleaf $node] {
            mimeleaf $node
        } else {
            mimewrapper $node
        }
    }

    # write something that could be a script to recreate the whole thing
    set fname [stree get $toplevel hash]
    set tf [open $fname.tree w]
    stree walk $toplevel -order post node {
        if [stree isleaf $node] {
            puts $tf "leaf [stree get $node hash] [fixprops $node l]"
        } else {
            set childlist [list]
            foreach child [stree children $node] {
                lappend childlist [stree get $child hash]
            }
            puts $tf "wrapper [stree get $node hash] [fixprops $node w] $childlist"
        }
    }
    close $tf

    exit 0