Skip to content

Commit 0fdc6d4

Browse files
committed
wip: bufmsg: new package
This package performs operations on buffered MIME messages. Reading a message with this package and writing it back as-is should produce the exact same bytes.
1 parent 67aae7b commit 0fdc6d4

File tree

1 file changed

+212
-0
lines changed

1 file changed

+212
-0
lines changed

bufmsg/bufmsg.go

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
package bufmsg
2+
3+
import (
4+
"bufio"
5+
"bytes"
6+
"fmt"
7+
"io"
8+
"io/ioutil"
9+
"strings"
10+
11+
"github.com/emersion/go-message"
12+
"github.com/emersion/go-message/textproto"
13+
)
14+
15+
type Entity struct {
16+
Header message.Header
17+
Children []*Entity
18+
19+
originalHeader message.Header
20+
rawBody []byte
21+
body []byte
22+
bodyUpdated bool
23+
}
24+
25+
func New(header message.Header) *Entity {
26+
return &Entity{
27+
Header: header,
28+
originalHeader: header.Copy(),
29+
}
30+
}
31+
32+
func Read(r io.Reader) (*Entity, error) {
33+
br := bufio.NewReader(r)
34+
th, err := textproto.ReadHeader(br)
35+
if err != nil {
36+
return nil, err
37+
}
38+
return readWithHeader(th, br)
39+
}
40+
41+
func readWithHeader(th textproto.Header, body io.Reader) (*Entity, error) {
42+
entity := &Entity{
43+
Header: message.Header{th},
44+
originalHeader: message.Header{th.Copy()},
45+
}
46+
47+
if boundary, ok := entity.multipartBoundary(); ok {
48+
mr := textproto.NewMultipartReader(body, boundary)
49+
for {
50+
part, err := mr.NextPart()
51+
if err == io.EOF {
52+
break
53+
} else if err != nil {
54+
return nil, err
55+
}
56+
57+
child, err := readWithHeader(part.Header, part)
58+
if err != nil {
59+
return nil, err
60+
}
61+
62+
entity.Children = append(entity.Children, child)
63+
}
64+
} else {
65+
b, err := ioutil.ReadAll(body)
66+
if err != nil {
67+
return nil, err
68+
}
69+
entity.rawBody = b
70+
}
71+
72+
return &Entity{
73+
Header: entity.Header,
74+
}, nil
75+
}
76+
77+
func (e *Entity) multipartBoundary() (string, bool) {
78+
mediaType, mediaParams, _ := e.Header.ContentType()
79+
return mediaParams["boundary"], strings.HasPrefix(mediaType, "multipart/")
80+
}
81+
82+
func (e *Entity) IsMultipart() bool {
83+
_, ok := e.multipartBoundary()
84+
return ok
85+
}
86+
87+
func (e *Entity) Body() ([]byte, error) {
88+
if e.body != nil {
89+
return e.body, nil
90+
}
91+
92+
if e.rawBody == nil {
93+
return nil, nil // entity is empty
94+
}
95+
96+
me, err := message.New(e.originalHeader, bytes.NewReader(e.rawBody))
97+
if err != nil {
98+
return nil, err
99+
}
100+
b, err := ioutil.ReadAll(me.Body)
101+
if err != nil {
102+
return nil, err
103+
}
104+
e.body = b
105+
return b, nil
106+
}
107+
108+
func (e *Entity) SetBody(b []byte) {
109+
e.body = b
110+
e.rawBody = nil
111+
e.bodyUpdated = true
112+
}
113+
114+
func (e *Entity) RawBody() ([]byte, error) {
115+
if e.IsMultipart() || e.bodyUpdated {
116+
var buf bytes.Buffer
117+
if err := e.writeBodyTo(&buf); err != nil {
118+
return nil, err
119+
}
120+
return buf.Bytes(), nil
121+
} else {
122+
return e.rawBody, nil
123+
}
124+
}
125+
126+
func (e *Entity) WriteTo(w io.Writer) error {
127+
if err := textproto.WriteHeader(w, e.Header.Header); err != nil {
128+
return err
129+
}
130+
return e.writeBodyTo(w)
131+
}
132+
133+
func (e *Entity) writeBodyTo(w io.Writer) error {
134+
if e.IsMultipart() {
135+
if e.bodyUpdated {
136+
return fmt.Errorf("bufmsg: SetBody was called on a multipart message")
137+
}
138+
139+
mw := textproto.NewMultipartWriter(w)
140+
141+
// TODO: grab boundary from header, if any -- otherwise set it
142+
143+
for _, child := range e.Children {
144+
pw, err := mw.CreatePart(child.Header.Header)
145+
if err != nil {
146+
return err
147+
}
148+
149+
if err := child.writeBodyTo(pw); err != nil {
150+
return err
151+
}
152+
}
153+
154+
return mw.Close()
155+
} else if e.bodyUpdated {
156+
return nil // TODO: encode body to w
157+
} else {
158+
_, err := w.Write(e.rawBody)
159+
return err
160+
}
161+
}
162+
163+
type WalkFunc func(path []int, entity *Entity) error
164+
165+
type walkQueueItem struct {
166+
path []int
167+
entity *Entity
168+
}
169+
170+
func (e *Entity) Walk(walkFunc WalkFunc) error {
171+
stack := []walkQueueItem{
172+
{nil, e},
173+
}
174+
seen := make(map[*Entity]bool)
175+
for len(stack) > 0 {
176+
// Pop an item out of the stack
177+
it := stack[len(stack)-1]
178+
stack = stack[:len(stack)-1]
179+
180+
if seen[it.entity] {
181+
return fmt.Errorf("bufmsg: cyclic multipart entity")
182+
}
183+
seen[it.entity] = true
184+
185+
if err := walkFunc(it.path, it.entity); err != nil {
186+
return err
187+
}
188+
189+
// Insert children into the stack in reverse order
190+
for i := len(it.entity.Children) - 1; i >= 0; i-- {
191+
child := it.entity.Children[i]
192+
193+
childPath := make([]int, len(it.path))
194+
copy(childPath, it.path)
195+
childPath = append(childPath, i)
196+
197+
stack = append(stack, walkQueueItem{childPath, child})
198+
}
199+
}
200+
return nil
201+
}
202+
203+
func (e *Entity) Child(path []int) *Entity {
204+
cur := e
205+
for _, index := range path {
206+
if index >= len(cur.Children) {
207+
return nil
208+
}
209+
cur = cur.Children[index]
210+
}
211+
return cur
212+
}

0 commit comments

Comments
 (0)