forked from Silverfish/proton-bridge
GODT-2187: Placeholder for unbuildable messages
This commit is contained in:
@ -438,12 +438,13 @@ func (user *User) handleCreateMessageEvent(ctx context.Context, event proton.Mes
|
||||
}).Info("Handling message created event")
|
||||
|
||||
return withAddrKR(user.apiUser, user.apiAddrs[event.Message.AddressID], user.vault.KeyPass(), func(_, addrKR *crypto.KeyRing) error {
|
||||
update, err := buildRFC822(user.apiLabels, full, addrKR).update.unpack()
|
||||
if err != nil {
|
||||
buildRes := buildRFC822(user.apiLabels, full, addrKR)
|
||||
|
||||
if buildRes.err != nil {
|
||||
return fmt.Errorf("failed to build RFC822 message: %w", err)
|
||||
}
|
||||
|
||||
user.updateCh[full.AddressID].Enqueue(imap.NewMessagesCreated(update))
|
||||
user.updateCh[full.AddressID].Enqueue(imap.NewMessagesCreated(buildRes.update))
|
||||
|
||||
return nil
|
||||
})
|
||||
@ -493,16 +494,17 @@ func (user *User) handleUpdateDraftEvent(ctx context.Context, event proton.Messa
|
||||
}
|
||||
|
||||
return withAddrKR(user.apiUser, user.apiAddrs[event.Message.AddressID], user.vault.KeyPass(), func(_, addrKR *crypto.KeyRing) error {
|
||||
update, err := buildRFC822(user.apiLabels, full, addrKR).update.unpack()
|
||||
if err != nil {
|
||||
buildRes := buildRFC822(user.apiLabels, full, addrKR)
|
||||
|
||||
if buildRes.err != nil {
|
||||
return fmt.Errorf("failed to build RFC822 draft: %w", err)
|
||||
}
|
||||
|
||||
user.updateCh[full.AddressID].Enqueue(imap.NewMessageUpdated(
|
||||
update.Message,
|
||||
update.Literal,
|
||||
update.MailboxIDs,
|
||||
update.ParsedMessage,
|
||||
buildRes.update.Message,
|
||||
buildRes.update.Literal,
|
||||
buildRes.update.MailboxIDs,
|
||||
buildRes.update.ParsedMessage,
|
||||
))
|
||||
|
||||
return nil
|
||||
|
||||
@ -294,32 +294,32 @@ func syncMessages(
|
||||
}
|
||||
}()
|
||||
|
||||
// Goroutine in charge of converting the messages into updates and building a waitable structure for progress
|
||||
// tracking.
|
||||
// Goroutine which converts the messages into updates and builds a waitable structure for progress tracking.
|
||||
go func() {
|
||||
defer close(flushUpdateCh)
|
||||
for batch := range flushCh {
|
||||
for _, res := range batch {
|
||||
if err := res.update.err(); err != nil {
|
||||
if res.err != nil {
|
||||
if err := vault.AddFailedMessageID(res.messageID); err != nil {
|
||||
logrus.WithError(err).Error("Failed to add failed message ID")
|
||||
}
|
||||
|
||||
if err := sentry.ReportMessageWithContext("Failed to sync message", reporter.Context{
|
||||
"messageID": res.messageID,
|
||||
"error": err,
|
||||
"error": res.err,
|
||||
}); err != nil {
|
||||
logrus.WithError(err).Error("Failed to report message sync error")
|
||||
}
|
||||
|
||||
// We could sync a placeholder message here, but for now we skip it entirely.
|
||||
continue
|
||||
} else {
|
||||
if err := vault.RemFailedMessageID(res.messageID); err != nil {
|
||||
logrus.WithError(err).Error("Failed to remove failed message ID")
|
||||
}
|
||||
|
||||
flushers[res.addressID].push(res.update.unwrap())
|
||||
}
|
||||
|
||||
flushers[res.addressID].push(res.update)
|
||||
}
|
||||
|
||||
for _, flusher := range flushers {
|
||||
|
||||
@ -18,47 +18,22 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"bytes"
|
||||
"html/template"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/gluon/imap"
|
||||
"github.com/ProtonMail/go-proton-api"
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/v3/pkg/message"
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
)
|
||||
|
||||
type result[T any] struct {
|
||||
v T
|
||||
e error
|
||||
}
|
||||
|
||||
func resOk[T any](v T) result[T] {
|
||||
return result[T]{v: v}
|
||||
}
|
||||
|
||||
func resErr[T any](e error) result[T] {
|
||||
return result[T]{e: e}
|
||||
}
|
||||
|
||||
func (r *result[T]) unwrap() T {
|
||||
if r.e != nil {
|
||||
panic(r.err)
|
||||
}
|
||||
|
||||
return r.v
|
||||
}
|
||||
|
||||
func (r *result[T]) unpack() (T, error) {
|
||||
return r.v, r.e
|
||||
}
|
||||
|
||||
func (r *result[T]) err() error {
|
||||
return r.e
|
||||
}
|
||||
|
||||
type buildRes struct {
|
||||
messageID string
|
||||
addressID string
|
||||
update result[*imap.MessageCreated]
|
||||
update *imap.MessageCreated
|
||||
err error
|
||||
}
|
||||
|
||||
func defaultJobOpts() message.JobOptions {
|
||||
@ -73,20 +48,26 @@ func defaultJobOpts() message.JobOptions {
|
||||
}
|
||||
|
||||
func buildRFC822(apiLabels map[string]proton.Label, full proton.FullMessage, addrKR *crypto.KeyRing) *buildRes {
|
||||
var update result[*imap.MessageCreated]
|
||||
var (
|
||||
update *imap.MessageCreated
|
||||
err error
|
||||
)
|
||||
|
||||
if literal, err := message.BuildRFC822(addrKR, full.Message, full.AttData, defaultJobOpts()); err != nil {
|
||||
update = resErr[*imap.MessageCreated](fmt.Errorf("failed to build RFC822 for message %s: %w", full.ID, err))
|
||||
} else if created, err := newMessageCreatedUpdate(apiLabels, full.MessageMetadata, literal); err != nil {
|
||||
update = resErr[*imap.MessageCreated](fmt.Errorf("failed to create IMAP update for message %s: %w", full.ID, err))
|
||||
if literal, buildErr := message.BuildRFC822(addrKR, full.Message, full.AttData, defaultJobOpts()); buildErr != nil {
|
||||
update = newMessageCreatedFailedUpdate(apiLabels, full.MessageMetadata, buildErr)
|
||||
err = buildErr
|
||||
} else if created, parseErr := newMessageCreatedUpdate(apiLabels, full.MessageMetadata, literal); parseErr != nil {
|
||||
update = newMessageCreatedFailedUpdate(apiLabels, full.MessageMetadata, parseErr)
|
||||
err = parseErr
|
||||
} else {
|
||||
update = resOk(created)
|
||||
update = created
|
||||
}
|
||||
|
||||
return &buildRes{
|
||||
messageID: full.ID,
|
||||
addressID: full.AddressID,
|
||||
update: update,
|
||||
err: err,
|
||||
}
|
||||
}
|
||||
|
||||
@ -107,3 +88,83 @@ func newMessageCreatedUpdate(
|
||||
ParsedMessage: parsedMessage,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func newMessageCreatedFailedUpdate(
|
||||
apiLabels map[string]proton.Label,
|
||||
message proton.MessageMetadata,
|
||||
err error,
|
||||
) *imap.MessageCreated {
|
||||
literal := newFailedMessageLiteral(message.ID, time.Unix(message.Time, 0), message.Subject, err)
|
||||
|
||||
parsedMessage, err := imap.NewParsedMessage(literal)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return &imap.MessageCreated{
|
||||
Message: toIMAPMessage(message),
|
||||
MailboxIDs: mapTo[string, imap.MailboxID](wantLabels(apiLabels, message.LabelIDs)),
|
||||
Literal: literal,
|
||||
ParsedMessage: parsedMessage,
|
||||
}
|
||||
}
|
||||
|
||||
func newFailedMessageLiteral(
|
||||
messageID string,
|
||||
date time.Time,
|
||||
subject string,
|
||||
syncErr error,
|
||||
) []byte {
|
||||
var buf bytes.Buffer
|
||||
|
||||
if tmpl, err := template.New("header").Parse(failedMessageHeaderTemplate); err != nil {
|
||||
panic(err)
|
||||
} else if b, err := tmplExec(tmpl, map[string]any{
|
||||
"Date": date.Format(time.RFC822),
|
||||
}); err != nil {
|
||||
panic(err)
|
||||
} else if _, err := buf.Write(b); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if tmpl, err := template.New("body").Parse(failedMessageBodyTemplate); err != nil {
|
||||
panic(err)
|
||||
} else if b, err := tmplExec(tmpl, map[string]any{
|
||||
"MessageID": messageID,
|
||||
"Subject": subject,
|
||||
"Error": syncErr.Error(),
|
||||
}); err != nil {
|
||||
panic(err)
|
||||
} else if _, err := buf.Write(lineWrap(b64Encode(b))); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func tmplExec(template *template.Template, data any) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
if err := template.Execute(&buf, data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func lineWrap(b []byte) []byte {
|
||||
return bytes.Join(xslices.Chunk(b, 76), []byte{'\r', '\n'})
|
||||
}
|
||||
|
||||
const failedMessageHeaderTemplate = `Date: {{.Date}}
|
||||
Subject: Message failed to build
|
||||
Content-Type: text/plain
|
||||
Content-Transfer-Encoding: base64
|
||||
|
||||
`
|
||||
|
||||
const failedMessageBodyTemplate = `Failed to build message:
|
||||
Subject: {{.Subject}}
|
||||
Error: {{.Error}}
|
||||
MessageID: {{.MessageID}}
|
||||
`
|
||||
|
||||
49
internal/user/sync_build_test.go
Normal file
49
internal/user/sync_build_test.go
Normal file
@ -0,0 +1,49 @@
|
||||
// Copyright (c) 2022 Proton AG
|
||||
//
|
||||
// This file is part of Proton Mail Bridge.
|
||||
//
|
||||
// Proton Mail 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.
|
||||
//
|
||||
// Proton Mail 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 Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package user
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ProtonMail/gluon/imap"
|
||||
"github.com/ProtonMail/gluon/rfc822"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestNewFailedMessageLiteral(t *testing.T) {
|
||||
literal := newFailedMessageLiteral("abcd-efgh", time.Unix(123456789, 0), "subject", errors.New("oops"))
|
||||
|
||||
header, err := rfc822.Parse(literal).ParseHeader()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "Message failed to build", header.Get("Subject"))
|
||||
require.Equal(t, "29 Nov 73 22:33 CET", header.Get("Date"))
|
||||
require.Equal(t, "text/plain", header.Get("Content-Type"))
|
||||
require.Equal(t, "base64", header.Get("Content-Transfer-Encoding"))
|
||||
|
||||
b, err := rfc822.Parse(literal).DecodedBody()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, string(b), "Failed to build message: \nSubject: subject\nError: oops\nMessageID: abcd-efgh\n")
|
||||
|
||||
parsed, err := imap.NewParsedMessage(literal)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, `("29 Nov 73 22:33 CET" "Message failed to build" NIL NIL NIL NIL NIL NIL NIL NIL)`, parsed.Envelope)
|
||||
require.Equal(t, `("text" "plain" () NIL NIL "base64" 114 2)`, parsed.Body)
|
||||
require.Equal(t, `("text" "plain" () NIL NIL "base64" 114 2 NIL NIL NIL NIL)`, parsed.Structure)
|
||||
}
|
||||
@ -60,6 +60,15 @@ func groupBy[Key comparable, Value any](items []Value, key func(Value) Key) map[
|
||||
|
||||
// b64Encode returns the base64 encoding of the given byte slice.
|
||||
func b64Encode(b []byte) []byte {
|
||||
enc := make([]byte, base64.StdEncoding.EncodedLen(len(b)))
|
||||
|
||||
base64.StdEncoding.Encode(enc, b)
|
||||
|
||||
return enc
|
||||
}
|
||||
|
||||
// b64RawEncode returns the base64 encoding of the given byte slice.
|
||||
func b64RawEncode(b []byte) []byte {
|
||||
enc := make([]byte, base64.RawURLEncoding.EncodedLen(len(b)))
|
||||
|
||||
base64.RawURLEncoding.Encode(enc, b)
|
||||
@ -67,8 +76,8 @@ func b64Encode(b []byte) []byte {
|
||||
return enc
|
||||
}
|
||||
|
||||
// b64Decode returns the bytes represented by the base64 encoding of the given byte slice.
|
||||
func b64Decode(b []byte) ([]byte, error) {
|
||||
// b64RawDecode returns the bytes represented by the base64 encoding of the given byte slice.
|
||||
func b64RawDecode(b []byte) ([]byte, error) {
|
||||
dec := make([]byte, base64.RawURLEncoding.DecodedLen(len(b)))
|
||||
|
||||
n, err := base64.RawURLEncoding.Decode(dec, b)
|
||||
|
||||
@ -355,7 +355,7 @@ func (user *User) GluonKey() []byte {
|
||||
|
||||
// BridgePass returns the user's bridge password, used for authentication over SMTP and IMAP.
|
||||
func (user *User) BridgePass() []byte {
|
||||
return b64Encode(user.vault.BridgePass())
|
||||
return b64RawEncode(user.vault.BridgePass())
|
||||
}
|
||||
|
||||
// UsedSpace returns the total space used by the user on the API.
|
||||
@ -431,7 +431,7 @@ func (user *User) CheckAuth(email string, password []byte) (string, error) {
|
||||
panic("your wish is my command.. I crash")
|
||||
}
|
||||
|
||||
dec, err := b64Decode(password)
|
||||
dec, err := b64RawDecode(password)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to decode password: %w", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user