GODT-213: Message Builder

This commit is contained in:
James Houlahan
2021-03-17 10:33:03 +01:00
parent 8db89a1a6c
commit 50550d42b4
42 changed files with 3041 additions and 816 deletions

View File

@ -44,13 +44,3 @@ func getAddresses(addrs []*mail.Address) (imapAddrs []*imap.Address) {
return
}
func formatAddressList(addrs []*mail.Address) (s string) {
for i, addr := range addrs {
if i > 0 {
s += ", "
}
s += addr.String()
}
return
}

View File

@ -1,78 +0,0 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package message
import (
"encoding/base64"
"fmt"
"io"
"mime/quotedprintable"
"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"
)
func WriteBody(w io.Writer, kr *crypto.KeyRing, m *pmapi.Message) error {
// Decrypt body.
if err := m.Decrypt(kr); err != nil && err != openpgperrors.ErrSignatureExpired {
return err
}
if m.MIMEType != pmapi.ContentTypeMultipartMixed {
// Encode it.
qp := quotedprintable.NewWriter(w)
if _, err := io.WriteString(qp, m.Body); err != nil {
return err
}
return qp.Close()
}
_, err := io.WriteString(w, m.Body)
return err
}
func WriteAttachmentBody(w io.Writer, kr *crypto.KeyRing, m *pmapi.Message, att *pmapi.Attachment, r io.Reader) (err error) {
// Decrypt it
var dr io.Reader
dr, err = att.Decrypt(r, kr)
if err == openpgperrors.ErrKeyIncorrect {
err = nil //nolint[wastedassing] Do not fail if attachment is encrypted with a different key.
dr = r
att.Name += ".gpg"
att.MIMEType = "application/pgp-encrypted" //nolint
} else if err != nil && err != openpgperrors.ErrSignatureExpired {
return fmt.Errorf("cannot decrypt attachment: %v", err)
}
// Don't encode message/rfc822 attachments; they should be embedded and preserved.
if att.MIMEType == rfc822Message {
if n, err := io.Copy(w, dr); err != nil {
return fmt.Errorf("cannot write attached message: %v (wrote %v bytes)", err, n)
}
return nil
}
// Encode it.
ww := textwrapper.NewRFC822(w)
bw := base64.NewEncoder(base64.StdEncoding, ww)
if n, err := io.Copy(bw, dr); err != nil {
return fmt.Errorf("cannot write attachment: %v (wrote %v bytes)", err, n)
}
return bw.Close()
}

View File

@ -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)
}

View File

@ -0,0 +1,43 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package message
import (
"crypto/sha256"
"encoding/hex"
)
type boundary struct {
val string
}
func newBoundary(seed string) *boundary {
return &boundary{val: seed}
}
func (bw *boundary) gen() string {
hash := sha256.New()
if _, err := hash.Write([]byte(bw.val)); err != nil {
panic(err)
}
bw.val = hex.EncodeToString(hash.Sum(nil))
return bw.val
}

View File

@ -0,0 +1,79 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package message
import (
"sync"
"github.com/pkg/errors"
)
type buildRes struct {
messageID string
literal []byte
err error
}
func newBuildResSuccess(messageID string, literal []byte) buildRes {
return buildRes{
messageID: messageID,
literal: literal,
}
}
func newBuildResFailure(messageID string, err error) buildRes {
return buildRes{
messageID: messageID,
err: err,
}
}
func startBuildWorkers(buildWorkers int) (chan fetchRes, chan buildRes) {
buildReqCh := make(chan fetchRes)
buildResCh := make(chan buildRes)
go func() {
defer close(buildResCh)
var wg sync.WaitGroup
wg.Add(buildWorkers)
for workerID := 0; workerID < buildWorkers; workerID++ {
go buildWorker(buildReqCh, buildResCh, &wg)
}
wg.Wait()
}()
return buildReqCh, buildResCh
}
func buildWorker(buildReqCh <-chan fetchRes, buildResCh chan<- buildRes, wg *sync.WaitGroup) {
defer wg.Done()
for req := range buildReqCh {
if kr, err := req.api.KeyRingForAddressID(req.msg.AddressID); err != nil {
buildResCh <- newBuildResFailure(req.msg.ID, errors.Wrap(ErrNoSuchKeyRing, err.Error()))
} else if literal, err := buildRFC822(kr, req.msg, req.atts, req.opts); err != nil {
buildResCh <- newBuildResFailure(req.msg.ID, err)
} else {
buildResCh <- newBuildResSuccess(req.msg.ID, literal)
}
}
}

View File

@ -0,0 +1,114 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package message
import (
"bytes"
"encoding/base64"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"net/textproto"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/emersion/go-textwrapper"
)
func BuildEncrypted(m *pmapi.Message, readers []io.Reader, kr *crypto.KeyRing) ([]byte, error) { //nolint[funlen]
b := &bytes.Buffer{}
// 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
}
// Write the body part.
bodyHeader := make(textproto.MIMEHeader)
bodyHeader.Set("Content-Type", m.MIMEType+"; charset=utf-8")
bodyHeader.Set("Content-Disposition", pmapi.DispositionInline)
bodyHeader.Set("Content-Transfer-Encoding", "7bit")
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
}
// 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
}
func WriteHeader(w io.Writer, h textproto.MIMEHeader) (err error) {
if err = http.Header(h).Write(w); err != nil {
return
}
_, err = io.WriteString(w, "\r\n")
return
}

135
pkg/message/build_fetch.go Normal file
View File

@ -0,0 +1,135 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package message
import (
"context"
"io/ioutil"
"sync"
"github.com/ProtonMail/proton-bridge/pkg/parallel"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
)
type fetchReq struct {
ctx context.Context
api Fetcher
messageID string
opts JobOptions
}
type fetchRes struct {
fetchReq
msg *pmapi.Message
atts [][]byte
err error
}
func newFetchResSuccess(req fetchReq, msg *pmapi.Message, atts [][]byte) fetchRes {
return fetchRes{
fetchReq: req,
msg: msg,
atts: atts,
}
}
func newFetchResFailure(req fetchReq, err error) fetchRes {
return fetchRes{
fetchReq: req,
err: err,
}
}
func startFetchWorkers(fetchWorkers, attachWorkers int) (chan fetchReq, chan fetchRes) {
fetchReqCh := make(chan fetchReq)
fetchResCh := make(chan fetchRes)
go func() {
defer close(fetchResCh)
var wg sync.WaitGroup
wg.Add(fetchWorkers)
for workerID := 0; workerID < fetchWorkers; workerID++ {
go fetchWorker(fetchReqCh, fetchResCh, attachWorkers, &wg)
}
wg.Wait()
}()
return fetchReqCh, fetchResCh
}
func fetchWorker(fetchReqCh <-chan fetchReq, fetchResCh chan<- fetchRes, attachWorkers int, wg *sync.WaitGroup) {
defer wg.Done()
for req := range fetchReqCh {
msg, atts, err := fetchMessage(req, attachWorkers)
if err != nil {
fetchResCh <- newFetchResFailure(req, err)
} else {
fetchResCh <- newFetchResSuccess(req, msg, atts)
}
}
}
func fetchMessage(req fetchReq, attachWorkers int) (*pmapi.Message, [][]byte, error) {
msg, err := req.api.GetMessage(req.messageID)
if err != nil {
return nil, nil, err
}
attList := make([]interface{}, len(msg.Attachments))
for i, att := range msg.Attachments {
attList[i] = att.ID
}
process := func(value interface{}) (interface{}, error) {
rc, err := req.api.GetAttachment(value.(string))
if err != nil {
return nil, err
}
b, err := ioutil.ReadAll(rc)
if err != nil {
return nil, err
}
if err := rc.Close(); err != nil {
return nil, err
}
return b, nil
}
attData := make([][]byte, len(msg.Attachments))
collect := func(idx int, value interface{}) error {
attData[idx] = value.([]byte)
return nil
}
if err := parallel.RunParallel(attachWorkers, attList, process, collect); err != nil {
return nil, nil, err
}
return msg, attData, nil
}

View File

@ -0,0 +1,331 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package message
import (
"bufio"
"bytes"
"encoding/base64"
"io"
"strings"
"testing"
"time"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/pkg/message/mocks"
"github.com/ProtonMail/proton-bridge/pkg/message/parser"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func newTestFetcher(
m *gomock.Controller,
kr *crypto.KeyRing,
msg *pmapi.Message,
attData ...[]byte,
) Fetcher {
f := mocks.NewMockFetcher(m)
f.EXPECT().GetMessage(msg.ID).Return(msg, nil)
for i, att := range msg.Attachments {
f.EXPECT().GetAttachment(att.ID).Return(newTestReadCloser(attData[i]), nil)
}
f.EXPECT().KeyRingForAddressID(msg.AddressID).Return(kr, nil)
return f
}
func newTestMessage(
t *testing.T,
kr *crypto.KeyRing,
messageID, addressID, mimeType, body string, // nolint[unparam]
date time.Time,
) *pmapi.Message {
enc, err := kr.Encrypt(crypto.NewPlainMessageFromString(body), kr)
require.NoError(t, err)
arm, err := enc.GetArmored()
require.NoError(t, err)
return &pmapi.Message{
ID: messageID,
AddressID: addressID,
MIMEType: mimeType,
Header: map[string][]string{
"Content-Type": {mimeType},
"Date": {date.In(time.UTC).Format(time.RFC1123Z)},
},
Body: arm,
Time: date.Unix(),
}
}
func addTestAttachment(
t *testing.T,
kr *crypto.KeyRing,
msg *pmapi.Message,
attachmentID, name, mimeType, disposition, data string,
) []byte {
enc, err := kr.EncryptAttachment(crypto.NewPlainMessageFromString(data), attachmentID+".bin")
require.NoError(t, err)
msg.Attachments = append(msg.Attachments, &pmapi.Attachment{
ID: attachmentID,
Name: name,
MIMEType: mimeType,
Header: map[string][]string{
"Content-Type": {mimeType},
"Content-Disposition": {disposition},
},
Disposition: disposition,
KeyPackets: base64.StdEncoding.EncodeToString(enc.GetBinaryKeyPacket()),
})
return enc.GetBinaryDataPacket()
}
type testReadCloser struct {
io.Reader
}
func newTestReadCloser(b []byte) *testReadCloser {
return &testReadCloser{Reader: bytes.NewReader(b)}
}
func (testReadCloser) Close() error {
return nil
}
type testSection struct {
t *testing.T
part *parser.Part
raw []byte
}
// NOTE: Each section is parsed individually --> cleaner test code but slower... improve this one day?
func section(t *testing.T, b []byte, section ...int) *testSection {
p, err := parser.New(bytes.NewReader(b))
assert.NoError(t, err)
part, err := p.Section(section)
require.NoError(t, err)
bs, err := NewBodyStructure(bytes.NewReader(b))
require.NoError(t, err)
raw, err := bs.GetSection(bytes.NewReader(b), append([]int{}, section...))
require.NoError(t, err)
return &testSection{
t: t,
part: part,
raw: raw,
}
}
func (s *testSection) expectBody(wantBody matcher) *testSection {
wantBody.match(s.t, string(s.part.Body))
return s
}
func (s *testSection) expectSection(wantSection matcher) *testSection { // nolint[unparam]
wantSection.match(s.t, string(s.raw))
return s
}
func (s *testSection) expectContentType(wantContentType matcher) *testSection {
mimeType, _, err := s.part.Header.ContentType()
require.NoError(s.t, err)
wantContentType.match(s.t, mimeType)
return s
}
func (s *testSection) expectContentTypeParam(key string, wantParam matcher) *testSection { // nolint[unparam]
_, params, err := s.part.Header.ContentType()
require.NoError(s.t, err)
wantParam.match(s.t, params[key])
return s
}
func (s *testSection) expectContentDisposition(wantDisposition matcher) *testSection {
disposition, _, err := s.part.Header.ContentDisposition()
require.NoError(s.t, err)
wantDisposition.match(s.t, disposition)
return s
}
func (s *testSection) expectContentDispositionParam(key string, wantParam matcher) *testSection { // nolint[unparam]
_, params, err := s.part.Header.ContentDisposition()
require.NoError(s.t, err)
wantParam.match(s.t, params[key])
return s
}
func (s *testSection) expectTransferEncoding(wantTransferEncoding matcher) *testSection {
wantTransferEncoding.match(s.t, s.part.Header.Get("Content-Transfer-Encoding"))
return s
}
func (s *testSection) expectDate(wantDate matcher) *testSection {
wantDate.match(s.t, s.part.Header.Get("Date"))
return s
}
func (s *testSection) expectHeader(key string, wantValue matcher) *testSection {
wantValue.match(s.t, s.part.Header.Get(key))
return s
}
func (s *testSection) expectDecodedHeader(key string, wantValue matcher) *testSection { // nolint[unparam]
dec, err := s.part.Header.Text(key)
require.NoError(s.t, err)
wantValue.match(s.t, dec)
return s
}
func (s *testSection) pubKey() *crypto.KeyRing {
key, err := crypto.NewKeyFromArmored(string(s.part.Body))
require.NoError(s.t, err)
kr, err := crypto.NewKeyRing(key)
require.NoError(s.t, err)
return kr
}
func (s *testSection) signature() *crypto.PGPSignature {
sig, err := crypto.NewPGPSignatureFromArmored(string(s.part.Body))
require.NoError(s.t, err)
return sig
}
type matcher interface {
match(*testing.T, string)
}
type isMatcher struct {
want string
}
func (matcher isMatcher) match(t *testing.T, have string) {
assert.Equal(t, matcher.want, have)
}
func is(want string) isMatcher {
return isMatcher{want: want}
}
func isMissing() isMatcher {
return isMatcher{}
}
type isNotMatcher struct {
notWant string
}
func (matcher isNotMatcher) match(t *testing.T, have string) {
assert.NotEqual(t, matcher.notWant, have)
}
func isNot(notWant string) isNotMatcher {
return isNotMatcher{notWant: notWant}
}
type containsMatcher struct {
contains string
}
func (matcher containsMatcher) match(t *testing.T, have string) {
assert.Contains(t, have, matcher.contains)
}
func contains(contains string) containsMatcher {
return containsMatcher{contains: contains}
}
type decryptsToMatcher struct {
kr *crypto.KeyRing
want string
}
func (matcher decryptsToMatcher) match(t *testing.T, have string) {
haveMsg, err := crypto.NewPGPMessageFromArmored(have)
require.NoError(t, err)
dec, err := matcher.kr.Decrypt(haveMsg, nil, crypto.GetUnixTime())
require.NoError(t, err)
assert.Equal(t, matcher.want, dec.GetString())
}
func decryptsTo(kr *crypto.KeyRing, want string) decryptsToMatcher {
return decryptsToMatcher{kr: kr, want: want}
}
type verifiesAgainstMatcher struct {
kr *crypto.KeyRing
sig *crypto.PGPSignature
}
func (matcher verifiesAgainstMatcher) match(t *testing.T, have string) {
assert.NoError(t, matcher.kr.VerifyDetached(
crypto.NewPlainMessage(bytes.TrimSuffix([]byte(have), []byte("\r\n"))),
matcher.sig,
crypto.GetUnixTime()),
)
}
func verifiesAgainst(kr *crypto.KeyRing, sig *crypto.PGPSignature) verifiesAgainstMatcher {
return verifiesAgainstMatcher{kr: kr, sig: sig}
}
type maxLineLengthMatcher struct {
wantMax int
}
func (matcher maxLineLengthMatcher) match(t *testing.T, have string) {
scanner := bufio.NewScanner(strings.NewReader(have))
for scanner.Scan() {
assert.Less(t, len(scanner.Text()), matcher.wantMax)
}
}
func hasMaxLineLength(wantMax int) maxLineLengthMatcher {
return maxLineLengthMatcher{wantMax: wantMax}
}

57
pkg/message/build_job.go Normal file
View File

@ -0,0 +1,57 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package message
type JobOptions struct {
IgnoreDecryptionErrors bool // Whether to ignore decryption errors and create a "custom message" instead.
SanitizeDate bool // Whether to replace all dates before 1970 with RFC822's birthdate.
AddInternalID bool // Whether to include MessageID as X-Pm-Internal-Id.
AddExternalID bool // Whether to include ExternalID as X-Pm-External-Id.
AddMessageDate bool // Whether to include message time as X-Pm-Date.
AddMessageIDReference bool // Whether to include the MessageID in References.
}
type BuildJob struct {
messageID string
literal []byte
err error
done chan struct{}
}
func newBuildJob(messageID string) *BuildJob {
return &BuildJob{
messageID: messageID,
done: make(chan struct{}),
}
}
func (job *BuildJob) GetResult() ([]byte, error) {
<-job.done
return job.literal, job.err
}
func (job *BuildJob) postSuccess(literal []byte) {
job.literal = literal
close(job.done)
}
func (job *BuildJob) postFailure(err error) {
job.err = err
close(job.done)
}

410
pkg/message/build_rfc822.go Normal file
View File

@ -0,0 +1,410 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package message
import (
"bytes"
"encoding/base64"
"io/ioutil"
"mime"
"net/mail"
"strings"
"time"
"unicode/utf8"
"github.com/ProtonMail/go-rfc5322"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/emersion/go-message"
"github.com/pkg/errors"
)
func buildRFC822(kr *crypto.KeyRing, msg *pmapi.Message, attData [][]byte, opts JobOptions) ([]byte, error) {
switch {
case len(msg.Attachments) > 0:
return buildMultipartRFC822(kr, msg, attData, opts)
case msg.MIMEType == "multipart/mixed":
return buildEncryptedRFC822(kr, msg, opts)
default:
return buildSimpleRFC822(kr, msg, opts)
}
}
func buildSimpleRFC822(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 buildMultipartRFC822(kr, msg, nil, opts)
}
hdr := getTextPartHeader(getMessageHeader(msg, opts), dec, msg.MIMEType)
buf := new(bytes.Buffer)
w, err := message.CreateWriter(buf, hdr)
if err != nil {
return nil, err
}
if _, err := w.Write(dec); err != nil {
return nil, err
}
if err := w.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func buildMultipartRFC822(
kr *crypto.KeyRing,
msg *pmapi.Message,
attData [][]byte,
opts JobOptions,
) ([]byte, error) {
boundary := newBoundary(msg.ID)
hdr := getMessageHeader(msg, opts)
hdr.SetContentType("multipart/mixed", map[string]string{"boundary": boundary.gen()})
buf := new(bytes.Buffer)
w, err := message.CreateWriter(buf, hdr)
if err != nil {
return nil, err
}
var (
inlineAtts []*pmapi.Attachment
inlineData [][]byte
attachAtts []*pmapi.Attachment
attachData [][]byte
)
for i, att := range msg.Attachments {
if att.Disposition == pmapi.DispositionInline {
inlineAtts = append(inlineAtts, att)
inlineData = append(inlineData, attData[i])
} else {
attachAtts = append(attachAtts, att)
attachData = append(attachData, attData[i])
}
}
if len(inlineAtts) > 0 {
if err := writeRelatedParts(w, kr, boundary, msg, inlineAtts, inlineData, opts); err != nil {
return nil, err
}
} else if err := writeTextPart(w, kr, msg, opts); err != nil {
return nil, err
}
for i, att := range attachAtts {
if err := writeAttachmentPart(w, kr, att, attachData[i], opts); err != nil {
return nil, err
}
}
if err := w.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func writeTextPart(
w *message.Writer,
kr *crypto.KeyRing,
msg *pmapi.Message,
opts JobOptions,
) error {
dec, err := msg.Decrypt(kr)
if err != nil {
if !opts.IgnoreDecryptionErrors {
return errors.Wrap(ErrDecryptionFailed, err.Error())
}
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()
}
func writeAttachmentPart(
w *message.Writer,
kr *crypto.KeyRing,
att *pmapi.Attachment,
attData []byte,
opts JobOptions,
) error {
kps, err := base64.StdEncoding.DecodeString(att.KeyPackets)
if err != nil {
return err
}
msg := crypto.NewPGPSplitMessage(kps, attData).GetPGPMessage()
dec, err := kr.Decrypt(msg, nil, crypto.GetUnixTime())
if err != nil {
if !opts.IgnoreDecryptionErrors {
return errors.Wrap(ErrDecryptionFailed, err.Error())
}
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()
}
func writeRelatedParts(
w *message.Writer,
kr *crypto.KeyRing,
boundary *boundary,
msg *pmapi.Message,
atts []*pmapi.Attachment,
attData [][]byte,
opts JobOptions,
) error {
hdr := message.Header{}
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 err
}
}
return rel.Close()
}
func buildEncryptedRFC822(kr *crypto.KeyRing, msg *pmapi.Message, opts JobOptions) ([]byte, error) {
hdr := getMessageHeader(msg, opts)
hdr.SetContentType("multipart/mixed", map[string]string{"boundary": newBoundary(msg.ID).gen()})
buf := new(bytes.Buffer)
w, err := message.CreateWriter(buf, hdr)
if err != nil {
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 {
return nil, err
}
if err := part.Close(); err != nil {
return nil, err
}
if err := w.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func getMessageHeader(msg *pmapi.Message, opts JobOptions) message.Header { // nolint[funlen]
hdr := toMessageHeader(msg.Header)
// SetText will RFC2047-encode.
if msg.Subject != "" {
hdr.SetText("Subject", msg.Subject)
}
// mail.Address.String() will RFC2047-encode if necessary.
if msg.Sender != nil {
hdr.Set("From", msg.Sender.String())
}
if len(msg.ReplyTos) > 0 {
hdr.Set("Reply-To", toAddressList(msg.ReplyTos))
}
if len(msg.ToList) > 0 {
hdr.Set("To", toAddressList(msg.ToList))
}
if len(msg.CCList) > 0 {
hdr.Set("Cc", toAddressList(msg.CCList))
}
if len(msg.BCCList) > 0 {
hdr.Set("Bcc", toAddressList(msg.BCCList))
}
// Set Message-Id from ExternalID or ID if it's not already set.
if hdr.Get("Message-Id") == "" {
if msg.ExternalID != "" {
hdr.Set("Message-Id", "<"+msg.ExternalID+">")
} else {
hdr.Set("Message-Id", "<"+msg.ID+"@"+pmapi.InternalIDDomain+">")
}
}
// Sanitize the date; it needs to have a valid unix timestamp.
if opts.SanitizeDate {
if date, err := rfc5322.ParseDateTime(hdr.Get("Date")); err != nil || date.Before(time.Unix(0, 0)) {
if msgTime := time.Unix(msg.Time, 0); msgTime.After(time.Unix(0, 0)) {
hdr.Set("Date", msgTime.In(time.UTC).Format(time.RFC1123Z))
} else {
// No message should realistically be older than RFC822 itself.
hdr.Set("Date", time.Date(1982, 8, 13, 0, 0, 0, 0, time.UTC).Format(time.RFC1123Z))
}
// We clobbered the date so we save it under X-Original-Date.
hdr.Set("X-Original-Date", date.In(time.UTC).Format(time.RFC1123Z))
}
}
// Set our internal ID if requested.
// This is important for us to detect whether APPENDed things are actually "move like outlook".
if opts.AddInternalID {
hdr.Set("X-Pm-Internal-Id", msg.ID)
}
// Set our external ID if requested.
// This was useful during debugging of applemail recovered messages; doesn't help with any behaviour.
if opts.AddExternalID {
hdr.Set("X-Pm-External-Id", "<"+msg.ExternalID+">")
}
// Set our server date if requested.
// Can be useful to see how long it took for a message to arrive.
if opts.AddMessageDate {
hdr.Set("X-Pm-Date", time.Unix(msg.Time, 0).In(time.UTC).Format(time.RFC1123Z))
}
// Include the message ID in the references (supposedly this somehow improves outlook support...).
if opts.AddMessageIDReference {
if references := hdr.Get("References"); !strings.Contains(references, msg.ID) {
hdr.Set("References", references+" <"+msg.ID+"@"+pmapi.InternalIDDomain+">")
}
}
return hdr
}
func getTextPartHeader(hdr message.Header, body []byte, mimeType string) message.Header {
params := make(map[string]string)
if utf8.Valid(body) {
params["charset"] = "utf-8"
}
hdr.SetContentType(mimeType, params)
// Use quoted-printable for all text/... parts
hdr.Set("Content-Transfer-Encoding", "quoted-printable")
return hdr
}
func getAttachmentPartHeader(att *pmapi.Attachment) message.Header {
hdr := toMessageHeader(mail.Header(att.Header))
// All attachments have a content type.
hdr.SetContentType(att.MIMEType, map[string]string{"name": mime.QEncoding.Encode("utf-8", att.Name)})
// All attachments have a content disposition.
hdr.SetContentDisposition(att.Disposition, map[string]string{"filename": mime.QEncoding.Encode("utf-8", att.Name)})
// Use base64 for all attachments except embedded RFC822 messages.
if att.MIMEType != "message/rfc822" {
hdr.Set("Content-Transfer-Encoding", "base64")
}
return hdr
}
func toMessageHeader(hdr mail.Header) message.Header {
var res message.Header
for key, val := range hdr {
for _, val := range val {
res.Add(key, val)
}
}
return res
}
func toAddressList(addrs []*mail.Address) string {
res := make([]string, len(addrs))
for i, addr := range addrs {
res[i] = addr.String()
}
return strings.Join(res, ", ")
}

View File

@ -0,0 +1,94 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package message
import (
"fmt"
"mime"
"github.com/ProtonMail/gopenpgp/v2/constants"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/emersion/go-message"
)
func writeCustomTextPart(
w *message.Writer,
msg *pmapi.Message,
decError error,
) error {
enc, err := crypto.NewPGPMessageFromArmored(msg.Body)
if err != nil {
return err
}
arm, err := enc.GetArmoredWithCustomHeaders(
fmt.Sprintf("This message could not be decrypted: %v", decError),
constants.ArmorHeaderVersion,
)
if err != nil {
return err
}
var hdr message.Header
hdr.SetContentType(msg.MIMEType, nil)
part, err := w.CreatePart(hdr)
if err != nil {
return err
}
if _, err := part.Write([]byte(arm)); err != nil {
return err
}
return nil
}
func writeCustomAttachmentPart(
w *message.Writer,
att *pmapi.Attachment,
msg *crypto.PGPMessage,
decError error,
) error {
arm, err := msg.GetArmoredWithCustomHeaders(
fmt.Sprintf("This attachment could not be decrypted: %v", decError),
constants.ArmorHeaderVersion,
)
if err != nil {
return err
}
var hdr message.Header
hdr.SetContentType("application/pgp-encrypted", map[string]string{"name": mime.QEncoding.Encode("utf-8", att.Name+".pgp")})
hdr.SetContentDisposition(att.Disposition, map[string]string{"filename": mime.QEncoding.Encode("utf-8", att.Name+".pgp")})
part, err := w.CreatePart(hdr)
if err != nil {
return err
}
if _, err := part.Write([]byte(arm)); err != nil {
return err
}
return part.Close()
}

1113
pkg/message/build_test.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -42,16 +42,16 @@ func GetHeader(msg *pmapi.Message) textproto.MIMEHeader { //nolint[funlen]
h.Set("From", pmmime.EncodeHeader(msg.Sender.String()))
}
if len(msg.ReplyTos) > 0 {
h.Set("Reply-To", pmmime.EncodeHeader(formatAddressList(msg.ReplyTos)))
h.Set("Reply-To", pmmime.EncodeHeader(toAddressList(msg.ReplyTos)))
}
if len(msg.ToList) > 0 {
h.Set("To", pmmime.EncodeHeader(formatAddressList(msg.ToList)))
h.Set("To", pmmime.EncodeHeader(toAddressList(msg.ToList)))
}
if len(msg.CCList) > 0 {
h.Set("Cc", pmmime.EncodeHeader(formatAddressList(msg.CCList)))
h.Set("Cc", pmmime.EncodeHeader(toAddressList(msg.CCList)))
}
if len(msg.BCCList) > 0 {
h.Set("Bcc", pmmime.EncodeHeader(formatAddressList(msg.BCCList)))
h.Set("Bcc", pmmime.EncodeHeader(toAddressList(msg.BCCList)))
}
// Add or rewrite date related fields.
@ -91,7 +91,7 @@ func GetHeader(msg *pmapi.Message) textproto.MIMEHeader { //nolint[funlen]
func SetBodyContentFields(h *textproto.MIMEHeader, m *pmapi.Message) {
h.Set("Content-Type", m.MIMEType+"; charset=utf-8")
h.Set("Content-Disposition", "inline")
h.Set("Content-Disposition", pmapi.DispositionInline)
h.Set("Content-Transfer-Encoding", "quoted-printable")
}
@ -120,8 +120,8 @@ func GetAttachmentHeader(att *pmapi.Attachment, buildForIMAP bool) textproto.MIM
encodedName := pmmime.EncodeHeader(att.Name)
disposition := "attachment" //nolint[goconst]
if strings.Contains(att.Header.Get("Content-Disposition"), "inline") {
disposition = "inline"
if strings.Contains(att.Header.Get("Content-Disposition"), pmapi.DispositionInline) {
disposition = pmapi.DispositionInline
}
h := make(textproto.MIMEHeader)

View File

@ -46,7 +46,7 @@ func GetRelatedBoundary(m *pmapi.Message) string {
func SeparateInlineAttachments(m *pmapi.Message) (atts, inlines []*pmapi.Attachment) {
for _, att := range m.Attachments {
if strings.Contains(att.Header.Get("Content-Disposition"), "inline") {
if strings.Contains(att.Header.Get("Content-Disposition"), pmapi.DispositionInline) {
inlines = append(inlines, att)
} else {
atts = append(atts, att)

View File

@ -0,0 +1,82 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/ProtonMail/proton-bridge/pkg/message (interfaces: Fetcher)
// Package mocks is a generated GoMock package.
package mocks
import (
io "io"
reflect "reflect"
crypto "github.com/ProtonMail/gopenpgp/v2/crypto"
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
gomock "github.com/golang/mock/gomock"
)
// MockFetcher is a mock of Fetcher interface
type MockFetcher struct {
ctrl *gomock.Controller
recorder *MockFetcherMockRecorder
}
// MockFetcherMockRecorder is the mock recorder for MockFetcher
type MockFetcherMockRecorder struct {
mock *MockFetcher
}
// NewMockFetcher creates a new mock instance
func NewMockFetcher(ctrl *gomock.Controller) *MockFetcher {
mock := &MockFetcher{ctrl: ctrl}
mock.recorder = &MockFetcherMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockFetcher) EXPECT() *MockFetcherMockRecorder {
return m.recorder
}
// GetAttachment mocks base method
func (m *MockFetcher) GetAttachment(arg0 string) (io.ReadCloser, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetAttachment", arg0)
ret0, _ := ret[0].(io.ReadCloser)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetAttachment indicates an expected call of GetAttachment
func (mr *MockFetcherMockRecorder) GetAttachment(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAttachment", reflect.TypeOf((*MockFetcher)(nil).GetAttachment), arg0)
}
// GetMessage mocks base method
func (m *MockFetcher) GetMessage(arg0 string) (*pmapi.Message, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetMessage", arg0)
ret0, _ := ret[0].(*pmapi.Message)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// GetMessage indicates an expected call of GetMessage
func (mr *MockFetcherMockRecorder) GetMessage(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetMessage", reflect.TypeOf((*MockFetcher)(nil).GetMessage), arg0)
}
// KeyRingForAddressID mocks base method
func (m *MockFetcher) KeyRingForAddressID(arg0 string) (*crypto.KeyRing, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "KeyRingForAddressID", arg0)
ret0, _ := ret[0].(*crypto.KeyRing)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// KeyRingForAddressID indicates an expected call of KeyRingForAddressID
func (mr *MockFetcherMockRecorder) KeyRingForAddressID(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KeyRingForAddressID", reflect.TypeOf((*MockFetcher)(nil).KeyRingForAddressID), arg0)
}

View File

@ -536,7 +536,7 @@ func parseAttachment(h message.Header) (*pmapi.Attachment, error) {
if h.Has("Content-Disposition") {
if disp, _, err := h.ContentDisposition(); err != nil {
return nil, err
} else if disp == "inline" {
} else if disp == pmapi.DispositionInline {
att.ContentID = strings.Trim(h.Get("Content-Id"), " <>")
}
} else if h.Has("Content-Id") {

View File

@ -201,7 +201,7 @@ func (bs *BodyStructure) parseAllChildSections(r io.Reader, currentPath []int, s
mediaType, params, _ := pmmime.ParseMediaType(info.Header.Get("Content-Type"))
// If multipart, call getAllParts, else read to count lines.
if (strings.HasPrefix(mediaType, "multipart/") || mediaType == rfc822Message) && params["boundary"] != "" {
if (strings.HasPrefix(mediaType, "multipart/") || mediaType == "message/rfc822") && params["boundary"] != "" {
newPath := append(currentPath, 1)
var br *boundaryReader

View File

@ -0,0 +1,33 @@
Content-Type: multipart/mixed; boundary="u5NoTcx3NkhqapFjjYFKJZdxCaEWvrsGw";
protected-headers="v1"
Subject: html no pubkey no sign
From: "pm.bridge.qa" <pm.bridge.qa@gmail.com>
To: schizofrenic@pm.me
Message-ID: <c38ad850-0916-e290-ee1c-326c3ff9fb5f@gmail.com>
--u5NoTcx3NkhqapFjjYFKJZdxCaEWvrsGw
Content-Type: text/html; charset=utf-8
Content-Language: en-US
Content-Transfer-Encoding: quoted-printable
<html>
<head>
<meta http-equiv=3D"content-type" content=3D"text/html; charset=3DUTF=
-8">
</head>
<body>
<ul>
<li><i>What do you call a poor Santa Claus?</i> <b>St.
Nickel-less.</b></li>
<li><i>Where do boats go when they're sick?</i> <b>To the boat
doc.</b><br>
</li>
</ul>
<p><br>
</p>
</body>
</html>
--u5NoTcx3NkhqapFjjYFKJZdxCaEWvrsGw--

View File

@ -0,0 +1,17 @@
Content-Type: multipart/mixed; boundary="unlHEst6hn6dMAzATXJvy5dCLgUfF9Vvs";
protected-headers="v1"
Subject: plain no pubkey no sign
From: "pm.bridge.qa" <pm.bridge.qa@gmail.com>
To: schizofrenic@pm.me
Message-ID: <564b9c7c-91eb-6508-107a-35108f383a44@gmail.com>
--unlHEst6hn6dMAzATXJvy5dCLgUfF9Vvs
Content-Type: text/plain; charset=utf-8; format=flowed
Content-Transfer-Encoding: quoted-printable
Content-Language: en-US
Where do fruits go on vacation? Pear-is!
--unlHEst6hn6dMAzATXJvy5dCLgUfF9Vvs--

View File

@ -0,0 +1,116 @@
Content-Type: multipart/signed; micalg=pgp-sha256;
protocol="application/pgp-signature";
boundary="pavrbLYh8Q4RWBboYnVxY3mNBBzan1Zz4"
This is an OpenPGP/MIME signed message (RFC 4880 and 3156)
--pavrbLYh8Q4RWBboYnVxY3mNBBzan1Zz4
Content-Type: multipart/mixed; boundary="avFoFILZo8SdHM1Pc1OUviN4UKQh16HyR";
protected-headers="v1"
Subject: simple html body
From: "pm.bridge.qa" <pm.bridge.qa@gmail.com>
To: schizofrenic@pm.me
Message-ID: <d9c99685-4e1c-8f95-8b68-c6b0fcfd62ef@gmail.com>
--avFoFILZo8SdHM1Pc1OUviN4UKQh16HyR
Content-Type: multipart/mixed;
boundary="------------9EAE2E1A715ACB9849E5C4E3"
Content-Language: en-US
This is a multi-part message in MIME format.
--------------9EAE2E1A715ACB9849E5C4E3
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: quoted-printable
<html>
<head>
<meta http-equiv=3D"content-type" content=3D"text/html; charset=3DUTF=
-8">
</head>
<body>
And this is HTML<br>
<ul>
<li><b>Do I enjoy making courthouse puns?</b> Guilty.=E2=80=94 <i>@=
baddadjokes</i></li>
<li><b>Can February March?</b> No, but April May. =E2=80=94<i>@Bear=
dedMOGuy</i></li>
</ul>
</body>
</html>
--------------9EAE2E1A715ACB9849E5C4E3
Content-Type: application/pgp-keys;
name="OpenPGP_0x161C0875822359F7.asc"
Content-Transfer-Encoding: quoted-printable
Content-Disposition: attachment;
filename="OpenPGP_0x161C0875822359F7.asc"
-----BEGIN PGP PUBLIC KEY BLOCK-----
xsBNBFxlUPwBCACx954Ey4SD88f8DSKFw9BaZNXrNwYxNYSgqaqOGHQ0WllF3mstEhTfuxxCZ=
pDh
I5IhWCXUNxanzsFkn88mRDwFRVl2sf2aAG4/P/p1381oh2kd0UElMRQaQGzoCadQMaQOL9WYT=
f4S
PWSCzjrPyKgjq5FbqjbF/ndu376na9L+tnsEXyL6RrI6aZhjWG73xlqxS65dzTIYzsyM/P97x=
Snd
NvlvWtGvLlpFkzxfAEGpVzfOYVYFKoc8rGmUDwrDWYfk5JczRDDogJnY+BNMZf9pjSqk6rTyB=
OfN
H5fpU8r7A5Q7l+HVakvMUQ9DzDWJtg2ru1Y8hexnJOF68avO4+a1ABEBAAHNKEJyaWRnZSBLe=
XUt
RWh5aiA8cG0uYnJpZGdlLnFhQGdtYWlsLmNvbT7CwJQEEwEIAD4CGwMFCwkIBwIGFQoJCAsCB=
BYC
AwECHgECF4AWIQRc5gl5cC8oW/Mo+bEWHAh1giNZ9wUCYC32ygUJB4sMzgAKCRAWHAh1giNZ9=
/K8
B/4qs84Ii/zKH+q+C8vwO4jUJkOM73qD0pgB7zBs651zWbpgopyol1YUKNpFaHlx/Qch7RDI7=
Vcz
1+60/KZJSJR19/N2EDVbCUdh8ueioUp9X/218YWV2TRJNxTnljd4FAn7smZnXuP1TsLjQ6sKO=
V0U
u6JoiG6LZFXqDgxYpA++58Rkl6xaY6R71VkmVQlbEKtubX9AjHydq97Y+Jvn11XzWZaKhv4L7=
6Pa
4tMKXvvrKh1oywMmh6mZJo+5ZA/ABTkr45cwlTPYqGTS9+uvOHt+PH/oYwwJB4ls2cIAUldSj=
TVQ
IsseYz3LlbcCfKJiiCFxeHOQXA5J6zNLKOT58TsczsBNBFxlUPwBCADh2HsX23yVnJt9fxFz3=
D07
kCBNvu4HQfps6h1rgNxGhE32VmpESHebvIB5xjL6xKbIqqRa3x/7KDVBNJvca0gUsqEt5kzYF=
88F
yf2NBcejpIbcP7BS/g+C6KOowYj+Et26T6GdwFXExUcl80JvoX8yHQOfvJpdiBRbjyB8UqfCa=
knm
3c7dNuXmhflz/w3aBj32q9ZyGqA1NpHCpLyVAlvSNQ/pat/rGUCPZ9duw4KhUUqEmatQPVFPk=
utT
ouEZQbMK+i+chOH3AsKCuNDfvCDwirnsSqIJmAgl1lC4de+bsWYCMqN9ei99hOCRUyhZ3g3sr=
8RB
owVAdcvjZxeIDKALABEBAAHCwHwEGAEIACYCGwwWIQRc5gl5cC8oW/Mo+bEWHAh1giNZ9wUCY=
C32
lAUJB4sMmAAKCRAWHAh1giNZ9+Y2B/9rTKZaKviae+ummXNumXcrKvbkAAvfuLpKUn53FlQLm=
L6H
jB++lJnPWvVSzdZxdv8FiPP3d632XHKUrkQRQM/9byRDXDommi7Qttx7YCkhd4JLVYqJqpnAQ=
xI5
RMkXiZNWyr1lz8JOM1XvDk1M7sJwPMWews8VOIE03E1nt7AsQGnvHtadgEnQaufrYNX3hFA8S=
osO
HSnedcys6yrzCSIGCqCD9VHbnMtS4DOv0XJGh2hwc8omzH0KZA517dyKBorJRwadcVauGXDKx=
Etv
Im4rl94PR/3An1Mj6HeeVVpLqDQ5Jb9J90BahWeQ53FzRa4EQzYCw0nLnxcsT1ZEEP5u
=3Dv/1p
-----END PGP PUBLIC KEY BLOCK-----
--------------9EAE2E1A715ACB9849E5C4E3--
--avFoFILZo8SdHM1Pc1OUviN4UKQh16HyR--
--pavrbLYh8Q4RWBboYnVxY3mNBBzan1Zz4
Content-Type: application/pgp-signature; name="OpenPGP_signature.asc"
Content-Description: OpenPGP digital signature
Content-Disposition: attachment; filename="OpenPGP_signature"
-----BEGIN PGP SIGNATURE-----
wsB5BAABCAAjFiEEXOYJeXAvKFvzKPmxFhwIdYIjWfcFAmBa9hAFAwAAAAAACgkQFhwIdYIjWffL
1AgApF18AVOPEm9y5R+d0NQmxqhSwAtvaqCwqQpG3mArIYK3Y0zrDkPQZZl/3emW8LWht7ZyYCAb
NZo7HoYxjLy3yxAOPUl/Pc0nJpEqk/wAZT58yOnzv8DU5Q9o+444FfTMJpcrcH/M5cXYyqRtVhas
k5wu5u2DEgSO3Kj/5l7lThb+CUgRC6wSiOuUkqGEWLiAguCdd88XDkLMbwrDnOu3PbhcA8o1msns
PfkBdq3mFjp4M8M4ha+D2MxmV6tBv1E7snWf/spBVb9fHIa7zI4ZS6shpzGHCnJarO0Jco0Qh3IZ
ZVfwhtJeFsmdqSm6DLvCmQWAYk2fDOZDMVKqe9IbUA==
=pkS0
-----END PGP SIGNATURE-----
--pavrbLYh8Q4RWBboYnVxY3mNBBzan1Zz4--

View File

@ -0,0 +1,58 @@
Content-Type: multipart/signed; micalg=pgp-sha256;
protocol="application/pgp-signature";
boundary="YxKjBoCQD3a9PdAWo8ztsilFlSghrT8M5"
This is an OpenPGP/MIME signed message (RFC 4880 and 3156)
--YxKjBoCQD3a9PdAWo8ztsilFlSghrT8M5
Content-Type: multipart/mixed; boundary="6GLjuOzexqUw1CoA6CFjmA6r51g9FOPK7";
protected-headers="v1"
Subject: html body no pubkey
From: "pm.bridge.qa" <pm.bridge.qa@gmail.com>
To: schizofrenic@pm.me
Message-ID: <5e22f83a-c4f0-d61a-55c8-8230854dc052@gmail.com>
--6GLjuOzexqUw1CoA6CFjmA6r51g9FOPK7
Content-Type: text/html; charset=utf-8
Content-Language: en-US
Content-Transfer-Encoding: quoted-printable
<html>
<head>
<meta http-equiv=3D"content-type" content=3D"text/html; charset=3DUTF=
-8">
</head>
<body>
Behold another <font color=3D"#ee24cc">HTML</font><br>
<ul>
<li><b>I only know 25 letters of the alphabet.</b> <b>I don't
know y.</b></li>
<li><b>What did one wall say to the other?</b><i> I'll meet you at
the corner.</i></li>
<li><b>What did the zero say to the eight?</b> <i>Damn, that belt
looks good on you.</i><br>
</li>
</ul>
</body>
</html>
--6GLjuOzexqUw1CoA6CFjmA6r51g9FOPK7--
--YxKjBoCQD3a9PdAWo8ztsilFlSghrT8M5
Content-Type: application/pgp-signature; name="OpenPGP_signature.asc"
Content-Description: OpenPGP digital signature
Content-Disposition: attachment; filename="OpenPGP_signature"
-----BEGIN PGP SIGNATURE-----
wsB5BAABCAAjFiEEXOYJeXAvKFvzKPmxFhwIdYIjWfcFAmBa+RsFAwAAAAAACgkQFhwIdYIjWfcK
aQf/a9w4OwdyFerAW5Y45SdjAOA7WKUbm0gnrifbM2zk03bMEsdgfJQawC1p0hVyUCeqFYNJ9JQ4
JF5/+7iWEe6oRFp3nW3LbBNr8wu3iN/dp5AWjTqnzx9VXLcvEryV/FJXwMUngO6z0eNVlxjdDFH/
ucomItcmXFmfDx68ghLkumyWwX4SDfd/W70Wqi1f35wLBjfVIeFik4AS0bmpGFfMt1MKHrgirn2S
+9sKPBiTQ+EFGK9V1wFrrDFleLDDE6oTMl75OUmY1Rr0y9q9jmws3cciEFYT3hTV9LNSwV9hMhZZ
IEKAzLTy6nYnVltYkFC1ggwAVouq4o6Bcw/5bUt2fA==
=lk/3
-----END PGP SIGNATURE-----
--YxKjBoCQD3a9PdAWo8ztsilFlSghrT8M5--

View File

@ -0,0 +1,103 @@
Content-Type: multipart/signed; micalg=pgp-sha256;
protocol="application/pgp-signature";
boundary="x4FrOFG2PnNJvlbzxe80NPwxzh2yUHABp"
This is an OpenPGP/MIME signed message (RFC 4880 and 3156)
--x4FrOFG2PnNJvlbzxe80NPwxzh2yUHABp
Content-Type: multipart/mixed; boundary="bBln6dwDJTLkin5LPHkHBQudqRLwIzTUH";
protected-headers="v1"
Subject: simple plaintext body
From: "pm.bridge.qa" <pm.bridge.qa@gmail.com>
To: schizofrenic@pm.me
Message-ID: <adb5ac5d-b8f6-c9a3-5cc0-0fb2e9677512@gmail.com>
--bBln6dwDJTLkin5LPHkHBQudqRLwIzTUH
Content-Type: multipart/mixed;
boundary="------------1B34C666A4C2FB03E0324F1A"
Content-Language: en-US
This is a multi-part message in MIME format.
--------------1B34C666A4C2FB03E0324F1A
Content-Type: text/plain; charset=utf-8; format=flowed
Content-Transfer-Encoding: quoted-printable
Why don't crabs give to charity? Because they're shellfish.
--------------1B34C666A4C2FB03E0324F1A
Content-Type: application/pgp-keys;
name="OpenPGP_0x161C0875822359F7.asc"
Content-Transfer-Encoding: quoted-printable
Content-Disposition: attachment;
filename="OpenPGP_0x161C0875822359F7.asc"
-----BEGIN PGP PUBLIC KEY BLOCK-----
xsBNBFxlUPwBCACx954Ey4SD88f8DSKFw9BaZNXrNwYxNYSgqaqOGHQ0WllF3mstEhTfuxxCZ=
pDh
I5IhWCXUNxanzsFkn88mRDwFRVl2sf2aAG4/P/p1381oh2kd0UElMRQaQGzoCadQMaQOL9WYT=
f4S
PWSCzjrPyKgjq5FbqjbF/ndu376na9L+tnsEXyL6RrI6aZhjWG73xlqxS65dzTIYzsyM/P97x=
Snd
NvlvWtGvLlpFkzxfAEGpVzfOYVYFKoc8rGmUDwrDWYfk5JczRDDogJnY+BNMZf9pjSqk6rTyB=
OfN
H5fpU8r7A5Q7l+HVakvMUQ9DzDWJtg2ru1Y8hexnJOF68avO4+a1ABEBAAHNKEJyaWRnZSBLe=
XUt
RWh5aiA8cG0uYnJpZGdlLnFhQGdtYWlsLmNvbT7CwJQEEwEIAD4CGwMFCwkIBwIGFQoJCAsCB=
BYC
AwECHgECF4AWIQRc5gl5cC8oW/Mo+bEWHAh1giNZ9wUCYC32ygUJB4sMzgAKCRAWHAh1giNZ9=
/K8
B/4qs84Ii/zKH+q+C8vwO4jUJkOM73qD0pgB7zBs651zWbpgopyol1YUKNpFaHlx/Qch7RDI7=
Vcz
1+60/KZJSJR19/N2EDVbCUdh8ueioUp9X/218YWV2TRJNxTnljd4FAn7smZnXuP1TsLjQ6sKO=
V0U
u6JoiG6LZFXqDgxYpA++58Rkl6xaY6R71VkmVQlbEKtubX9AjHydq97Y+Jvn11XzWZaKhv4L7=
6Pa
4tMKXvvrKh1oywMmh6mZJo+5ZA/ABTkr45cwlTPYqGTS9+uvOHt+PH/oYwwJB4ls2cIAUldSj=
TVQ
IsseYz3LlbcCfKJiiCFxeHOQXA5J6zNLKOT58TsczsBNBFxlUPwBCADh2HsX23yVnJt9fxFz3=
D07
kCBNvu4HQfps6h1rgNxGhE32VmpESHebvIB5xjL6xKbIqqRa3x/7KDVBNJvca0gUsqEt5kzYF=
88F
yf2NBcejpIbcP7BS/g+C6KOowYj+Et26T6GdwFXExUcl80JvoX8yHQOfvJpdiBRbjyB8UqfCa=
knm
3c7dNuXmhflz/w3aBj32q9ZyGqA1NpHCpLyVAlvSNQ/pat/rGUCPZ9duw4KhUUqEmatQPVFPk=
utT
ouEZQbMK+i+chOH3AsKCuNDfvCDwirnsSqIJmAgl1lC4de+bsWYCMqN9ei99hOCRUyhZ3g3sr=
8RB
owVAdcvjZxeIDKALABEBAAHCwHwEGAEIACYCGwwWIQRc5gl5cC8oW/Mo+bEWHAh1giNZ9wUCY=
C32
lAUJB4sMmAAKCRAWHAh1giNZ9+Y2B/9rTKZaKviae+ummXNumXcrKvbkAAvfuLpKUn53FlQLm=
L6H
jB++lJnPWvVSzdZxdv8FiPP3d632XHKUrkQRQM/9byRDXDommi7Qttx7YCkhd4JLVYqJqpnAQ=
xI5
RMkXiZNWyr1lz8JOM1XvDk1M7sJwPMWews8VOIE03E1nt7AsQGnvHtadgEnQaufrYNX3hFA8S=
osO
HSnedcys6yrzCSIGCqCD9VHbnMtS4DOv0XJGh2hwc8omzH0KZA517dyKBorJRwadcVauGXDKx=
Etv
Im4rl94PR/3An1Mj6HeeVVpLqDQ5Jb9J90BahWeQ53FzRa4EQzYCw0nLnxcsT1ZEEP5u
=3Dv/1p
-----END PGP PUBLIC KEY BLOCK-----
--------------1B34C666A4C2FB03E0324F1A--
--bBln6dwDJTLkin5LPHkHBQudqRLwIzTUH--
--x4FrOFG2PnNJvlbzxe80NPwxzh2yUHABp
Content-Type: application/pgp-signature; name="OpenPGP_signature.asc"
Content-Description: OpenPGP digital signature
Content-Disposition: attachment; filename="OpenPGP_signature"
-----BEGIN PGP SIGNATURE-----
wsB5BAABCAAjFiEEXOYJeXAvKFvzKPmxFhwIdYIjWfcFAmBa9YIFAwAAAAAACgkQFhwIdYIjWfem
vQgAjUMAaxL7D6fRtFBqLjdQGr7PkDBigeQD9ax17CJFld7Zfo2dAYUzYJRi0HP0Kn1YCSBppF0w
5/P8458H2sqfPC32ptbDCZ/seL0Rpt/gRx6yufbz7wQC0iUZxqxBq2Ox9PGZYSCrTO837lAVYxUo
aMnDL/K9ohAGIyTZVv31z+r3LLWQsFpfpB5hJFqsjQXA9IGKSQIkWbaeE+0wveJSwqxdTwYvsHs2
xjBw+s8tRHO/whP4pvzL185fGsHAb8x9a9oyoDVcszhw5xBpiWW37mI58qkQ6g+4wTarreuXGTp3
RKgPupoYOMJja90yh3TWovcmuZz6QOgne5Rbn3s+Vg==
=hUb8
-----END PGP SIGNATURE-----
--x4FrOFG2PnNJvlbzxe80NPwxzh2yUHABp--

View File

@ -0,0 +1,43 @@
Content-Type: multipart/signed; micalg=pgp-sha256;
protocol="application/pgp-signature";
boundary="M2HYr2fNKsmidMKeWqsSlKJaGCe2l1guZ"
This is an OpenPGP/MIME signed message (RFC 4880 and 3156)
--M2HYr2fNKsmidMKeWqsSlKJaGCe2l1guZ
Content-Type: multipart/mixed; boundary="ijQgYCMAVOgOyTMqn30h68dd5lQKbMzCn";
protected-headers="v1"
Subject: plain body no pubkey
From: "pm.bridge.qa" <pm.bridge.qa@gmail.com>
To: schizofrenic@pm.me
Message-ID: <7414d726-2f14-54bf-3abe-75805aa6cc7f@gmail.com>
--ijQgYCMAVOgOyTMqn30h68dd5lQKbMzCn
Content-Type: text/plain; charset=utf-8; format=flowed
Content-Transfer-Encoding: quoted-printable
Content-Language: en-US
Why do seagulls fly over the ocean?
Because if they flew over the bay, we'd call them bagels.
--ijQgYCMAVOgOyTMqn30h68dd5lQKbMzCn--
--M2HYr2fNKsmidMKeWqsSlKJaGCe2l1guZ
Content-Type: application/pgp-signature; name="OpenPGP_signature.asc"
Content-Description: OpenPGP digital signature
Content-Disposition: attachment; filename="OpenPGP_signature"
-----BEGIN PGP SIGNATURE-----
wsB5BAABCAAjFiEEXOYJeXAvKFvzKPmxFhwIdYIjWfcFAmBa+F4FAwAAAAAACgkQFhwIdYIjWfew
6wf/Ts05KX3py8C2L3FPKkdNf+Ci1hd5aE7ARM8Zp5l0cFuuf6M3+Lud94VKYonoayNu5XfSGoyA
OO1HtpW+8hf5A+KSnyh8jp2dA/aLnU1RPZsfEN2cmgamMd6NyTL5cpYuAfxcSmWT79xeCcxPcjor
GtrVAojN1tkP2bynYzNI09uygWXzfzgB5f25povN2pAj7DFMAqRKf9bt3nZxO1wIh/aKHoEyjU3w
tO2AEKnn7dUnPS37wKomZr/LI1ZbNSLBJ+Gaan4w5c92gfEixttEuHXq2GwkJzJq6SInrxmyZQdl
dGR/kiAy9wFwQlErhyjI5lTtd12y3XNTyhaO5cS0bQ==
=Th/B
-----END PGP SIGNATURE-----
--M2HYr2fNKsmidMKeWqsSlKJaGCe2l1guZ--

View File

@ -1,85 +0,0 @@
// Copyright (c) 2021 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package message
import (
"bytes"
"html/template"
"io"
"net/http"
"net/mail"
"net/textproto"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
)
func WriteHeader(w io.Writer, h textproto.MIMEHeader) (err error) {
if err = http.Header(h).Write(w); err != nil {
return
}
_, err = io.WriteString(w, "\r\n")
return
}
const customMessageTemplate = `
<html>
<head></head>
<body style="font-family: Arial,'Helvetica Neue',Helvetica,sans-serif; font-size: 14px;">
<div style="color:#555; background-color:#cf9696; padding:20px; border-radius: 4px;">
<strong>Decryption error</strong><br/>
Decryption of this message's encrypted content failed.
<pre>{{.Error}}</pre>
</div>
{{if .AttachBody}}
<div style="color:#333; background-color:#f4f4f4; border: 1px solid #acb0bf; border-radius: 2px; padding:1rem; margin:1rem 0; font-family:monospace; font-size: 1em;">
<pre>{{.Body}}</pre>
</div>
{{- end}}
</body>
</html>
`
type customMessageData struct {
Error string
AttachBody bool
Body string
}
func CustomMessage(m *pmapi.Message, decodeError error, attachBody bool) error {
t := template.Must(template.New("customMessage").Parse(customMessageTemplate))
b := new(bytes.Buffer)
if err := t.Execute(b, customMessageData{
Error: decodeError.Error(),
AttachBody: attachBody,
Body: m.Body,
}); err != nil {
return err
}
m.MIMEType = pmapi.ContentTypeHTML
m.Body = b.String()
// NOTE: we need to set header in custom message header, so we check that is non-nil.
if m.Header == nil {
m.Header = make(mail.Header)
}
return nil
}

View File

@ -65,16 +65,22 @@ func (h *header) UnmarshalJSON(b []byte) error {
return nil
}
const (
DispositionInline = "inline"
DispositionAttachment = "attachment"
)
// Attachment represents a message attachment.
type Attachment struct {
ID string `json:",omitempty"`
MessageID string `json:",omitempty"` // msg v3 ???
Name string `json:",omitempty"`
Size int64 `json:",omitempty"`
MIMEType string `json:",omitempty"`
ContentID string `json:",omitempty"`
KeyPackets string `json:",omitempty"`
Signature string `json:",omitempty"`
ID string `json:",omitempty"`
MessageID string `json:",omitempty"` // msg v3 ???
Name string `json:",omitempty"`
Size int64 `json:",omitempty"`
MIMEType string `json:",omitempty"`
ContentID string `json:",omitempty"`
Disposition string
KeyPackets string `json:",omitempty"`
Signature string `json:",omitempty"`
Header textproto.MIMEHeader `json:"-"`
}

View File

@ -93,7 +93,7 @@ func (c *client) DecryptAndVerifyCards(cards []Card) ([]Card, error) {
if err != nil {
return nil, err
}
card.Data = signedCard
card.Data = string(signedCard)
}
if isSignedCardType(card.Type) {
err := c.verify(card.Data, card.Signature)

View File

@ -190,13 +190,13 @@ func encrypt(encrypter *crypto.KeyRing, plain string, signer *crypto.KeyRing) (a
return pgpMessage.GetArmored()
}
func (c *client) decrypt(armored string) (plain string, err error) {
func (c *client) decrypt(armored string) (plain []byte, err error) {
return decrypt(c.userKeyRing, armored)
}
func decrypt(decrypter *crypto.KeyRing, armored string) (plainBody string, err error) {
func decrypt(decrypter *crypto.KeyRing, armored string) (plainBody []byte, err error) {
if decrypter == nil {
return "", ErrNoKeyringAvailable
return nil, ErrNoKeyringAvailable
}
pgpMessage, err := crypto.NewPGPMessageFromArmored(armored)
if err != nil {
@ -206,7 +206,7 @@ func decrypt(decrypter *crypto.KeyRing, armored string) (plainBody string, err e
if err != nil {
return
}
return plainMessage.GetString(), nil
return plainMessage.GetBinary(), nil
}
func (c *client) sign(plain string) (armoredSignature string, err error) {

View File

@ -272,26 +272,26 @@ func (m *Message) IsLegacyMessage() bool {
strings.Contains(m.Body, MessageTail)
}
func (m *Message) Decrypt(kr *crypto.KeyRing) (err error) {
func (m *Message) Decrypt(kr *crypto.KeyRing) ([]byte, error) {
if m.IsLegacyMessage() {
return m.DecryptLegacy(kr)
return m.decryptLegacy(kr)
}
if !m.IsBodyEncrypted() {
return
return []byte(m.Body), nil
}
armored := strings.TrimSpace(m.Body)
body, err := decrypt(kr, armored)
if err != nil {
return
return nil, err
}
m.Body = body
return
return body, nil
}
func (m *Message) DecryptLegacy(kr *crypto.KeyRing) (err error) {
func (m *Message) decryptLegacy(kr *crypto.KeyRing) (dec []byte, err error) {
randomKeyStart := strings.Index(m.Body, RandomKeyHeader) + len(RandomKeyHeader)
randomKeyEnd := strings.Index(m.Body, RandomKeyTail)
randomKey := m.Body[randomKeyStart:randomKeyEnd]
@ -300,7 +300,7 @@ func (m *Message) DecryptLegacy(kr *crypto.KeyRing) (err error) {
if err != nil {
return
}
bytesKey, err := decodeBase64UTF8(signedKey)
bytesKey, err := decodeBase64UTF8(string(signedKey))
if err != nil {
return
}
@ -345,8 +345,7 @@ func (m *Message) DecryptLegacy(kr *crypto.KeyRing) (err error) {
return
}
m.Body = string(bytesPlaintext)
return err
return bytesPlaintext, nil
}
func decodeBase64UTF8(input string) (output []byte, err error) {

View File

@ -134,9 +134,9 @@ func TestMessage_IsBodyEncrypted(t *testing.T) {
func TestMessage_Decrypt(t *testing.T) {
msg := &Message{Body: testMessageEncrypted}
err := msg.Decrypt(testPrivateKeyRing)
dec, err := msg.Decrypt(testPrivateKeyRing)
Ok(t, err)
Equals(t, testMessageCleartext, msg.Body)
Equals(t, testMessageCleartext, string(dec))
}
func TestMessage_Decrypt_Legacy(t *testing.T) {
@ -153,17 +153,17 @@ func TestMessage_Decrypt_Legacy(t *testing.T) {
msg := &Message{Body: testMessageEncryptedLegacy}
err = msg.Decrypt(testPrivateKeyRingLegacy)
dec, err := msg.Decrypt(testPrivateKeyRingLegacy)
Ok(t, err)
Equals(t, testMessageCleartextLegacy, msg.Body)
Equals(t, testMessageCleartextLegacy, string(dec))
}
func TestMessage_Decrypt_signed(t *testing.T) {
msg := &Message{Body: testMessageSigned}
err := msg.Decrypt(testPrivateKeyRing)
dec, err := msg.Decrypt(testPrivateKeyRing)
Ok(t, err)
Equals(t, testMessageCleartext, msg.Body)
Equals(t, testMessageCleartext, string(dec))
}
func TestMessage_Encrypt(t *testing.T) {
@ -176,10 +176,10 @@ func TestMessage_Encrypt(t *testing.T) {
msg := &Message{Body: testMessageCleartext}
Ok(t, msg.Encrypt(testPrivateKeyRing, testPrivateKeyRing))
err = msg.Decrypt(testPrivateKeyRing)
dec, err := msg.Decrypt(testPrivateKeyRing)
Ok(t, err)
Equals(t, testMessageCleartext, msg.Body)
Equals(t, testMessageCleartext, string(dec))
Equals(t, testIdentity, signer.GetIdentities()[0])
}