Repository: emersion/go-imap
Pull request #363 (2020-06-19 14:54:40)

backendutil: Implement message size and lines counting

codecov[bot]: # [Codecov](https://codecov.io/gh/emersion/go-imap/pull/363?src=pr&el=h1) Report
> Merging [#363](https://codecov.io/gh/emersion/go-imap/pull/363?src=pr&el=desc) into [master](https://codecov.io/gh/emersion/go-imap/commit/4881b2308cc9257e5c0bcfa72567aa459061f14d&el=desc) will **decrease** coverage by `0.15%`.
> The diff coverage is `56.25%`.

[![Impacted file tree graph](https://codecov.io/gh/emersion/go-imap/pull/363/graphs/tree.svg?width=650&height=150&src=pr&token=toR3ygEJFD)](https://codecov.io/gh/emersion/go-imap/pull/363?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #363      +/-   ##
==========================================
- Coverage   73.35%   73.20%   -0.16%     
==========================================
  Files          32       32              
  Lines        3528     3560      +32     
==========================================
+ Hits         2588     2606      +18     
- Misses        648      660      +12     
- Partials      292      294       +2     
```


| [Impacted Files](https://codecov.io/gh/emersion/go-imap/pull/363?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [backend/backendutil/bodystructure.go](https://codecov.io/gh/emersion/go-imap/pull/363/diff?src=pr&el=tree#diff-YmFja2VuZC9iYWNrZW5kdXRpbC9ib2R5c3RydWN0dXJlLmdv) | `67.18% <56.25%> (-10.94%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/emersion/go-imap/pull/363?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/emersion/go-imap/pull/363?src=pr&el=footer). Last update [4881b23...f23d92b](https://codecov.io/gh/emersion/go-imap/pull/363?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/README.md b/README.md
index 189d23e..e533728 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
 # go-imap
 
 [![GoDoc](https://godoc.org/github.com/emersion/go-imap?status.svg)](https://godoc.org/github.com/emersion/go-imap)
-[![builds.sr.ht status](https://builds.sr.ht/~emersion/go-imap.svg)](https://builds.sr.ht/~emersion/go-imap?)
+[![builds.sr.ht status](https://builds.sr.ht/~emersion/go-imap/commits.svg)](https://builds.sr.ht/~emersion/go-imap/commits?)
 [![Codecov](https://codecov.io/gh/emersion/go-imap/branch/master/graph/badge.svg)](https://codecov.io/gh/emersion/go-imap)
 
 An [IMAP4rev1](https://tools.ietf.org/html/rfc3501) library written in Go. It
diff --git a/backend/backendutil/bodystructure.go b/backend/backendutil/bodystructure.go
index 101f16f..dbe139c 100644
--- a/backend/backendutil/bodystructure.go
+++ b/backend/backendutil/bodystructure.go
@@ -1,7 +1,10 @@
 package backendutil
 
 import (
+	"bufio"
+	"bytes"
 	"io"
+	"io/ioutil"
 	"mime"
 	"strings"
 
@@ -9,6 +12,29 @@ import (
 	"github.com/emersion/go-message/textproto"
 )
 
+type countReader struct {
+	r          io.Reader
+	bytes      uint32
+	newlines   uint32
+	endsWithLF bool
+}
+
+func (r *countReader) Read(b []byte) (int, error) {
+	n, err := r.r.Read(b)
+	r.bytes += uint32(n)
+	if n != 0 {
+		r.newlines += uint32(bytes.Count(b[:n], []byte{'\n'}))
+		r.endsWithLF = b[n-1] == '\n'
+	}
+	// If the stream does not end with a newline - count missing newline.
+	if err == io.EOF {
+		if !r.endsWithLF {
+			r.newlines++
+		}
+	}
+	return n, err
+}
+
 // FetchBodyStructure computes a message's body structure from its content.
 func FetchBodyStructure(header textproto.Header, body io.Reader, extended bool) (*imap.BodyStructure, error) {
 	bs := new(imap.BodyStructure)
@@ -29,7 +55,6 @@ func FetchBodyStructure(header textproto.Header, body io.Reader, extended bool)
 	bs.Id = header.Get("Content-Id")
 	bs.Description = header.Get("Content-Description")
 	bs.Encoding = header.Get("Content-Transfer-Encoding")
-	// TODO: bs.Size
 
 	if mr := multipartReader(header, body); mr != nil {
 		var parts []*imap.BodyStructure
@@ -48,14 +73,40 @@ func FetchBodyStructure(header textproto.Header, body io.Reader, extended bool)
 			parts = append(parts, pbs)
 		}
 		bs.Parts = parts
+	} else {
+		countedBody := countReader{r: body}
+		needLines := false
+		if bs.MIMEType == "message" && bs.MIMESubType == "rfc822" {
+			// This will result in double-buffering if body is already a
+			// bufio.Reader (most likely it is). :\
+			bufBody := bufio.NewReader(&countedBody)
+			subMsgHdr, err := textproto.ReadHeader(bufBody)
+			if err != nil {
+				return nil, err
+			}
+			bs.Envelope, err = FetchEnvelope(subMsgHdr)
+			if err != nil {
+				return nil, err
+			}
+			bs.BodyStructure, err = FetchBodyStructure(subMsgHdr, bufBody, extended)
+			if err != nil {
+				return nil, err
+			}
+			needLines = true
+		} else if bs.MIMEType == "text" {
+			needLines = true
+		}
+		if _, err := io.Copy(ioutil.Discard, &countedBody); err != nil {
+			return nil, err
+		}
+		bs.Size = countedBody.bytes
+		if needLines {
+			bs.Lines = countedBody.newlines
+		}
 	}
 
-	// TODO: bs.Envelope, bs.BodyStructure
-	// TODO: bs.Lines
-
 	if extended {
 		bs.Extended = true
-
 		bs.Disposition, bs.DispositionParams, _ = mime.ParseMediaType(header.Get("Content-Disposition"))
 
 		// TODO: bs.Language, bs.Location


Test patch:
diff --git a/backend/backendutil/backendutil_test.go b/backend/backendutil/backendutil_test.go
index 7963330..db48e48 100644
--- a/backend/backendutil/backendutil_test.go
+++ b/backend/backendutil/backendutil_test.go
@@ -53,7 +53,7 @@ const testHTMLHeaderString = "Content-Disposition: inline\r\n" +
 	"Content-Type: text/html\r\n" +
 	"\r\n"
 
-const testHTMLBodyString = "<div>What's <i>your</i> name?</div>"
+const testHTMLBodyString = "<div>What's <i>your\r\n</i> name?</div>"
 
 const testHTMLString = testHTMLHeaderString + testHTMLBodyString
 
diff --git a/backend/backendutil/bodystructure_test.go b/backend/backendutil/bodystructure_test.go
index 8a4e06b..ff3196e 100644
--- a/backend/backendutil/bodystructure_test.go
+++ b/backend/backendutil/bodystructure_test.go
@@ -28,6 +28,8 @@ var testBodyStructure = &imap.BodyStructure{
 					Extended:          true,
 					Disposition:       "inline",
 					DispositionParams: map[string]string{},
+					Lines:             1,
+					Size:              17,
 				},
 				{
 					MIMEType:          "text",
@@ -36,6 +38,8 @@ var testBodyStructure = &imap.BodyStructure{
 					Extended:          true,
 					Disposition:       "inline",
 					DispositionParams: map[string]string{},
+					Lines:             2,
+					Size:              37,
 				},
 			},
 		},
@@ -46,6 +50,8 @@ var testBodyStructure = &imap.BodyStructure{
 			Extended:          true,
 			Disposition:       "attachment",
 			DispositionParams: map[string]string{"filename": "note.txt"},
+			Lines:             1,
+			Size:              19,
 		},
 	},
 	Extended: true,