diff --git a/pkg/message/build_framework_test.go b/pkg/message/build_framework_test.go index e40a1664..49e76bef 100644 --- a/pkg/message/build_framework_test.go +++ b/pkg/message/build_framework_test.go @@ -291,7 +291,7 @@ func (matcher decryptsToMatcher) match(t *testing.T, have string) { dec, err := matcher.kr.Decrypt(haveMsg, nil, crypto.GetUnixTime()) require.NoError(t, err) - assert.Equal(t, matcher.want, dec.GetString()) + assert.Equal(t, matcher.want, string(dec.GetBinary())) } func decryptsTo(kr *crypto.KeyRing, want string) decryptsToMatcher { diff --git a/pkg/message/build_rfc822.go b/pkg/message/build_rfc822.go index 4a117843..b8cc5ca0 100644 --- a/pkg/message/build_rfc822.go +++ b/pkg/message/build_rfc822.go @@ -40,7 +40,7 @@ func buildRFC822(kr *crypto.KeyRing, msg *pmapi.Message, attData [][]byte, opts return buildMultipartRFC822(kr, msg, attData, opts) case msg.MIMEType == "multipart/mixed": - return buildEncryptedRFC822(kr, msg, opts) + return buildExternallyEncryptedRFC822(kr, msg, opts) default: return buildSimpleRFC822(kr, msg, opts) @@ -146,25 +146,10 @@ func writeTextPart( return errors.Wrap(ErrDecryptionFailed, err.Error()) } - /* - if len(msg.Attachments) > 0 { - return writeCustomTextPartAsAttachment(w, msg, err) - } - */ - return writeCustomTextPart(w, msg, err) } - part, err := w.CreatePart(getTextPartHeader(message.Header{}, dec, msg.MIMEType)) - if err != nil { - return err - } - - if _, err := part.Write(dec); err != nil { - return err - } - - return part.Close() + return writePart(w, getTextPartHeader(message.Header{}, dec, msg.MIMEType), dec) } func writeAttachmentPart( @@ -196,16 +181,7 @@ func writeAttachmentPart( return writeCustomAttachmentPart(w, att, msg, err) } - part, err := w.CreatePart(getAttachmentPartHeader(att)) - if err != nil { - return err - } - - if _, err := part.Write(dec.GetBinary()); err != nil { - return err - } - - return part.Close() + return writePart(w, getAttachmentPartHeader(att), dec.GetBinary()) } func writeRelatedParts( @@ -221,25 +197,31 @@ func writeRelatedParts( hdr.SetContentType("multipart/related", map[string]string{"boundary": boundary.gen()}) - rel, err := w.CreatePart(hdr) - if err != nil { - return err - } - - if err := writeTextPart(rel, kr, msg, opts); err != nil { - return err - } - - for i, att := range atts { - if err := writeAttachmentPart(rel, kr, att, attData[i], opts); err != nil { + return createPart(w, hdr, func(rel *message.Writer) error { + if err := writeTextPart(rel, kr, msg, opts); err != nil { return err } - } - return rel.Close() + for i, att := range atts { + if err := writeAttachmentPart(rel, kr, att, attData[i], opts); err != nil { + return err + } + } + + return nil + }) } -func buildEncryptedRFC822(kr *crypto.KeyRing, msg *pmapi.Message, opts JobOptions) ([]byte, error) { +func buildExternallyEncryptedRFC822(kr *crypto.KeyRing, msg *pmapi.Message, opts JobOptions) ([]byte, error) { + dec, err := msg.Decrypt(kr) + if err != nil { + if !opts.IgnoreDecryptionErrors { + return nil, errors.Wrap(ErrDecryptionFailed, err.Error()) + } + + return buildPGPMIMERFC822(msg) + } + hdr := getMessageHeader(msg, opts) hdr.SetContentType("multipart/mixed", map[string]string{"boundary": newBoundary(msg.ID).gen()}) @@ -251,31 +233,58 @@ func buildEncryptedRFC822(kr *crypto.KeyRing, msg *pmapi.Message, opts JobOption return nil, err } - dec, err := msg.Decrypt(kr) - if err != nil { - return nil, errors.Wrap(ErrDecryptionFailed, err.Error()) - } - ent, err := message.Read(bytes.NewReader(dec)) if err != nil { return nil, err } - part, err := w.CreatePart(ent.Header) - if err != nil { - return nil, err - } - body, err := ioutil.ReadAll(ent.Body) if err != nil { return nil, err } - if _, err := part.Write(body); err != nil { + if err := writePart(w, ent.Header, body); err != nil { return nil, err } - if err := part.Close(); err != nil { + if err := w.Close(); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +func buildPGPMIMERFC822(msg *pmapi.Message) ([]byte, error) { + var hdr message.Header + + hdr.SetContentType("multipart/encrypted", map[string]string{ + "boundary": newBoundary(msg.ID).gen(), + "protocol": "application/pgp-encrypted", + }) + + buf := new(bytes.Buffer) + + w, err := message.CreateWriter(buf, hdr) + if err != nil { + return nil, err + } + + var encHdr message.Header + + encHdr.SetContentType("application/pgp-encrypted", nil) + encHdr.Set("Content-Description", "PGP/MIME version identification") + + if err := writePart(w, encHdr, []byte("Version: 1")); err != nil { + return nil, err + } + + var dataHdr message.Header + + dataHdr.SetContentType("application/octet-stream", map[string]string{"name": "encrypted.asc"}) + dataHdr.SetContentDisposition("inline", map[string]string{"filename": "encrypted.asc"}) + dataHdr.Set("Content-Description", "OpenPGP encrypted message") + + if err := writePart(w, dataHdr, []byte(msg.Body)); err != nil { return nil, err } @@ -432,3 +441,26 @@ func toAddressList(addrs []*mail.Address) string { return strings.Join(res, ", ") } + +func createPart(w *message.Writer, hdr message.Header, fn func(*message.Writer) error) error { + part, err := w.CreatePart(hdr) + if err != nil { + return err + } + + if err := fn(part); err != nil { + return err + } + + return part.Close() +} + +func writePart(w *message.Writer, hdr message.Header, body []byte) error { + return createPart(w, hdr, func(part *message.Writer) error { + if _, err := part.Write(body); err != nil { + return errors.Wrap(err, "failed to write part body") + } + + return nil + }) +} diff --git a/pkg/message/build_test.go b/pkg/message/build_test.go index f569bc3a..a205cf97 100644 --- a/pkg/message/build_test.go +++ b/pkg/message/build_test.go @@ -1008,6 +1008,48 @@ func TestBuildCustomMessageHTML(t *testing.T) { expectTransferEncoding(isMissing()) } +func TestBuildCustomMessageEncrypted(t *testing.T) { + m := gomock.NewController(t) + defer m.Finish() + + b := NewBuilder(1, 1, 1) + defer b.Done() + + kr := tests.MakeKeyRing(t) + + body := readerToString(getFileReader("pgp-mime-body-plaintext.eml")) + + // Use a different keyring for encrypting the message; it won't be decryptable. + foreignKR := tests.MakeKeyRing(t) + msg := newTestMessage(t, foreignKR, "messageID", "addressID", "multipart/mixed", body, time.Date(2020, time.January, 1, 0, 0, 0, 0, time.UTC)) + + // Tell the job to ignore decryption errors; a custom message will be returned instead of an error. + res, err := b.NewJobWithOptions( + context.Background(), + newTestFetcher(m, kr, msg), + msg.ID, + JobOptions{IgnoreDecryptionErrors: true}, + ).GetResult() + require.NoError(t, err) + + section(t, res). + expectContentType(is(`multipart/encrypted`)). + expectContentTypeParam(`protocol`, is(`application/pgp-encrypted`)) + + section(t, res, 1). + expectContentType(is(`application/pgp-encrypted`)). + expectHeader(`Content-Description`, is(`PGP/MIME version identification`)). + expectBody(is(`Version: 1`)) + + section(t, res, 2). + expectContentType(is(`application/octet-stream`)). + expectContentTypeParam(`name`, is(`encrypted.asc`)). + expectContentDisposition(is(`inline`)). + expectContentDispositionParam(`filename`, is(`encrypted.asc`)). + expectHeader(`Content-Description`, is(`OpenPGP encrypted message`)). + expectBody(decryptsTo(foreignKR, body)) +} + func TestBuildCustomMessagePlainWithAttachment(t *testing.T) { m := gomock.NewController(t) defer m.Finish()