Store factory to make store optional

This commit is contained in:
Michal Horejsek
2020-05-25 15:34:18 +02:00
parent 3b0b1a457b
commit 40d8c458d2
16 changed files with 338 additions and 235 deletions

View File

@ -5,6 +5,8 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
## Unreleased ## Unreleased
### Changed ### Changed
* GODT-388 support for both bridge and import/export credentials by package users
* GODT-387 store factory to make store optional
* GODT-386 renamed bridge to general users and keep bridge only for bridge stuff * GODT-386 renamed bridge to general users and keep bridge only for bridge stuff
* GODT-308 better user error message when request is canceled * GODT-308 better user error message when request is canceled
* GODT-312 validate recipient emails in send before asking for their public keys * GODT-312 validate recipient emails in send before asking for their public keys

View File

@ -171,7 +171,7 @@ coverage: test
go tool cover -html=/tmp/coverage.out -o=coverage.html go tool cover -html=/tmp/coverage.out -o=coverage.html
mocks: mocks:
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/users Configer,PreferenceProvider,PanicHandler,ClientManager,CredentialsStorer > internal/users/mocks/mocks.go mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/users Configer,PanicHandler,ClientManager,CredentialsStorer,StoreMaker > internal/users/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/store PanicHandler,ClientManager,BridgeUser > internal/store/mocks/mocks.go mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/store PanicHandler,ClientManager,BridgeUser > internal/store/mocks/mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/listener Listener > internal/store/mocks/utils_mocks.go mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/listener Listener > internal/store/mocks/utils_mocks.go
mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/pmapi Client > pkg/pmapi/mocks/mocks.go mockgen --package mocks github.com/ProtonMail/proton-bridge/pkg/pmapi Client > pkg/pmapi/mocks/mocks.go

View File

@ -19,6 +19,11 @@
package bridge package bridge
import ( import (
"strconv"
"time"
"github.com/ProtonMail/proton-bridge/internal/metrics"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/internal/users" "github.com/ProtonMail/proton-bridge/internal/users"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
@ -32,6 +37,7 @@ var (
type Bridge struct { type Bridge struct {
*users.Users *users.Users
pref PreferenceProvider
clientManager users.ClientManager clientManager users.ClientManager
userAgentClientName string userAgentClientName string
@ -40,19 +46,53 @@ type Bridge struct {
} }
func New( func New(
config users.Configer, config Configer,
pref users.PreferenceProvider, pref PreferenceProvider,
panicHandler users.PanicHandler, panicHandler users.PanicHandler,
eventListener listener.Listener, eventListener listener.Listener,
clientManager users.ClientManager, clientManager users.ClientManager,
credStorer users.CredentialsStorer, credStorer users.CredentialsStorer,
) *Bridge { ) *Bridge {
u := users.New(config, pref, panicHandler, eventListener, clientManager, credStorer) storeFactory := newStoreFactory(config, panicHandler, clientManager, eventListener)
return &Bridge{ u := users.New(config, panicHandler, eventListener, clientManager, credStorer, storeFactory)
b := &Bridge{
Users: u, Users: u,
pref: pref,
clientManager: clientManager, clientManager: clientManager,
} }
// Allow DoH before starting the app if the user has previously set this setting.
// This allows us to start even if protonmail is blocked.
if pref.GetBool(preferences.AllowProxyKey) {
b.AllowProxy()
}
if pref.GetBool(preferences.FirstStartKey) {
b.SendMetric(metrics.New(metrics.Setup, metrics.FirstStart, metrics.Label(config.GetVersion())))
}
go b.heartbeat()
return b
}
// heartbeat sends a heartbeat signal once a day.
func (b *Bridge) heartbeat() {
ticker := time.NewTicker(1 * time.Minute)
for range ticker.C {
next, err := strconv.ParseInt(b.pref.Get(preferences.NextHeartbeatKey), 10, 64)
if err != nil {
continue
}
nextTime := time.Unix(next, 0)
if time.Now().After(nextTime) {
b.SendMetric(metrics.New(metrics.Heartbeat, metrics.Daily, metrics.NoLabel))
nextTime = nextTime.Add(24 * time.Hour)
b.pref.Set(preferences.NextHeartbeatKey, strconv.FormatInt(nextTime.Unix(), 10))
}
}
} }
// GetCurrentClient returns currently connected client (e.g. Thunderbird). // GetCurrentClient returns currently connected client (e.g. Thunderbird).

View File

@ -0,0 +1,69 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge
import (
"fmt"
"path/filepath"
"github.com/ProtonMail/proton-bridge/internal/store"
"github.com/ProtonMail/proton-bridge/internal/users"
"github.com/ProtonMail/proton-bridge/pkg/listener"
)
type storeFactory struct {
config StoreFactoryConfiger
panicHandler users.PanicHandler
clientManager users.ClientManager
eventListener listener.Listener
storeCache *store.Cache
}
func newStoreFactory(
config StoreFactoryConfiger,
panicHandler users.PanicHandler,
clientManager users.ClientManager,
eventListener listener.Listener,
) *storeFactory {
return &storeFactory{
config: config,
panicHandler: panicHandler,
clientManager: clientManager,
eventListener: eventListener,
storeCache: store.NewCache(config.GetIMAPCachePath()),
}
}
// New creates new store for given user.
func (f *storeFactory) New(user store.BridgeUser) (*store.Store, error) {
storePath := getUserStorePath(f.config.GetDBDir(), user.ID())
return store.New(f.panicHandler, user, f.clientManager, f.eventListener, storePath, f.storeCache)
}
// Remove removes all store files for given user.
func (f *storeFactory) Remove(userID string) error {
storePath := getUserStorePath(f.config.GetDBDir(), userID)
return store.RemoveStore(f.storeCache, storePath, userID)
}
// getUserStorePath returns the file path of the store database for the given userID.
func getUserStorePath(storeDir string, userID string) (path string) {
fileName := fmt.Sprintf("mailbox-%v.db", userID)
return filepath.Join(storeDir, fileName)
}

37
internal/bridge/types.go Normal file
View File

@ -0,0 +1,37 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package bridge
import "github.com/ProtonMail/proton-bridge/internal/users"
type Configer interface {
users.Configer
StoreFactoryConfiger
}
type StoreFactoryConfiger interface {
GetDBDir() string
GetIMAPCachePath() string
}
type PreferenceProvider interface {
Get(key string) string
GetBool(key string) bool
GetInt(key string) int
Set(key string, value string)
}

View File

@ -30,12 +30,17 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
) )
const sep = "\x00" const (
sep = "\x00"
itemLengthBridge = 9
itemLengthImportExport = 6 // Old format for Import/Export.
)
var ( var (
log = logrus.WithField("pkg", "credentials") //nolint[gochecknoglobals] log = logrus.WithField("pkg", "credentials") //nolint[gochecknoglobals]
ErrWrongFormat = errors.New("backend/creds: malformed password") ErrWrongFormat = errors.New("malformed credentials")
) )
type Credentials struct { type Credentials struct {
@ -85,7 +90,7 @@ func (s *Credentials) Unmarshal(secret string) error {
} }
items := strings.Split(string(b), sep) items := strings.Split(string(b), sep)
if len(items) != 9 { if len(items) != itemLengthBridge && len(items) != itemLengthImportExport {
return ErrWrongFormat return ErrWrongFormat
} }
@ -93,16 +98,26 @@ func (s *Credentials) Unmarshal(secret string) error {
s.Emails = items[1] s.Emails = items[1]
s.APIToken = items[2] s.APIToken = items[2]
s.MailboxPassword = items[3] s.MailboxPassword = items[3]
s.BridgePassword = items[4]
s.Version = items[5] switch len(items) {
if _, err = fmt.Sscan(items[6], &s.Timestamp); err != nil { case itemLengthBridge:
s.Timestamp = 0 s.BridgePassword = items[4]
} s.Version = items[5]
if s.IsHidden = false; items[7] == "1" { if _, err = fmt.Sscan(items[6], &s.Timestamp); err != nil {
s.IsHidden = true s.Timestamp = 0
} }
if s.IsCombinedAddressMode = false; items[8] == "1" { if s.IsHidden = false; items[7] == "1" {
s.IsCombinedAddressMode = true s.IsHidden = true
}
if s.IsCombinedAddressMode = false; items[8] == "1" {
s.IsCombinedAddressMode = true
}
case itemLengthImportExport:
s.Version = items[4]
if _, err = fmt.Sscan(items[5], &s.Timestamp); err != nil {
s.Timestamp = 0
}
} }
return nil return nil
} }

View File

@ -0,0 +1,67 @@
// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
package credentials
import (
"encoding/base64"
"fmt"
"strings"
"testing"
"time"
r "github.com/stretchr/testify/require"
)
var wantCredentials = Credentials{
UserID: "1",
Name: "name",
Emails: "email1;email2",
APIToken: "token",
MailboxPassword: "mailbox pass",
BridgePassword: "bridge pass",
Version: "k11",
Timestamp: time.Now().Unix(),
IsHidden: false,
IsCombinedAddressMode: false,
}
func TestUnmarshallBridge(t *testing.T) {
encoded := wantCredentials.Marshal()
haveCredentials := Credentials{UserID: "1"}
r.NoError(t, haveCredentials.Unmarshal(encoded))
r.Equal(t, wantCredentials, haveCredentials)
}
func TestUnmarshallImportExport(t *testing.T) {
items := []string{
wantCredentials.Name,
wantCredentials.Emails,
wantCredentials.APIToken,
wantCredentials.MailboxPassword,
"k11",
fmt.Sprint(wantCredentials.Timestamp),
}
str := strings.Join(items, sep)
encoded := base64.StdEncoding.EncodeToString([]byte(str))
haveCredentials := Credentials{UserID: "1"}
haveCredentials.BridgePassword = wantCredentials.BridgePassword // This one is not used.
r.NoError(t, haveCredentials.Unmarshal(encoded))
r.Equal(t, wantCredentials, haveCredentials)
}

View File

@ -1,15 +1,15 @@
// Code generated by MockGen. DO NOT EDIT. // Code generated by MockGen. DO NOT EDIT.
// Source: github.com/ProtonMail/proton-bridge/internal/users (interfaces: Configer,PreferenceProvider,PanicHandler,ClientManager,CredentialsStorer) // Source: github.com/ProtonMail/proton-bridge/internal/users (interfaces: Configer,PanicHandler,ClientManager,CredentialsStorer,StoreMaker)
// Package mocks is a generated GoMock package. // Package mocks is a generated GoMock package.
package mocks package mocks
import ( import (
reflect "reflect" store "github.com/ProtonMail/proton-bridge/internal/store"
credentials "github.com/ProtonMail/proton-bridge/internal/users/credentials" credentials "github.com/ProtonMail/proton-bridge/internal/users/credentials"
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi" pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
gomock "github.com/golang/mock/gomock" gomock "github.com/golang/mock/gomock"
reflect "reflect"
) )
// MockConfiger is a mock of Configer interface // MockConfiger is a mock of Configer interface
@ -63,34 +63,6 @@ func (mr *MockConfigerMockRecorder) GetAPIConfig() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAPIConfig", reflect.TypeOf((*MockConfiger)(nil).GetAPIConfig)) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAPIConfig", reflect.TypeOf((*MockConfiger)(nil).GetAPIConfig))
} }
// GetDBDir mocks base method
func (m *MockConfiger) GetDBDir() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetDBDir")
ret0, _ := ret[0].(string)
return ret0
}
// GetDBDir indicates an expected call of GetDBDir
func (mr *MockConfigerMockRecorder) GetDBDir() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetDBDir", reflect.TypeOf((*MockConfiger)(nil).GetDBDir))
}
// GetIMAPCachePath mocks base method
func (m *MockConfiger) GetIMAPCachePath() string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetIMAPCachePath")
ret0, _ := ret[0].(string)
return ret0
}
// GetIMAPCachePath indicates an expected call of GetIMAPCachePath
func (mr *MockConfigerMockRecorder) GetIMAPCachePath() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetIMAPCachePath", reflect.TypeOf((*MockConfiger)(nil).GetIMAPCachePath))
}
// GetVersion mocks base method // GetVersion mocks base method
func (m *MockConfiger) GetVersion() string { func (m *MockConfiger) GetVersion() string {
m.ctrl.T.Helper() m.ctrl.T.Helper()
@ -105,83 +77,6 @@ func (mr *MockConfigerMockRecorder) GetVersion() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVersion", reflect.TypeOf((*MockConfiger)(nil).GetVersion)) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetVersion", reflect.TypeOf((*MockConfiger)(nil).GetVersion))
} }
// MockPreferenceProvider is a mock of PreferenceProvider interface
type MockPreferenceProvider struct {
ctrl *gomock.Controller
recorder *MockPreferenceProviderMockRecorder
}
// MockPreferenceProviderMockRecorder is the mock recorder for MockPreferenceProvider
type MockPreferenceProviderMockRecorder struct {
mock *MockPreferenceProvider
}
// NewMockPreferenceProvider creates a new mock instance
func NewMockPreferenceProvider(ctrl *gomock.Controller) *MockPreferenceProvider {
mock := &MockPreferenceProvider{ctrl: ctrl}
mock.recorder = &MockPreferenceProviderMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockPreferenceProvider) EXPECT() *MockPreferenceProviderMockRecorder {
return m.recorder
}
// Get mocks base method
func (m *MockPreferenceProvider) Get(arg0 string) string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Get", arg0)
ret0, _ := ret[0].(string)
return ret0
}
// Get indicates an expected call of Get
func (mr *MockPreferenceProviderMockRecorder) Get(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Get", reflect.TypeOf((*MockPreferenceProvider)(nil).Get), arg0)
}
// GetBool mocks base method
func (m *MockPreferenceProvider) GetBool(arg0 string) bool {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetBool", arg0)
ret0, _ := ret[0].(bool)
return ret0
}
// GetBool indicates an expected call of GetBool
func (mr *MockPreferenceProviderMockRecorder) GetBool(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBool", reflect.TypeOf((*MockPreferenceProvider)(nil).GetBool), arg0)
}
// GetInt mocks base method
func (m *MockPreferenceProvider) GetInt(arg0 string) int {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "GetInt", arg0)
ret0, _ := ret[0].(int)
return ret0
}
// GetInt indicates an expected call of GetInt
func (mr *MockPreferenceProviderMockRecorder) GetInt(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInt", reflect.TypeOf((*MockPreferenceProvider)(nil).GetInt), arg0)
}
// Set mocks base method
func (m *MockPreferenceProvider) Set(arg0, arg1 string) {
m.ctrl.T.Helper()
m.ctrl.Call(m, "Set", arg0, arg1)
}
// Set indicates an expected call of Set
func (mr *MockPreferenceProviderMockRecorder) Set(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Set", reflect.TypeOf((*MockPreferenceProvider)(nil).Set), arg0, arg1)
}
// MockPanicHandler is a mock of PanicHandler interface // MockPanicHandler is a mock of PanicHandler interface
type MockPanicHandler struct { type MockPanicHandler struct {
ctrl *gomock.Controller ctrl *gomock.Controller
@ -483,3 +378,55 @@ func (mr *MockCredentialsStorerMockRecorder) UpdateToken(arg0, arg1 interface{})
mr.mock.ctrl.T.Helper() mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateToken", reflect.TypeOf((*MockCredentialsStorer)(nil).UpdateToken), arg0, arg1) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateToken", reflect.TypeOf((*MockCredentialsStorer)(nil).UpdateToken), arg0, arg1)
} }
// MockStoreMaker is a mock of StoreMaker interface
type MockStoreMaker struct {
ctrl *gomock.Controller
recorder *MockStoreMakerMockRecorder
}
// MockStoreMakerMockRecorder is the mock recorder for MockStoreMaker
type MockStoreMakerMockRecorder struct {
mock *MockStoreMaker
}
// NewMockStoreMaker creates a new mock instance
func NewMockStoreMaker(ctrl *gomock.Controller) *MockStoreMaker {
mock := &MockStoreMaker{ctrl: ctrl}
mock.recorder = &MockStoreMakerMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use
func (m *MockStoreMaker) EXPECT() *MockStoreMakerMockRecorder {
return m.recorder
}
// New mocks base method
func (m *MockStoreMaker) New(arg0 store.BridgeUser) (*store.Store, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "New", arg0)
ret0, _ := ret[0].(*store.Store)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// New indicates an expected call of New
func (mr *MockStoreMakerMockRecorder) New(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "New", reflect.TypeOf((*MockStoreMaker)(nil).New), arg0)
}
// Remove mocks base method
func (m *MockStoreMaker) Remove(arg0 string) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Remove", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// Remove indicates an expected call of Remove
func (mr *MockStoreMakerMockRecorder) Remove(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Remove", reflect.TypeOf((*MockStoreMaker)(nil).Remove), arg0)
}

View File

@ -18,25 +18,17 @@
package users package users
import ( import (
"github.com/ProtonMail/proton-bridge/internal/store"
"github.com/ProtonMail/proton-bridge/internal/users/credentials" "github.com/ProtonMail/proton-bridge/internal/users/credentials"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
) )
type Configer interface { type Configer interface {
ClearData() error ClearData() error
GetDBDir() string
GetVersion() string GetVersion() string
GetIMAPCachePath() string
GetAPIConfig() *pmapi.ClientConfig GetAPIConfig() *pmapi.ClientConfig
} }
type PreferenceProvider interface {
Get(key string) string
GetBool(key string) bool
GetInt(key string) int
Set(key string, value string)
}
type PanicHandler interface { type PanicHandler interface {
HandlePanic() HandlePanic()
} }
@ -62,3 +54,8 @@ type ClientManager interface {
CheckConnection() error CheckConnection() error
SetUserAgent(clientName, clientVersion, os string) SetUserAgent(clientName, clientVersion, os string)
} }
type StoreMaker interface {
New(user store.BridgeUser) (*store.Store, error)
Remove(userID string) error
}

View File

@ -18,8 +18,6 @@
package users package users
import ( import (
"fmt"
"path/filepath"
"runtime" "runtime"
"strings" "strings"
"sync" "sync"
@ -47,9 +45,8 @@ type User struct {
imapUpdatesChannel chan imapBackend.Update imapUpdatesChannel chan imapBackend.Update
store *store.Store storeFactory StoreMaker
storeCache *store.Cache store *store.Store
storePath string
userID string userID string
creds *credentials.Credentials creds *credentials.Credentials
@ -68,8 +65,7 @@ func newUser(
eventListener listener.Listener, eventListener listener.Listener,
credStorer CredentialsStorer, credStorer CredentialsStorer,
clientManager ClientManager, clientManager ClientManager,
storeCache *store.Cache, storeFactory StoreMaker,
storeDir string,
) (u *User, err error) { ) (u *User, err error) {
log := log.WithField("user", userID) log := log.WithField("user", userID)
log.Debug("Creating or loading user") log.Debug("Creating or loading user")
@ -85,8 +81,7 @@ func newUser(
listener: eventListener, listener: eventListener,
credStorer: credStorer, credStorer: credStorer,
clientManager: clientManager, clientManager: clientManager,
storeCache: storeCache, storeFactory: storeFactory,
storePath: getUserStorePath(storeDir, userID),
userID: userID, userID: userID,
creds: creds, creds: creds,
} }
@ -140,7 +135,7 @@ func (u *User) init(idleUpdates chan imapBackend.Update) (err error) {
} }
u.store = nil u.store = nil
} }
store, err := store.New(u.panicHandler, u, u.clientManager, u.listener, u.storePath, u.storeCache) store, err := u.storeFactory.New(u)
if err != nil { if err != nil {
return errors.Wrap(err, "failed to create store") return errors.Wrap(err, "failed to create store")
} }
@ -267,7 +262,7 @@ func (u *User) clearStore() error {
} }
} else { } else {
u.log.Warn("Store is not initialized: cleaning up store files manually") u.log.Warn("Store is not initialized: cleaning up store files manually")
if err := store.RemoveStore(u.storeCache, u.storePath, u.userID); err != nil { if err := u.storeFactory.Remove(u.userID); err != nil {
return errors.Wrap(err, "failed to remove store manually") return errors.Wrap(err, "failed to remove store manually")
} }
} }
@ -287,12 +282,6 @@ func (u *User) closeStore() error {
return nil return nil
} }
// getUserStorePath returns the file path of the store database for the given userID.
func getUserStorePath(storeDir string, userID string) (path string) {
fileName := fmt.Sprintf("mailbox-%v.db", userID)
return filepath.Join(storeDir, fileName)
}
// GetTemporaryPMAPIClient returns an authorised PMAPI client. // GetTemporaryPMAPIClient returns an authorised PMAPI client.
// Do not use! It's only for backward compatibility of old SMTP and IMAP implementations. // Do not use! It's only for backward compatibility of old SMTP and IMAP implementations.
// After proper refactor of SMTP and IMAP remove this method. // After proper refactor of SMTP and IMAP remove this method.

View File

@ -190,11 +190,7 @@ func TestCheckBridgeLoginLoggedOut(t *testing.T) {
m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil) m.credentialsStore.EXPECT().Get("user").Return(testCredentialsDisconnected, nil)
user, err := newUser( user, err := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.clientManager, m.storeMaker)
m.PanicHandler, "user",
m.eventListener, m.credentialsStore,
m.clientManager, m.storeCache, "/tmp",
)
assert.NoError(t, err) assert.NoError(t, err)
m.clientManager.EXPECT().GetClient(gomock.Any()).Return(m.pmapiClient).MinTimes(1) m.clientManager.EXPECT().GetClient(gomock.Any()).Return(m.pmapiClient).MinTimes(1)

View File

@ -34,7 +34,7 @@ func TestNewUserNoCredentialsStore(t *testing.T) {
m.credentialsStore.EXPECT().Get("user").Return(nil, errors.New("fail")) m.credentialsStore.EXPECT().Get("user").Return(nil, errors.New("fail"))
_, err := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.clientManager, m.storeCache, "/tmp") _, err := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.clientManager, m.storeMaker)
a.Error(t, err) a.Error(t, err)
} }
@ -162,7 +162,7 @@ func TestNewUser(t *testing.T) {
} }
func checkNewUserHasCredentials(creds *credentials.Credentials, m mocks) { func checkNewUserHasCredentials(creds *credentials.Credentials, m mocks) {
user, _ := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.clientManager, m.storeCache, "/tmp") user, _ := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.clientManager, m.storeMaker)
defer cleanUpUserData(user) defer cleanUpUserData(user)
_ = user.init(nil) _ = user.init(nil)

View File

@ -38,7 +38,7 @@ func testNewUser(m mocks) *User {
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil).MaxTimes(1), m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil).MaxTimes(1),
) )
user, err := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.clientManager, m.storeCache, "/tmp") user, err := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.clientManager, m.storeMaker)
assert.NoError(m.t, err) assert.NoError(m.t, err)
err = user.init(nil) err = user.init(nil)
@ -60,7 +60,7 @@ func testNewUserForLogout(m mocks) *User {
m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil).MaxTimes(1), m.pmapiClient.EXPECT().ListMessages(gomock.Any()).Return([]*pmapi.Message{}, 0, nil).MaxTimes(1),
) )
user, err := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.clientManager, m.storeCache, "/tmp") user, err := newUser(m.PanicHandler, "user", m.eventListener, m.credentialsStore, m.clientManager, m.storeMaker)
assert.NoError(m.t, err) assert.NoError(m.t, err)
err = user.init(nil) err = user.init(nil)

View File

@ -19,15 +19,11 @@
package users package users
import ( import (
"strconv"
"strings" "strings"
"sync" "sync"
"time"
"github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/metrics" "github.com/ProtonMail/proton-bridge/internal/metrics"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/internal/store"
"github.com/ProtonMail/proton-bridge/pkg/listener" "github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
imapBackend "github.com/emersion/go-imap/backend" imapBackend "github.com/emersion/go-imap/backend"
@ -44,12 +40,11 @@ var (
// Users is a struct handling users. // Users is a struct handling users.
type Users struct { type Users struct {
config Configer config Configer
pref PreferenceProvider
panicHandler PanicHandler panicHandler PanicHandler
events listener.Listener events listener.Listener
clientManager ClientManager clientManager ClientManager
credStorer CredentialsStorer credStorer CredentialsStorer
storeCache *store.Cache storeFactory StoreMaker
// users is a list of accounts that have been added to the app. // users is a list of accounts that have been added to the app.
// They are stored sorted in the credentials store in the order // They are stored sorted in the credentials store in the order
@ -70,33 +65,26 @@ type Users struct {
func New( func New(
config Configer, config Configer,
pref PreferenceProvider,
panicHandler PanicHandler, panicHandler PanicHandler,
eventListener listener.Listener, eventListener listener.Listener,
clientManager ClientManager, clientManager ClientManager,
credStorer CredentialsStorer, credStorer CredentialsStorer,
storeFactory StoreMaker,
) *Users { ) *Users {
log.Trace("Creating new users") log.Trace("Creating new users")
u := &Users{ u := &Users{
config: config, config: config,
pref: pref,
panicHandler: panicHandler, panicHandler: panicHandler,
events: eventListener, events: eventListener,
clientManager: clientManager, clientManager: clientManager,
credStorer: credStorer, credStorer: credStorer,
storeCache: store.NewCache(config.GetIMAPCachePath()), storeFactory: storeFactory,
idleUpdates: make(chan imapBackend.Update), idleUpdates: make(chan imapBackend.Update),
lock: sync.RWMutex{}, lock: sync.RWMutex{},
stopAll: make(chan struct{}), stopAll: make(chan struct{}),
} }
// Allow DoH before starting the app if the user has previously set this setting.
// This allows us to start even if protonmail is blocked.
if pref.GetBool(preferences.AllowProxyKey) {
u.AllowProxy()
}
go func() { go func() {
defer panicHandler.HandlePanic() defer panicHandler.HandlePanic()
u.watchAppOutdated() u.watchAppOutdated()
@ -107,45 +95,15 @@ func New(
u.watchAPIAuths() u.watchAPIAuths()
}() }()
go u.heartbeat()
if u.credStorer == nil { if u.credStorer == nil {
log.Error("No credentials store is available") log.Error("No credentials store is available")
} else if err := u.loadUsersFromCredentialsStore(); err != nil { } else if err := u.loadUsersFromCredentialsStore(); err != nil {
log.WithError(err).Error("Could not load all users from credentials store") log.WithError(err).Error("Could not load all users from credentials store")
} }
if pref.GetBool(preferences.FirstStartKey) {
u.SendMetric(metrics.New(metrics.Setup, metrics.FirstStart, metrics.Label(config.GetVersion())))
}
return u return u
} }
// heartbeat sends a heartbeat signal once a day.
func (u *Users) heartbeat() {
ticker := time.NewTicker(1 * time.Minute)
for {
select {
case <-ticker.C:
next, err := strconv.ParseInt(u.pref.Get(preferences.NextHeartbeatKey), 10, 64)
if err != nil {
continue
}
nextTime := time.Unix(next, 0)
if time.Now().After(nextTime) {
u.SendMetric(metrics.New(metrics.Heartbeat, metrics.Daily, metrics.NoLabel))
nextTime = nextTime.Add(24 * time.Hour)
u.pref.Set(preferences.NextHeartbeatKey, strconv.FormatInt(nextTime.Unix(), 10))
}
case <-u.stopAll:
return
}
}
}
func (u *Users) loadUsersFromCredentialsStore() (err error) { func (u *Users) loadUsersFromCredentialsStore() (err error) {
u.lock.Lock() u.lock.Lock()
defer u.lock.Unlock() defer u.lock.Unlock()
@ -158,7 +116,7 @@ func (u *Users) loadUsersFromCredentialsStore() (err error) {
for _, userID := range userIDs { for _, userID := range userIDs {
l := log.WithField("user", userID) l := log.WithField("user", userID)
user, newUserErr := newUser(u.panicHandler, userID, u.events, u.credStorer, u.clientManager, u.storeCache, u.config.GetDBDir()) user, newUserErr := newUser(u.panicHandler, userID, u.events, u.credStorer, u.clientManager, u.storeFactory)
if newUserErr != nil { if newUserErr != nil {
l.WithField("user", userID).WithError(newUserErr).Warn("Could not load user, skipping") l.WithField("user", userID).WithError(newUserErr).Warn("Could not load user, skipping")
continue continue
@ -345,7 +303,7 @@ func (u *Users) addNewUser(apiUser *pmapi.User, auth *pmapi.Auth, hashedPassword
return errors.Wrap(err, "failed to add user to credentials store") return errors.Wrap(err, "failed to add user to credentials store")
} }
user, err := newUser(u.panicHandler, apiUser.ID, u.events, u.credStorer, u.clientManager, u.storeCache, u.config.GetDBDir()) user, err := newUser(u.panicHandler, apiUser.ID, u.events, u.credStorer, u.clientManager, u.storeFactory)
if err != nil { if err != nil {
return errors.Wrap(err, "failed to create user") return errors.Wrap(err, "failed to create user")
} }

View File

@ -22,8 +22,6 @@ import (
"testing" "testing"
"github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/metrics"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/internal/users/credentials" "github.com/ProtonMail/proton-bridge/internal/users/credentials"
"github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/ProtonMail/proton-bridge/pkg/pmapi"
gomock "github.com/golang/mock/gomock" gomock "github.com/golang/mock/gomock"
@ -162,13 +160,7 @@ func TestNewUsersFirstStart(t *testing.T) {
m := initMocks(t) m := initMocks(t)
defer m.ctrl.Finish() defer m.ctrl.Finish()
gomock.InOrder( m.credentialsStore.EXPECT().List().Return([]string{}, nil)
m.credentialsStore.EXPECT().List().Return([]string{}, nil),
m.prefProvider.EXPECT().GetBool(preferences.FirstStartKey).Return(true),
m.clientManager.EXPECT().GetAnonymousClient().Return(m.pmapiClient),
m.pmapiClient.EXPECT().SendSimpleMetric(string(metrics.Setup), string(metrics.FirstStart), gomock.Any()),
m.pmapiClient.EXPECT().Logout(),
)
testNewUsers(t, m) testNewUsers(t, m)
} }

View File

@ -26,8 +26,6 @@ import (
"time" "time"
"github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/metrics"
"github.com/ProtonMail/proton-bridge/internal/preferences"
"github.com/ProtonMail/proton-bridge/internal/store" "github.com/ProtonMail/proton-bridge/internal/store"
"github.com/ProtonMail/proton-bridge/internal/users/credentials" "github.com/ProtonMail/proton-bridge/internal/users/credentials"
usersmocks "github.com/ProtonMail/proton-bridge/internal/users/mocks" usersmocks "github.com/ProtonMail/proton-bridge/internal/users/mocks"
@ -133,9 +131,9 @@ type mocks struct {
ctrl *gomock.Controller ctrl *gomock.Controller
config *usersmocks.MockConfiger config *usersmocks.MockConfiger
PanicHandler *usersmocks.MockPanicHandler PanicHandler *usersmocks.MockPanicHandler
prefProvider *usersmocks.MockPreferenceProvider
clientManager *usersmocks.MockClientManager clientManager *usersmocks.MockClientManager
credentialsStore *usersmocks.MockCredentialsStorer credentialsStore *usersmocks.MockCredentialsStorer
storeMaker *usersmocks.MockStoreMaker
eventListener *MockListener eventListener *MockListener
pmapiClient *pmapimocks.MockClient pmapiClient *pmapimocks.MockClient
@ -174,9 +172,9 @@ func initMocks(t *testing.T) mocks {
ctrl: mockCtrl, ctrl: mockCtrl,
config: usersmocks.NewMockConfiger(mockCtrl), config: usersmocks.NewMockConfiger(mockCtrl),
PanicHandler: usersmocks.NewMockPanicHandler(mockCtrl), PanicHandler: usersmocks.NewMockPanicHandler(mockCtrl),
prefProvider: usersmocks.NewMockPreferenceProvider(mockCtrl),
clientManager: usersmocks.NewMockClientManager(mockCtrl), clientManager: usersmocks.NewMockClientManager(mockCtrl),
credentialsStore: usersmocks.NewMockCredentialsStorer(mockCtrl), credentialsStore: usersmocks.NewMockCredentialsStorer(mockCtrl),
storeMaker: usersmocks.NewMockStoreMaker(mockCtrl),
eventListener: NewMockListener(mockCtrl), eventListener: NewMockListener(mockCtrl),
pmapiClient: pmapimocks.NewMockClient(mockCtrl), pmapiClient: pmapimocks.NewMockClient(mockCtrl),
@ -184,14 +182,17 @@ func initMocks(t *testing.T) mocks {
storeCache: store.NewCache(cacheFile.Name()), storeCache: store.NewCache(cacheFile.Name()),
} }
// Ignore heartbeat calls because they always happen.
m.pmapiClient.EXPECT().SendSimpleMetric(string(metrics.Heartbeat), gomock.Any(), gomock.Any()).AnyTimes()
m.prefProvider.EXPECT().Get(preferences.NextHeartbeatKey).AnyTimes()
m.prefProvider.EXPECT().Set(preferences.NextHeartbeatKey, gomock.Any()).AnyTimes()
// Called during clean-up. // Called during clean-up.
m.PanicHandler.EXPECT().HandlePanic().AnyTimes() m.PanicHandler.EXPECT().HandlePanic().AnyTimes()
// Set up store factory.
m.storeMaker.EXPECT().New(gomock.Any()).DoAndReturn(func(user store.BridgeUser) (*store.Store, error) {
dbFile, err := ioutil.TempFile("", "bridge-store-db-*.db")
require.NoError(t, err, "could not get temporary file for store db")
return store.New(m.PanicHandler, user, m.clientManager, m.eventListener, dbFile.Name(), m.storeCache)
}).AnyTimes()
m.storeMaker.EXPECT().Remove(gomock.Any()).AnyTimes()
return m return m
} }
@ -236,19 +237,12 @@ func testNewUsersWithUsers(t *testing.T, m mocks) *Users {
return users return users
} }
func testNewUsers(t *testing.T, m mocks) *Users { func testNewUsers(t *testing.T, m mocks) *Users { //nolint[unparam]
cacheFile, err := ioutil.TempFile("", "bridge-store-cache-*.db")
require.NoError(t, err, "could not get temporary file for store cache")
m.prefProvider.EXPECT().GetBool(preferences.FirstStartKey).Return(false).AnyTimes()
m.prefProvider.EXPECT().GetBool(preferences.AllowProxyKey).Return(false).AnyTimes()
m.config.EXPECT().GetDBDir().Return("/tmp").AnyTimes()
m.config.EXPECT().GetIMAPCachePath().Return(cacheFile.Name()).AnyTimes()
m.config.EXPECT().GetVersion().Return("ver").AnyTimes() m.config.EXPECT().GetVersion().Return("ver").AnyTimes()
m.eventListener.EXPECT().Add(events.UpgradeApplicationEvent, gomock.Any()) m.eventListener.EXPECT().Add(events.UpgradeApplicationEvent, gomock.Any())
m.clientManager.EXPECT().GetAuthUpdateChannel().Return(make(chan pmapi.ClientAuth)) m.clientManager.EXPECT().GetAuthUpdateChannel().Return(make(chan pmapi.ClientAuth))
users := New(m.config, m.prefProvider, m.PanicHandler, m.eventListener, m.clientManager, m.credentialsStore) users := New(m.config, m.PanicHandler, m.eventListener, m.clientManager, m.credentialsStore, m.storeMaker)
waitForEvents() waitForEvents()