mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-15 22:56:48 +00:00
Try load messages one-by-one
This commit is contained in:
@ -16,6 +16,8 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/)
|
|||||||
|
|
||||||
|
|
||||||
## [Bridge 1.5.0] Golden Gate
|
## [Bridge 1.5.0] Golden Gate
|
||||||
|
* GODT-701 Try load messages one-by-one if IMAP server errors with batch load and not interrupt the transfer
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* Updated go-mbox dependency back to upstream.
|
* Updated go-mbox dependency back to upstream.
|
||||||
|
|
||||||
|
|||||||
2
Makefile
2
Makefile
@ -196,7 +196,7 @@ coverage: test
|
|||||||
|
|
||||||
mocks:
|
mocks:
|
||||||
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/users Configer,PanicHandler,ClientManager,CredentialsStorer,StoreMaker > internal/users/mocks/mocks.go
|
||||||
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/transfer PanicHandler,ClientManager > internal/transfer/mocks/mocks.go
|
mockgen --package mocks github.com/ProtonMail/proton-bridge/internal/transfer PanicHandler,ClientManager,IMAPClientProvider > internal/transfer/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
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
// Code generated by MockGen. DO NOT EDIT.
|
// Code generated by MockGen. DO NOT EDIT.
|
||||||
// Source: github.com/ProtonMail/proton-bridge/internal/transfer (interfaces: PanicHandler,ClientManager)
|
// Source: github.com/ProtonMail/proton-bridge/internal/transfer (interfaces: PanicHandler,ClientManager,IMAPClientProvider)
|
||||||
|
|
||||||
// Package mocks is a generated GoMock package.
|
// Package mocks is a generated GoMock package.
|
||||||
package mocks
|
package mocks
|
||||||
@ -8,6 +8,8 @@ import (
|
|||||||
reflect "reflect"
|
reflect "reflect"
|
||||||
|
|
||||||
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
pmapi "github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||||
|
imap "github.com/emersion/go-imap"
|
||||||
|
sasl "github.com/emersion/go-sasl"
|
||||||
gomock "github.com/golang/mock/gomock"
|
gomock "github.com/golang/mock/gomock"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -96,3 +98,170 @@ func (mr *MockClientManagerMockRecorder) GetClient(arg0 interface{}) *gomock.Cal
|
|||||||
mr.mock.ctrl.T.Helper()
|
mr.mock.ctrl.T.Helper()
|
||||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClient", reflect.TypeOf((*MockClientManager)(nil).GetClient), arg0)
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetClient", reflect.TypeOf((*MockClientManager)(nil).GetClient), arg0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MockIMAPClientProvider is a mock of IMAPClientProvider interface
|
||||||
|
type MockIMAPClientProvider struct {
|
||||||
|
ctrl *gomock.Controller
|
||||||
|
recorder *MockIMAPClientProviderMockRecorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockIMAPClientProviderMockRecorder is the mock recorder for MockIMAPClientProvider
|
||||||
|
type MockIMAPClientProviderMockRecorder struct {
|
||||||
|
mock *MockIMAPClientProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMockIMAPClientProvider creates a new mock instance
|
||||||
|
func NewMockIMAPClientProvider(ctrl *gomock.Controller) *MockIMAPClientProvider {
|
||||||
|
mock := &MockIMAPClientProvider{ctrl: ctrl}
|
||||||
|
mock.recorder = &MockIMAPClientProviderMockRecorder{mock}
|
||||||
|
return mock
|
||||||
|
}
|
||||||
|
|
||||||
|
// EXPECT returns an object that allows the caller to indicate expected use
|
||||||
|
func (m *MockIMAPClientProvider) EXPECT() *MockIMAPClientProviderMockRecorder {
|
||||||
|
return m.recorder
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate mocks base method
|
||||||
|
func (m *MockIMAPClientProvider) Authenticate(arg0 sasl.Client) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "Authenticate", arg0)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authenticate indicates an expected call of Authenticate
|
||||||
|
func (mr *MockIMAPClientProviderMockRecorder) Authenticate(arg0 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Authenticate", reflect.TypeOf((*MockIMAPClientProvider)(nil).Authenticate), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capability mocks base method
|
||||||
|
func (m *MockIMAPClientProvider) Capability() (map[string]bool, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "Capability")
|
||||||
|
ret0, _ := ret[0].(map[string]bool)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capability indicates an expected call of Capability
|
||||||
|
func (mr *MockIMAPClientProviderMockRecorder) Capability() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Capability", reflect.TypeOf((*MockIMAPClientProvider)(nil).Capability))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch mocks base method
|
||||||
|
func (m *MockIMAPClientProvider) Fetch(arg0 *imap.SeqSet, arg1 []imap.FetchItem, arg2 chan *imap.Message) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "Fetch", arg0, arg1, arg2)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch indicates an expected call of Fetch
|
||||||
|
func (mr *MockIMAPClientProviderMockRecorder) Fetch(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Fetch", reflect.TypeOf((*MockIMAPClientProvider)(nil).Fetch), arg0, arg1, arg2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// List mocks base method
|
||||||
|
func (m *MockIMAPClientProvider) List(arg0, arg1 string, arg2 chan *imap.MailboxInfo) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "List", arg0, arg1, arg2)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// List indicates an expected call of List
|
||||||
|
func (mr *MockIMAPClientProviderMockRecorder) List(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "List", reflect.TypeOf((*MockIMAPClientProvider)(nil).List), arg0, arg1, arg2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login mocks base method
|
||||||
|
func (m *MockIMAPClientProvider) Login(arg0, arg1 string) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "Login", arg0, arg1)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login indicates an expected call of Login
|
||||||
|
func (mr *MockIMAPClientProviderMockRecorder) Login(arg0, arg1 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Login", reflect.TypeOf((*MockIMAPClientProvider)(nil).Login), arg0, arg1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select mocks base method
|
||||||
|
func (m *MockIMAPClientProvider) Select(arg0 string, arg1 bool) (*imap.MailboxStatus, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "Select", arg0, arg1)
|
||||||
|
ret0, _ := ret[0].(*imap.MailboxStatus)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select indicates an expected call of Select
|
||||||
|
func (mr *MockIMAPClientProviderMockRecorder) Select(arg0, arg1 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Select", reflect.TypeOf((*MockIMAPClientProvider)(nil).Select), arg0, arg1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// State mocks base method
|
||||||
|
func (m *MockIMAPClientProvider) State() imap.ConnState {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "State")
|
||||||
|
ret0, _ := ret[0].(imap.ConnState)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// State indicates an expected call of State
|
||||||
|
func (mr *MockIMAPClientProviderMockRecorder) State() *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "State", reflect.TypeOf((*MockIMAPClientProvider)(nil).State))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Support mocks base method
|
||||||
|
func (m *MockIMAPClientProvider) Support(arg0 string) (bool, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "Support", arg0)
|
||||||
|
ret0, _ := ret[0].(bool)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Support indicates an expected call of Support
|
||||||
|
func (mr *MockIMAPClientProviderMockRecorder) Support(arg0 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Support", reflect.TypeOf((*MockIMAPClientProvider)(nil).Support), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SupportAuth mocks base method
|
||||||
|
func (m *MockIMAPClientProvider) SupportAuth(arg0 string) (bool, error) {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "SupportAuth", arg0)
|
||||||
|
ret0, _ := ret[0].(bool)
|
||||||
|
ret1, _ := ret[1].(error)
|
||||||
|
return ret0, ret1
|
||||||
|
}
|
||||||
|
|
||||||
|
// SupportAuth indicates an expected call of SupportAuth
|
||||||
|
func (mr *MockIMAPClientProviderMockRecorder) SupportAuth(arg0 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SupportAuth", reflect.TypeOf((*MockIMAPClientProvider)(nil).SupportAuth), arg0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UidFetch mocks base method
|
||||||
|
func (m *MockIMAPClientProvider) UidFetch(arg0 *imap.SeqSet, arg1 []imap.FetchItem, arg2 chan *imap.Message) error {
|
||||||
|
m.ctrl.T.Helper()
|
||||||
|
ret := m.ctrl.Call(m, "UidFetch", arg0, arg1, arg2)
|
||||||
|
ret0, _ := ret[0].(error)
|
||||||
|
return ret0
|
||||||
|
}
|
||||||
|
|
||||||
|
// UidFetch indicates an expected call of UidFetch
|
||||||
|
func (mr *MockIMAPClientProviderMockRecorder) UidFetch(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||||
|
mr.mock.ctrl.T.Helper()
|
||||||
|
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UidFetch", reflect.TypeOf((*MockIMAPClientProvider)(nil).UidFetch), arg0, arg1, arg2)
|
||||||
|
}
|
||||||
|
|||||||
@ -21,28 +21,49 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
imapClient "github.com/emersion/go-imap/client"
|
"github.com/emersion/go-imap"
|
||||||
|
"github.com/emersion/go-sasl"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type IMAPClientProvider interface {
|
||||||
|
Capability() (map[string]bool, error)
|
||||||
|
Support(cap string) (bool, error)
|
||||||
|
State() imap.ConnState
|
||||||
|
SupportAuth(mech string) (bool, error)
|
||||||
|
Authenticate(auth sasl.Client) error
|
||||||
|
Login(username, password string) error
|
||||||
|
List(ref, name string, ch chan *imap.MailboxInfo) error
|
||||||
|
Select(name string, readOnly bool) (*imap.MailboxStatus, error)
|
||||||
|
Fetch(seqset *imap.SeqSet, items []imap.FetchItem, ch chan *imap.Message) error
|
||||||
|
UidFetch(seqset *imap.SeqSet, items []imap.FetchItem, ch chan *imap.Message) error
|
||||||
|
}
|
||||||
|
|
||||||
// IMAPProvider implements export from IMAP server.
|
// IMAPProvider implements export from IMAP server.
|
||||||
type IMAPProvider struct {
|
type IMAPProvider struct {
|
||||||
username string
|
username string
|
||||||
password string
|
password string
|
||||||
addr string
|
addr string
|
||||||
|
|
||||||
client *imapClient.Client
|
clientDialer func(addr string) (IMAPClientProvider, error)
|
||||||
|
client IMAPClientProvider
|
||||||
|
|
||||||
timeIt *timeIt
|
timeIt *timeIt
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewIMAPProvider returns new IMAPProvider.
|
// NewIMAPProvider returns new IMAPProvider.
|
||||||
func NewIMAPProvider(username, password, host, port string) (*IMAPProvider, error) {
|
func NewIMAPProvider(username, password, host, port string) (*IMAPProvider, error) {
|
||||||
|
return newIMAPProvider(imapClientDial, username, password, host, port)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newIMAPProvider(clientDialer func(string) (IMAPClientProvider, error), username, password, host, port string) (*IMAPProvider, error) {
|
||||||
p := &IMAPProvider{
|
p := &IMAPProvider{
|
||||||
username: username,
|
username: username,
|
||||||
password: password,
|
password: password,
|
||||||
addr: net.JoinHostPort(host, port),
|
addr: net.JoinHostPort(host, port),
|
||||||
|
|
||||||
timeIt: newTimeIt("imap"),
|
timeIt: newTimeIt("imap"),
|
||||||
|
|
||||||
|
clientDialer: clientDialer,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := p.auth(); err != nil {
|
if err := p.auth(); err != nil {
|
||||||
|
|||||||
@ -84,12 +84,37 @@ func (p *IMAPProvider) loadMessagesInfo(rule *Rule, progress *Progress, uidValid
|
|||||||
p.timeIt.start("load", rule.SourceMailbox.Name)
|
p.timeIt.start("load", rule.SourceMailbox.Name)
|
||||||
defer p.timeIt.stop("load", rule.SourceMailbox.Name)
|
defer p.timeIt.stop("load", rule.SourceMailbox.Name)
|
||||||
|
|
||||||
|
log := log.WithField("mailbox", rule.SourceMailbox.Name)
|
||||||
messagesInfo := map[string]imapMessageInfo{}
|
messagesInfo := map[string]imapMessageInfo{}
|
||||||
|
|
||||||
|
fetchItems := []imap.FetchItem{imap.FetchUid, imap.FetchRFC822Size}
|
||||||
|
if rule.HasTimeLimit() {
|
||||||
|
fetchItems = append(fetchItems, imap.FetchEnvelope)
|
||||||
|
}
|
||||||
|
|
||||||
|
processMessageCallback := func(imapMessage *imap.Message) {
|
||||||
|
if rule.HasTimeLimit() {
|
||||||
|
t := imapMessage.Envelope.Date.Unix()
|
||||||
|
if t != 0 && !rule.isTimeInRange(t) {
|
||||||
|
log.WithField("uid", imapMessage.Uid).Debug("Message skipped due to time")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
id := getUniqueMessageID(rule.SourceMailbox.Name, uidValidity, imapMessage.Uid)
|
||||||
|
// We use ID as key to ensure we have every unique message only once.
|
||||||
|
// Some IMAP servers responded twice the same message...
|
||||||
|
messagesInfo[id] = imapMessageInfo{
|
||||||
|
id: id,
|
||||||
|
uid: imapMessage.Uid,
|
||||||
|
size: imapMessage.Size,
|
||||||
|
}
|
||||||
|
progress.addMessage(id, []string{rule.SourceMailbox.Name}, rule.TargetMailboxNames())
|
||||||
|
}
|
||||||
|
|
||||||
pageStart := uint32(1)
|
pageStart := uint32(1)
|
||||||
pageEnd := imapPageSize
|
pageEnd := imapPageSize
|
||||||
for {
|
for {
|
||||||
if progress.shouldStop() {
|
if progress.shouldStop() || pageStart > count {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,45 +125,21 @@ func (p *IMAPProvider) loadMessagesInfo(rule *Rule, progress *Progress, uidValid
|
|||||||
|
|
||||||
seqSet := &imap.SeqSet{}
|
seqSet := &imap.SeqSet{}
|
||||||
seqSet.AddRange(pageStart, pageEnd)
|
seqSet.AddRange(pageStart, pageEnd)
|
||||||
|
err := p.fetch(rule.SourceMailbox.Name, seqSet, fetchItems, processMessageCallback)
|
||||||
items := []imap.FetchItem{imap.FetchUid, imap.FetchRFC822Size}
|
if err != nil {
|
||||||
if rule.HasTimeLimit() {
|
log.WithError(err).WithField("idx", seqSet).Warning("Load batch fetch failed, trying one by one")
|
||||||
items = append(items, imap.FetchEnvelope)
|
for ; pageStart <= pageEnd; pageStart++ {
|
||||||
}
|
seqSet := &imap.SeqSet{}
|
||||||
|
seqSet.AddNum(pageStart)
|
||||||
pageMsgCount := uint32(0)
|
if err := p.fetch(rule.SourceMailbox.Name, seqSet, fetchItems, processMessageCallback); err != nil {
|
||||||
processMessageCallback := func(imapMessage *imap.Message) {
|
log.WithError(err).WithField("idx", seqSet).Warning("Load fetch failed, skipping the message")
|
||||||
pageMsgCount++
|
|
||||||
if rule.HasTimeLimit() {
|
|
||||||
t := imapMessage.Envelope.Date.Unix()
|
|
||||||
if t != 0 && !rule.isTimeInRange(t) {
|
|
||||||
log.WithField("uid", imapMessage.Uid).Debug("Message skipped due to time")
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
id := getUniqueMessageID(rule.SourceMailbox.Name, uidValidity, imapMessage.Uid)
|
|
||||||
// We use ID as key to ensure we have every unique message only once.
|
|
||||||
// Some IMAP servers responded twice the same message...
|
|
||||||
messagesInfo[id] = imapMessageInfo{
|
|
||||||
id: id,
|
|
||||||
uid: imapMessage.Uid,
|
|
||||||
size: imapMessage.Size,
|
|
||||||
}
|
|
||||||
progress.addMessage(id, []string{rule.SourceMailbox.Name}, rule.TargetMailboxNames())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
progress.callWrap(func() error {
|
pageStart = pageEnd + 1
|
||||||
return p.fetch(rule.SourceMailbox.Name, seqSet, items, processMessageCallback)
|
|
||||||
})
|
|
||||||
|
|
||||||
if pageMsgCount < imapPageSize {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
pageStart = pageEnd
|
|
||||||
pageEnd += imapPageSize
|
pageEnd += imapPageSize
|
||||||
}
|
}
|
||||||
|
|
||||||
return messagesInfo
|
return messagesInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
100
internal/transfer/provider_imap_test.go
Normal file
100
internal/transfer/provider_imap_test.go
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
// 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 transfer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/emersion/go-imap"
|
||||||
|
gomock "github.com/golang/mock/gomock"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
r "github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newTestIMAPProvider(t *testing.T, m mocks) *IMAPProvider {
|
||||||
|
m.imapClientProvider.EXPECT().State().Return(imap.ConnectedState).AnyTimes()
|
||||||
|
m.imapClientProvider.EXPECT().Capability().Return(map[string]bool{
|
||||||
|
"AUTH": true,
|
||||||
|
}, nil).AnyTimes()
|
||||||
|
|
||||||
|
dialer := func(string) (IMAPClientProvider, error) {
|
||||||
|
return m.imapClientProvider, nil
|
||||||
|
}
|
||||||
|
provider, err := newIMAPProvider(dialer, "user", "pass", "host", "42")
|
||||||
|
r.NoError(t, err)
|
||||||
|
return provider
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProviderIMAPLoadMessagesInfo(t *testing.T) {
|
||||||
|
m := initMocks(t)
|
||||||
|
defer m.ctrl.Finish()
|
||||||
|
|
||||||
|
provider := newTestIMAPProvider(t, m)
|
||||||
|
|
||||||
|
progress := newProgress(log, nil)
|
||||||
|
drainProgressUpdateChannel(&progress)
|
||||||
|
|
||||||
|
rule := &Rule{SourceMailbox: Mailbox{Name: "Mailbox"}}
|
||||||
|
uidValidity := 1
|
||||||
|
count := 2200
|
||||||
|
failingIndex := 2100
|
||||||
|
|
||||||
|
m.imapClientProvider.EXPECT().Select(rule.SourceMailbox.Name, gomock.Any()).Return(&imap.MailboxStatus{}, nil).AnyTimes()
|
||||||
|
m.imapClientProvider.EXPECT().
|
||||||
|
Fetch(gomock.Any(), gomock.Any(), gomock.Any()).
|
||||||
|
DoAndReturn(func(seqSet *imap.SeqSet, items []imap.FetchItem, ch chan *imap.Message) error {
|
||||||
|
defer close(ch)
|
||||||
|
for _, seq := range seqSet.Set {
|
||||||
|
for i := seq.Start; i <= seq.Stop; i++ {
|
||||||
|
if int(i) == failingIndex {
|
||||||
|
return errors.New("internal server error")
|
||||||
|
}
|
||||||
|
ch <- &imap.Message{
|
||||||
|
SeqNum: i,
|
||||||
|
Uid: i * 10,
|
||||||
|
Size: i * 100,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}).
|
||||||
|
// 2200 messages is split into two batches (2000 and 200),
|
||||||
|
// the second one fails and makes 200 calls (one-by-one).
|
||||||
|
// Plus two failed requests are repeated `imapRetries` times.
|
||||||
|
Times(2 + 200 + (2 * (imapRetries - 1)))
|
||||||
|
|
||||||
|
messageInfo := provider.loadMessagesInfo(rule, &progress, uint32(uidValidity), uint32(count))
|
||||||
|
|
||||||
|
r.Equal(t, count-1, len(messageInfo)) // One message produces internal server error.
|
||||||
|
for index := 1; index <= count; index++ {
|
||||||
|
uid := index * 10
|
||||||
|
key := fmt.Sprintf("%s_%d:%d", rule.SourceMailbox.Name, uidValidity, uid)
|
||||||
|
|
||||||
|
if index == failingIndex {
|
||||||
|
r.Empty(t, messageInfo[key])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Equal(t, imapMessageInfo{
|
||||||
|
id: key,
|
||||||
|
uid: uint32(uid),
|
||||||
|
size: uint32(index * 100),
|
||||||
|
}, messageInfo[key])
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -24,10 +24,11 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
imapID "github.com/ProtonMail/go-imap-id"
|
imapID "github.com/ProtonMail/go-imap-id"
|
||||||
|
"github.com/ProtonMail/proton-bridge/pkg/constants"
|
||||||
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
|
||||||
"github.com/emersion/go-imap"
|
"github.com/emersion/go-imap"
|
||||||
imapClient "github.com/emersion/go-imap/client"
|
imapClient "github.com/emersion/go-imap/client"
|
||||||
sasl "github.com/emersion/go-sasl"
|
"github.com/emersion/go-sasl"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
@ -51,6 +52,43 @@ func (l *imapErrorLogger) Println(v ...interface{}) {
|
|||||||
l.log.Errorln(v...)
|
l.log.Errorln(v...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func imapClientDial(addr string) (IMAPClientProvider, error) {
|
||||||
|
if _, err := net.DialTimeout("tcp", addr, imapDialTimeout); err != nil {
|
||||||
|
return nil, errors.Wrap(err, "failed to dial server")
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := imapClientDialHelper(addr)
|
||||||
|
if err == nil {
|
||||||
|
client.ErrorLog = &imapErrorLogger{logrus.WithField("pkg", "imap-client")}
|
||||||
|
// Logrus `WriterLevel` fails for big messages because of bufio.MaxScanTokenSize limit.
|
||||||
|
// Also, this spams a lot, uncomment once needed during development.
|
||||||
|
//client.SetDebug(imap.NewDebugWriter(
|
||||||
|
// logrus.WithField("pkg", "imap/client").WriterLevel(logrus.TraceLevel),
|
||||||
|
// logrus.WithField("pkg", "imap/server").WriterLevel(logrus.TraceLevel),
|
||||||
|
//))
|
||||||
|
}
|
||||||
|
return client, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func imapClientDialHelper(addr string) (*imapClient.Client, error) {
|
||||||
|
host, _, _ := net.SplitHostPort(addr)
|
||||||
|
if host == "127.0.0.1" {
|
||||||
|
return imapClient.Dial(addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IMAP mail.yahoo.com has problem with golang TLS 1.3 implementation
|
||||||
|
// with weird behaviour, i.e., Yahoo does not return error during dial
|
||||||
|
// or handshake but server does logs out right after successful login
|
||||||
|
// leaving no time to perform any action.
|
||||||
|
// Limiting TLS to version 1.2 is working just fine.
|
||||||
|
var tlsConf *tls.Config
|
||||||
|
if strings.Contains(strings.ToLower(host), "yahoo") {
|
||||||
|
log.Warning("Yahoo server detected: limiting maximal TLS version to 1.2.")
|
||||||
|
tlsConf = &tls.Config{MaxVersion: tls.VersionTLS12}
|
||||||
|
}
|
||||||
|
return imapClient.DialTLS(addr, tlsConf)
|
||||||
|
}
|
||||||
|
|
||||||
func (p *IMAPProvider) ensureConnection(callback func() error) error {
|
func (p *IMAPProvider) ensureConnection(callback func() error) error {
|
||||||
return p.ensureConnectionAndSelection(callback, "")
|
return p.ensureConnectionAndSelection(callback, "")
|
||||||
}
|
}
|
||||||
@ -138,41 +176,10 @@ func (p *IMAPProvider) auth() error { //nolint[funlen]
|
|||||||
|
|
||||||
log.Info("Connecting to server")
|
log.Info("Connecting to server")
|
||||||
|
|
||||||
if _, err := net.DialTimeout("tcp", p.addr, imapDialTimeout); err != nil {
|
client, err := p.clientDialer(p.addr)
|
||||||
return ErrIMAPConnection{imapError{Err: err, Message: "failed to dial server"}}
|
|
||||||
}
|
|
||||||
|
|
||||||
var client *imapClient.Client
|
|
||||||
var err error
|
|
||||||
host, _, _ := net.SplitHostPort(p.addr)
|
|
||||||
if host == "127.0.0.1" {
|
|
||||||
client, err = imapClient.Dial(p.addr)
|
|
||||||
} else {
|
|
||||||
// IMAP.mail.yahoo.com have problem with golang TLS1.3
|
|
||||||
// implementation with weird behaviour i.e. Yahoo
|
|
||||||
// no error during dial or handshake but server logs out right
|
|
||||||
// after successful login leaving no time to perform any
|
|
||||||
// action. It was discovered that limiting to maximum TLS
|
|
||||||
// version 1.2 for yahoo servers is working solution.
|
|
||||||
|
|
||||||
var tlsConf *tls.Config
|
|
||||||
if strings.Contains(strings.ToLower(host), "yahoo") {
|
|
||||||
log.Warning("Yahoo server detected: limiting maximal TLS version to 1.2.")
|
|
||||||
tlsConf = &tls.Config{MaxVersion: tls.VersionTLS12}
|
|
||||||
}
|
|
||||||
client, err = imapClient.DialTLS(p.addr, tlsConf)
|
|
||||||
}
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ErrIMAPConnection{imapError{Err: err, Message: "failed to connect to server"}}
|
return ErrIMAPConnection{imapError{Err: err, Message: "failed to connect to server"}}
|
||||||
}
|
}
|
||||||
|
|
||||||
client.ErrorLog = &imapErrorLogger{logrus.WithField("pkg", "imap-client")}
|
|
||||||
// Logrus `WriterLevel` fails for big messages because of bufio.MaxScanTokenSize limit.
|
|
||||||
// Also, this spams a lot, uncomment once needed during development.
|
|
||||||
//client.SetDebug(imap.NewDebugWriter(
|
|
||||||
// logrus.WithField("pkg", "imap/client").WriterLevel(logrus.TraceLevel),
|
|
||||||
// logrus.WithField("pkg", "imap/server").WriterLevel(logrus.TraceLevel),
|
|
||||||
//))
|
|
||||||
p.client = client
|
p.client = client
|
||||||
|
|
||||||
log.Info("Connected")
|
log.Info("Connected")
|
||||||
@ -210,13 +217,15 @@ func (p *IMAPProvider) auth() error { //nolint[funlen]
|
|||||||
|
|
||||||
log.Info("Logged in")
|
log.Info("Logged in")
|
||||||
|
|
||||||
idClient := imapID.NewClient(p.client)
|
if c, ok := p.client.(*imapClient.Client); ok {
|
||||||
if ok, err := idClient.SupportID(); err == nil && ok {
|
idClient := imapID.NewClient(c)
|
||||||
serverID, err := idClient.ID(imapID.ID{
|
if ok, err := idClient.SupportID(); err == nil && ok {
|
||||||
imapID.FieldName: "ImportExport",
|
serverID, err := idClient.ID(imapID.ID{
|
||||||
imapID.FieldVersion: "beta",
|
imapID.FieldName: "ImportExport",
|
||||||
})
|
imapID.FieldVersion: constants.Version,
|
||||||
log.WithField("ID", serverID).WithError(err).Debug("Server info")
|
})
|
||||||
|
log.WithField("ID", serverID).WithError(err).Debug("Server info")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return err
|
return err
|
||||||
|
|||||||
@ -31,11 +31,12 @@ import (
|
|||||||
type mocks struct {
|
type mocks struct {
|
||||||
t *testing.T
|
t *testing.T
|
||||||
|
|
||||||
ctrl *gomock.Controller
|
ctrl *gomock.Controller
|
||||||
panicHandler *transfermocks.MockPanicHandler
|
panicHandler *transfermocks.MockPanicHandler
|
||||||
clientManager *transfermocks.MockClientManager
|
clientManager *transfermocks.MockClientManager
|
||||||
pmapiClient *pmapimocks.MockClient
|
imapClientProvider *transfermocks.MockIMAPClientProvider
|
||||||
pmapiConfig *pmapi.ClientConfig
|
pmapiClient *pmapimocks.MockClient
|
||||||
|
pmapiConfig *pmapi.ClientConfig
|
||||||
|
|
||||||
keyring *crypto.KeyRing
|
keyring *crypto.KeyRing
|
||||||
}
|
}
|
||||||
@ -46,12 +47,13 @@ func initMocks(t *testing.T) mocks {
|
|||||||
m := mocks{
|
m := mocks{
|
||||||
t: t,
|
t: t,
|
||||||
|
|
||||||
ctrl: mockCtrl,
|
ctrl: mockCtrl,
|
||||||
panicHandler: transfermocks.NewMockPanicHandler(mockCtrl),
|
panicHandler: transfermocks.NewMockPanicHandler(mockCtrl),
|
||||||
clientManager: transfermocks.NewMockClientManager(mockCtrl),
|
clientManager: transfermocks.NewMockClientManager(mockCtrl),
|
||||||
pmapiClient: pmapimocks.NewMockClient(mockCtrl),
|
imapClientProvider: transfermocks.NewMockIMAPClientProvider(mockCtrl),
|
||||||
pmapiConfig: &pmapi.ClientConfig{},
|
pmapiClient: pmapimocks.NewMockClient(mockCtrl),
|
||||||
keyring: newTestKeyring(),
|
pmapiConfig: &pmapi.ClientConfig{},
|
||||||
|
keyring: newTestKeyring(),
|
||||||
}
|
}
|
||||||
|
|
||||||
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).AnyTimes()
|
m.clientManager.EXPECT().GetClient("user").Return(m.pmapiClient).AnyTimes()
|
||||||
|
|||||||
Reference in New Issue
Block a user