Import/Export backend prep

This commit is contained in:
Michal Horejsek
2020-05-14 15:22:29 +02:00
parent 9d65192ad7
commit b598779c0f
92 changed files with 6983 additions and 188 deletions

View File

@ -228,6 +228,11 @@ func (c *Config) GetPreferencesPath() string {
return filepath.Join(c.appDirsVersion.UserCache(), "prefs.json")
}
// GetTransferDir returns folder for import/export rule and report files.
func (c *Config) GetTransferDir() string {
return filepath.Join(c.appDirsVersion.UserCache())
}
// GetDefaultAPIPort returns default Bridge local API port.
func (c *Config) GetDefaultAPIPort() int {
return 1042

View File

@ -55,7 +55,7 @@ func WriteAttachmentBody(w io.Writer, kr *crypto.KeyRing, m *pmapi.Message, att
dr = r
err = nil
att.Name += ".gpg"
att.MIMEType = "application/pgp-encrypted"
att.MIMEType = "application/pgp-encrypted" //nolint
} else if err != nil && err != openpgperrors.ErrSignatureExpired {
err = fmt.Errorf("cannot decrypt attachment: %v", err)
return

348
pkg/message/build.go Normal file
View File

@ -0,0 +1,348 @@
// Copyright (c) 2020 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"
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"mime/quotedprintable"
"net/textproto"
"github.com/ProtonMail/gopenpgp/crypto"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/emersion/go-textwrapper"
openpgperrors "golang.org/x/crypto/openpgp/errors"
)
// 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
succDcrpt bool
}
// 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, succDcrpt: false}
}
// fetchMessage will update original PM message if successful
func (bld *Builder) fetchMessage() (err error) {
if bld.msg.Body != "" {
return nil
}
complete, err := bld.cl.GetMessage(bld.msg.ID)
if err != nil {
return
}
*bld.msg = *complete
return
}
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)
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
}
_ = 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
}
attachmentHeader := GetAttachmentHeader(att)
if partWriter, err = mw.CreatePart(attachmentHeader); err != nil {
return nil, nil, err
}
_, _ = buf.WriteTo(partWriter)
}
_ = mw.Close()
}
// wee need to copy buffer before building body structure
message = bodyBuf.Bytes()
structure, err = NewBodyStructure(bodyBuf)
return structure, message, err
}
// SuccessfullyDecrypted is true when message was fetched and decrypted successfully
func (bld *Builder) SuccessfullyDecrypted() bool { return bld.succDcrpt }
// 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
}
// decrypt body
if err := bld.msg.Decrypt(kr); err != nil && err != openpgperrors.ErrSignatureExpired {
return err
}
bld.succDcrpt = 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 {
// Do not fail if attachment is encrypted with a different key
dr = attReader
err = nil
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 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", "inline")
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)
p, err := mw.CreatePart(h)
if err != nil {
return nil, err
}
// Create line wrapper writer.
ww := textwrapper.NewRFC822(p)
// Create base64 writer.
bw := base64.NewEncoder(base64.StdEncoding, ww)
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
}
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
}

View File

@ -23,8 +23,11 @@ import (
"strings"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/sirupsen/logrus"
)
var log = logrus.WithField("pkg", "pkg/message") //nolint[gochecknoglobals]
func GetBoundary(m *pmapi.Message) string {
// The boundary needs to be deterministic because messages are not supposed to
// change.

View File

@ -37,7 +37,6 @@ import (
pmmime "github.com/ProtonMail/proton-bridge/pkg/mime"
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/jaytaylor/html2text"
log "github.com/sirupsen/logrus"
)
func parseAttachment(filename string, mediaType string, h textproto.MIMEHeader) (att *pmapi.Attachment) {

85
pkg/message/utils.go Normal file
View File

@ -0,0 +1,85 @@
// Copyright (c) 2020 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

@ -347,6 +347,14 @@ func (cm *ClientManager) CheckConnection() error {
return nil
}
// CheckConnection returns an error if there is no internet connection.
func CheckConnection() error {
client := &http.Client{Timeout: time.Second * 10}
retStatus := make(chan error)
checkConnection(client, "http://protonstatus.com/vpn_status", retStatus)
return <-retStatus
}
func checkConnection(client *http.Client, url string, errorChannel chan error) {
resp, err := client.Get(url)
if err != nil {

View File

@ -95,6 +95,11 @@ type ImportMsgReq struct {
LabelIDs []string
}
func (req ImportMsgReq) String() string {
data, _ := json.Marshal(req)
return string(data)
}
// ImportRes is a response to an import request.
type ImportRes struct {
Res

View File

@ -120,7 +120,7 @@ func (u *Updates) CreateJSONAndSign(deployDir, goos string) error {
return nil
}
func (u *Updates) CheckIsBridgeUpToDate() (isUpToDate bool, latestVersion VersionInfo, err error) {
func (u *Updates) CheckIsUpToDate() (isUpToDate bool, latestVersion VersionInfo, err error) {
localVersion := u.GetLocalVersion()
latestVersion, err = u.getLatestVersion()
if err != nil {

View File

@ -71,14 +71,14 @@ func startServer() {
func TestCheckBridgeIsUpToDate(t *testing.T) {
updates := newTestUpdates("1.1.6")
isUpToDate, _, err := updates.CheckIsBridgeUpToDate()
isUpToDate, _, err := updates.CheckIsUpToDate()
require.NoError(t, err)
require.True(t, isUpToDate, "Bridge should be up to date")
}
func TestCheckBridgeIsNotUpToDate(t *testing.T) {
updates := newTestUpdates("1.1.5")
isUpToDate, _, err := updates.CheckIsBridgeUpToDate()
isUpToDate, _, err := updates.CheckIsUpToDate()
require.NoError(t, err)
require.True(t, !isUpToDate, "Bridge should not be up to date")
}