forked from Silverfish/proton-bridge
GODT-213: Message Builder
This commit is contained in:
@ -18,328 +18,126 @@
|
||||
package message
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"context"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"mime/multipart"
|
||||
"mime/quotedprintable"
|
||||
"net/textproto"
|
||||
"sync"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||
"github.com/emersion/go-textwrapper"
|
||||
openpgperrors "golang.org/x/crypto/openpgp/errors"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrDecryptionFailed = errors.New("message could not be decrypted")
|
||||
ErrNoSuchKeyRing = errors.New("the keyring to decrypt this message could not be found")
|
||||
)
|
||||
|
||||
// Builder for converting PM message to RFC822. Builder will directly write
|
||||
// changes to message when fetching or building message.
|
||||
type Builder struct {
|
||||
cl pmapi.Client
|
||||
msg *pmapi.Message
|
||||
|
||||
EncryptedToHTML bool
|
||||
successfullyDecrypted bool
|
||||
reqs chan fetchReq
|
||||
done chan struct{}
|
||||
jobs map[string]*BuildJob
|
||||
locker sync.Mutex
|
||||
}
|
||||
|
||||
// NewBuilder initiated with client and message meta info.
|
||||
func NewBuilder(client pmapi.Client, message *pmapi.Message) *Builder {
|
||||
return &Builder{cl: client, msg: message, EncryptedToHTML: true, successfullyDecrypted: false}
|
||||
type Fetcher interface {
|
||||
GetMessage(string) (*pmapi.Message, error)
|
||||
GetAttachment(string) (io.ReadCloser, error)
|
||||
KeyRingForAddressID(string) (*crypto.KeyRing, error)
|
||||
}
|
||||
|
||||
// fetchMessage will update original PM message if successful.
|
||||
func (bld *Builder) fetchMessage() (err error) {
|
||||
if bld.msg.Body != "" {
|
||||
return nil
|
||||
}
|
||||
func NewBuilder(fetchWorkers, attachWorkers, buildWorkers int) *Builder {
|
||||
b := newBuilder()
|
||||
|
||||
complete, err := bld.cl.GetMessage(bld.msg.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fetchReqCh, fetchResCh := startFetchWorkers(fetchWorkers, attachWorkers)
|
||||
buildReqCh, buildResCh := startBuildWorkers(buildWorkers)
|
||||
|
||||
*bld.msg = *complete
|
||||
go func() {
|
||||
defer close(fetchReqCh)
|
||||
|
||||
return
|
||||
}
|
||||
for {
|
||||
select {
|
||||
case req := <-b.reqs:
|
||||
fetchReqCh <- req
|
||||
|
||||
func (bld *Builder) writeMessageBody(w io.Writer) error {
|
||||
if err := bld.fetchMessage(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err := bld.WriteBody(w)
|
||||
if err != nil {
|
||||
_, _ = io.WriteString(w, "\r\n")
|
||||
if bld.EncryptedToHTML {
|
||||
_ = CustomMessage(bld.msg, err, true)
|
||||
}
|
||||
_, err = io.WriteString(w, bld.msg.Body)
|
||||
_, _ = io.WriteString(w, "\r\n")
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (bld *Builder) writeAttachmentBody(w io.Writer, att *pmapi.Attachment) error {
|
||||
// Retrieve encrypted attachment
|
||||
r, err := bld.cl.GetAttachment(att.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Close() //nolint[errcheck]
|
||||
|
||||
if err := bld.WriteAttachmentBody(w, att, r); err != nil {
|
||||
// Returning an error here makes e-mail clients like Thunderbird behave
|
||||
// badly, trying to retrieve the message again and again
|
||||
log.Warnln("Cannot write attachment body:", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bld *Builder) writeRelatedPart(p io.Writer, inlines []*pmapi.Attachment) error {
|
||||
related := multipart.NewWriter(p)
|
||||
|
||||
_ = related.SetBoundary(GetRelatedBoundary(bld.msg))
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
if err := bld.writeMessageBody(buf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write the body part
|
||||
h := GetBodyHeader(bld.msg)
|
||||
|
||||
var err error
|
||||
if p, err = related.CreatePart(h); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, _ = buf.WriteTo(p)
|
||||
|
||||
for _, inline := range inlines {
|
||||
buf = &bytes.Buffer{}
|
||||
if err = bld.writeAttachmentBody(buf, inline); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
h := GetAttachmentHeader(inline, false)
|
||||
if p, err = related.CreatePart(h); err != nil {
|
||||
return err
|
||||
}
|
||||
_, _ = buf.WriteTo(p)
|
||||
}
|
||||
|
||||
_ = related.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// BuildMessage converts PM message to body structure (not RFC3501) and bytes
|
||||
// of RC822 message. If successful the original PM message will contain decrypted body.
|
||||
func (bld *Builder) BuildMessage() (structure *BodyStructure, message []byte, err error) { //nolint[funlen]
|
||||
if err = bld.fetchMessage(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
bodyBuf := &bytes.Buffer{}
|
||||
|
||||
mainHeader := GetHeader(bld.msg)
|
||||
mainHeader.Set("Content-Type", "multipart/mixed; boundary="+GetBoundary(bld.msg))
|
||||
if err = WriteHeader(bodyBuf, mainHeader); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
_, _ = io.WriteString(bodyBuf, "\r\n")
|
||||
|
||||
// NOTE: Do we really need extra encapsulation? i.e. Bridge-IMAP message is always multipart/mixed
|
||||
|
||||
if bld.msg.MIMEType == pmapi.ContentTypeMultipartMixed {
|
||||
_, _ = io.WriteString(bodyBuf, "\r\n--"+GetBoundary(bld.msg)+"\r\n")
|
||||
if err = bld.writeMessageBody(bodyBuf); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
_, _ = io.WriteString(bodyBuf, "\r\n--"+GetBoundary(bld.msg)+"--\r\n")
|
||||
} else {
|
||||
mw := multipart.NewWriter(bodyBuf)
|
||||
_ = mw.SetBoundary(GetBoundary(bld.msg))
|
||||
|
||||
var partWriter io.Writer
|
||||
atts, inlines := SeparateInlineAttachments(bld.msg)
|
||||
|
||||
if len(inlines) > 0 {
|
||||
relatedHeader := GetRelatedHeader(bld.msg)
|
||||
if partWriter, err = mw.CreatePart(relatedHeader); err != nil {
|
||||
return nil, nil, err
|
||||
case <-b.done:
|
||||
return
|
||||
}
|
||||
_ = bld.writeRelatedPart(partWriter, inlines)
|
||||
} else {
|
||||
buf := &bytes.Buffer{}
|
||||
if err = bld.writeMessageBody(buf); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Write the body part
|
||||
bodyHeader := GetBodyHeader(bld.msg)
|
||||
if partWriter, err = mw.CreatePart(bodyHeader); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
_, _ = buf.WriteTo(partWriter)
|
||||
}
|
||||
}()
|
||||
|
||||
// Write the attachments parts
|
||||
for _, att := range atts {
|
||||
buf := &bytes.Buffer{}
|
||||
if err = bld.writeAttachmentBody(buf, att); err != nil {
|
||||
return nil, nil, err
|
||||
go func() {
|
||||
defer close(buildReqCh)
|
||||
|
||||
for res := range fetchResCh {
|
||||
if res.err != nil {
|
||||
b.jobFailure(res.messageID, res.err)
|
||||
} else {
|
||||
buildReqCh <- res
|
||||
}
|
||||
|
||||
attachmentHeader := GetAttachmentHeader(att, false)
|
||||
if partWriter, err = mw.CreatePart(attachmentHeader); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
_, _ = buf.WriteTo(partWriter)
|
||||
}
|
||||
}()
|
||||
|
||||
_ = mw.Close()
|
||||
}
|
||||
go func() {
|
||||
for res := range buildResCh {
|
||||
if res.err != nil {
|
||||
b.jobFailure(res.messageID, res.err)
|
||||
} else {
|
||||
b.jobSuccess(res.messageID, res.literal)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// wee need to copy buffer before building body structure
|
||||
message = bodyBuf.Bytes()
|
||||
structure, err = NewBodyStructure(bodyBuf)
|
||||
return structure, message, err
|
||||
return b
|
||||
}
|
||||
|
||||
// SuccessfullyDecrypted is true when message was fetched and decrypted successfully.
|
||||
func (bld *Builder) SuccessfullyDecrypted() bool { return bld.successfullyDecrypted }
|
||||
|
||||
// WriteBody decrypts PM message and writes main body section. The external PGP
|
||||
// message is written as is (including attachments).
|
||||
func (bld *Builder) WriteBody(w io.Writer) error {
|
||||
kr, err := bld.cl.KeyRingForAddressID(bld.msg.AddressID)
|
||||
if err != nil {
|
||||
return err
|
||||
func newBuilder() *Builder {
|
||||
return &Builder{
|
||||
reqs: make(chan fetchReq),
|
||||
done: make(chan struct{}),
|
||||
jobs: make(map[string]*BuildJob),
|
||||
}
|
||||
// decrypt body
|
||||
if err := bld.msg.Decrypt(kr); err != nil && err != openpgperrors.ErrSignatureExpired {
|
||||
return err
|
||||
}
|
||||
bld.successfullyDecrypted = true
|
||||
if bld.msg.MIMEType != pmapi.ContentTypeMultipartMixed {
|
||||
// transfer encoding
|
||||
qp := quotedprintable.NewWriter(w)
|
||||
if _, err := io.WriteString(qp, bld.msg.Body); err != nil {
|
||||
return err
|
||||
}
|
||||
return qp.Close()
|
||||
}
|
||||
_, err = io.WriteString(w, bld.msg.Body)
|
||||
return err
|
||||
}
|
||||
|
||||
// WriteAttachmentBody decrypts and writes the attachments.
|
||||
func (bld *Builder) WriteAttachmentBody(w io.Writer, att *pmapi.Attachment, attReader io.Reader) (err error) {
|
||||
kr, err := bld.cl.KeyRingForAddressID(bld.msg.AddressID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Decrypt it
|
||||
var dr io.Reader
|
||||
dr, err = att.Decrypt(attReader, kr)
|
||||
if err == openpgperrors.ErrKeyIncorrect {
|
||||
err = nil //nolint[wastedasign] Do not fail if attachment is encrypted with a different key
|
||||
dr = attReader
|
||||
att.Name += ".gpg"
|
||||
att.MIMEType = "application/pgp-encrypted"
|
||||
} else if err != nil && err != openpgperrors.ErrSignatureExpired {
|
||||
err = fmt.Errorf("cannot decrypt attachment: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// transfer encoding
|
||||
ww := textwrapper.NewRFC822(w)
|
||||
bw := base64.NewEncoder(base64.StdEncoding, ww)
|
||||
|
||||
var n int64
|
||||
if n, err = io.Copy(bw, dr); err != nil {
|
||||
err = fmt.Errorf("cannot write attachment: %v (wrote %v bytes)", err, n)
|
||||
}
|
||||
|
||||
_ = bw.Close()
|
||||
return err
|
||||
func (b *Builder) NewJob(ctx context.Context, api Fetcher, messageID string) *BuildJob {
|
||||
return b.NewJobWithOptions(ctx, api, messageID, JobOptions{})
|
||||
}
|
||||
|
||||
func BuildEncrypted(m *pmapi.Message, readers []io.Reader, kr *crypto.KeyRing) ([]byte, error) { //nolint[funlen]
|
||||
b := &bytes.Buffer{}
|
||||
func (b *Builder) NewJobWithOptions(ctx context.Context, api Fetcher, messageID string, opts JobOptions) *BuildJob {
|
||||
b.locker.Lock()
|
||||
defer b.locker.Unlock()
|
||||
|
||||
// Overwrite content for main header for import.
|
||||
// Even if message has just simple body we should upload as multipart/mixed.
|
||||
// Each part has encrypted body and header reflects the original header.
|
||||
mainHeader := GetHeader(m)
|
||||
mainHeader.Set("Content-Type", "multipart/mixed; boundary="+GetBoundary(m))
|
||||
mainHeader.Del("Content-Disposition")
|
||||
mainHeader.Del("Content-Transfer-Encoding")
|
||||
if err := WriteHeader(b, mainHeader); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mw := multipart.NewWriter(b)
|
||||
if err := mw.SetBoundary(GetBoundary(m)); err != nil {
|
||||
return nil, err
|
||||
if job, ok := b.jobs[messageID]; ok {
|
||||
return job
|
||||
}
|
||||
|
||||
// Write the body part.
|
||||
bodyHeader := make(textproto.MIMEHeader)
|
||||
bodyHeader.Set("Content-Type", m.MIMEType+"; charset=utf-8")
|
||||
bodyHeader.Set("Content-Disposition", "inline")
|
||||
bodyHeader.Set("Content-Transfer-Encoding", "7bit")
|
||||
b.jobs[messageID] = newBuildJob(messageID)
|
||||
|
||||
p, err := mw.CreatePart(bodyHeader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// First, encrypt the message body.
|
||||
if err := m.Encrypt(kr, kr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, err := io.WriteString(p, m.Body); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
go func() { b.reqs <- fetchReq{ctx: ctx, api: api, messageID: messageID, opts: opts} }()
|
||||
|
||||
// Write the attachments parts.
|
||||
for i := 0; i < len(m.Attachments); i++ {
|
||||
att := m.Attachments[i]
|
||||
r := readers[i]
|
||||
h := GetAttachmentHeader(att, false)
|
||||
p, err := mw.CreatePart(h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Create encrypted writer.
|
||||
pgpMessage, err := kr.Encrypt(crypto.NewPlainMessage(data), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ww := textwrapper.NewRFC822(p)
|
||||
bw := base64.NewEncoder(base64.StdEncoding, ww)
|
||||
if _, err := bw.Write(pgpMessage.GetBinary()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := bw.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if err := mw.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return b.Bytes(), nil
|
||||
return b.jobs[messageID]
|
||||
}
|
||||
|
||||
func (b *Builder) Done() {
|
||||
b.locker.Lock()
|
||||
defer b.locker.Unlock()
|
||||
|
||||
close(b.done)
|
||||
}
|
||||
|
||||
func (b *Builder) jobSuccess(messageID string, literal []byte) {
|
||||
b.locker.Lock()
|
||||
defer b.locker.Unlock()
|
||||
|
||||
b.jobs[messageID].postSuccess(literal)
|
||||
|
||||
delete(b.jobs, messageID)
|
||||
}
|
||||
|
||||
func (b *Builder) jobFailure(messageID string, err error) {
|
||||
b.locker.Lock()
|
||||
defer b.locker.Unlock()
|
||||
|
||||
b.jobs[messageID].postFailure(err)
|
||||
|
||||
delete(b.jobs, messageID)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user