Repository: emersion/go-imap
Pull request #321 (2019-12-13 11:40:04)

Introduce BodyStructure.Walk
This is a helper to iterate over a body structure tree.

Closes: https://github.com/emersion/go-imap/issues/319

codecov[bot]: # [Codecov](https://codecov.io/gh/emersion/go-imap/pull/321?src=pr&el=h1) Report
> Merging [#321](https://codecov.io/gh/emersion/go-imap/pull/321?src=pr&el=desc) into [master](https://codecov.io/gh/emersion/go-imap/commit/b051681b6c75f7e11bae3cdf9023eb0d9ecb92af?src=pr&el=desc) will **increase** coverage by `0.09%`.
> The diff coverage is `100%`.

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

```diff
@@            Coverage Diff             @@
##           master     #321      +/-   ##
==========================================
+ Coverage   73.29%   73.39%   +0.09%     
==========================================
  Files          32       32              
  Lines        3501     3514      +13     
==========================================
+ Hits         2566     2579      +13     
  Misses        642      642              
  Partials      293      293
```


| [Impacted Files](https://codecov.io/gh/emersion/go-imap/pull/321?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [message.go](https://codecov.io/gh/emersion/go-imap/pull/321/diff?src=pr&el=tree#diff-bWVzc2FnZS5nbw==) | `76.2% <100%> (+0.56%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/emersion/go-imap/pull/321?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/321?src=pr&el=footer). Last update [b051681...04e3306](https://codecov.io/gh/emersion/go-imap/pull/321?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

emersion: >I think it would be a good idea to document that path argument can be nil for multipart "root". Otherwise it might become the source of unexpected panics when users will try to use it.

How could this trigger a panic? `len(nil)` and `range nil` are legal, and you need to check the length to access an element by index anyway.
foxcpp: Alright, I guess we can get away saying "assuming that slices have minimal length (1 in this case) unless it is documented is silly so do not do it".

Patch:
diff --git a/message.go b/message.go
index 87bc4b2..123e45e 100644
--- a/message.go
+++ b/message.go
@@ -1100,3 +1100,38 @@ func (bs *BodyStructure) Filename() (string, error) {
 	}
 	return decodeHeader(raw)
 }
+
+// BodyStructureWalkFunc is the type of the function called for each body
+// structure visited by BodyStructure.Walk. The path argument contains the IMAP
+// part path (see BodyPartName).
+//
+// The function should return true to visit all of the part's children or false
+// to skip them.
+type BodyStructureWalkFunc func(path []int, part *BodyStructure) (walkChildren bool)
+
+// Walk walks the body structure tree, calling f for each part in the tree,
+// including bs itself. The parts are visited in DFS pre-order.
+func (bs *BodyStructure) Walk(f BodyStructureWalkFunc) {
+	// Non-multipart messages only have part 1
+	if len(bs.Parts) == 0 {
+		f([]int{1}, bs)
+		return
+	}
+
+	bs.walk(f, nil)
+}
+
+func (bs *BodyStructure) walk(f BodyStructureWalkFunc, path []int) {
+	if !f(path, bs) {
+		return
+	}
+
+	for i, part := range bs.Parts {
+		num := i + 1
+
+		partPath := append([]int(nil), path...)
+		partPath = append(partPath, num)
+
+		part.walk(f, partPath)
+	}
+}


Test patch:
diff --git a/message_test.go b/message_test.go
index c383947..c5dad20 100644
--- a/message_test.go
+++ b/message_test.go
@@ -642,3 +642,99 @@ func TestBodyStructureFilename(t *testing.T) {
 		}
 	}
 }
+
+func TestBodyStructureWalk(t *testing.T) {
+	textPlain := &BodyStructure{
+		MIMEType: "text",
+		MIMESubType: "plain",
+	}
+
+	textHTML := &BodyStructure{
+		MIMEType: "text",
+		MIMESubType: "plain",
+	}
+
+	multipartAlternative := &BodyStructure{
+		MIMEType: "multipart",
+		MIMESubType: "alternative",
+		Parts: []*BodyStructure{textPlain, textHTML},
+	}
+
+	imagePNG := &BodyStructure{
+		MIMEType: "image",
+		MIMESubType: "png",
+	}
+
+	multipartMixed := &BodyStructure{
+		MIMEType: "multipart",
+		MIMESubType: "mixed",
+		Parts: []*BodyStructure{multipartAlternative, imagePNG},
+	}
+
+	type testNode struct {
+		path []int
+		part *BodyStructure
+	}
+
+	tests := []struct {
+		bs *BodyStructure
+		nodes []testNode
+		walkChildren bool
+	}{
+		{
+			bs: textPlain,
+			nodes: []testNode{
+				{ path: []int{1}, part: textPlain },
+			},
+		},
+		{
+			bs: multipartAlternative,
+			nodes: []testNode{
+				{ path: nil, part: multipartAlternative },
+				{ path: []int{1}, part: textPlain },
+				{ path: []int{2}, part: textHTML },
+			},
+			walkChildren: true,
+		},
+		{
+			bs: multipartMixed,
+			nodes: []testNode{
+				{ path: nil, part: multipartMixed },
+				{ path: []int{1}, part: multipartAlternative },
+				{ path: []int{1, 1}, part: textPlain },
+				{ path: []int{1, 2}, part: textHTML },
+				{ path: []int{2}, part: imagePNG },
+			},
+			walkChildren: true,
+		},
+		{
+			bs: multipartMixed,
+			nodes: []testNode{
+				{ path: nil, part: multipartMixed },
+			},
+			walkChildren: false,
+		},
+	}
+
+	for i, test := range tests {
+		j := 0
+		test.bs.Walk(func(path []int, part *BodyStructure) bool {
+			if j >= len(test.nodes) {
+				t.Errorf("Test #%v: invalid node count: got > %v, want %v", i, j, len(test.nodes))
+				return false
+			}
+			n := &test.nodes[j]
+			if !reflect.DeepEqual(path, n.path) {
+				t.Errorf("Test #%v: node #%v: invalid path: got %v, want %v", i, j, path, n.path)
+			}
+			if part != n.part {
+				t.Errorf("Test #%v: node #%v: invalid part: got %v, want %v", i, j, part, n.part)
+			}
+			j++
+			return test.walkChildren
+		})
+		if j != len(test.nodes) {
+			t.Errorf("Test #%v: invalid node count: got %v, want %v", i, j, len(test.nodes))
+		}
+	}
+}