Browse Source

new mime reader wip

flashmob 6 years ago
parent
commit
404f0f4b7c
2 changed files with 1074 additions and 0 deletions
  1. 847 0
      backends/s_mime.go
  2. 227 0
      backends/s_mime_test.go

+ 847 - 0
backends/s_mime.go

@@ -0,0 +1,847 @@
+package backends
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+	"github.com/flashmob/go-guerrilla/mail"
+	"io"
+	"net/textproto"
+	"strconv"
+)
+
+// ----------------------------------------------------------------------------------
+// Name          : Mime Analyzer
+// ----------------------------------------------------------------------------------
+// Description   : analyse the MIME structure of a stream
+// ----------------------------------------------------------------------------------
+// Config Options:
+// --------------:-------------------------------------------------------------------
+// Input         :
+// ----------------------------------------------------------------------------------
+// Output        :
+// ----------------------------------------------------------------------------------
+
+func init() {
+	streamers["mimeanalyzer"] = func() *StreamDecorator {
+		return StreamMimeAnalyzer()
+	}
+}
+
+type mimepart struct {
+	/*
+			[starting-pos] => 0
+		    [starting-pos-body] => 270
+		    [ending-pos] => 2651
+		    [ending-pos-body] => 2651
+		    [line-count] => 72
+		    [body-line-count] => 65
+		    [charset] => us-ascii
+		    [transfer-encoding] => 8bit
+		    [content-boundary] => D7F------------D7FD5A0B8AB9C65CCDBFA872
+		    [content-type] => multipart/mixed
+		    [content-base] => /
+
+			[starting-pos] => 2023
+		    [starting-pos-body] => 2172
+		    [ending-pos] => 2561
+		    [ending-pos-body] => 2561
+		    [line-count] => 9
+		    [body-line-count] => 5
+		    [charset] => us-ascii
+		    [transfer-encoding] => base64
+		    [content-name] => map_of_Argentina.gif
+		    [content-type] => image/gif
+		    [disposition-fi1ename] => map_of_Argentina.gif
+		    [content-disposition] => in1ine
+		    [content-base] => /
+	*/
+}
+
+type parser struct {
+	state int
+
+	accept bytes.Buffer
+
+	once            bool
+	boundaryMatched int
+
+	// related to the buffer
+	buf                         []byte
+	pos                         int
+	ch                          byte
+	gotNewSlice, consumed, halt chan bool
+	result                      chan parserMsg
+	isHalting                   bool
+
+	// mime variables
+	parts   []mimeHeader
+	msgPos  uint
+	msgLine uint
+}
+
+type mimeHeader struct {
+	headers textproto.MIMEHeader
+
+	part string
+
+	startingPos      uint // including header (after boundary, 0 at the top)
+	startingPosBody  uint // after header \n\n
+	endingPos        uint // redundant (same as endingPos)
+	endingPosBody    uint // the char before the boundary marker
+	lineCount        uint
+	bodyLineCount    uint
+	charset          string
+	transferEncoding string
+	contentBoundary  string
+	contentType      string
+	contentBase      string
+
+	dispositionFi1eName string
+	contentDisposition  string
+	contentName         string
+}
+
+func NewMimeHeader() *mimeHeader {
+	mh := new(mimeHeader)
+	mh.headers = make(textproto.MIMEHeader, 1)
+	return mh
+}
+
+func (p *parser) addPart(mh *mimeHeader, id string) {
+
+}
+
+func (p *parser) endBody(mh *mimeHeader) {
+
+}
+
+//
+func (p *parser) more() bool {
+	p.consumed <- true // signal that we've reached the end of available input
+	select {
+	// wait for a new new slice
+	case gotMore := <-p.gotNewSlice:
+		if !gotMore {
+			// no more data, closing
+			return false
+		}
+	case <-p.halt:
+		p.isHalting = true
+		return false
+	}
+	return true
+}
+
+func (p *parser) next() byte {
+	// wait for a new new slice if reached the end
+	if p.pos+1 >= len(p.buf) {
+		if !p.more() {
+			p.ch = 0
+			return 0
+		}
+	}
+
+	// go to the next byte
+	p.pos++
+	p.ch = p.buf[p.pos]
+	p.msgPos++
+	if p.ch == '\n' {
+		p.msgLine++
+	}
+	return p.ch
+}
+
+func (p *parser) peek() byte {
+
+	// reached the end?
+	if p.pos+1 >= len(p.buf) {
+		if !p.more() {
+			p.ch = 0
+			return 0
+		}
+	}
+
+	// peek the next byte
+	if p.pos+1 < len(p.buf) {
+		return p.buf[p.pos+1]
+	}
+	return 0
+}
+
+// simulate a byte stream
+func (p *parser) inject(input ...[]byte) {
+	go func() {
+		for i := 1; i < len(input); i++ {
+			<-p.consumed
+			p.set(input[i])
+			p.gotNewSlice <- true
+		}
+		<-p.consumed
+		p.gotNewSlice <- false // no more data
+	}()
+	p.set(input[0])
+	p.pos = 0
+	p.ch = p.buf[0]
+}
+
+func (p *parser) set(input []byte) {
+	if p.pos != -1 {
+		// rewind
+		p.pos = -1
+	}
+	p.buf = input
+
+}
+
+// boundary scans until next boundary string, returns error if not found
+func (p *parser) boundary(part *mimeHeader) (err error) {
+	if len(part.contentBoundary) < 5 {
+		err = errors.New("content boundary too short")
+	}
+	p.boundaryMatched = 0
+	for {
+		if i := bytes.Index(p.buf, []byte(part.contentBoundary)); i > -1 {
+			// advance the pointer to 1 char before the end of the boundary
+			// then let next() to advance the last char.
+			// in case the boundary is the tail part of buffer, calling next()
+			// will wait until we get a new buffer
+			p.pos = i + len(part.contentBoundary) - 1
+			p.next()
+			return nil
+
+		} else {
+			// search the tail for partial match
+			// if one is found, load more data and continue the match
+			// if matched, advance buffer in same way as above
+			start := len(p.buf) - len(part.contentBoundary) + 1
+			if start < 0 {
+				start = 0
+			}
+			subject := p.buf[start:]
+
+			for i := 0; i < len(subject); i++ {
+				if subject[i] == part.contentBoundary[p.boundaryMatched] {
+					p.boundaryMatched++
+				} else {
+					p.boundaryMatched = 0
+				}
+			}
+			p.pos = len(p.buf) - 1
+			p.next()
+			if p.ch == 0 {
+				return io.EOF
+			} else if p.boundaryMatched > 0 {
+				if bytes.Compare(
+					p.buf[0:len(part.contentBoundary)-p.boundaryMatched],
+					[]byte(part.contentBoundary[p.boundaryMatched:])) == 0 {
+					// todo: move the pointer
+					return nil
+				}
+				p.boundaryMatched = 0
+			}
+			_ = subject
+		}
+	}
+}
+
+type parserMsg struct {
+	err error
+}
+
+func (p *parser) engine() {
+	var err error
+
+	p.next() // load in some bytes
+	for {
+		err = p.message()
+		p.result <- parserMsg{err}
+		//p.next()
+		p.next()
+		if p.isHalting {
+			return
+		}
+	}
+
+}
+
+func (p *parser) message() error {
+	var err error
+
+	if p.isWSP(p.ch) {
+		err = errors.New("headers cannot start with w-space")
+		return err
+	}
+	mh := NewMimeHeader()
+	if err = p.header(mh); err != nil {
+		return err
+	}
+
+	if p.ch == '\n' && p.next() == '\n' {
+		err = p.body(mh)
+	} else {
+		err = errors.New("body not found")
+	}
+	return err
+}
+
+func (p *parser) header(mh *mimeHeader) (err error) {
+	var state int
+	var name string
+
+	defer func() {
+		fmt.Println(mh.headers)
+		p.accept.Reset()
+
+	}()
+	mh.startingPos = p.msgPos
+	for {
+
+		switch state {
+		case 0:
+			if (p.ch >= 33 && p.ch <= 126) && p.ch != ':' {
+				// capture
+				p.accept.WriteByte(p.ch)
+			} else if p.ch == ':' {
+				state = 1
+			} else if p.ch == ' ' && p.peek() == ':' { // tolerate a SP before the :
+				p.next()
+				state = 1
+			} else {
+				pc := p.peek()
+				err = errors.New("unexpected char:" + string(p.ch) + string(pc))
+				return
+			}
+			if state == 1 {
+				if p.accept.Len() < 2 {
+					err = errors.New("header field too short")
+					return
+				}
+				name = p.accept.String()
+				p.accept.Reset()
+				if c := p.peek(); c == ' ' {
+					// skip the space
+					p.next()
+				}
+				p.next()
+				continue
+			}
+
+		case 1:
+
+			if name == "Content-Type" {
+				var err error
+				contentType, err := p.contentType()
+				if err != nil {
+					return err
+				}
+				fmt.Println(contentType, err)
+				state = 0
+			} else {
+				if (p.ch >= 33 && p.ch <= 126) || p.isWSP(p.ch) {
+					p.accept.WriteByte(p.ch)
+				} else if p.ch == '\n' {
+					c := p.peek()
+
+					if p.isWSP(c) {
+						break // skip \n
+					} else {
+						mh.headers.Add(name, p.accept.String())
+						p.accept.Reset()
+
+						state = 0
+					}
+				} else {
+					err = errors.New("parse error")
+					return
+				}
+			}
+
+		}
+		if p.ch == '\n' && p.peek() == '\n' {
+			return nil
+		}
+		p.next()
+
+		if p.ch == 0 {
+			return io.EOF
+		}
+
+	}
+
+}
+
+func (p *parser) isWSP(b byte) bool {
+	return b == ' ' || b == '\t'
+}
+
+// type "/" subtype
+// *(";" parameter)
+
+type contentType struct {
+	superType  string
+	subType    string
+	parameters map[string]string
+}
+
+// content disposition
+// The Content-Disposition Header Field (rfc2183)
+// https://stackoverflow.com/questions/48347574/do-rfc-standards-require-the-filename-value-for-mime-attachment-to-be-encapsulat
+func (p *parser) contentDisposition() (result contentType, err error) {
+	result = contentType{}
+	return
+}
+
+func (p *parser) contentType() (result contentType, err error) {
+	result = contentType{}
+
+	if result.superType, err = p.mimeType(); err != nil {
+		return
+	}
+	if p.ch != '/' {
+		return result, errors.New("missing subtype")
+	}
+	p.next()
+
+	if result.subType, err = p.mimeSubType(); err != nil {
+		return
+	}
+	if p.ch == ';' {
+		p.next()
+		for {
+			if p.ch == '\n' {
+				c := p.peek()
+				if p.isWSP(c) {
+					p.next() // skip \n (FWS)
+					continue
+				}
+				if c == '\n' { // end of header
+					return
+				}
+			}
+			if p.isWSP(p.ch) { // skip WSP
+				p.next()
+				continue
+			}
+			if p.ch == '(' {
+				if err = p.comment(); err != nil {
+					return
+				}
+				continue
+			}
+
+			if key, val, err := p.parameter(); err != nil {
+				return result, err
+			} else {
+				if result.parameters == nil {
+					result.parameters = make(map[string]string, 1)
+				}
+				result.parameters[key] = val
+			}
+		}
+	}
+
+	return
+}
+
+var isTokenSpecial = [128]bool{
+	'(':  true,
+	')':  true,
+	'<':  true,
+	'>':  true,
+	'@':  true,
+	',':  true,
+	';':  true,
+	':':  true,
+	'\\': true,
+	'"':  true,
+	'/':  true,
+	'[':  true,
+	']':  true,
+	'?':  true,
+	'=':  true,
+}
+var isTokenAlphaDash = [128]bool{
+
+	'-': true,
+	'A': true,
+	'B': true,
+	'C': true,
+	'D': true,
+	'E': true,
+	'F': true,
+	'G': true,
+	'H': true,
+	'I': true,
+	'J': true,
+	'K': true,
+	'L': true,
+	'M': true,
+	'N': true,
+	'O': true,
+	'P': true,
+	'Q': true,
+	'R': true,
+	'S': true,
+	'T': true,
+	'U': true,
+	'W': true,
+	'V': true,
+	'X': true,
+	'Y': true,
+	'Z': true,
+	'a': true,
+	'b': true,
+	'c': true,
+	'd': true,
+	'e': true,
+	'f': true,
+	'g': true,
+	'h': true,
+	'i': true,
+	'j': true,
+	'k': true,
+	'l': true,
+	'm': true,
+	'n': true,
+	'o': true,
+	'p': true,
+	'q': true,
+	'r': true,
+	's': true,
+	't': true,
+	'u': true,
+	'v': true,
+	'w': true,
+	'x': true,
+	'y': true,
+	'z': true,
+}
+
+func (p *parser) mimeType() (str string, err error) {
+
+	defer func() {
+		if p.accept.Len() > 0 {
+			str = p.accept.String()
+			p.accept.Reset()
+		}
+	}()
+	if p.ch < 128 && isTokenAlphaDash[p.ch] {
+		for {
+			p.accept.WriteByte(p.ch)
+			p.next()
+			if !(p.ch < 128 && isTokenAlphaDash[p.ch]) {
+				return
+			}
+
+		}
+	} else {
+		err = errors.New("unexpected tok")
+		return
+	}
+}
+
+func (p *parser) mimeSubType() (str string, err error) {
+	return p.mimeType()
+}
+
+// comment     =  "(" *(ctext / quoted-pair / comment) ")"
+//
+// ctext       =  <any CHAR excluding "(",     ; => may be folded
+//                     ")", "\" & CR, & including
+//                     linear-white-space>
+//
+// quoted-pair =  "\" CHAR                     ; may quote any char
+func (p *parser) comment() (err error) {
+	// all header fields except for Content-Disposition
+	// can include RFC 822 comments
+	if p.ch != '(' {
+		err = errors.New("unexpected token")
+	}
+
+	for {
+		p.next()
+		if p.ch == ')' {
+			p.next()
+			return
+		}
+	}
+
+}
+
+func (p *parser) token() (str string, err error) {
+	defer func() {
+		if err == nil {
+			str = p.accept.String()
+		}
+		if p.accept.Len() > 0 {
+			p.accept.Reset()
+		}
+	}()
+	var once bool // must match at least 1 good char
+	for {
+		if p.ch > 32 && p.ch < 128 && !isTokenSpecial[p.ch] {
+			p.accept.WriteByte(p.ch)
+			once = true
+		} else if !once {
+			err = errors.New("invalid token")
+			return
+		} else {
+			return
+		}
+		p.next()
+	}
+}
+
+// quoted-string  = ( <"> *(qdtext | quoted-pair ) <"> )
+// quoted-pair    = "\" CHAR
+// CHAR           = <any US-ASCII character (octets 0 - 127)>
+// qdtext         = <any TEXT except <">>
+// TEXT           = <any OCTET except CTLs, but including LWS>
+func (p *parser) quotedString() (str string, err error) {
+	defer func() {
+		if err == nil {
+			str = p.accept.String()
+		}
+		if p.accept.Len() > 0 {
+			p.accept.Reset()
+		}
+	}()
+
+	if p.ch != '"' {
+		err = errors.New("unexpected token")
+		return
+	}
+	p.next()
+	state := 0
+	for {
+		switch state {
+		case 0: // inside quotes
+
+			if p.ch == '"' {
+				p.next()
+				return
+			}
+			if p.ch == '\\' {
+				state = 1
+				break
+			}
+			if (p.ch < 127 && p.ch > 32) || p.isWSP(p.ch) {
+				p.accept.WriteByte(p.ch)
+			} else {
+				err = errors.New("unexpected token")
+				return
+			}
+		case 1:
+			// escaped (<any US-ASCII character (octets 0 - 127)>)
+			if p.ch != 0 && p.ch <= 127 {
+				p.accept.WriteByte(p.ch)
+				state = 0
+			} else {
+				err = errors.New("unexpected token")
+				return
+			}
+		}
+		p.next()
+	}
+}
+
+// parameter := attribute "=" value
+// attribute := token
+// token := 1*<any (US-ASCII) CHAR except SPACE, CTLs, or tspecials>
+// value := token / quoted-string
+// CTL := %x00-1F / %x7F
+// quoted-string : <"> <">
+func (p *parser) parameter() (attribute, value string, err error) {
+	defer func() {
+		p.accept.Reset()
+	}()
+
+	if attribute, err = p.token(); err != nil {
+		return "", "", err
+	}
+	if p.ch != '=' {
+		return "", "", errors.New("expecting =")
+	}
+	p.next()
+	if p.ch == '"' {
+		if value, err = p.quotedString(); err != nil {
+			return
+		}
+		// OK, return  the attribute and value
+		return
+	} else {
+		if value, err = p.token(); err != nil {
+			// err
+			return
+		}
+		// OK, return the attribute and value
+		return
+	}
+}
+
+func (p *parser) body(mh *mimeHeader) (err error) {
+	var body bytes.Buffer
+	//var ct contentType
+	//if content, ok := p.headers["Content-Type"]; ok {
+	//	_ = content
+	//}
+
+	if mh.contentBoundary != "" {
+		if err = p.boundary(mh); err != nil {
+			return err
+		}
+		mh.endingPosBody = p.msgPos
+		return
+	} else {
+		for {
+
+			p.next()
+			if p.ch == 0 {
+				return io.EOF
+			}
+			if p.ch == '\n' && p.peek() == '\n' {
+				p.next()
+				mh.endingPosBody = p.msgPos
+				return
+			}
+
+			body.WriteByte(p.ch)
+
+		}
+	}
+
+}
+
+func (p *parser) mimeMsg(parent *mimeHeader, depth string) (err error) {
+	count := 0
+	for {
+		count++
+		depth = depth + "." + strconv.Itoa(count)
+		{
+			h := NewMimeHeader()
+			err = p.header(h)
+			if err != nil {
+				return err
+			}
+			p.addPart(h, depth)
+			if h.contentBoundary != "" && parent.contentBoundary != h.contentBoundary {
+				if err = p.boundary(h); err != nil {
+					return err
+				}
+				return p.mimeMsg(h, depth)
+			} else {
+				if err = p.body(parent); err != nil {
+					return err
+				}
+
+			}
+		}
+
+	}
+}
+
+func (p *parser) close() error {
+
+	p.msgPos = 0
+	p.msgLine = 0
+	p.gotNewSlice <- false // signal to engine() that there's no more data
+
+	r := <-p.result
+	return r.err
+	//return nil
+}
+
+func (p *parser) parse(buf []byte) error {
+
+	if !p.once {
+		<-p.consumed
+		p.once = true
+	}
+
+	p.set(buf)
+	p.gotNewSlice <- true // unblock
+
+	// make sure that engine() is blocked or stopped before we return
+	select {
+	case <-p.consumed: // wait for it to block on p.gotNewSlice
+		return nil
+	case r := <-p.result:
+
+		return r.err
+	}
+
+}
+
+func newMimeParser() *parser {
+	p := new(parser)
+	p.consumed = make(chan bool)
+	p.gotNewSlice = make(chan bool)
+	p.halt = make(chan bool)
+	p.result = make(chan parserMsg, 1)
+
+	return p
+}
+
+func (p *parser) start() {
+	go p.engine()
+}
+
+func StreamMimeAnalyzer() *StreamDecorator {
+
+	sd := &StreamDecorator{}
+	sd.p =
+
+		func(sp StreamProcessor) StreamProcessor {
+
+			var (
+				envelope *mail.Envelope
+				parseErr error
+				parser   *parser
+			)
+			Svc.AddInitializer(InitializeWith(func(backendConfig BackendConfig) error {
+				parser = newMimeParser()
+				parser.start()
+				return nil
+			}))
+
+			Svc.AddShutdowner(ShutdownWith(func() error {
+				//<-parser.end
+				//parser.halt <- true
+				return nil
+			}))
+
+			sd.Open = func(e *mail.Envelope) error {
+				envelope = e
+				return nil
+			}
+
+			sd.Close = func() error {
+				if parseErr != nil {
+					return nil
+				}
+				err := parser.close()
+				if err != nil {
+					fmt.Println("parse err", err)
+				}
+				return nil
+			}
+
+			return StreamProcessWith(func(p []byte) (int, error) {
+				_ = envelope
+				if len(envelope.Header) > 0 {
+
+				}
+				if parseErr == nil {
+					parseErr = parser.parse(p)
+					if parseErr != nil {
+						fmt.Println("parse err", parseErr)
+					}
+				}
+
+				return sp.Write(p)
+			})
+		}
+
+	return sd
+}

+ 227 - 0
backends/s_mime_test.go

@@ -0,0 +1,227 @@
+package backends
+
+import (
+	"bytes"
+	"testing"
+)
+
+var p *parser
+
+func init() {
+	p = newMimeParser()
+}
+func TestInject(t *testing.T) {
+	var b bytes.Buffer
+
+	// it should read from both slices
+	// as if it's a continuous stream
+	p.inject([]byte("abcd"), []byte("efgh"), []byte("ijkl"))
+	for i := 0; i < 12; i++ {
+		b.WriteByte(p.ch)
+		p.next()
+		if p.ch == 0 {
+			break
+		}
+	}
+	if b.String() != "abcdefghijkl" {
+		t.Error("expecting abcdefghijkl, got:", b.String())
+	}
+}
+func TestMimeType(t *testing.T) {
+
+	if isTokenAlphaDash[byte('9')] {
+		t.Error("9 should not be in the set")
+	}
+
+	if isTokenSpecial['-'] {
+		t.Error("- should not be in the set")
+	}
+
+	p.inject([]byte("text/plain; charset=us-ascii"))
+	str, err := p.mimeType()
+	if err != nil {
+		t.Error(err)
+	}
+	if str != "text" {
+		t.Error("mime type should be: text")
+	}
+
+}
+
+func TestMimeContentType(t *testing.T) {
+	go func() {
+		<-p.consumed
+		p.gotNewSlice <- false
+	}()
+	p.inject([]byte("text/plain; charset=us-ascii"))
+	contentType, err := p.contentType()
+	if err != nil {
+		t.Error(err)
+	}
+	if contentType.subType != "plain" {
+		t.Error("contentType.subType expecting 'plain', got:", contentType.subType)
+	}
+
+	if contentType.superType != "text" {
+		t.Error("contentType.subType expecting 'text', got:", contentType.superType)
+	}
+}
+
+func TestEmailHeader(t *testing.T) {
+	in := `From: Al Gore <[email protected]>
+To: White House Transportation Coordinator <[email protected]>
+Subject: [Fwd: Map of Argentina with Description]
+MIME-Version: 1.0
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed; s=ncr424; d=reliancegeneral.co.in;
+ h=List-Unsubscribe:MIME-Version:From:To:Reply-To:Date:Subject:Content-Type:Content-Transfer-Encoding:Message-ID; [email protected];
+ bh=F4UQPGEkpmh54C7v3DL8mm2db1QhZU4gRHR1jDqffG8=;
+ b=MVltcq6/I9b218a370fuNFLNinR9zQcdBSmzttFkZ7TvV2mOsGrzrwORT8PKYq4KNJNOLBahswXf
+   GwaMjDKT/5TXzegdX/L3f/X4bMAEO1einn+nUkVGLK4zVQus+KGqm4oP7uVXjqp70PWXScyWWkbT
+   1PGUwRfPd/HTJG5IUqs=
+Content-Type: multipart/mixed;
+ boundary="D7F------------D7FD5A0B8AB9C65CCDBFA872"
+
+This is a multi-part message in MIME format.
+--D7F------------D7FD5A0B8AB9C65CCDBFA872
+Content-Type: text/plain; charset=us-ascii
+Content-Transfer-Encoding: 7bit
+
+Fred,
+
+Fire up Air Force One!  We\'re going South!
+
+Thanks,
+Al
+--D7F------------D7FD5A0B8AB9C65CCDBFA872
+This
+`
+	p.inject([]byte(in))
+	h := NewMimeHeader()
+	err := p.header(h)
+	if err != nil {
+		t.Error(err)
+	}
+	if err := p.boundary(h); err != nil {
+		t.Error(err)
+	} else {
+		//_ = part
+		//p.addPart(part)
+
+		//nextPart := NewMimeHeader()
+		//err = p.body(part)
+		//if err != nil {
+		//	t.Error(err)
+		//}
+	}
+}
+
+func msg() (err error) {
+	main := NewMimeHeader()
+	err = p.header(main)
+	if err != nil {
+		return err
+	}
+	p.addPart(main, "1")
+	if main.contentBoundary != "" {
+		// it's a message with mime parts
+		if err = p.boundary(main); err != nil {
+			return err
+		}
+		if err = p.mimeMsg(main, "1"); err != nil {
+			return err
+		}
+	} else {
+		// only contains one part (the body)
+		if err := p.body(main); err != nil {
+			return err
+		}
+	}
+	p.endBody(main)
+
+	return
+}
+
+func TestBoundary(t *testing.T) {
+	var err error
+	part := NewMimeHeader()
+	part.contentBoundary = "-wololo-"
+
+	// in the middle of the string
+	p.inject([]byte("The quick brown fo-wololo-x jumped over the lazy dog"))
+
+	err = p.boundary(part)
+	if err != nil {
+		t.Error(err)
+	}
+
+	p.inject([]byte("The quick brown fox jumped over the lazy dog-wololo-"))
+	err = p.boundary(part)
+	if err != nil {
+		t.Error(err)
+	}
+
+	// the boundary with an additional buffer in between
+	p.inject([]byte("The quick brown fox jumped over the lazy dog"),
+		[]byte("this is the middle"),
+		[]byte("and thats the end-wololo-"))
+
+	err = p.boundary(part)
+	if err != nil {
+		t.Error(err)
+	}
+
+	// boundary is split over multiple slices
+	p.inject(
+		[]byte("The quick brown fox jumped ov-wolo"),
+		[]byte("lo-er the lazy dog"))
+	err = p.boundary(part)
+	if err != nil {
+		t.Error(err)
+	}
+
+}
+
+func TestMimeContentQuotedParams(t *testing.T) {
+
+	// quoted
+	p.inject([]byte("text/plain; charset=\"us-ascii\""))
+	contentType, err := p.contentType()
+	if err != nil {
+		t.Error(err)
+	}
+
+	// with whitespace & tab
+	p.inject([]byte("text/plain; charset=\"us-ascii\"  \tboundary=\"D7F------------D7FD5A0B8AB9C65CCDBFA872\""))
+	contentType, err = p.contentType()
+	if err != nil {
+		t.Error(err)
+	}
+
+	// with comment (ignored)
+	p.inject([]byte("text/plain; charset=\"us-ascii\" (a comment) \tboundary=\"D7F------------D7FD5A0B8AB9C65CCDBFA872\""))
+	contentType, err = p.contentType()
+
+	if contentType.subType != "plain" {
+		t.Error("contentType.subType expecting 'plain', got:", contentType.subType)
+	}
+
+	if contentType.superType != "text" {
+		t.Error("contentType.subType expecting 'text', got:", contentType.superType)
+	}
+
+	if len(contentType.parameters) != 2 {
+		t.Error("expecting 2 elements in parameters")
+	} else {
+		if _, ok := contentType.parameters["charset"]; !ok {
+			t.Error("charset parameter not present")
+		}
+		if b, ok := contentType.parameters["boundary"]; !ok {
+			t.Error("charset parameter not present")
+		} else {
+			if b != "D7F------------D7FD5A0B8AB9C65CCDBFA872" {
+				t.Error("boundary should be: D7F------------D7FD5A0B8AB9C65CCDBFA872")
+			}
+		}
+	}
+
+}