Compare commits

..

8 Commits

Author SHA1 Message Date
f070314524 Other: Bridge Perth Narrows v3.0.6 2022-12-07 07:38:47 +01:00
75c88eaa55 GODT-2187: Handle unbuildable messages in event loop 2022-12-06 19:27:55 +01:00
bd6ae2ac2b GODT-2187: Placeholder for unbuildable messages 2022-12-06 16:35:32 +01:00
58d04f9693 GODT-2187: Skip messages during sync that fail to build/parse 2022-12-06 14:07:13 +00:00
01c12655b8 Other: Update Gluon to latest version
Fixes: #316
2022-12-06 11:49:39 +01:00
d4198737a6 Other: Bridge Perth Narrows v3.0.5 2022-12-05 15:42:49 +01:00
04881b9b78 GODT-2178: Bump go-proton-api to fix drafts 2022-12-05 15:14:30 +01:00
990b8cda96 GODT-2180: Allow login with FIDO2
The API docs didn't specify what the "integer" meant. Turns out it's a
bitfield; we can't compare with equality.
2022-12-05 14:22:38 +01:00
20 changed files with 327 additions and 111 deletions

View File

@ -2,6 +2,17 @@
Changelog [format](http://keepachangelog.com/en/1.0.0/)
## [Bridge 3.0.6] Perth Narrows
### Fixed
* GODT-2187: Skip messages during sync that fail to build/parse.
## [Bridge 3.0.5] Perth Narrows
### Fixed
* GODT-2178: Bump go-proton-api to fix drafts.
* GODT-2180: Allow login with FIDO2.
## [Bridge 3.0.4] Perth Narrows
### Changed

View File

@ -11,7 +11,7 @@ ROOT_DIR:=$(shell dirname $(realpath $(firstword $(MAKEFILE_LIST))))
.PHONY: build build-gui build-nogui build-launcher versioner hasher
# Keep version hardcoded so app build works also without Git repository.
BRIDGE_APP_VERSION?=3.0.4+git
BRIDGE_APP_VERSION?=3.0.6+git
APP_VERSION:=${BRIDGE_APP_VERSION}
APP_FULL_NAME:=Proton Mail Bridge
APP_VENDOR:=Proton AG

4
go.mod
View File

@ -5,9 +5,9 @@ go 1.18
require (
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
github.com/Masterminds/semver/v3 v3.1.1
github.com/ProtonMail/gluon v0.14.2-0.20221202093012-ad1570c49c8c
github.com/ProtonMail/gluon v0.14.2-0.20221206104410-725ddb9db68a
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
github.com/ProtonMail/go-proton-api v0.1.5
github.com/ProtonMail/go-proton-api v0.2.1
github.com/ProtonMail/go-rfc5322 v0.11.0
github.com/ProtonMail/gopenpgp/v2 v2.4.10
github.com/PuerkitoBio/goquery v1.8.0

8
go.sum
View File

@ -28,8 +28,8 @@ github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
github.com/ProtonMail/gluon v0.14.2-0.20221202093012-ad1570c49c8c h1:DzVlJERHOHDQjYz/P12VlORS4rF2Ii83cWcYHsXGdng=
github.com/ProtonMail/gluon v0.14.2-0.20221202093012-ad1570c49c8c/go.mod h1:z2AxLIiBCT1K+0OBHyaDI7AEaO5qI6/BEC2TE42vs4Q=
github.com/ProtonMail/gluon v0.14.2-0.20221206104410-725ddb9db68a h1:BwWVZcvvf9Pw353+wZGD3X433kPFT4SjQVnYKD0YBRY=
github.com/ProtonMail/gluon v0.14.2-0.20221206104410-725ddb9db68a/go.mod h1:z2AxLIiBCT1K+0OBHyaDI7AEaO5qI6/BEC2TE42vs4Q=
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
@ -43,8 +43,8 @@ github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753/go.mod h1:NB
github.com/ProtonMail/go-mime v0.0.0-20220302105931-303f85f7fe0f/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4=
github.com/ProtonMail/go-mime v0.0.0-20220429130430-2192574d760f h1:4IWzKjHzZxdrW9k4zl/qCwenOVHDbVDADPPHFLjs0Oc=
github.com/ProtonMail/go-mime v0.0.0-20220429130430-2192574d760f/go.mod h1:qRZgbeASl2a9OwmsV85aWwRqic0NHPh+9ewGAzb4cgM=
github.com/ProtonMail/go-proton-api v0.1.5 h1:6RJO3jXP3opFfGqrXNvFTefdD4MlfxKjIebT8r5ROf8=
github.com/ProtonMail/go-proton-api v0.1.5/go.mod h1:jqvJ2HqLHqiPJoEb+BTIB1IF7wvr6p+8ZfA6PO2NRNk=
github.com/ProtonMail/go-proton-api v0.2.1 h1:M15/zzfx6EPiskv2+gogUkmvx7Y1SmRRtLT6GiBh5T0=
github.com/ProtonMail/go-proton-api v0.2.1/go.mod h1:jqvJ2HqLHqiPJoEb+BTIB1IF7wvr6p+8ZfA6PO2NRNk=
github.com/ProtonMail/go-rfc5322 v0.11.0 h1:o5Obrm4DpmQEffvgsVqG6S4BKwC1Wat+hYwjIp2YcCY=
github.com/ProtonMail/go-rfc5322 v0.11.0/go.mod h1:6oOKr0jXvpoE6pwTx/HukigQpX2J9WUf6h0auplrFTw=
github.com/ProtonMail/go-srp v0.0.5 h1:xhUioxZgDbCnpo9JehyFhwwsn9JLWkUGfB0oiKXgiGg=

View File

@ -182,7 +182,7 @@ func (bridge *Bridge) LoginFull(
return "", fmt.Errorf("failed to begin login process: %w", err)
}
if auth.TwoFA.Enabled == proton.TOTPEnabled {
if auth.TwoFA.Enabled&proton.HasTOTP != 0 {
logrus.WithField("userID", auth.UserID).Info("Requesting TOTP")
totp, err := getTOTP()
@ -441,9 +441,9 @@ func (bridge *Bridge) addUserWithVault(
ctx,
vault,
client,
bridge.reporter,
apiUser,
bridge.crashHandler,
bridge.reporter,
bridge.vault.SyncWorkers(),
bridge.vault.GetShowAllMail(),
)

View File

@ -149,7 +149,7 @@ func (f *frontendCLI) loginAccount(c *ishell.Context) { //nolint:funlen
return
}
if auth.TwoFA.Enabled == proton.TOTPEnabled {
if auth.TwoFA.Enabled&proton.HasTOTP != 0 {
code := f.readStringInAttempts("Two factor code", c.ReadLine, isNotEmpty)
if code == "" {
f.printAndLogError("Cannot login: need two factor code")

View File

@ -406,7 +406,7 @@ func (s *Service) Login(ctx context.Context, login *LoginRequest) (*emptypb.Empt
s.auth = auth
switch {
case auth.TwoFA.Enabled == proton.TOTPEnabled:
case auth.TwoFA.Enabled&proton.HasTOTP != 0:
_ = s.SendEvent(NewLoginTfaRequestedEvent(login.Username))
case auth.PasswordMode == proton.TwoPasswordMode:

View File

@ -23,6 +23,7 @@ import (
"github.com/ProtonMail/gluon/imap"
"github.com/ProtonMail/gluon/queue"
"github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
@ -438,12 +439,30 @@ 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 {
buildRes, err := buildRFC822(user.apiLabels, full, addrKR)
if err != nil {
return fmt.Errorf("failed to build RFC822 message: %w", err)
res := buildRFC822(user.apiLabels, full, addrKR)
if res.err != nil {
user.log.WithError(err).Error("Failed to build RFC822 message")
if err := user.vault.AddFailedMessageID(event.ID); err != nil {
user.log.WithError(err).Error("Failed to add failed message ID to vault")
}
if err := user.reporter.ReportMessageWithContext("Failed to build message (event create)", reporter.Context{
"messageID": res.messageID,
"error": res.err,
}); err != nil {
user.log.WithError(err).Error("Failed to report message build error")
}
return nil
}
user.updateCh[full.AddressID].Enqueue(imap.NewMessagesCreated(buildRes.update))
if err := user.vault.RemFailedMessageID(event.ID); err != nil {
user.log.WithError(err).Error("Failed to remove failed message ID from vault")
}
user.updateCh[full.AddressID].Enqueue(imap.NewMessagesCreated(res.update))
return nil
})
@ -493,16 +512,34 @@ 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 {
buildRes, err := buildRFC822(user.apiLabels, full, addrKR)
if err != nil {
return fmt.Errorf("failed to build RFC822 draft: %w", err)
res := buildRFC822(user.apiLabels, full, addrKR)
if res.err != nil {
logrus.WithError(err).Error("Failed to build RFC822 message")
if err := user.vault.AddFailedMessageID(event.ID); err != nil {
user.log.WithError(err).Error("Failed to add failed message ID to vault")
}
if err := user.reporter.ReportMessageWithContext("Failed to build message (event update)", reporter.Context{
"messageID": res.messageID,
"error": res.err,
}); err != nil {
logrus.WithError(err).Error("Failed to report message build error")
}
return nil
}
if err := user.vault.RemFailedMessageID(event.ID); err != nil {
user.log.WithError(err).Error("Failed to remove failed message ID from vault")
}
user.updateCh[full.AddressID].Enqueue(imap.NewMessageUpdated(
buildRes.update.Message,
buildRes.update.Literal,
buildRes.update.MailboxIDs,
buildRes.update.ParsedMessage,
res.update.Message,
res.update.Literal,
res.update.MailboxIDs,
res.update.ParsedMessage,
))
return nil

View File

@ -534,20 +534,10 @@ func (conn *imapConnector) createDraft(ctx context.Context, literal []byte, addr
decBody = string(message.RichBody)
}
encBody, err := addrKR.Encrypt(crypto.NewPlainMessageFromString(decBody), nil)
if err != nil {
return proton.Message{}, fmt.Errorf("failed to encrypt message body: %w", err)
}
armBody, err := encBody.GetArmored()
if err != nil {
return proton.Message{}, fmt.Errorf("failed to get armored message body: %w", err)
}
draft, err := conn.client.CreateDraft(ctx, proton.CreateDraftReq{
draft, err := conn.client.CreateDraft(ctx, addrKR, proton.CreateDraftReq{
Message: proton.DraftTemplate{
Subject: message.Subject,
Body: armBody,
Body: decBody,
MIMEType: message.MIMEType,
Sender: &mail.Address{Name: sender.DisplayName, Address: sender.Email},

View File

@ -188,19 +188,9 @@ func sendWithKey( //nolint:funlen
return proton.Message{}, fmt.Errorf("unsupported MIME type: %v", message.MIMEType)
}
encBody, err := addrKR.Encrypt(crypto.NewPlainMessageFromString(decBody), nil)
if err != nil {
return proton.Message{}, fmt.Errorf("failed to encrypt message body: %w", err)
}
armBody, err := encBody.GetArmored()
if err != nil {
return proton.Message{}, fmt.Errorf("failed to get armored message body: %w", err)
}
draft, err := createDraft(ctx, client, emails, from, to, parentID, message.InReplyTo, proton.DraftTemplate{
draft, err := createDraft(ctx, client, addrKR, emails, from, to, parentID, message.InReplyTo, proton.DraftTemplate{
Subject: message.Subject,
Body: armBody,
Body: decBody,
MIMEType: message.MIMEType,
Sender: message.Sender,
@ -312,6 +302,7 @@ func getParentID( //nolint:funlen
func createDraft(
ctx context.Context,
client *proton.Client,
addrKR *crypto.KeyRing,
emails []string,
from string,
to []string,
@ -357,7 +348,7 @@ func createDraft(
action = proton.ForwardAction
}
return client.CreateDraft(ctx, proton.CreateDraftReq{
return client.CreateDraft(ctx, addrKR, proton.CreateDraftReq{
Message: template,
ParentID: parentID,
Action: action,

View File

@ -26,6 +26,7 @@ import (
"github.com/ProtonMail/gluon/imap"
"github.com/ProtonMail/gluon/queue"
"github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
@ -36,6 +37,7 @@ import (
"github.com/google/uuid"
"github.com/sirupsen/logrus"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
)
const (
@ -87,6 +89,7 @@ func (user *User) doSync(ctx context.Context) error {
return nil
}
// nolint:funlen
func (user *User) sync(ctx context.Context) error {
return safe.RLockRet(func() error {
return withAddrKRs(user.apiUser, user.apiAddrs, user.vault.KeyPass(), func(_ *crypto.KeyRing, addrKRs map[string]*crypto.KeyRing) error {
@ -109,10 +112,32 @@ func (user *User) sync(ctx context.Context) error {
if !user.vault.SyncStatus().HasMessages {
user.log.Info("Syncing messages")
// Determine which messages to sync.
messageIDs, err := user.client.GetMessageIDs(ctx, "")
if err != nil {
return fmt.Errorf("failed to get message IDs to sync: %w", err)
}
// Remove any messages that have already failed to sync.
messageIDs = xslices.Filter(messageIDs, func(messageID string) bool {
return !slices.Contains(user.vault.SyncStatus().FailedMessageIDs, messageID)
})
// Reverse the order of the message IDs so that the newest messages are synced first.
xslices.Reverse(messageIDs)
// If we have a message ID that we've already synced, then we can skip all messages before it.
if idx := xslices.Index(messageIDs, user.vault.SyncStatus().LastMessageID); idx >= 0 {
messageIDs = messageIDs[idx+1:]
}
// Sync the messages.
if err := syncMessages(
ctx,
user.ID(),
messageIDs,
user.client,
user.reporter,
user.vault,
user.apiLabels,
addrKRs,
@ -183,7 +208,9 @@ func syncLabels(ctx context.Context, apiLabels map[string]proton.Label, updateCh
func syncMessages(
ctx context.Context,
userID string,
messageIDs []string,
client *proton.Client,
sentry reporter.Reporter,
vault *vault.User,
apiLabels map[string]proton.Label,
addrKRs map[string]*crypto.KeyRing,
@ -194,20 +221,6 @@ func syncMessages(
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// Determine which messages to sync.
messageIDs, err := client.GetMessageIDs(ctx, "")
if err != nil {
return fmt.Errorf("failed to get message IDs to sync: %w", err)
}
// Reverse the order of the message IDs so that the newest messages are synced first.
xslices.Reverse(messageIDs)
// If we have a message ID that we've already synced, then we can skip all messages before it.
if idx := xslices.Index(messageIDs, vault.SyncStatus().LastMessageID); idx >= 0 {
messageIDs = messageIDs[idx+1:]
}
// Track the amount of time to process all the messages.
syncStartTime := time.Now()
defer func() { logrus.WithField("duration", time.Since(syncStartTime)).Info("Message sync completed") }()
@ -222,14 +235,12 @@ func syncMessages(
flushers := make(map[string]*flusher, len(updateCh))
for addrID, updateCh := range updateCh {
flusher := newFlusher(updateCh, maxUpdateSize)
flushers[addrID] = flusher
flushers[addrID] = newFlusher(updateCh, maxUpdateSize)
}
// Create a reporter to report sync progress updates.
reporter := newReporter(userID, eventCh, len(messageIDs), time.Second)
defer reporter.done()
syncReporter := newSyncReporter(userID, eventCh, len(messageIDs), time.Second)
defer syncReporter.done()
type flushUpdate struct {
messageID string
@ -267,7 +278,7 @@ func syncMessages(
return nil, ctx.Err()
}
return buildRFC822(apiLabels, msg, addrKRs[msg.AddressID])
return buildRFC822(apiLabels, msg, addrKRs[msg.AddressID]), nil
})
if err != nil {
errorCh <- err
@ -283,12 +294,31 @@ 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 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 build message (sync)", reporter.Context{
"messageID": res.messageID,
"error": res.err,
}); err != nil {
logrus.WithError(err).Error("Failed to report message build 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)
}
@ -321,7 +351,7 @@ func syncMessages(
return fmt.Errorf("failed to set last synced message ID: %w", err)
}
reporter.add(flushUpdate.batchLen)
syncReporter.add(flushUpdate.batchLen)
}
return <-errorCh

View File

@ -18,18 +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 buildRes struct {
messageID string
addressID string
update *imap.MessageCreated
err error
}
func defaultJobOpts() message.JobOptions {
@ -43,22 +47,28 @@ func defaultJobOpts() message.JobOptions {
}
}
func buildRFC822(apiLabels map[string]proton.Label, full proton.FullMessage, addrKR *crypto.KeyRing) (*buildRes, error) {
literal, err := message.BuildRFC822(addrKR, full.Message, full.AttData, defaultJobOpts())
if err != nil {
return nil, fmt.Errorf("failed to build message %s: %w", full.ID, err)
}
func buildRFC822(apiLabels map[string]proton.Label, full proton.FullMessage, addrKR *crypto.KeyRing) *buildRes {
var (
update *imap.MessageCreated
err error
)
update, err := newMessageCreatedUpdate(apiLabels, full.MessageMetadata, literal)
if err != nil {
return nil, 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 = created
}
return &buildRes{
messageID: full.ID,
addressID: full.AddressID,
update: update,
}, nil
err: err,
}
}
func newMessageCreatedUpdate(
@ -78,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.In(time.UTC).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}}
`

View 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 21:33 UTC", 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 21:33 UTC" "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)
}

View File

@ -24,7 +24,7 @@ import (
"github.com/ProtonMail/proton-bridge/v3/internal/events"
)
type reporter struct {
type syncReporter struct {
userID string
eventCh *queue.QueuedChannel[events.Event]
@ -36,8 +36,8 @@ type reporter struct {
freq time.Duration
}
func newReporter(userID string, eventCh *queue.QueuedChannel[events.Event], total int, freq time.Duration) *reporter {
return &reporter{
func newSyncReporter(userID string, eventCh *queue.QueuedChannel[events.Event], total int, freq time.Duration) *syncReporter {
return &syncReporter{
userID: userID,
eventCh: eventCh,
@ -47,7 +47,7 @@ func newReporter(userID string, eventCh *queue.QueuedChannel[events.Event], tota
}
}
func (rep *reporter) add(delta int) {
func (rep *syncReporter) add(delta int) {
rep.count += delta
if time.Since(rep.last) > rep.freq {
@ -62,7 +62,7 @@ func (rep *reporter) add(delta int) {
}
}
func (rep *reporter) done() {
func (rep *syncReporter) done() {
rep.eventCh.Enqueue(events.SyncProgress{
UserID: rep.userID,
Progress: 1,

View File

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

View File

@ -29,7 +29,7 @@ import (
"github.com/ProtonMail/gluon/connector"
"github.com/ProtonMail/gluon/imap"
"github.com/ProtonMail/gluon/queue"
gluonReporter "github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/gluon/reporter"
"github.com/ProtonMail/go-proton-api"
"github.com/ProtonMail/proton-bridge/v3/internal/async"
"github.com/ProtonMail/proton-bridge/v3/internal/events"
@ -57,6 +57,7 @@ type User struct {
vault *vault.User
client *proton.Client
reporter reporter.Reporter
eventCh *queue.QueuedChannel[events.Event]
sendHash *sendRecorder
@ -72,8 +73,6 @@ type User struct {
updateCh map[string]*queue.QueuedChannel[imap.Update]
updateChLock safe.RWMutex
reporter gluonReporter.Reporter
tasks *async.Group
abortable async.Abortable
goSync func()
@ -92,9 +91,9 @@ func New(
ctx context.Context,
encVault *vault.User,
client *proton.Client,
reporter reporter.Reporter,
apiUser proton.User,
crashHandler async.PanicHandler,
reporter gluonReporter.Reporter,
syncWorkers int,
showAllMail bool,
) (*User, error) { //nolint:funlen
@ -118,6 +117,7 @@ func New(
vault: encVault,
client: client,
reporter: reporter,
eventCh: queue.NewQueuedChannel[events.Event](0, 0),
sendHash: newSendRecorder(sendEntryExpiry),
@ -133,8 +133,6 @@ func New(
updateCh: make(map[string]*queue.QueuedChannel[imap.Update]),
updateChLock: safe.NewRWMutex(),
reporter: reporter,
tasks: async.NewGroup(context.Background(), crashHandler),
pollAPIEventsCh: make(chan chan struct{}),
@ -357,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.
@ -433,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)
}

View File

@ -218,7 +218,7 @@ func withUser(tb testing.TB, ctx context.Context, _ *server.Server, m *proton.Ma
vaultUser, err := vault.AddUser(apiUser.ID, username, apiAuth.UID, apiAuth.RefreshToken, saltedKeyPass)
require.NoError(tb, err)
user, err := New(ctx, vaultUser, client, apiUser, nil, nil, vault.SyncWorkers(), true)
user, err := New(ctx, vaultUser, client, nil, apiUser, nil, vault.SyncWorkers(), true)
require.NoError(tb, err)
defer user.Close()

View File

@ -60,9 +60,10 @@ func (mode AddressMode) String() string {
}
type SyncStatus struct {
HasLabels bool
HasMessages bool
LastMessageID string
HasLabels bool
HasMessages bool
LastMessageID string
FailedMessageIDs []string
}
func (status SyncStatus) IsComplete() bool {

View File

@ -21,6 +21,8 @@ import (
"fmt"
"github.com/ProtonMail/gluon/imap"
"github.com/bradenaw/juniper/xslices"
"golang.org/x/exp/slices"
)
type User struct {
@ -158,6 +160,24 @@ func (user *User) SetLastMessageID(messageID string) error {
})
}
// AddFailedMessageID adds a message ID to the list of failed message IDs.
func (user *User) AddFailedMessageID(messageID string) error {
return user.vault.modUser(user.userID, func(data *UserData) {
if !slices.Contains(data.SyncStatus.FailedMessageIDs, messageID) {
data.SyncStatus.FailedMessageIDs = append(data.SyncStatus.FailedMessageIDs, messageID)
}
})
}
// RemFailedMessageID removes a message ID from the list of failed message IDs.
func (user *User) RemFailedMessageID(messageID string) error {
return user.vault.modUser(user.userID, func(data *UserData) {
data.SyncStatus.FailedMessageIDs = xslices.Filter(data.SyncStatus.FailedMessageIDs, func(otherID string) bool {
return otherID != messageID
})
})
}
// ClearSyncStatus clears the user's sync status.
func (user *User) ClearSyncStatus() error {
return user.vault.modUser(user.userID, func(data *UserData) {

View File

@ -269,20 +269,10 @@ func (s *scenario) theFollowingFieldsWereChangedInDraftForAddressOfAccount(draft
}
if wantMessages[0].Body != "" {
enc, err := addrKR.Encrypt(crypto.NewPlainMessageFromString(wantMessages[0].Body), addrKR)
if err != nil {
return fmt.Errorf("failed to encrypt message body: %w", err)
}
arm, err := enc.GetArmored()
if err != nil {
return fmt.Errorf("failed to get armored message: %w", err)
}
changes.Body = arm
changes.Body = wantMessages[0].Body
}
if _, err := c.UpdateDraft(ctx, draftID, proton.UpdateDraftReq{Message: changes}); err != nil {
if _, err := c.UpdateDraft(ctx, draftID, addrKR, proton.UpdateDraftReq{Message: changes}); err != nil {
return fmt.Errorf("failed to update draft: %w", err)
}