diff --git a/pkg/message/parser.go b/pkg/message/parser.go index 35ff7f65..dab77874 100644 --- a/pkg/message/parser.go +++ b/pkg/message/parser.go @@ -37,6 +37,7 @@ import ( "github.com/ProtonMail/proton-bridge/pkg/message/parser" pmmime "github.com/ProtonMail/proton-bridge/pkg/mime" "github.com/ProtonMail/proton-bridge/pkg/pmapi" + "github.com/emersion/go-message" "github.com/jaytaylor/html2text" ) @@ -412,13 +413,41 @@ func (pka *PublicKeyAttacher) Accept(partReader io.Reader, header textproto.MIME } // ======= Parser ========== +func parseGoMessageHeader(h message.Header) (m *pmapi.Message, err error) { + m = pmapi.NewMessage() + + m.Header = make(mail.Header) + + fields := h.Fields() + + for fields.Next() { + switch strings.ToLower(fields.Key()) { + case "subject": + if m.Subject, err = fields.Text(); err != nil { + return + } + + // TODO: Set these thingies. + case "from": + case "to": + case "reply-to": + case "cc": + case "bcc": + case "date": + } + } + + return +} func ParseGoMessage(r io.Reader) (m *pmapi.Message, mimeBody string, plainContents string, atts []io.Reader, err error) { + // Parse the message. p, err := parser.New(r) if err != nil { return } + // Collect attachments, convert html to plaintext. walker := p. NewWalker(). WithContentDispositionHandler("attachment", func(p *parser.Part, _ parser.PartHandler) (err error) { @@ -431,6 +460,7 @@ func ParseGoMessage(r io.Reader) (m *pmapi.Message, mimeBody string, plainConten plain = string(p.Body) } + // TODO: Do we need newline here? plainContents += plain return @@ -440,10 +470,10 @@ func ParseGoMessage(r io.Reader) (m *pmapi.Message, mimeBody string, plainConten return } + // Write out a mime body that doesn't include attachments. writer := p. NewWriter(). WithCondition(func(p *parser.Part) (keep bool) { - // We don't write if the content disposition says it's an attachment. if disp, _, err := p.Header.ContentDisposition(); err == nil && disp == "attachment" { return false } @@ -459,6 +489,11 @@ func ParseGoMessage(r io.Reader) (m *pmapi.Message, mimeBody string, plainConten mimeBody = buf.String() + // Parse the header to build a pmapi message. + if m, err = parseGoMessageHeader(p.Header()); err != nil { + return + } + return } diff --git a/pkg/message/parser/parser.go b/pkg/message/parser/parser.go index 009877fe..ccecd0f6 100644 --- a/pkg/message/parser/parser.go +++ b/pkg/message/parser/parser.go @@ -1,6 +1,7 @@ package parser import ( + "errors" "io" "io/ioutil" @@ -30,6 +31,25 @@ func (p *Parser) NewWriter() *Writer { return newWriter(p.root) } +func (p *Parser) Header() message.Header { + return p.root.Header +} + +func (p *Parser) Part(number []int) (part *Part, err error) { + part = p.root + + for _, n := range number { + if len(part.children) < n { + err = errors.New("no such part") + return + } + + part = part.children[n-1] + } + + return +} + func (p *Parser) parse(r io.Reader) (err error) { e, err := message.Read(r) if err != nil { diff --git a/pkg/message/parser/part_test.go b/pkg/message/parser/part_test.go new file mode 100644 index 00000000..5fd62c17 --- /dev/null +++ b/pkg/message/parser/part_test.go @@ -0,0 +1,56 @@ +package parser + +import ( + "strconv" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPart(t *testing.T) { + p := newTestParser(t, "complex_structure.eml") + + wantParts := map[string]string{ + "": "multipart/mixed", + "1": "text/plain", + "2": "application/octet-stream", + "3": "multipart/mixed", + "3.1": "text/plain", + "3.2": "application/octet-stream", + "4": "multipart/mixed", + "4.1": "image/gif", + "4.2": "multipart/mixed", + "4.2.1": "text/plain", + "4.2.2": "multipart/alternative", + "4.2.2.1": "text/plain", + "4.2.2.2": "text/html", + } + + for partNumber, wantContType := range wantParts { + part, err := p.Part(getPartNumber(partNumber)) + require.NoError(t, err) + + contType, _, err := part.Header.ContentType() + require.NoError(t, err) + assert.Equal(t, wantContType, contType) + } +} + +func getPartNumber(s string) (part []int) { + if s == "" { + return + } + + for _, number := range strings.Split(s, ".") { + i64, err := strconv.ParseInt(number, 10, 64) + if err != nil { + panic(err) + } + + part = append(part, int(i64)) + } + + return +} diff --git a/pkg/message/parser/testdata/complex_structure.eml b/pkg/message/parser/testdata/complex_structure.eml new file mode 100644 index 00000000..403e58d3 --- /dev/null +++ b/pkg/message/parser/testdata/complex_structure.eml @@ -0,0 +1,91 @@ +Subject: Sample mail +From: John Doe +To: Mary Smith +Date: Fri, 21 Nov 1997 09:55:06 -0600 +Content-Type: multipart/mixed; boundary="0000MAIN" + +main summary + +--0000MAIN +Content-Type: text/plain + +1. main message + +--0000MAIN +Content-Type: application/octet-stream +Content-Disposition: inline; filename="main_signature.sig" +Content-Transfer-Encoding: base64 + +aWYgeW91IGFyZSByZWFkaW5nIHRoaXMsIGhpIQ== + +--0000MAIN +Subject: Inside mail 3 +From: Mary Smith +To: John Doe +Date: Fri, 20 Nov 1997 09:55:06 -0600 +Content-Type: multipart/mixed; boundary="0003MSG" + +3. message summary + +--0003MSG +Content-Type: text/plain + +3.1 message text + +--0003MSG +Content-Type: application/octet-stream +Content-Disposition: attachment; filename="msg_3_signature.sig" +Content-Transfer-Encoding: base64 + +aWYgeW91IGFyZSByZWFkaW5nIHRoaXMsIGhpIQ== + +--0003MSG-- + +--0000MAIN +Content-Type: multipart/mixed; boundary="0004ATTACH" + +4 attach summary + +--0004ATTACH +Content-Type: image/gif +Content-Disposition: attachment; filename="att4.1_gif.sig" +Content-Transfer-Encoding: base64 + +aWYgeW91IGFyZSByZWFkaW5nIHRoaXMsIGhpIQ== + +--0004ATTACH +Subject: Inside mail 4.2 +From: Mary Smith +To: John Doe +Date: Fri, 10 Nov 1997 09:55:06 -0600 +Content-Type: multipart/mixed; boundary="0042MSG" + +4.2 message summary + +--0042MSG +Content-Type: text/plain + +4.2.1 message text + +--0042MSG +Content-Type: multipart/alternative; boundary="0422ALTER" + +4.2.2 alternative summary + +--0422ALTER +Content-Type: text/plain + +4.2.2.1 plain text + +--0422ALTER +Content-Type: text/html + +

4.2.2.2 html text

+ +--0422ALTER-- + +--0042MSG-- + +--0004ATTACH-- + +--0000MAIN-- diff --git a/pkg/message/parser/testdata/text_html.eml b/pkg/message/parser/testdata/text_html.eml index 7b7a1fe8..9db0a3f4 100644 --- a/pkg/message/parser/testdata/text_html.eml +++ b/pkg/message/parser/testdata/text_html.eml @@ -1,9 +1,9 @@ -From: Sender -To: Receiver -Content-Type: multipart/mixed; boundary=longrandomstring - ---longrandomstring -Content-Type: text/html - -This is body of HTML mail with attachment ---longrandomstring-- +From: Sender +To: Receiver +Content-Type: multipart/mixed; boundary=longrandomstring + +--longrandomstring +Content-Type: text/html + +This is body of HTML mail with attachment +--longrandomstring-- diff --git a/pkg/message/parser/testdata/text_html_octet_attachment.eml b/pkg/message/parser/testdata/text_html_octet_attachment.eml index 7491ee83..ea9fd167 100644 --- a/pkg/message/parser/testdata/text_html_octet_attachment.eml +++ b/pkg/message/parser/testdata/text_html_octet_attachment.eml @@ -1,15 +1,15 @@ -From: Sender -To: Receiver -Content-Type: multipart/mixed; boundary=longrandomstring - ---longrandomstring -Content-Type: text/html - -This is body of HTML mail with attachment ---longrandomstring -Content-Type: application/octet-stream -Content-Transfer-Encoding: base64 -Content-Disposition: attachment - -aWYgeW91IGFyZSByZWFkaW5nIHRoaXMsIGhpIQ== ---longrandomstring-- +From: Sender +To: Receiver +Content-Type: multipart/mixed; boundary=longrandomstring + +--longrandomstring +Content-Type: text/html + +This is body of HTML mail with attachment +--longrandomstring +Content-Type: application/octet-stream +Content-Transfer-Encoding: base64 +Content-Disposition: attachment + +aWYgeW91IGFyZSByZWFkaW5nIHRoaXMsIGhpIQ== +--longrandomstring-- diff --git a/pkg/message/parser/writer_test.go b/pkg/message/parser/writer_test.go index e9ccd07c..16ef0dfd 100644 --- a/pkg/message/parser/writer_test.go +++ b/pkg/message/parser/writer_test.go @@ -2,6 +2,7 @@ package parser import ( "bytes" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -15,7 +16,7 @@ func TestParserWrite(t *testing.T) { buf := new(bytes.Buffer) assert.NoError(t, w.Write(buf)) - assert.Equal(t, s("text_html_octet_attachment.eml"), buf.String()) + assert.Equal(t, s("text_html_octet_attachment.eml"), crlf(buf.String())) } func TestParserWriteNoAttachments(t *testing.T) { @@ -35,5 +36,9 @@ func TestParserWriteNoAttachments(t *testing.T) { buf := new(bytes.Buffer) assert.NoError(t, w.Write(buf)) - assert.Equal(t, s("text_html.eml"), buf.String()) + assert.Equal(t, s("text_html.eml"), crlf(buf.String())) +} + +func crlf(s string) string { + return strings.ReplaceAll(s, "\r\n", "\n") }