forked from Silverfish/proton-bridge
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e8ee9de5b9 | |||
| 47ea4b226a | |||
| 00059e6754 | |||
| 031ed9c203 | |||
| 30bf941979 | |||
| 55ee6a9d13 | |||
| 2b25fe1fa4 | |||
| 57d563d488 | |||
| ebb04d8a14 | |||
| 3c24ac26d5 | |||
| f070314524 | |||
| 75c88eaa55 | |||
| bd6ae2ac2b | |||
| 58d04f9693 | |||
| 01c12655b8 |
25
Changelog.md
25
Changelog.md
@ -2,12 +2,35 @@
|
||||
|
||||
Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
|
||||
## [Bridge 3.0.8] Perth Narrows
|
||||
|
||||
### Fixed
|
||||
* Other: Add sentry reports for event processing failures.
|
||||
* Other: Do not fail on label events.
|
||||
|
||||
|
||||
## [Bridge 3.0.7] Perth Narrows
|
||||
|
||||
### Fixed
|
||||
* Other: Increase default UIDVALIDITY.
|
||||
* GODT-2173: fix: Migrate Bridge passwords from v2.X.
|
||||
* GODT-2207: Fix encoding of non utf7 mailbox names.
|
||||
* Other: Increase worker count (2 -> 4).
|
||||
|
||||
|
||||
## [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
|
||||
@ -21,6 +44,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* GODT-2170: Update draft event means delete old and create new message.
|
||||
* GODT-2170: User create draft route: first steps.
|
||||
|
||||
|
||||
## [Bridge 3.0.3] Perth Narrows
|
||||
|
||||
### Fixed
|
||||
@ -51,6 +75,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
||||
* Other: Ensure context is string in sentry reports.
|
||||
* GODT-2160: Ensure we can safely move cache file.
|
||||
|
||||
|
||||
## [Bridge 3.0.1] Perth Narrows
|
||||
|
||||
### Changed
|
||||
|
||||
2
Makefile
2
Makefile
@ -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.5+git
|
||||
BRIDGE_APP_VERSION?=3.0.8+git
|
||||
APP_VERSION:=${BRIDGE_APP_VERSION}
|
||||
APP_FULL_NAME:=Proton Mail Bridge
|
||||
APP_VENDOR:=Proton AG
|
||||
|
||||
2
go.mod
2
go.mod
@ -5,7 +5,7 @@ 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.20221214122222-2ab5c92d3546
|
||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
||||
github.com/ProtonMail/go-proton-api v0.2.1
|
||||
github.com/ProtonMail/go-rfc5322 v0.11.0
|
||||
|
||||
4
go.sum
4
go.sum
@ -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.20221214122222-2ab5c92d3546 h1:iyN4eO1Z0N+inMukpoBCmfbI+ubAop4Op/sdzmmUcm4=
|
||||
github.com/ProtonMail/gluon v0.14.2-0.20221214122222-2ab5c92d3546/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=
|
||||
|
||||
@ -32,6 +32,7 @@ import (
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
"github.com/ProtonMail/proton-bridge/v3/pkg/algo"
|
||||
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
|
||||
"github.com/allan-simon/go-singleinstance"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
@ -43,6 +44,16 @@ import (
|
||||
func migrateKeychainHelper(locations *locations.Locations) error {
|
||||
logrus.Info("Migrating keychain helper")
|
||||
|
||||
settings, err := locations.ProvideSettingsPath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get settings path: %w", err)
|
||||
}
|
||||
|
||||
if keychainName, _ := vault.GetHelper(settings); keychainName != "" {
|
||||
// If uncorupted keychain file is already there do not migrate again.
|
||||
return nil
|
||||
}
|
||||
|
||||
configDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user config dir: %w", err)
|
||||
@ -63,11 +74,6 @@ func migrateKeychainHelper(locations *locations.Locations) error {
|
||||
return fmt.Errorf("failed to unmarshal old prefs file: %w", err)
|
||||
}
|
||||
|
||||
settings, err := locations.ProvideSettingsPath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get settings path: %w", err)
|
||||
}
|
||||
|
||||
return vault.SetHelper(settings, prefs.Helper)
|
||||
}
|
||||
|
||||
@ -115,26 +121,52 @@ func migrateOldAccounts(locations *locations.Locations, v *vault.Vault) error {
|
||||
return fmt.Errorf("failed to create credentials store: %w", err)
|
||||
}
|
||||
|
||||
var migrationErrors error
|
||||
|
||||
for _, userID := range users {
|
||||
logrus.WithField("userID", userID).Info("Migrating account")
|
||||
|
||||
creds, err := store.Get(userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user: %w", err)
|
||||
if err := migrateOldAccount(userID, store, v); err != nil {
|
||||
migrationErrors = multierror.Append(migrationErrors, err)
|
||||
}
|
||||
}
|
||||
|
||||
authUID, authRef, err := creds.SplitAPIToken()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to split api token: %w", err)
|
||||
}
|
||||
return migrationErrors
|
||||
}
|
||||
|
||||
user, err := v.AddUser(creds.UserID, creds.EmailList()[0], authUID, authRef, creds.MailboxPassword)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add user: %w", err)
|
||||
}
|
||||
func migrateOldAccount(userID string, store *credentials.Store, v *vault.Vault) error {
|
||||
creds, err := store.Get(userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user %q: %w", userID, err)
|
||||
}
|
||||
|
||||
authUID, authRef, err := creds.SplitAPIToken()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to split api token for user %q: %w", userID, err)
|
||||
}
|
||||
|
||||
user, err := v.AddUser(creds.UserID, creds.EmailList()[0], authUID, authRef, creds.MailboxPassword)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to add user %q: %w", userID, err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := user.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close user: %w", err)
|
||||
logrus.WithField("userID", userID).WithError(err).Error("Failed to close vault user after migration")
|
||||
}
|
||||
}()
|
||||
|
||||
dec, err := algo.B64RawDecode([]byte(creds.BridgePassword))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode bridge password for user %q: %w", userID, err)
|
||||
}
|
||||
|
||||
if err := user.SetBridgePass(dec); err != nil {
|
||||
return fmt.Errorf("failed to set bridge password to user %q: %w", userID, err)
|
||||
}
|
||||
|
||||
if !creds.IsCombinedAddressMode {
|
||||
if err := user.SetAddressMode(vault.SplitMode); err != nil {
|
||||
return fmt.Errorf("failed to set split address mode to user %q: %w", userID, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -22,11 +22,19 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/cookies"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/legacy/credentials"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/locations"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/updater"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
"github.com/ProtonMail/proton-bridge/v3/pkg/algo"
|
||||
"github.com/ProtonMail/proton-bridge/v3/pkg/keychain"
|
||||
dockerCredentials "github.com/docker/docker-credential-helpers/credentials"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@ -79,3 +87,113 @@ func TestMigratePrefsToVault(t *testing.T) {
|
||||
// There should be a cookie for the API.
|
||||
require.NotEmpty(t, cookies.Cookies(url))
|
||||
}
|
||||
|
||||
func TestKeychainMigration(t *testing.T) {
|
||||
// Migration tested only for linux.
|
||||
if runtime.GOOS != "linux" {
|
||||
return
|
||||
}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
|
||||
// Prepare for keychain migration test
|
||||
{
|
||||
require.NoError(t, os.Setenv("XDG_CONFIG_HOME", tmpDir))
|
||||
oldCacheDir := filepath.Join(tmpDir, "protonmail", "bridge")
|
||||
require.NoError(t, os.MkdirAll(oldCacheDir, 0o700))
|
||||
|
||||
oldPrefs, err := os.ReadFile(filepath.Join("testdata", "prefs.json"))
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, os.WriteFile(
|
||||
filepath.Join(oldCacheDir, "prefs.json"),
|
||||
oldPrefs, 0o600,
|
||||
))
|
||||
}
|
||||
|
||||
locations := locations.New(bridge.NewTestLocationsProvider(tmpDir), "config-name")
|
||||
settingsFolder, err := locations.ProvideSettingsPath()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Check that there is nothing yet
|
||||
keychainName, err := vault.GetHelper(settingsFolder)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "", keychainName)
|
||||
|
||||
// Check migration
|
||||
require.NoError(t, migrateKeychainHelper(locations))
|
||||
keychainName, err = vault.GetHelper(settingsFolder)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "secret-service", keychainName)
|
||||
|
||||
// Change the migrated value
|
||||
require.NoError(t, vault.SetHelper(settingsFolder, "different"))
|
||||
|
||||
// Calling migration again will not overwrite existing prefs
|
||||
require.NoError(t, migrateKeychainHelper(locations))
|
||||
keychainName, err = vault.GetHelper(settingsFolder)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "different", keychainName)
|
||||
}
|
||||
|
||||
func TestUserMigration(t *testing.T) {
|
||||
keychainHelper := keychain.NewTestHelper()
|
||||
|
||||
keychain.Helpers["mock"] = func(string) (dockerCredentials.Helper, error) { return keychainHelper, nil }
|
||||
|
||||
kc, err := keychain.NewKeychain("mock", "bridge")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, kc.Put("brokenID", "broken"))
|
||||
require.NoError(t, kc.Put(
|
||||
"emptyID",
|
||||
(&credentials.Credentials{}).Marshal(),
|
||||
))
|
||||
|
||||
wantUID := "uidtoken"
|
||||
wantRefresh := "refreshtoken"
|
||||
|
||||
wantCredentials := credentials.Credentials{
|
||||
UserID: "validID",
|
||||
Name: "user@pm.me",
|
||||
Emails: "user@pm.me;alias@pm.me",
|
||||
APIToken: wantUID + ":" + wantRefresh,
|
||||
MailboxPassword: []byte("secret"),
|
||||
BridgePassword: "bElu2Q1Vusy28J3Wf56cIg",
|
||||
Version: "v2.3.X",
|
||||
Timestamp: 100,
|
||||
IsCombinedAddressMode: true,
|
||||
}
|
||||
require.NoError(t, kc.Put(
|
||||
wantCredentials.UserID,
|
||||
wantCredentials.Marshal(),
|
||||
))
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
locations := locations.New(bridge.NewTestLocationsProvider(tmpDir), "config-name")
|
||||
settingsFolder, err := locations.ProvideSettingsPath()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, vault.SetHelper(settingsFolder, "mock"))
|
||||
|
||||
token, err := crypto.RandomToken(32)
|
||||
require.NoError(t, err)
|
||||
|
||||
v, corrupt, err := vault.New(settingsFolder, settingsFolder, token)
|
||||
require.NoError(t, err)
|
||||
require.False(t, corrupt)
|
||||
|
||||
require.NoError(t, migrateOldAccounts(locations, v))
|
||||
require.Equal(t, []string{wantCredentials.UserID}, v.GetUserIDs())
|
||||
|
||||
require.NoError(t, v.GetUser(wantCredentials.UserID, func(u *vault.User) {
|
||||
require.Equal(t, wantCredentials.UserID, u.UserID())
|
||||
require.Equal(t, wantUID, u.AuthUID())
|
||||
require.Equal(t, wantRefresh, u.AuthRef())
|
||||
require.Equal(t, wantCredentials.MailboxPassword, u.KeyPass())
|
||||
require.Equal(t,
|
||||
[]byte(wantCredentials.BridgePassword),
|
||||
algo.B64RawEncode(u.BridgePass()),
|
||||
)
|
||||
require.Equal(t, vault.CombinedMode, u.AddressMode())
|
||||
}))
|
||||
}
|
||||
|
||||
@ -73,7 +73,7 @@ func TestBridge_Refresh(t *testing.T) {
|
||||
for _, name := range names {
|
||||
status, err := client.Select("Folders/"+name, false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint32(1), status.UidValidity)
|
||||
require.Equal(t, uint32(1000), status.UidValidity)
|
||||
}
|
||||
})
|
||||
|
||||
@ -106,7 +106,7 @@ func TestBridge_Refresh(t *testing.T) {
|
||||
for _, name := range names {
|
||||
status, err := client.Select("Folders/"+name, false)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, uint32(2), status.UidValidity)
|
||||
require.Equal(t, uint32(1001), status.UidValidity)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@ -441,9 +441,9 @@ func (bridge *Bridge) addUserWithVault(
|
||||
ctx,
|
||||
vault,
|
||||
client,
|
||||
bridge.reporter,
|
||||
apiUser,
|
||||
bridge.crashHandler,
|
||||
bridge.reporter,
|
||||
bridge.vault.SyncWorkers(),
|
||||
bridge.vault.GetShowAllMail(),
|
||||
)
|
||||
|
||||
@ -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"
|
||||
@ -152,16 +153,31 @@ func (user *User) handleAddressEvents(ctx context.Context, addressEvents []proto
|
||||
switch event.Action {
|
||||
case proton.EventCreate:
|
||||
if err := user.handleCreateAddressEvent(ctx, event); err != nil {
|
||||
if rerr := user.reporter.ReportMessageWithContext("Failed to apply address create event", reporter.Context{
|
||||
"error": err,
|
||||
}); rerr != nil {
|
||||
user.log.WithError(err).Error("Failed to report address create event error")
|
||||
}
|
||||
return fmt.Errorf("failed to handle create address event: %w", err)
|
||||
}
|
||||
|
||||
case proton.EventUpdate, proton.EventUpdateFlags:
|
||||
if err := user.handleUpdateAddressEvent(ctx, event); err != nil {
|
||||
if rerr := user.reporter.ReportMessageWithContext("Failed to apply address update event", reporter.Context{
|
||||
"error": err,
|
||||
}); rerr != nil {
|
||||
user.log.WithError(err).Error("Failed to report address update event error")
|
||||
}
|
||||
return fmt.Errorf("failed to handle update address event: %w", err)
|
||||
}
|
||||
|
||||
case proton.EventDelete:
|
||||
if err := user.handleDeleteAddressEvent(ctx, event); err != nil {
|
||||
if rerr := user.reporter.ReportMessageWithContext("Failed to apply address delete event", reporter.Context{
|
||||
"error": err,
|
||||
}); rerr != nil {
|
||||
user.log.WithError(err).Error("Failed to report address delete event error")
|
||||
}
|
||||
return fmt.Errorf("failed to delete address: %w", err)
|
||||
}
|
||||
}
|
||||
@ -300,11 +316,9 @@ func (user *User) handleCreateLabelEvent(_ context.Context, event proton.LabelEv
|
||||
}).Info("Handling label created event")
|
||||
|
||||
if _, ok := user.apiLabels[event.Label.ID]; ok {
|
||||
return fmt.Errorf("label %q already exists", event.ID)
|
||||
user.apiLabels[event.Label.ID] = event.Label
|
||||
}
|
||||
|
||||
user.apiLabels[event.Label.ID] = event.Label
|
||||
|
||||
for _, updateCh := range user.updateCh {
|
||||
updateCh.Enqueue(newMailboxCreatedUpdate(imap.MailboxID(event.ID), getMailboxName(event.Label)))
|
||||
}
|
||||
@ -327,11 +341,9 @@ func (user *User) handleUpdateLabelEvent(_ context.Context, event proton.LabelEv
|
||||
}).Info("Handling label updated event")
|
||||
|
||||
if _, ok := user.apiLabels[event.Label.ID]; !ok {
|
||||
return fmt.Errorf("label %q does not exist", event.ID)
|
||||
user.apiLabels[event.Label.ID] = event.Label
|
||||
}
|
||||
|
||||
user.apiLabels[event.Label.ID] = event.Label
|
||||
|
||||
for _, updateCh := range user.updateCh {
|
||||
updateCh.Enqueue(imap.NewMailboxUpdated(
|
||||
imap.MailboxID(event.ID),
|
||||
@ -355,11 +367,9 @@ func (user *User) handleDeleteLabelEvent(_ context.Context, event proton.LabelEv
|
||||
|
||||
label, ok := user.apiLabels[event.ID]
|
||||
if !ok {
|
||||
return fmt.Errorf("label %q does not exist", event.ID)
|
||||
delete(user.apiLabels, event.ID)
|
||||
}
|
||||
|
||||
delete(user.apiLabels, event.ID)
|
||||
|
||||
for _, updateCh := range user.updateCh {
|
||||
updateCh.Enqueue(imap.NewMailboxDeleted(imap.MailboxID(event.ID)))
|
||||
}
|
||||
@ -375,7 +385,7 @@ func (user *User) handleDeleteLabelEvent(_ context.Context, event proton.LabelEv
|
||||
}
|
||||
|
||||
// handleMessageEvents handles the given message events.
|
||||
func (user *User) handleMessageEvents(ctx context.Context, messageEvents []proton.MessageEvent) error {
|
||||
func (user *User) handleMessageEvents(ctx context.Context, messageEvents []proton.MessageEvent) error { //nolint:funlen
|
||||
for _, event := range messageEvents {
|
||||
ctx = logging.WithLogrusField(ctx, "messageID", event.ID)
|
||||
|
||||
@ -385,6 +395,11 @@ func (user *User) handleMessageEvents(ctx context.Context, messageEvents []proto
|
||||
logging.WithLogrusField(ctx, "action", "create message"),
|
||||
event,
|
||||
); err != nil {
|
||||
if rerr := user.reporter.ReportMessageWithContext("Failed to apply create message event", reporter.Context{
|
||||
"error": err,
|
||||
}); rerr != nil {
|
||||
user.log.WithError(err).Error("Failed to report create message event error")
|
||||
}
|
||||
return fmt.Errorf("failed to handle create message event: %w", err)
|
||||
}
|
||||
|
||||
@ -395,6 +410,11 @@ func (user *User) handleMessageEvents(ctx context.Context, messageEvents []proto
|
||||
logging.WithLogrusField(ctx, "action", "update draft"),
|
||||
event,
|
||||
); err != nil {
|
||||
if rerr := user.reporter.ReportMessageWithContext("Failed to apply update draft message event", reporter.Context{
|
||||
"error": err,
|
||||
}); rerr != nil {
|
||||
user.log.WithError(err).Error("Failed to report update draft message event error")
|
||||
}
|
||||
return fmt.Errorf("failed to handle update draft event: %w", err)
|
||||
}
|
||||
|
||||
@ -409,6 +429,11 @@ func (user *User) handleMessageEvents(ctx context.Context, messageEvents []proto
|
||||
logging.WithLogrusField(ctx, "action", "update message"),
|
||||
event,
|
||||
); err != nil {
|
||||
if rerr := user.reporter.ReportMessageWithContext("Failed to apply update message event", reporter.Context{
|
||||
"error": err,
|
||||
}); rerr != nil {
|
||||
user.log.WithError(err).Error("Failed to report update message event error")
|
||||
}
|
||||
return fmt.Errorf("failed to handle update message event: %w", err)
|
||||
}
|
||||
|
||||
@ -417,6 +442,11 @@ func (user *User) handleMessageEvents(ctx context.Context, messageEvents []proto
|
||||
logging.WithLogrusField(ctx, "action", "delete message"),
|
||||
event,
|
||||
); err != nil {
|
||||
if rerr := user.reporter.ReportMessageWithContext("Failed to apply delete message event", reporter.Context{
|
||||
"error": err,
|
||||
}); rerr != nil {
|
||||
user.log.WithError(err).Error("Failed to report delete message event error")
|
||||
}
|
||||
return fmt.Errorf("failed to handle delete message event: %w", err)
|
||||
}
|
||||
}
|
||||
@ -438,12 +468,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 +541,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 draft 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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -18,18 +18,23 @@
|
||||
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/algo"
|
||||
"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 +48,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 +89,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(algo.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 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)
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -18,7 +18,6 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
@ -58,27 +57,6 @@ func groupBy[Key comparable, Value any](items []Value, key func(Value) Key) map[
|
||||
return groups
|
||||
}
|
||||
|
||||
// b64Encode returns the base64 encoding of the given byte slice.
|
||||
func b64Encode(b []byte) []byte {
|
||||
enc := make([]byte, base64.RawURLEncoding.EncodedLen(len(b)))
|
||||
|
||||
base64.RawURLEncoding.Encode(enc, b)
|
||||
|
||||
return enc
|
||||
}
|
||||
|
||||
// b64Decode returns the bytes represented by the base64 encoding of the given byte slice.
|
||||
func b64Decode(b []byte) ([]byte, error) {
|
||||
dec := make([]byte, base64.RawURLEncoding.DecodedLen(len(b)))
|
||||
|
||||
n, err := base64.RawURLEncoding.Decode(dec, b)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dec[:n], nil
|
||||
}
|
||||
|
||||
// getAddrID returns the address ID for the given email address.
|
||||
func getAddrID(apiAddrs map[string]proton.Address, email string) (string, error) {
|
||||
for _, addr := range apiAddrs {
|
||||
|
||||
@ -29,13 +29,14 @@ 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"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/logging"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/safe"
|
||||
"github.com/ProtonMail/proton-bridge/v3/internal/vault"
|
||||
"github.com/ProtonMail/proton-bridge/v3/pkg/algo"
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
"github.com/go-resty/resty/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
@ -57,6 +58,7 @@ type User struct {
|
||||
|
||||
vault *vault.User
|
||||
client *proton.Client
|
||||
reporter reporter.Reporter
|
||||
eventCh *queue.QueuedChannel[events.Event]
|
||||
sendHash *sendRecorder
|
||||
|
||||
@ -72,8 +74,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 +92,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 +118,7 @@ func New(
|
||||
|
||||
vault: encVault,
|
||||
client: client,
|
||||
reporter: reporter,
|
||||
eventCh: queue.NewQueuedChannel[events.Event](0, 0),
|
||||
sendHash: newSendRecorder(sendEntryExpiry),
|
||||
|
||||
@ -133,8 +134,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 +356,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 algo.B64RawEncode(user.vault.BridgePass())
|
||||
}
|
||||
|
||||
// UsedSpace returns the total space used by the user on the API.
|
||||
@ -433,7 +432,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 := algo.B64RawDecode(password)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to decode password: %w", err)
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
@ -29,14 +29,16 @@ type Keychain struct {
|
||||
Helper string
|
||||
}
|
||||
|
||||
func GetHelper(vaultDir string) (string, error) {
|
||||
filePath := filepath.Clean(filepath.Join(vaultDir, "keychain.json"))
|
||||
func getKeychainPrefPath(vaultDir string) string {
|
||||
return filepath.Clean(filepath.Join(vaultDir, "keychain.json"))
|
||||
}
|
||||
|
||||
if _, err := os.Stat(filePath); errors.Is(err, fs.ErrNotExist) {
|
||||
func GetHelper(vaultDir string) (string, error) {
|
||||
if _, err := os.Stat(getKeychainPrefPath(vaultDir)); errors.Is(err, fs.ErrNotExist) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
b, err := os.ReadFile(filePath)
|
||||
b, err := os.ReadFile(getKeychainPrefPath(vaultDir))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@ -56,5 +58,5 @@ func SetHelper(vaultDir, helper string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(filepath.Clean(filepath.Join(vaultDir, "keychain.json")), b, 0o600)
|
||||
return os.WriteFile(getKeychainPrefPath(vaultDir), b, 0o600)
|
||||
}
|
||||
|
||||
@ -52,7 +52,7 @@ type Settings struct {
|
||||
func GetDefaultSyncWorkerCount() int {
|
||||
const minSyncWorkers = 16
|
||||
|
||||
syncWorkers := runtime.NumCPU() * 2
|
||||
syncWorkers := runtime.NumCPU() * 4
|
||||
|
||||
if syncWorkers < minSyncWorkers {
|
||||
syncWorkers = minSyncWorkers
|
||||
|
||||
@ -28,7 +28,7 @@ type UserData struct {
|
||||
GluonKey []byte
|
||||
GluonIDs map[string]string
|
||||
UIDValidity map[string]imap.UID
|
||||
BridgePass []byte
|
||||
BridgePass []byte // raw token represented as byte slice (needs to be encoded)
|
||||
AddressMode AddressMode
|
||||
|
||||
AuthUID string
|
||||
@ -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 {
|
||||
|
||||
@ -21,6 +21,8 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/ProtonMail/gluon/imap"
|
||||
"github.com/bradenaw/juniper/xslices"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
@ -72,7 +74,7 @@ func (user *User) GetUIDValidity(addrID string) imap.UID {
|
||||
return validity
|
||||
}
|
||||
|
||||
if err := user.SetUIDValidity(addrID, 1); err != nil {
|
||||
if err := user.SetUIDValidity(addrID, 1000); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@ -97,11 +99,18 @@ func (user *User) SetAddressMode(mode AddressMode) error {
|
||||
})
|
||||
}
|
||||
|
||||
// BridgePass returns the user's bridge password (unencoded).
|
||||
// BridgePass returns the user's bridge password as raw token bytes (unencoded).
|
||||
func (user *User) BridgePass() []byte {
|
||||
return user.vault.getUser(user.userID).BridgePass
|
||||
}
|
||||
|
||||
// SetBridgePass saves bridge password as raw token bytes (unecoded).
|
||||
func (user *User) SetBridgePass(newPass []byte) error {
|
||||
return user.vault.modUser(user.userID, func(data *UserData) {
|
||||
data.BridgePass = newPass
|
||||
})
|
||||
}
|
||||
|
||||
// AuthUID returns the user's auth UID.
|
||||
func (user *User) AuthUID() string {
|
||||
return user.vault.getUser(user.userID).AuthUID
|
||||
@ -158,6 +167,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) {
|
||||
|
||||
50
pkg/algo/encode.go
Normal file
50
pkg/algo/encode.go
Normal file
@ -0,0 +1,50 @@
|
||||
// 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 algo
|
||||
|
||||
import "encoding/base64"
|
||||
|
||||
// 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)
|
||||
|
||||
return enc
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return dec[:n], nil
|
||||
}
|
||||
@ -21,7 +21,6 @@ import (
|
||||
"encoding/base64"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/docker-credential-helpers/credentials"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@ -33,7 +32,7 @@ var testData = map[string]string{ //nolint:gochecknoglobals
|
||||
}
|
||||
|
||||
func TestInsertReadRemove(t *testing.T) {
|
||||
keychain := newKeychain(newTestHelper(), hostURL("bridge"))
|
||||
keychain := newKeychain(NewTestHelper(), hostURL("bridge"))
|
||||
|
||||
for id, secret := range testData {
|
||||
expectedList, _ := keychain.List()
|
||||
@ -115,35 +114,3 @@ func TestInsertReadRemove(t *testing.T) {
|
||||
require.NotContains(t, actualList, id)
|
||||
}
|
||||
}
|
||||
|
||||
type testHelper map[string]*credentials.Credentials
|
||||
|
||||
func newTestHelper() testHelper {
|
||||
return make(testHelper)
|
||||
}
|
||||
|
||||
func (h testHelper) Add(creds *credentials.Credentials) error {
|
||||
h[creds.ServerURL] = creds
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h testHelper) Delete(url string) error {
|
||||
delete(h, url)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h testHelper) Get(url string) (string, string, error) {
|
||||
creds := h[url]
|
||||
|
||||
return creds.Username, creds.Secret, nil
|
||||
}
|
||||
|
||||
func (h testHelper) List() (map[string]string, error) {
|
||||
list := make(map[string]string)
|
||||
|
||||
for url, creds := range h {
|
||||
list[url] = creds.Username
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
52
pkg/keychain/test_helper.go
Normal file
52
pkg/keychain/test_helper.go
Normal file
@ -0,0 +1,52 @@
|
||||
// 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 keychain
|
||||
|
||||
import "github.com/docker/docker-credential-helpers/credentials"
|
||||
|
||||
type TestHelper map[string]*credentials.Credentials
|
||||
|
||||
func NewTestHelper() TestHelper {
|
||||
return make(TestHelper)
|
||||
}
|
||||
|
||||
func (h TestHelper) Add(creds *credentials.Credentials) error {
|
||||
h[creds.ServerURL] = creds
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h TestHelper) Delete(url string) error {
|
||||
delete(h, url)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h TestHelper) Get(url string) (string, string, error) {
|
||||
creds := h[url]
|
||||
|
||||
return creds.Username, creds.Secret, nil
|
||||
}
|
||||
|
||||
func (h TestHelper) List() (map[string]string, error) {
|
||||
list := make(map[string]string)
|
||||
|
||||
for url, creds := range h {
|
||||
list[url] = creds.Username
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
Reference in New Issue
Block a user