From 2b36d3ab7be91be17912057fa3b4a24fced06069 Mon Sep 17 00:00:00 2001 From: James Houlahan Date: Thu, 2 Jul 2020 16:17:04 +0200 Subject: [PATCH] feat: attach public key --- internal/imap/mailbox_message.go | 2 +- internal/smtp/user.go | 3 +- pkg/message/parser.go | 39 +++++++++---- pkg/message/parser/parser.go | 10 +--- pkg/message/parser/part.go | 18 ++++++ pkg/message/parser_test.go | 96 +++++++++++++++----------------- 6 files changed, 98 insertions(+), 70 deletions(-) diff --git a/internal/imap/mailbox_message.go b/internal/imap/mailbox_message.go index 93f46d16..730f7079 100644 --- a/internal/imap/mailbox_message.go +++ b/internal/imap/mailbox_message.go @@ -68,7 +68,7 @@ func (im *imapMailbox) CreateMessage(flags []string, date time.Time, body imap.L // Called from go-imap in goroutines - we need to handle panics for each function. defer im.panicHandler.HandlePanic() - m, _, _, readers, err := message.Parse(body) + m, _, _, readers, err := message.Parse(body, "", "") if err != nil { return err } diff --git a/internal/smtp/user.go b/internal/smtp/user.go index 521b8972..ae14f30f 100644 --- a/internal/smtp/user.go +++ b/internal/smtp/user.go @@ -182,8 +182,7 @@ func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err attachedPublicKeyName = "publickey - " + kr.GetIdentities()[0].Name } - // TODO: Include public keys here! - message, mimeBody, plainBody, attReaders, err := message.Parse(messageReader) + message, mimeBody, plainBody, attReaders, err := message.Parse(messageReader, attachedPublicKey, attachedPublicKeyName) if err != nil { return } diff --git a/pkg/message/parser.go b/pkg/message/parser.go index cebe5340..4b7148db 100644 --- a/pkg/message/parser.go +++ b/pkg/message/parser.go @@ -19,6 +19,7 @@ package message import ( "bytes" + "fmt" "io" "mime" "net/mail" @@ -30,7 +31,7 @@ import ( "github.com/jaytaylor/html2text" ) -func Parse(r io.Reader) (m *pmapi.Message, mimeBody, plainBody string, atts []io.Reader, err error) { +func Parse(r io.Reader, key, keyName string) (m *pmapi.Message, mime, plain string, atts []io.Reader, err error) { p, err := parser.New(r) if err != nil { return @@ -38,7 +39,7 @@ func Parse(r io.Reader) (m *pmapi.Message, mimeBody, plainBody string, atts []io m = pmapi.NewMessage() - if err = parseHeader(m, p.Header()); err != nil { + if err = parseHeader(m, p.Root().Header); err != nil { return } @@ -46,14 +47,15 @@ func Parse(r io.Reader) (m *pmapi.Message, mimeBody, plainBody string, atts []io return } - parts, plainParts, err := collectBodyParts(p) - if err != nil { + if m.Body, plain, err = collectBodyParts(p); err != nil { return } - m.Body = strings.Join(parts, "\r\n") - plainBody = strings.Join(plainParts, "\r\n") - if mimeBody, err = writeMimeBody(p); err != nil { + if key != "" { + attachPublicKey(p.Root(), key, keyName) + } + + if mime, err = writeMIMEMessage(p); err != nil { return } @@ -82,7 +84,9 @@ func collectAttachments(p *parser.Parser) (atts []*pmapi.Attachment, data []io.R return } -func collectBodyParts(p *parser.Parser) (parts, plainParts []string, err error) { +func collectBodyParts(p *parser.Parser) (body, plain string, err error) { + var parts, plainParts []string + w := p. NewWalker(). WithContentTypeHandler("text/plain", func(p *parser.Part) (err error) { @@ -106,10 +110,10 @@ func collectBodyParts(p *parser.Parser) (parts, plainParts []string, err error) return } - return + return strings.Join(parts, "\r\n"), strings.Join(plainParts, "\r\n"), nil } -func writeMimeBody(p *parser.Parser) (mimeBody string, err error) { +func writeMIMEMessage(p *parser.Parser) (mime string, err error) { writer := p. NewWriter(). WithCondition(func(p *parser.Part) (keep bool) { @@ -126,6 +130,21 @@ func writeMimeBody(p *parser.Parser) (mimeBody string, err error) { return buf.String(), nil } +func attachPublicKey(p *parser.Part, key, keyName string) { + h := message.Header{} + + h.Set("Content-Type", fmt.Sprintf(`application/pgp-key; name="%v"`, keyName)) + h.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%v.asc.pgp"`, keyName)) + h.Set("Content-Transfer-Encoding", "base64") + + // TODO: Split body at col width 72. + + p.AddChild(&parser.Part{ + Header: h, + Body: []byte(key), + }) +} + func parseHeader(m *pmapi.Message, h message.Header) (err error) { m.Header = make(mail.Header) diff --git a/pkg/message/parser/parser.go b/pkg/message/parser/parser.go index ccecd0f6..01c11657 100644 --- a/pkg/message/parser/parser.go +++ b/pkg/message/parser/parser.go @@ -1,7 +1,6 @@ package parser import ( - "errors" "io" "io/ioutil" @@ -31,20 +30,17 @@ func (p *Parser) NewWriter() *Writer { return newWriter(p.root) } -func (p *Parser) Header() message.Header { - return p.root.Header +func (p *Parser) Root() *Part { + return p.root } 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") + if part, err = part.Part(n); err != nil { return } - - part = part.children[n-1] } return diff --git a/pkg/message/parser/part.go b/pkg/message/parser/part.go index 1e2e3fc8..9d3e7c91 100644 --- a/pkg/message/parser/part.go +++ b/pkg/message/parser/part.go @@ -1,6 +1,8 @@ package parser import ( + "errors" + "github.com/emersion/go-message" ) @@ -10,6 +12,22 @@ type Part struct { children []*Part } +func (p *Part) Part(n int) (part *Part, err error) { + if len(p.children) < n { + return nil, errors.New("no such part") + } + + return p.children[n-1], nil +} + +func (p *Part) Parts() (n int) { + return len(p.children) +} + +func (p *Part) AddChild(child *Part) { + p.children = append(p.children, child) +} + func (p *Part) visit(w *Walker) (err error) { if err = p.handle(w); err != nil { return diff --git a/pkg/message/parser_test.go b/pkg/message/parser_test.go index 53bbf13b..cf875a59 100644 --- a/pkg/message/parser_test.go +++ b/pkg/message/parser_test.go @@ -27,6 +27,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "golang.org/x/text/encoding/charmap" ) @@ -64,8 +65,8 @@ func TestParseMessageTextPlain(t *testing.T) { f := f("text_plain.eml") defer func() { _ = f.Close() }() - m, mimeBody, plainContents, atts, err := Parse(f) - assert.NoError(t, err) + m, mimeBody, plainContents, atts, err := Parse(f, "", "") + require.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, `"Receiver" `, m.ToList[0].String()) @@ -81,8 +82,8 @@ func TestParseMessageTextPlainUTF8(t *testing.T) { f := f("text_plain_utf8.eml") defer func() { _ = f.Close() }() - m, mimeBody, plainContents, atts, err := Parse(f) - assert.NoError(t, err) + m, mimeBody, plainContents, atts, err := Parse(f, "", "") + require.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, `"Receiver" `, m.ToList[0].String()) @@ -98,8 +99,8 @@ func TestParseMessageTextPlainLatin1(t *testing.T) { f := f("text_plain_latin1.eml") defer func() { _ = f.Close() }() - m, mimeBody, plainContents, atts, err := Parse(f) - assert.NoError(t, err) + m, mimeBody, plainContents, atts, err := Parse(f, "", "") + require.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, `"Receiver" `, m.ToList[0].String()) @@ -115,8 +116,8 @@ func TestParseMessageTextPlainUnknownCharsetIsActuallyLatin1(t *testing.T) { f := f("text_plain_unknown_latin1.eml") defer func() { _ = f.Close() }() - m, mimeBody, plainContents, atts, err := Parse(f) - assert.NoError(t, err) + m, mimeBody, plainContents, atts, err := Parse(f, "", "") + require.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, `"Receiver" `, m.ToList[0].String()) @@ -132,8 +133,8 @@ func TestParseMessageTextPlainUnknownCharsetIsActuallyLatin2(t *testing.T) { f := f("text_plain_unknown_latin2.eml") defer func() { _ = f.Close() }() - m, mimeBody, plainContents, atts, err := Parse(f) - assert.NoError(t, err) + m, mimeBody, plainContents, atts, err := Parse(f, "", "") + require.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, `"Receiver" `, m.ToList[0].String()) @@ -155,8 +156,8 @@ func TestParseMessageTextPlainAlready7Bit(t *testing.T) { f := f("text_plain_7bit.eml") defer func() { _ = f.Close() }() - m, mimeBody, plainContents, atts, err := Parse(f) - assert.NoError(t, err) + m, mimeBody, plainContents, atts, err := Parse(f, "", "") + require.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, `"Receiver" `, m.ToList[0].String()) @@ -172,8 +173,8 @@ func TestParseMessageTextPlainWithOctetAttachment(t *testing.T) { f := f("text_plain_octet_attachment.eml") defer func() { _ = f.Close() }() - m, mimeBody, plainContents, atts, err := Parse(f) - assert.NoError(t, err) + m, mimeBody, plainContents, atts, err := Parse(f, "", "") + require.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, `"Receiver" `, m.ToList[0].String()) @@ -182,7 +183,7 @@ func TestParseMessageTextPlainWithOctetAttachment(t *testing.T) { assert.Equal(t, s("text_plain_octet_attachment.mime"), mimeBody) assert.Equal(t, "body", plainContents) - assert.Len(t, atts, 1) + require.Len(t, atts, 1) assert.Equal(t, readerToString(atts[0]), "if you are reading this, hi!") } @@ -191,7 +192,7 @@ func TestParseMessageTextPlainWithOctetAttachmentGoodFilename(t *testing.T) { defer func() { _ = f.Close() }() m, mimeBody, plainContents, atts, err := Parse(f, "", "") - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, `"Receiver" `, m.ToList[0].String()) @@ -210,7 +211,7 @@ func TestParseMessageTextPlainWithOctetAttachmentBadFilename(t *testing.T) { defer func() { _ = f.Close() }() m, mimeBody, plainContents, atts, err := Parse(f, "", "") - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, `"Receiver" `, m.ToList[0].String()) @@ -228,8 +229,8 @@ func TestParseMessageTextPlainWithPlainAttachment(t *testing.T) { f := f("text_plain_plain_attachment.eml") defer func() { _ = f.Close() }() - m, mimeBody, plainContents, atts, err := Parse(f) - assert.NoError(t, err) + m, mimeBody, plainContents, atts, err := Parse(f, "", "") + require.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, `"Receiver" `, m.ToList[0].String()) @@ -238,7 +239,7 @@ func TestParseMessageTextPlainWithPlainAttachment(t *testing.T) { assert.Equal(t, s("text_plain_plain_attachment.mime"), mimeBody) assert.Equal(t, "body", plainContents) - assert.Len(t, atts, 1) + require.Len(t, atts, 1) assert.Equal(t, readerToString(atts[0]), "attachment") } @@ -246,8 +247,8 @@ func TestParseMessageTextPlainWithImageInline(t *testing.T) { f := f("text_plain_image_inline.eml") defer func() { _ = f.Close() }() - m, mimeBody, plainContents, atts, err := Parse(f) - assert.NoError(t, err) + m, mimeBody, plainContents, atts, err := Parse(f, "", "") + require.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, `"Receiver" `, m.ToList[0].String()) @@ -257,9 +258,9 @@ func TestParseMessageTextPlainWithImageInline(t *testing.T) { assert.Equal(t, "body", plainContents) // The inline image is an 8x8 mic-dropping gopher. - assert.Len(t, atts, 1) + require.Len(t, atts, 1) img, err := png.DecodeConfig(atts[0]) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, 8, img.Width) assert.Equal(t, 8, img.Height) } @@ -268,8 +269,8 @@ func TestParseMessageWithMultipleTextParts(t *testing.T) { f := f("multiple_text_parts.eml") defer func() { _ = f.Close() }() - m, mimeBody, plainContents, atts, err := Parse(f) - assert.NoError(t, err) + m, mimeBody, plainContents, atts, err := Parse(f, "", "") + require.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, `"Receiver" `, m.ToList[0].String()) @@ -287,8 +288,8 @@ func TestParseMessageTextHTML(t *testing.T) { f := f("text_html.eml") defer func() { _ = f.Close() }() - m, mimeBody, plainContents, atts, err := Parse(f) - assert.NoError(t, err) + m, mimeBody, plainContents, atts, err := Parse(f, "", "") + require.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, `"Receiver" `, m.ToList[0].String()) @@ -306,7 +307,7 @@ func TestParseMessageTextHTMLAlready7Bit(t *testing.T) { f := f("text_html_7bit.eml") defer func() { _ = f.Close() }() - m, mimeBody, plainContents, atts, err := Parse(f) + m, mimeBody, plainContents, atts, err := Parse(f, "", "") assert.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) @@ -325,8 +326,8 @@ func TestParseMessageTextHTMLWithOctetAttachment(t *testing.T) { f := f("text_html_octet_attachment.eml") defer func() { _ = f.Close() }() - m, mimeBody, plainContents, atts, err := Parse(f) - assert.NoError(t, err) + m, mimeBody, plainContents, atts, err := Parse(f, "", "") + require.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, `"Receiver" `, m.ToList[0].String()) @@ -335,19 +336,18 @@ func TestParseMessageTextHTMLWithOctetAttachment(t *testing.T) { assert.Equal(t, s("text_html_octet_attachment.mime"), mimeBody) assert.Equal(t, "This is body of *HTML mail* with attachment", plainContents) - assert.Len(t, atts, 1) + require.Len(t, atts, 1) assert.Equal(t, readerToString(atts[0]), "if you are reading this, hi!") } -// NOTE: Enable when bug is fixed. -func _TestParseMessageTextHTMLWithPlainAttachment(t *testing.T) { // nolint[deadcode] +func TestParseMessageTextHTMLWithPlainAttachment(t *testing.T) { // nolint[deadcode] rand.Seed(0) f := f("text_html_plain_attachment.eml") defer func() { _ = f.Close() }() - m, mimeBody, plainContents, atts, err := Parse(f) - assert.NoError(t, err) + m, mimeBody, plainContents, atts, err := Parse(f, "", "") + require.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, `"Receiver" `, m.ToList[0].String()) @@ -357,7 +357,7 @@ func _TestParseMessageTextHTMLWithPlainAttachment(t *testing.T) { // nolint[dead assert.Equal(t, s("text_html_plain_attachment.mime"), mimeBody) assert.Equal(t, "This is body of *HTML mail* with attachment", plainContents) - assert.Len(t, atts, 1) + require.Len(t, atts, 1) assert.Equal(t, readerToString(atts[0]), "attachment") } @@ -367,7 +367,7 @@ func TestParseMessageTextHTMLWithImageInline(t *testing.T) { f := f("text_html_image_inline.eml") defer func() { _ = f.Close() }() - m, mimeBody, plainContents, atts, err := Parse(f) + m, mimeBody, plainContents, atts, err := Parse(f, "", "") assert.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) @@ -378,22 +378,20 @@ func TestParseMessageTextHTMLWithImageInline(t *testing.T) { assert.Equal(t, "This is body of *HTML mail* with attachment", plainContents) // The inline image is an 8x8 mic-dropping gopher. - assert.Len(t, atts, 1) + require.Len(t, atts, 1) img, err := png.DecodeConfig(atts[0]) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, 8, img.Width) assert.Equal(t, 8, img.Height) } -// NOTE: Enable when bug is fixed. -/* -func _TestParseMessageWithAttachedPublicKey(t *testing.T) { // nolint[deadcode] +func TestParseMessageWithAttachedPublicKey(t *testing.T) { // nolint[deadcode] f := f("text_plain.eml") defer func() { _ = f.Close() }() // BAD: Public Key is not attached unless Content-Type is specified (not required)! m, mimeBody, plainContents, atts, err := Parse(f, "publickey", "publickeyname") - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, `"Receiver" `, m.ToList[0].String()) @@ -403,19 +401,17 @@ func _TestParseMessageWithAttachedPublicKey(t *testing.T) { // nolint[deadcode] assert.Equal(t, "body", plainContents) // BAD: Public key not available as an attachment! - assert.Len(t, atts, 1) + require.Len(t, atts, 1) } -*/ -// NOTE: Enable when bug is fixed. -func _TestParseMessageTextHTMLWithEmbeddedForeignEncoding(t *testing.T) { // nolint[deadcode] +func TestParseMessageTextHTMLWithEmbeddedForeignEncoding(t *testing.T) { // nolint[deadcode] rand.Seed(0) f := f("text_html_embedded_foreign_encoding.eml") defer func() { _ = f.Close() }() - m, mimeBody, plainContents, atts, err := Parse(f) - assert.NoError(t, err) + m, mimeBody, plainContents, atts, err := Parse(f, "", "") + require.NoError(t, err) assert.Equal(t, `"Sender" `, m.Sender.String()) assert.Equal(t, `"Receiver" `, m.ToList[0].String())