瀏覽代碼

- add transport type to email (8bitmime or 7bit)
- add 8BITMIME support
- base64 and q-printable decoding and tests

flashmob 6 年之前
父節點
當前提交
3528fbc964
共有 16 個文件被更改,包括 336 次插入122 次删除
  1. 2 1
      Makefile
  2. 1 1
      api_test.go
  3. 125 33
      chunk/chunk_test.go
  4. 53 34
      chunk/decoder.go
  5. 5 1
      chunk/processor.go
  6. 32 17
      chunk/reader.go
  7. 8 6
      chunk/store.go
  8. 5 1
      chunk/store_memory.go
  9. 5 4
      chunk/store_sql.go
  10. 5 5
      client.go
  11. 4 1
      mail/envelope.go
  12. 1 1
      mail/mime/mime.go
  13. 33 4
      mail/smtp/parse.go
  14. 28 1
      mail/smtp/parse_test.go
  15. 26 9
      server.go
  16. 3 3
      tests/guerrilla_test.go

+ 2 - 1
Makefile

@@ -38,7 +38,8 @@ test: *.go */*.go */*/*.go
 	$(GO_VARS) $(GO) test -v ./mail/mime
 	$(GO_VARS) $(GO) test -v ./mail/encoding
 	$(GO_VARS) $(GO) test -v ./mail/iconv
-	$(GO_VARS) $(GO) test -v ./mail/rfc5321
+	$(GO_VARS) $(GO) test -v ./mail/smtp
+	$(GO_VARS) $(GO) test -v ./chunk
 
 testrace: *.go */*.go */*/*.go
 	$(GO_VARS) $(GO) test -v . -race

+ 1 - 1
api_test.go

@@ -447,7 +447,7 @@ func talkToServer(address string, body string) (err error) {
 	if err != nil {
 		return err
 	}
-	_, err = fmt.Fprint(conn, "MAIL FROM:<[email protected]>\r\n")
+	_, err = fmt.Fprint(conn, "MAIL FROM:<[email protected]> BODY=8BITMIME\r\n")
 	if err != nil {
 		return err
 	}

+ 125 - 33
chunk/chunk_test.go

@@ -340,39 +340,97 @@ func TestHashBytes(t *testing.T) {
 		t.Error("expecting 3hcDgAEXA4ABFwP/ARcDgA got", h.String())
 	}
 }
-func TestChunkSaverWrite(t *testing.T) {
 
-	// place the parse result in an envelope
-	e := mail.NewEnvelope("127.0.0.1", 1)
-	to, _ := mail.NewAddress("[email protected]")
-	e.RcptTo = append(e.RcptTo, to)
-	e.MailFrom, _ = mail.NewAddress("[email protected]")
+func TestChunkSaverReader(t *testing.T) {
+	store, chunksaver, mimeanalyzer, stream := initTestStream()
+	buf := make([]byte, 64)
+	var result bytes.Buffer
+	if _, err := io.CopyBuffer(stream, bytes.NewBuffer([]byte(email3)), buf); err != nil {
+		t.Error(err)
+	} else {
+		_ = mimeanalyzer.Close()
+		_ = chunksaver.Close()
 
-	store := new(StoreMemory)
-	chunkBuffer := NewChunkedBytesBufferMime()
-	//chunkBuffer.setDatabase(store)
-	// instantiate the chunk saver
-	chunksaver := backends.Streamers["chunksaver"]()
-	mimeanalyzer := backends.Streamers["mimeanalyzer"]()
+		email, err := store.GetEmail(1)
+		if err != nil {
+			t.Error("email not found")
+			return
+		}
 
-	// add the default processor as the underlying processor for chunksaver
-	// and chain it with mimeanalyzer.
-	// Call order: mimeanalyzer -> chunksaver -> default (terminator)
-	// This will also set our Open, Close and Initialize functions
-	// we also inject a Storage and a ChunkingBufferMime
+		// this should read all parts
+		r, err := NewChunkedReader(store, email, 0)
+		buf2 := make([]byte, 64)
+		if w, err := io.CopyBuffer(&result, r, buf2); err != nil {
+			t.Error(err)
+		} else if w != email.size {
+			t.Error("email.size != number of bytes copied from reader", w, email.size)
+		}
 
-	stream := mimeanalyzer.Decorate(chunksaver.Decorate(backends.DefaultStreamProcessor{}, store, chunkBuffer))
+		if !strings.Contains(result.String(), "k+DQo8L2h0bWw+DQo") {
+			t.Error("Looks like it didn;t read the entire email, was expecting k+DQo8L2h0bWw+DQo")
+		}
+		result.Reset()
 
-	// configure the buffer cap
-	bc := backends.BackendConfig{}
-	bc["chunksaver_chunk_size"] = 8000
-	bc["chunksaver_storage_engine"] = "memory"
-	bc["chunksaver_compress_level"] = 0
-	_ = backends.Svc.Initialize(bc)
+		// Test the decoder, hit the decoderStateMatchNL state
+		r, err = NewChunkedReader(store, email, 0)
+		if err != nil {
+			t.Error(err)
+		}
+		part := email.partsInfo.Parts[0]
+		encoding := transportQuotedPrintable
+		if strings.Contains(part.TransferEncoding, "base") {
+			encoding = transportBase64
+		}
+		dr, err := NewDecoder(r, encoding, part.Charset)
+		_ = dr
+		if err != nil {
+			t.Error(err)
+			t.FailNow()
+		}
 
-	// give it the envelope with the parse results
-	_ = chunksaver.Open(e)
-	_ = mimeanalyzer.Open(e)
+		buf3 := make([]byte, 1253) // 1253 intentionally causes the decoderStateMatchNL state to hit
+		_, err = io.CopyBuffer(&result, dr, buf3)
+		if err != nil {
+			t.Error()
+		}
+		if !strings.Contains(result.String(), "</html") {
+			t.Error("looks like it didn't decode, expecting </html>")
+		}
+		result.Reset()
+
+		// test the decoder, hit the decoderStateFindHeader state
+		r, err = NewChunkedReader(store, email, 0)
+		if err != nil {
+			t.Error(err)
+		}
+		part = email.partsInfo.Parts[0]
+		encoding = transportQuotedPrintable
+		if strings.Contains(part.TransferEncoding, "base") {
+			encoding = transportBase64
+		}
+		dr, err = NewDecoder(r, encoding, part.Charset)
+		_ = dr
+		if err != nil {
+			t.Error(err)
+			t.FailNow()
+		}
+
+		buf4 := make([]byte, 64) // state decoderStateFindHeader will hit
+		_, err = io.CopyBuffer(&result, dr, buf4)
+		if err != nil {
+			t.Error()
+		}
+		if !strings.Contains(result.String(), "</html") {
+			t.Error("looks like it didn't decode, expecting </html>")
+		}
+
+	}
+
+}
+
+func TestChunkSaverWrite(t *testing.T) {
+
+	store, chunksaver, mimeanalyzer, stream := initTestStream()
 
 	buf := make([]byte, 128)
 	if written, err := io.CopyBuffer(stream, bytes.NewBuffer([]byte(email3)), buf); err != nil {
@@ -398,7 +456,7 @@ func TestChunkSaverWrite(t *testing.T) {
 		if w, err := io.Copy(os.Stdout, r); err != nil {
 			t.Error(err)
 		} else if w != email.size {
-			t.Error("email.size != number of bytes copied from reader")
+			t.Error("email.size != number of bytes copied from reader", w, email.size)
 		}
 
 		// test the seek feature
@@ -427,10 +485,10 @@ func TestChunkSaverWrite(t *testing.T) {
 		if err != nil {
 			t.Error(err)
 		}
-		part := email.partsInfo.Parts[4]
-		encoding := encodingTypeQP
+		part := email.partsInfo.Parts[0]
+		encoding := transportQuotedPrintable
 		if strings.Contains(part.TransferEncoding, "base") {
-			encoding = encodingTypeBase64
+			encoding = transportBase64
 		}
 		dr, err := NewDecoder(r, encoding, part.Charset)
 		_ = dr
@@ -438,8 +496,42 @@ func TestChunkSaverWrite(t *testing.T) {
 			t.Error(err)
 			t.FailNow()
 		}
-		var decoded bytes.Buffer
-		io.Copy(&decoded, dr)
+		//var decoded bytes.Buffer
+		//io.Copy(&decoded, dr)
+		io.Copy(os.Stdout, dr)
 
 	}
 }
+
+func initTestStream() (*StoreMemory, *backends.StreamDecorator, *backends.StreamDecorator, backends.StreamProcessor) {
+	// place the parse result in an envelope
+	e := mail.NewEnvelope("127.0.0.1", 1)
+	to, _ := mail.NewAddress("[email protected]")
+	e.RcptTo = append(e.RcptTo, to)
+	e.MailFrom, _ = mail.NewAddress("[email protected]")
+	store := new(StoreMemory)
+	chunkBuffer := NewChunkedBytesBufferMime()
+	//chunkBuffer.setDatabase(store)
+	// instantiate the chunk saver
+	chunksaver := backends.Streamers["chunksaver"]()
+	mimeanalyzer := backends.Streamers["mimeanalyzer"]()
+	// add the default processor as the underlying processor for chunksaver
+	// and chain it with mimeanalyzer.
+	// Call order: mimeanalyzer -> chunksaver -> default (terminator)
+	// This will also set our Open, Close and Initialize functions
+	// we also inject a Storage and a ChunkingBufferMime
+	stream := mimeanalyzer.Decorate(chunksaver.Decorate(backends.DefaultStreamProcessor{}, store, chunkBuffer))
+
+	// configure the buffer cap
+	bc := backends.BackendConfig{}
+	bc["chunksaver_chunk_size"] = 8000
+	bc["chunksaver_storage_engine"] = "memory"
+	bc["chunksaver_compress_level"] = 0
+	_ = backends.Svc.Initialize(bc)
+
+	// give it the envelope with the parse results
+	_ = chunksaver.Open(e)
+	_ = mimeanalyzer.Open(e)
+
+	return store, chunksaver, mimeanalyzer, stream
+}

+ 53 - 34
chunk/decoder.go

@@ -2,38 +2,39 @@ package chunk
 
 import (
 	"bytes"
+	"encoding/base64"
+	"github.com/flashmob/go-guerrilla/mail"
 	"io"
+	"mime/quotedprintable"
+	"strings"
+
+	_ "github.com/flashmob/go-guerrilla/mail/encoding"
 )
 
 type transportEncoding int
 
 const (
-	encodingTypeBase64 transportEncoding = iota
-	encodingTypeQP
+	transportBase64 transportEncoding = iota
+	transportQuotedPrintable
+	transport7bit // default, 1-127, 13 & 10 at line endings
+	transport8bit // 998 octets per line,  13 & 10 at line endings
+
 )
 
 // decoder decodes base64 and q-printable, then converting charset to UTF-8
 type decoder struct {
-	buf     []byte
-	state   int
-	charset string
-
-	r io.Reader
+	buf       []byte
+	state     int
+	charset   string
+	transport transportEncoding
+	r         io.Reader
 }
 
-// db Storage, email *Email, part int)
-/*
-
-r, err := NewChunkedReader(db, email, part)
-	if err != nil {
-		return nil, err
-	}
-
-*/
-
 // NewDecoder reads from an underlying reader r and decodes base64, quoted-printable and decodes
-func NewDecoder(r io.Reader, encoding transportEncoding, charset string) (*decoder, error) {
+func NewDecoder(r io.Reader, transport transportEncoding, charset string) (*decoder, error) {
 	decoder := new(decoder)
+	decoder.transport = transport
+	decoder.charset = strings.ToLower(charset)
 	decoder.r = r
 	return decoder, nil
 }
@@ -43,53 +44,71 @@ const chunkSaverNL = '\n'
 const (
 	decoderStateFindHeader int = iota
 	decoderStateMatchNL
+	decoderStateDecodeSetup
 	decoderStateDecode
 )
 
 func (r *decoder) Read(p []byte) (n int, err error) {
-
 	r.buf = make([]byte, len(p), cap(p))
 	var start, buffered int
-
-	buffered, err = r.r.Read(r.buf)
-	if buffered == 0 {
-		return
+	if r.state != decoderStateDecode {
+		buffered, err = r.r.Read(r.buf)
+		if buffered == 0 {
+			return
+		}
 	}
 	for {
 		switch r.state {
 		case decoderStateFindHeader:
 			// finding the start of the header
 			if start = bytes.Index(r.buf, []byte{chunkSaverNL, chunkSaverNL}); start != -1 {
-				start += 2                   // skip the \n\n
-				r.state = decoderStateDecode // found the header
-				continue                     // continue scanning
+				start += 2                        // skip the \n\n
+				r.state = decoderStateDecodeSetup // found the header
+				continue
 			} else if r.buf[len(r.buf)-1] == chunkSaverNL {
 				// the last char is a \n so next call to Read will check if it starts with a matching \n
 				r.state = decoderStateMatchNL
 			}
 		case decoderStateMatchNL:
+			// check the first char if it is a '\n' because last time we matched a '\n'
 			if r.buf[0] == '\n' {
 				// found the header
 				start = 1
-				r.state = decoderStateDecode
+				r.state = decoderStateDecodeSetup
 				continue
 			} else {
 				r.state = decoderStateFindHeader
 				continue
 			}
+		case decoderStateDecodeSetup:
+			if start != buffered {
+				// include any bytes that have already been read
+				r.r = io.MultiReader(bytes.NewBuffer(r.buf[start:buffered]), r.r)
+			}
+			switch r.transport {
+			case transportQuotedPrintable:
+				r.r = quotedprintable.NewReader(r.r)
+			case transportBase64:
+				r.r = base64.NewDecoder(base64.StdEncoding, r.r)
+			default:
 
-		case decoderStateDecode:
-			if start < len(r.buf) {
-				// todo decode here (q-printable, base64, charset)
-				n += copy(p[:], r.buf[start:buffered])
 			}
-			return
+			// conversion to utf-8
+			if r.charset != "utf-8" {
+				r.r, err = mail.Dec.CharsetReader(r.charset, r.r)
+				if err != nil {
+					return
+				}
+			}
+			r.state = decoderStateDecode
+			continue
+		case decoderStateDecode:
+			return r.r.Read(p)
 		}
-
+		start = 0
 		buffered, err = r.r.Read(r.buf)
 		if buffered == 0 {
 			return
 		}
 	}
-
 }

+ 5 - 1
chunk/processor.go

@@ -143,7 +143,9 @@ func Chunksaver() *backends.StreamDecorator {
 					e.RcptTo[0].String(),
 					ip,
 					e.MailFrom.String(),
-					e.TLS)
+					e.TLS,
+					e.TransportType,
+				)
 				if err != nil {
 					return err
 				}
@@ -227,6 +229,7 @@ func Chunksaver() *backends.StreamDecorator {
 								return count, err
 							}
 							chunkBuffer.CurrentPart(part)
+							// end of a part here
 							fmt.Println("->N")
 							pos += count
 							msgPos = part.StartingPos
@@ -241,6 +244,7 @@ func Chunksaver() *backends.StreamDecorator {
 								return count, err
 							}
 							chunkBuffer.CurrentPart(part)
+							// end of a header here
 							fmt.Println("->H")
 							pos += count
 							msgPos = part.StartingPosBody

+ 32 - 17
chunk/reader.go

@@ -21,7 +21,6 @@ type chunkedReader struct {
 func NewChunkedReader(db Storage, email *Email, part int) (*chunkedReader, error) {
 	r := new(chunkedReader)
 	r.db = db
-	r.part = part
 	if email == nil {
 		return nil, errors.New("nil email")
 	} else {
@@ -37,6 +36,7 @@ func NewChunkedReader(db Storage, email *Email, part int) (*chunkedReader, error
 }
 
 // SeekPart resets the reader. The part argument chooses which part Read will read in
+// If part is 1, it will return the first part
 // If part is 0, Read will return the entire message
 func (r *chunkedReader) SeekPart(part int) error {
 	if parts := len(r.email.partsInfo.Parts); parts == 0 {
@@ -44,7 +44,10 @@ func (r *chunkedReader) SeekPart(part int) error {
 	} else if part > parts {
 		return errors.New("no such part available")
 	}
-	r.i = part
+	r.part = part
+	if part > 0 {
+		r.i = part - 1
+	}
 	r.j = 0
 	return nil
 }
@@ -122,13 +125,16 @@ func (c *cachedChunks) get(i int) (*Chunk, error) {
 				c.hashIndex[j] = toGet[j-i]
 			}
 			// remove any old ones (walk back)
-			for j := i; j > -1; j-- {
-				if c.chunks[j] != nil {
-					c.chunks[j] = nil
-				} else {
-					break
+			if i-1 > -1 {
+				for j := i - 1; j > -1; j-- {
+					if c.chunks[j] != nil {
+						c.chunks[j] = nil
+					} else {
+						break
+					}
 				}
 			}
+
 			// return the chunk asked for
 			return chunks[0], nil
 		}
@@ -160,19 +166,28 @@ func (r *chunkedReader) Read(p []byte) (n int, err error) {
 				return nRead, err
 			}
 			nRead, err = chunk.data.Read(p)
-			if err == io.EOF {
-				r.j++ // advance to the next chunk
+			if err == io.EOF { // we've read the entire chunk
+
+				if closer, ok := chunk.data.(io.ReadCloser); ok {
+					err = closer.Close()
+					if err != nil {
+						return nRead, err
+					}
+				}
+				r.j++ // advance to the next chunk the part
 				err = nil
-			}
-			if r.j == length { // last chunk in a part?
-				r.j = 0 // reset chunk index
-				r.i++   // advance to the next part
-				if r.i == len(r.email.partsInfo.Parts) || r.part > 0 {
-					// there are no more parts to return
-					err = io.EOF
-					r.cache.empty()
+
+				if r.j == length { // last chunk in a part?
+					r.j = 0 // reset chunk index
+					r.i++   // advance to the next part
+					if r.i == len(r.email.partsInfo.Parts) || r.part > 0 {
+						// there are no more parts to return
+						err = io.EOF
+						r.cache.empty()
+					}
 				}
 			}
+
 			// unless there's an error, the next time this function will be
 			// called, it will read the next chunk
 			return nRead, err

+ 8 - 6
chunk/store.go

@@ -2,6 +2,7 @@ package chunk
 
 import (
 	"github.com/flashmob/go-guerrilla/backends"
+	"github.com/flashmob/go-guerrilla/mail/smtp"
 	"io"
 	"net"
 	"time"
@@ -10,7 +11,7 @@ import (
 // Storage defines an interface to the storage layer (the database)
 type Storage interface {
 	// OpenMessage is used to begin saving an email. An email id is returned and used to call CloseMessage later
-	OpenMessage(from string, helo string, recipient string, ipAddress net.IPAddr, returnPath string, isTLS bool) (mailID uint64, err error)
+	OpenMessage(from string, helo string, recipient string, ipAddress net.IPAddr, returnPath string, isTLS bool, transport smtp.TransportType) (mailID uint64, err error)
 	// CloseMessage finalizes the writing of an email. Additional data collected while parsing the email is saved
 	CloseMessage(mailID uint64, size int64, partsInfo *PartsInfo, subject string, deliveryID string, to string, from string) error
 	// AddChunk saves a chunk of bytes to a given hash key
@@ -36,11 +37,12 @@ type Email struct {
 	helo       string // helo message given by the client when the message was transmitted
 	subject    string // subject stores the value from the first "Subject" header field
 	deliveryID string
-	recipient  string     // recipient is the email address that the server received from the RCPT TO command
-	ipv4       net.IPAddr // set to a value if client connected via ipv4
-	ipv6       net.IPAddr // set to a value if client connected via ipv6
-	returnPath string     // returnPath is the email address that the server received from the MAIL FROM command
-	isTLS      bool       // isTLS is true when TLS was used to connect
+	recipient  string             // recipient is the email address that the server received from the RCPT TO command
+	ipv4       net.IPAddr         // set to a value if client connected via ipv4
+	ipv6       net.IPAddr         // set to a value if client connected via ipv6
+	returnPath string             // returnPath is the email address that the server received from the MAIL FROM command
+	isTLS      bool               // isTLS is true when TLS was used to connect
+	transport  smtp.TransportType // did the sender signal 8bitmime?
 }
 
 type Chunk struct {

+ 5 - 1
chunk/store_memory.go

@@ -5,6 +5,7 @@ import (
 	"compress/zlib"
 	"errors"
 	"github.com/flashmob/go-guerrilla/backends"
+	"github.com/flashmob/go-guerrilla/mail/smtp"
 	"net"
 	"time"
 )
@@ -32,6 +33,7 @@ type memoryEmail struct {
 	ipv6       net.IPAddr
 	returnPath string
 	isTLS      bool
+	is8Bit     smtp.TransportType
 }
 
 type memoryChunk struct {
@@ -41,7 +43,7 @@ type memoryChunk struct {
 }
 
 // OpenMessage implements the Storage interface
-func (m *StoreMemory) OpenMessage(from string, helo string, recipient string, ipAddress net.IPAddr, returnPath string, isTLS bool) (mailID uint64, err error) {
+func (m *StoreMemory) OpenMessage(from string, helo string, recipient string, ipAddress net.IPAddr, returnPath string, isTLS bool, transport smtp.TransportType) (mailID uint64, err error) {
 	var ip4, ip6 net.IPAddr
 	if ip := ipAddress.IP.To4(); ip != nil {
 		ip4 = ipAddress
@@ -58,6 +60,7 @@ func (m *StoreMemory) OpenMessage(from string, helo string, recipient string, ip
 		ipv6:       ip6,
 		returnPath: returnPath,
 		isTLS:      isTLS,
+		is8Bit:     transport,
 	}
 	m.emails = append(m.emails, &email)
 	m.nextID++
@@ -160,6 +163,7 @@ func (m *StoreMemory) GetEmail(mailID uint64) (*Email, error) {
 		ipv6:       email.ipv6,
 		returnPath: email.returnPath,
 		isTLS:      email.isTLS,
+		transport:  email.is8Bit,
 	}, nil
 }
 

+ 5 - 4
chunk/store_sql.go

@@ -5,6 +5,7 @@ import (
 	"encoding/binary"
 	"encoding/json"
 	"github.com/flashmob/go-guerrilla/backends"
+	"github.com/flashmob/go-guerrilla/mail/smtp"
 	"net"
 )
 
@@ -44,8 +45,8 @@ func (s *StoreSQL) prepareSql() error {
 
 	if stmt, err := s.db.Prepare(`INSERT INTO ` +
 		s.config.EmailTable +
-		` (from, helo, recipient, ipv4_addr, ipv6_addr, return_path, is_tls) 
- VALUES(?, ?, ?, ?, ?, ?, ?)`); err != nil {
+		` (from, helo, recipient, ipv4_addr, ipv6_addr, return_path, is_tls, is_8bit) 
+ VALUES(?, ?, ?, ?, ?, ?, ?, ?)`); err != nil {
 		return err
 	} else {
 		s.statements["insertEmail"] = stmt
@@ -121,7 +122,7 @@ func (s *StoreSQL) prepareSql() error {
 }
 
 // OpenMessage implements the Storage interface
-func (s *StoreSQL) OpenMessage(from string, helo string, recipient string, ipAddress net.IPAddr, returnPath string, isTLS bool) (mailID uint64, err error) {
+func (s *StoreSQL) OpenMessage(from string, helo string, recipient string, ipAddress net.IPAddr, returnPath string, isTLS bool, transport smtp.TransportType) (mailID uint64, err error) {
 
 	// if it's ipv4 then we want ipv6 to be 0, and vice-versa
 	var ip4 uint32
@@ -131,7 +132,7 @@ func (s *StoreSQL) OpenMessage(from string, helo string, recipient string, ipAdd
 	} else {
 		_ = copy(ip6, ipAddress.IP)
 	}
-	r, err := s.statements["insertEmail"].Exec(from, helo, recipient, ip4, ip6, returnPath, isTLS)
+	r, err := s.statements["insertEmail"].Exec(from, helo, recipient, ip4, ip6, returnPath, isTLS, transport)
 	if err != nil {
 		return 0, err
 	}

+ 5 - 5
client.go

@@ -8,7 +8,7 @@ import (
 	"fmt"
 	"github.com/flashmob/go-guerrilla/log"
 	"github.com/flashmob/go-guerrilla/mail"
-	"github.com/flashmob/go-guerrilla/mail/rfc5321"
+	"github.com/flashmob/go-guerrilla/mail/smtp"
 	"github.com/flashmob/go-guerrilla/response"
 	"net"
 	"sync"
@@ -51,7 +51,7 @@ type client struct {
 	// guards access to conn
 	connGuard sync.Mutex
 	log       log.Logger
-	parser    rfc5321.Parser
+	parser    smtp.Parser
 }
 
 // NewClient allocates a new client.
@@ -210,7 +210,7 @@ type pathParser func([]byte) error
 func (c *client) parsePath(in []byte, p pathParser) (mail.Address, error) {
 	address := mail.Address{}
 	var err error
-	if len(in) > rfc5321.LimitPath {
+	if len(in) > smtp.LimitPath {
 		return address, errors.New(response.Canned.FailPathTooLong.String())
 	}
 	if err = p(in); err != nil {
@@ -218,9 +218,9 @@ func (c *client) parsePath(in []byte, p pathParser) (mail.Address, error) {
 	} else if c.parser.NullPath {
 		// bounce has empty from address
 		address = mail.Address{}
-	} else if len(c.parser.LocalPart) > rfc5321.LimitLocalPart {
+	} else if len(c.parser.LocalPart) > smtp.LimitLocalPart {
 		err = errors.New(response.Canned.FailLocalPartTooLong.String())
-	} else if len(c.parser.Domain) > rfc5321.LimitDomain {
+	} else if len(c.parser.Domain) > smtp.LimitDomain {
 		err = errors.New(response.Canned.FailDomainTooLong.String())
 	} else {
 		address = mail.Address{

+ 4 - 1
mail/envelope.go

@@ -5,6 +5,7 @@ import (
 	"crypto/md5"
 	"errors"
 	"fmt"
+	"github.com/flashmob/go-guerrilla/mail/smtp"
 	"io"
 	"mime"
 	"net/mail"
@@ -35,7 +36,7 @@ type Address struct {
 	// ADL is at-domain list if matched
 	ADL []string
 	// PathParams contains any ESTMP parameters that were matched
-	PathParams [][]string
+	PathParams []smtp.PathParam
 	// NullPath is true if <> was received
 	NullPath bool
 }
@@ -97,6 +98,8 @@ type Envelope struct {
 	DeliveryHeader string
 	// Email(s) will be queued with this id
 	QueuedId string
+	// TransportType indicates whenever 8BITMIME extension has been signaled
+	TransportType smtp.TransportType
 	// When locked, it means that the envelope is being processed by the backend
 	sync.Mutex
 }

+ 1 - 1
mail/mime/mime.go

@@ -499,7 +499,7 @@ func (p *Parser) header(mh *Part) (err error) {
 				mh.Headers.Add(contentTypeHeader, contentType.String())
 				state = 0
 			} else {
-				if (p.ch >= 33 && p.ch <= 126) || p.isWSP(p.ch) {
+				if p.ch != '\n' || p.isWSP(p.ch) {
 					_ = p.accept.WriteByte(p.ch)
 				} else if p.ch == '\n' {
 					c := p.peek()

+ 33 - 4
mail/rfc5321/parse.go → mail/smtp/parse.go

@@ -1,4 +1,4 @@
-package rfc5321
+package smtp
 
 // Parse RFC5321 productions, no regex
 
@@ -7,6 +7,7 @@ import (
 	"errors"
 	"net"
 	"strconv"
+	"strings"
 )
 
 const (
@@ -21,6 +22,34 @@ const (
 	LimitRecipients = 100
 )
 
+type PathParam []string
+
+type TransportType int
+
+const (
+	TransportType7bit TransportType = iota
+	TransportType8bit
+	TransportTypeUnspecified
+	TransportTypeInvalid
+)
+
+// is8BitMime checks for the BODY parameter as
+func (p PathParam) Transport() TransportType {
+	if len(p) != 2 {
+		return TransportTypeUnspecified
+	}
+	if strings.ToUpper(p[0]) != "BODY" {
+		// this is not a 'BODY' param
+		return TransportTypeUnspecified
+	}
+	if strings.ToUpper(p[1]) == "8BITMIME" {
+		return TransportType8bit
+	} else if strings.ToUpper(p[1]) == "7BIT" {
+		return TransportType7bit
+	}
+	return TransportTypeInvalid
+}
+
 // Parse Email Addresses according to https://tools.ietf.org/html/rfc5321
 type Parser struct {
 	NullPath  bool
@@ -28,7 +57,7 @@ type Parser struct {
 	Domain    string
 
 	ADL        []string
-	PathParams [][]string
+	PathParams []PathParam
 
 	pos int
 	ch  byte
@@ -149,8 +178,8 @@ func (s *Parser) RcptTo(input []byte) (err error) {
 }
 
 // esmtp-param *(SP esmtp-param)
-func (s *Parser) parameters() ([][]string, error) {
-	params := make([][]string, 0)
+func (s *Parser) parameters() ([]PathParam, error) {
+	params := make([]PathParam, 0)
 	for {
 		if result, err := s.param(); err != nil {
 			return params, err

+ 28 - 1
mail/rfc5321/parse_test.go → mail/smtp/parse_test.go

@@ -1,4 +1,4 @@
-package rfc5321
+package smtp
 
 import (
 	"strings"
@@ -581,3 +581,30 @@ func TestParse(t *testing.T) {
 	}
 
 }
+
+func TestTransport(t *testing.T) {
+
+	path := PathParam([]string{"BODY", "8bitmime"})
+	transport := path.Transport()
+	if transport != TransportType8bit {
+		t.Error("transport was not 8bit")
+	}
+
+	path = []string{"BODY", "7bit"}
+	transport = path.Transport()
+	if transport != TransportType7bit {
+		t.Error("transport was not 7bit")
+	}
+
+	path = []string{"BODY", "invalid"}
+	transport = path.Transport()
+	if transport != TransportTypeInvalid {
+		t.Error("transport was not invalid")
+	}
+
+	path = []string{}
+	transport = path.Transport()
+	if transport != TransportTypeUnspecified {
+		t.Error("transport was not unspecified")
+	}
+}

+ 26 - 9
server.go

@@ -18,7 +18,7 @@ import (
 	"github.com/flashmob/go-guerrilla/backends"
 	"github.com/flashmob/go-guerrilla/log"
 	"github.com/flashmob/go-guerrilla/mail"
-	"github.com/flashmob/go-guerrilla/mail/rfc5321"
+	"github.com/flashmob/go-guerrilla/mail/smtp"
 	"github.com/flashmob/go-guerrilla/response"
 )
 
@@ -348,7 +348,16 @@ func (s *server) isShuttingDown() bool {
 	return s.clientPool.IsShuttingDown()
 }
 
-// Handles an entire client SMTP exchange
+const advertisePipelining = "250-PIPELINING\r\n"
+const advertiseStartTLS = "250-STARTTLS\r\n"
+const advertiseEnhancedStatusCodes = "250-ENHANCEDSTATUSCODES\r\n"
+const advertise8BitMime = "250-8BITMIME\r\n"
+
+// The last line doesn't need \r\n since string will be printed as a new line.
+// Also, Last line has no dash -
+const advertiseHelp = "250 HELP"
+
+// handleClient handles an entire client SMTP exchange
 func (s *server) handleClient(client *client) {
 	defer client.closeConn()
 	sc := s.configStore.Load().(ServerConfig)
@@ -365,12 +374,9 @@ func (s *server) handleClient(client *client) {
 
 	// Extended feature advertisements
 	messageSize := fmt.Sprintf("250-SIZE %d\r\n", sc.MaxSize)
-	pipelining := "250-PIPELINING\r\n"
-	advertiseTLS := "250-STARTTLS\r\n"
-	advertiseEnhancedStatusCodes := "250-ENHANCEDSTATUSCODES\r\n"
+	advertiseTLS := advertiseStartTLS
 	// The last line doesn't need \r\n since string will be printed as a new line.
 	// Also, Last line has no dash -
-	help := "250 HELP"
 
 	if sc.TLS.AlwaysOn {
 		tlsConfig, ok := s.tlsConfigStore.Load().(*tls.Config)
@@ -434,10 +440,11 @@ func (s *server) handleClient(client *client) {
 				client.resetTransaction()
 				client.sendResponse(ehlo,
 					messageSize,
-					pipelining,
+					advertisePipelining,
 					advertiseTLS,
 					advertiseEnhancedStatusCodes,
-					help)
+					advertise8BitMime,
+					advertiseHelp)
 
 			case cmdHELP.match(cmd):
 				quote := response.GetQuote()
@@ -475,10 +482,20 @@ func (s *server) handleClient(client *client) {
 					// bounce has empty from address
 					client.MailFrom = mail.Address{}
 				}
+				client.TransportType = smtp.TransportTypeUnspecified
+				for i := range client.MailFrom.PathParams {
+					if tt := client.MailFrom.PathParams[i].Transport(); tt != smtp.TransportTypeUnspecified {
+						client.TransportType = tt
+						if tt == smtp.TransportTypeInvalid {
+							continue
+						}
+						break
+					}
+				}
 				client.sendResponse(r.SuccessMailCmd)
 
 			case cmdRCPT.match(cmd):
-				if len(client.RcptTo) > rfc5321.LimitRecipients {
+				if len(client.RcptTo) > smtp.LimitRecipients {
 					client.sendResponse(r.ErrorTooManyRecipients)
 					break
 				}

+ 3 - 3
tests/guerrilla_test.go

@@ -16,7 +16,7 @@ package test
 
 import (
 	"encoding/json"
-	"github.com/flashmob/go-guerrilla/mail/rfc5321"
+	"github.com/flashmob/go-guerrilla/mail/smtp"
 	"testing"
 
 	"time"
@@ -434,7 +434,7 @@ func TestRFC2832LimitLocalPart(t *testing.T) {
 				t.Error("Hello command failed", err.Error())
 			}
 			// repeat > 64 characters in local part
-			response, err := Command(conn, bufin, fmt.Sprintf("RCPT TO:<%[email protected]>", strings.Repeat("a", rfc5321.LimitLocalPart+1)))
+			response, err := Command(conn, bufin, fmt.Sprintf("RCPT TO:<%[email protected]>", strings.Repeat("a", smtp.LimitLocalPart+1)))
 			if err != nil {
 				t.Error("rcpt command failed", err.Error())
 			}
@@ -444,7 +444,7 @@ func TestRFC2832LimitLocalPart(t *testing.T) {
 			}
 			// what about if it's exactly 64?
 			// repeat > 64 characters in local part
-			response, err = Command(conn, bufin, fmt.Sprintf("RCPT TO:<%[email protected]>", strings.Repeat("a", rfc5321.LimitLocalPart-1)))
+			response, err = Command(conn, bufin, fmt.Sprintf("RCPT TO:<%[email protected]>", strings.Repeat("a", smtp.LimitLocalPart-1)))
 			if err != nil {
 				t.Error("rcpt command failed", err.Error())
 			}