diff --git a/Makefile b/Makefile index 58a59637..1a492412 100644 --- a/Makefile +++ b/Makefile @@ -290,8 +290,10 @@ gofiles: ./internal/bridge/credits.go ./internal/importexport/credits.go ## Run and debug .PHONY: run run-qt run-qt-cli run-nogui run-nogui-cli run-debug run-qml-preview run-ie-qml-preview run-ie run-ie-qt run-ie-qt-cli run-ie-nogui run-ie-nogui-cli clean-vendor clean-frontend-qt clean-frontend-qt-ie clean-frontend-qt-common clean -VERBOSITY?=debug -RUN_FLAGS:=-m -l=${VERBOSITY} +LOG?=debug +LOG_IMAP?=client # client/server/all, or empty to turn it off +LOG_SMTP?=--log-smtp # empty to turn it off +RUN_FLAGS?=-m -l=${LOG} --log-imap=${LOG_IMAP} ${LOG_SMTP} run: run-nogui-cli diff --git a/go.sum b/go.sum index 32090e86..9aaae71f 100644 --- a/go.sum +++ b/go.sum @@ -274,6 +274,8 @@ github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGr github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/urfave/cli v1.22.4 h1:u7tSpNPPswAFymm8IehJhy4uJMlUuU/GmqSkvJ1InXA= +github.com/urfave/cli v1.22.4/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4= github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= github.com/urfave/negroni v1.0.0/go.mod h1:Meg73S6kFm/4PpbYdq35yYWoCZ9mS/YSx+lKnmiohz4= diff --git a/internal/app/base/base.go b/internal/app/base/base.go index 46232bd9..7ce454d5 100644 --- a/internal/app/base/base.go +++ b/internal/app/base/base.go @@ -158,13 +158,13 @@ func New( // nolint[funlen] apiConfig := pmapi.GetAPIConfig(configName, constants.Version) apiConfig.ConnectionOffHandler = func() { - eventListener.Emit(events.InternetOffEvent, "") + listener.Emit(events.InternetOffEvent, "") } apiConfig.ConnectionOnHandler = func() { - eventListener.Emit(events.InternetOnEvent, "") + listener.Emit(events.InternetOnEvent, "") } apiConfig.UpgradeApplicationHandler = func() { - eventListener.Emit(events.UpgradeApplicationEvent, "") + listener.Emit(events.UpgradeApplicationEvent, "") } cm := pmapi.NewClientManager(apiConfig) cm.SetRoundTripper(pmapi.GetRoundTripper(cm, listener)) diff --git a/internal/bridge/credits.go b/internal/bridge/credits.go index 30e6ef9d..b7205f24 100644 --- a/internal/bridge/credits.go +++ b/internal/bridge/credits.go @@ -15,8 +15,8 @@ // You should have received a copy of the GNU General Public License // along with ProtonMail Bridge. If not, see . -// Code generated by ./credits.sh at Mon Jan 4 03:19:07 PM CET 2021. DO NOT EDIT. +// Code generated by ./credits.sh at Fri Jan 22 11:28:55 CET 2021. DO NOT EDIT. package bridge -const Credits = "github.com/0xAX/notificator;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-mbox;github.com/emersion/go-message;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/sentry-go;github.com/golang/mock;github.com/google/go-cmp;github.com/google/uuid;github.com/gopherjs/gopherjs;github.com/go-resty/resty/v2;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/Masterminds/semver/v3;github.com/mattn/go-runewidth;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/olekukonko/tablewriter;github.com/pkg/errors;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/gopenpgp/v2;github.com/ProtonMail/go-rfc5322;github.com/ProtonMail/go-vcard;github.com/PuerkitoBio/goquery;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/ssor/bom;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli/v2;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;" +const Credits = "github.com/0xAX/notificator;github.com/Masterminds/semver/v3;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/go-rfc5322;github.com/ProtonMail/go-vcard;github.com/ProtonMail/gopenpgp/v2;github.com/PuerkitoBio/goquery;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-unselect;github.com/emersion/go-mbox;github.com/emersion/go-message;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/sentry-go;github.com/go-resty/resty/v2;github.com/golang/mock;github.com/google/go-cmp;github.com/google/uuid;github.com/gopherjs/gopherjs;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/mattn/go-runewidth;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/olekukonko/tablewriter;github.com/pkg/errors;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/ssor/bom;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;github.com/urfave/cli/v2;github.com/vmihailenco/msgpack/v5;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;" diff --git a/internal/imap/cache/cache.go b/internal/imap/cache/cache.go index 54ce89e3..6048f460 100644 --- a/internal/imap/cache/cache.go +++ b/internal/imap/cache/cache.go @@ -23,7 +23,7 @@ import ( "sync" "time" - backendMessage "github.com/ProtonMail/proton-bridge/pkg/message" + pkgMsg "github.com/ProtonMail/proton-bridge/pkg/message" ) type key struct { @@ -41,7 +41,7 @@ func (s oldestFirst) Less(i, j int) bool { return s[i].Timestamp < s[j].Timestam type cachedMessage struct { key data []byte - structure backendMessage.BodyStructure + structure pkgMsg.BodyStructure } //nolint[gochecknoglobals] @@ -101,7 +101,7 @@ func BuildUnlock(messageID string) { delete(buildLocks, messageID) } -func LoadMail(mID string) (reader *bytes.Reader, structure *backendMessage.BodyStructure) { +func LoadMail(mID string) (reader *bytes.Reader, structure *pkgMsg.BodyStructure) { reader = &bytes.Reader{} cacheMutex.Lock() defer cacheMutex.Unlock() @@ -115,7 +115,7 @@ func LoadMail(mID string) (reader *bytes.Reader, structure *backendMessage.BodyS return } -func SaveMail(mID string, msg []byte, structure *backendMessage.BodyStructure) { +func SaveMail(mID string, msg []byte, structure *pkgMsg.BodyStructure) { cacheMutex.Lock() defer cacheMutex.Unlock() diff --git a/internal/imap/cache/cache_test.go b/internal/imap/cache/cache_test.go index 38e6c21a..90696239 100644 --- a/internal/imap/cache/cache_test.go +++ b/internal/imap/cache/cache_test.go @@ -22,11 +22,11 @@ import ( "testing" "time" - bckMsg "github.com/ProtonMail/proton-bridge/pkg/message" + pkgMsg "github.com/ProtonMail/proton-bridge/pkg/message" "github.com/stretchr/testify/require" ) -var bs = &bckMsg.BodyStructure{} //nolint[gochecknoglobals] +var bs = &pkgMsg.BodyStructure{} //nolint[gochecknoglobals] const testUID = "testmsg" func TestSaveAndLoad(t *testing.T) { diff --git a/internal/imap/mailbox.go b/internal/imap/mailbox.go index 3593112b..eea2fead 100644 --- a/internal/imap/mailbox.go +++ b/internal/imap/mailbox.go @@ -197,8 +197,8 @@ func (im *imapMailbox) Expunge() error { // UIDExpunge permanently removes messages that have the \Deleted flag set // and UID passed from SeqSet from the currently selected mailbox. func (im *imapMailbox) UIDExpunge(seqSet *imap.SeqSet) error { - im.user.backend.setUpdatesBeBlocking(im.user.currentAddressLowercase, im.name, operationDeleteMessage) - defer im.user.backend.unsetUpdatesBeBlocking(im.user.currentAddressLowercase, im.name, operationDeleteMessage) + im.user.backend.updates.block(im.user.currentAddressLowercase, im.name, operationDeleteMessage) + defer im.user.backend.updates.unblock(im.user.currentAddressLowercase, im.name, operationDeleteMessage) messageIDs, err := im.apiIDsFromSeqSet(true, seqSet) if err != nil || len(messageIDs) == 0 { diff --git a/internal/imap/mailbox_message.go b/internal/imap/mailbox_message.go index 1673d905..2899093a 100644 --- a/internal/imap/mailbox_message.go +++ b/internal/imap/mailbox_message.go @@ -328,12 +328,21 @@ func (im *imapMailbox) getBodyAndStructure(storeMessage storeMessageProvider) ( var body []byte structure, body, err = im.buildMessage(m) m.Size = int64(len(body)) + // Save size and body structure even for messages unable to decrypt + // so the size or body structure doesn't have to be computed every time. if err := storeMessage.SetSize(m.Size); err != nil { im.log.WithError(err). WithField("newSize", m.Size). WithField("msgID", m.ID). Warn("Cannot update size while building") } + if structure != nil && !isMessageInDraftFolder(m) { + if err := storeMessage.SetBodyStructure(structure); err != nil { + im.log.WithError(err). + WithField("msgID", m.ID). + Warn("Cannot update bodystructure while building") + } + } if err == nil && structure != nil && len(body) > 0 { if err := storeMessage.SetContentTypeAndHeader(m.MIMEType, m.Header); err != nil { im.log.WithError(err). @@ -342,11 +351,6 @@ func (im *imapMailbox) getBodyAndStructure(storeMessage storeMessageProvider) ( } // Drafts can change and we don't want to cache them. if !isMessageInDraftFolder(m) { - if err := storeMessage.SetBodyStructure(structure); err != nil { - im.log.WithError(err). - WithField("msgID", m.ID). - Warn("Cannot update bodystructure while building") - } cache.SaveMail(id, body, structure) } bodyReader = bytes.NewReader(body) diff --git a/internal/imap/server.go b/internal/imap/server.go index c1ca74d0..b00b46a4 100644 --- a/internal/imap/server.go +++ b/internal/imap/server.go @@ -23,6 +23,7 @@ import ( "io" "net" "strings" + "sync/atomic" "time" imapid "github.com/ProtonMail/go-imap-id" @@ -31,6 +32,7 @@ import ( "github.com/ProtonMail/proton-bridge/internal/imap/id" "github.com/ProtonMail/proton-bridge/internal/imap/uidplus" "github.com/ProtonMail/proton-bridge/pkg/listener" + "github.com/ProtonMail/proton-bridge/pkg/ports" "github.com/emersion/go-imap" imapappendlimit "github.com/emersion/go-imap-appendlimit" imapidle "github.com/emersion/go-imap-idle" @@ -48,6 +50,8 @@ type imapServer struct { eventListener listener.Listener debugClient bool debugServer bool + port int + isRunning atomic.Value } // NewIMAPServer constructs a new IMAP server configured with the given options. @@ -96,13 +100,16 @@ func NewIMAPServer(panicHandler panicHandler, debugClient, debugServer bool, por uidplus.NewExtension(), ) - return &imapServer{ + server := &imapServer{ panicHandler: panicHandler, server: s, eventListener: eventListener, debugClient: debugClient, debugServer: debugServer, + port: port, } + server.isRunning.Store(false) + return server } // Starts the server. @@ -114,6 +121,11 @@ func (s *imapServer) ListenAndServe() { } func (s *imapServer) listenAndServe() { + if s.isRunning.Load().(bool) { + return + } + s.isRunning.Store(true) + log.Info("IMAP server listening at ", s.server.Addr) l, err := net.Listen("tcp", s.server.Addr) if err != nil { @@ -126,19 +138,13 @@ func (s *imapServer) listenAndServe() { Listener: l, server: s, }) - if err != nil { - failed := true - if netErr, ok := err.(*net.OpError); ok { - originalErr := netErr.Unwrap() - if originalErr != nil && originalErr.Error() == "use of closed network connection" { - failed = false - } - } - if failed { - s.eventListener.Emit(events.ErrorEvent, "IMAP failed: "+err.Error()) - log.Error("IMAP failed: ", err) - return - } + // Serve returns error every time, even after closing the server. + // User shouldn't be notified about error if server shouldn't be running, + // but it should in case it was not closed by `s.Close()`. + if err != nil && s.isRunning.Load().(bool) { + s.eventListener.Emit(events.ErrorEvent, "IMAP failed: "+err.Error()) + log.Error("IMAP failed: ", err) + return } defer s.server.Close() //nolint[errcheck] @@ -147,6 +153,11 @@ func (s *imapServer) listenAndServe() { // Stops the server. func (s *imapServer) Close() { + if !s.isRunning.Load().(bool) { + return + } + s.isRunning.Store(false) + log.Info("Closing IMAP server") if err := s.server.Close(); err != nil { log.WithError(err).Error("Failed to close the connection") @@ -159,29 +170,32 @@ func (s *imapServer) monitorInternetConnection() { off := make(chan string) s.eventListener.Add(events.InternetOffEvent, off) - isOn := true for { + var expectedIsPortFree bool select { case <-on: - if isOn { - continue - } - isOn = true go func() { defer s.panicHandler.HandlePanic() s.listenAndServe() }() + expectedIsPortFree = false case <-off: - if !isOn { - continue - } - isOn = false s.Close() + expectedIsPortFree = true + } + + start := time.Now() + for { + if ports.IsPortFree(s.port) == expectedIsPortFree { + break + } + // Safety stop if something went wrong. + if time.Since(start) > 15*time.Second { + log.WithField("expectedIsPortFree", expectedIsPortFree).Warn("Server start/stop check timeouted") + break + } + time.Sleep(100 * time.Millisecond) } - // Give it some time to serve or close server before changing it again. - // E.g., if we get quickly off-on signal, starting or closing could - // fail because server is still running or not yet, respectively. - time.Sleep(10 * time.Second) } } diff --git a/internal/imap/server_test.go b/internal/imap/server_test.go new file mode 100644 index 00000000..565e045e --- /dev/null +++ b/internal/imap/server_test.go @@ -0,0 +1,65 @@ +// Copyright (c) 2021 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 . + +package imap + +import ( + "fmt" + "testing" + "time" + + "github.com/ProtonMail/proton-bridge/internal/bridge" + "github.com/ProtonMail/proton-bridge/internal/events" + "github.com/ProtonMail/proton-bridge/pkg/listener" + "github.com/ProtonMail/proton-bridge/pkg/ports" + imapserver "github.com/emersion/go-imap/server" + + "github.com/stretchr/testify/require" +) + +type testPanicHandler struct{} + +func (ph *testPanicHandler) HandlePanic() {} + +func TestIMAPServerTurnOffAndOnAgain(t *testing.T) { + panicHandler := &testPanicHandler{} + + eventListener := listener.New() + + port := ports.FindFreePortFrom(12345) + server := imapserver.New(nil) + server.Addr = fmt.Sprintf("%v:%v", bridge.Host, port) + + s := &imapServer{ + panicHandler: panicHandler, + server: server, + eventListener: eventListener, + } + s.isRunning.Store(false) + + go s.ListenAndServe() + time.Sleep(5 * time.Second) + require.False(t, ports.IsPortFree(port)) + + eventListener.Emit(events.InternetOffEvent, "") + time.Sleep(10 * time.Second) + require.True(t, ports.IsPortFree(port)) + + eventListener.Emit(events.InternetOnEvent, "") + time.Sleep(10 * time.Second) + require.False(t, ports.IsPortFree(port)) +} diff --git a/internal/imap/store.go b/internal/imap/store.go index 4970ca13..005108c3 100644 --- a/internal/imap/store.go +++ b/internal/imap/store.go @@ -24,7 +24,7 @@ import ( "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/ProtonMail/proton-bridge/internal/imap/uidplus" "github.com/ProtonMail/proton-bridge/internal/store" - backendMessage "github.com/ProtonMail/proton-bridge/pkg/message" + pkgMsg "github.com/ProtonMail/proton-bridge/pkg/message" "github.com/ProtonMail/proton-bridge/pkg/pmapi" ) @@ -100,8 +100,8 @@ type storeMessageProvider interface { SetSize(int64) error SetContentTypeAndHeader(string, mail.Header) error - SetBodyStructure(*backendMessage.BodyStructure) error - GetBodyStructure() (*backendMessage.BodyStructure, error) + SetBodyStructure(*pkgMsg.BodyStructure) error + GetBodyStructure() (*pkgMsg.BodyStructure, error) } type storeUserWrap struct { diff --git a/internal/imap/updates_test.go b/internal/imap/updates_test.go new file mode 100644 index 00000000..7390461f --- /dev/null +++ b/internal/imap/updates_test.go @@ -0,0 +1,60 @@ +// Copyright (c) 2021 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 . + +package imap + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestUpdatesCanDelete(t *testing.T) { + u := newIMAPUpdates() + + can, _ := u.CanDelete("mbox") + require.True(t, can) + + u.forbidExpunge("mbox") + u.allowExpunge("mbox") + + can, _ = u.CanDelete("mbox") + require.True(t, can) +} + +func TestUpdatesCannotDelete(t *testing.T) { + u := newIMAPUpdates() + + u.forbidExpunge("mbox") + can, wait := u.CanDelete("mbox") + require.False(t, can) + + ch := make(chan time.Duration) + go func() { + start := time.Now() + wait() + ch <- time.Since(start) + close(ch) + }() + + time.Sleep(200 * time.Millisecond) + u.allowExpunge("mbox") + duration := <-ch + + require.True(t, duration > 200*time.Millisecond) +} diff --git a/internal/importexport/credits.go b/internal/importexport/credits.go index 5be3cb3d..89feddd1 100644 --- a/internal/importexport/credits.go +++ b/internal/importexport/credits.go @@ -15,8 +15,8 @@ // You should have received a copy of the GNU General Public License // along with ProtonMail Bridge. If not, see . -// Code generated by ./credits.sh at Mon Jan 4 03:19:07 PM CET 2021. DO NOT EDIT. +// Code generated by ./credits.sh at Fri Jan 22 11:28:55 CET 2021. DO NOT EDIT. package importexport -const Credits = "github.com/0xAX/notificator;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-specialuse;github.com/emersion/go-imap-unselect;github.com/emersion/go-mbox;github.com/emersion/go-message;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/sentry-go;github.com/golang/mock;github.com/google/go-cmp;github.com/google/uuid;github.com/gopherjs/gopherjs;github.com/go-resty/resty/v2;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/Masterminds/semver/v3;github.com/mattn/go-runewidth;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/olekukonko/tablewriter;github.com/pkg/errors;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/gopenpgp/v2;github.com/ProtonMail/go-rfc5322;github.com/ProtonMail/go-vcard;github.com/PuerkitoBio/goquery;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/ssor/bom;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli/v2;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;" +const Credits = "github.com/0xAX/notificator;github.com/Masterminds/semver/v3;github.com/ProtonMail/bcrypt;github.com/ProtonMail/crypto;github.com/ProtonMail/docker-credential-helpers;github.com/ProtonMail/go-appdir;github.com/ProtonMail/go-apple-mobileconfig;github.com/ProtonMail/go-autostart;github.com/ProtonMail/go-imap;github.com/ProtonMail/go-imap-id;github.com/ProtonMail/go-rfc5322;github.com/ProtonMail/go-vcard;github.com/ProtonMail/gopenpgp/v2;github.com/PuerkitoBio/goquery;github.com/abiosoft/ishell;github.com/abiosoft/readline;github.com/allan-simon/go-singleinstance;github.com/chzyer/logex;github.com/chzyer/test;github.com/cucumber/godog;github.com/docker/docker-credential-helpers;github.com/emersion/go-imap;github.com/emersion/go-imap-appendlimit;github.com/emersion/go-imap-idle;github.com/emersion/go-imap-move;github.com/emersion/go-imap-quota;github.com/emersion/go-imap-unselect;github.com/emersion/go-mbox;github.com/emersion/go-message;github.com/emersion/go-sasl;github.com/emersion/go-smtp;github.com/emersion/go-textwrapper;github.com/emersion/go-vcard;github.com/fatih/color;github.com/flynn-archive/go-shlex;github.com/getsentry/sentry-go;github.com/go-resty/resty/v2;github.com/golang/mock;github.com/google/go-cmp;github.com/google/uuid;github.com/gopherjs/gopherjs;github.com/hashicorp/go-multierror;github.com/jameskeane/bcrypt;github.com/jaytaylor/html2text;github.com/keybase/go-keychain;github.com/logrusorgru/aurora;github.com/mattn/go-runewidth;github.com/miekg/dns;github.com/myesui/uuid;github.com/nsf/jsondiff;github.com/olekukonko/tablewriter;github.com/pkg/errors;github.com/sirupsen/logrus;github.com/skratchdot/open-golang;github.com/ssor/bom;github.com/stretchr/testify;github.com/therecipe/qt;github.com/twinj/uuid;github.com/urfave/cli;github.com/urfave/cli/v2;github.com/vmihailenco/msgpack/v5;go.etcd.io/bbolt;golang.org/x/crypto;golang.org/x/net;golang.org/x/text;gopkg.in/stretchr/testify.v1;;Font Awesome 4.7.0;;Qt 5.13 by Qt group;" diff --git a/internal/smtp/user.go b/internal/smtp/user.go index fd857dee..a6dc829f 100644 --- a/internal/smtp/user.go +++ b/internal/smtp/user.go @@ -31,7 +31,7 @@ import ( "github.com/ProtonMail/gopenpgp/v2/crypto" "github.com/ProtonMail/proton-bridge/internal/events" "github.com/ProtonMail/proton-bridge/pkg/listener" - pkgMessage "github.com/ProtonMail/proton-bridge/pkg/message" + pkgMsg "github.com/ProtonMail/proton-bridge/pkg/message" "github.com/ProtonMail/proton-bridge/pkg/message/parser" "github.com/ProtonMail/proton-bridge/pkg/pmapi" goSMTPBackend "github.com/emersion/go-smtp" @@ -229,7 +229,7 @@ func (su *smtpUser) Send(returnPath string, to []string, messageReader io.Reader err = errors.Wrap(err, "failed to create new parser") return } - message, plainBody, attReaders, err := pkgMessage.ParserWithParser(parser) + message, plainBody, attReaders, err := pkgMsg.ParserWithParser(parser) if err != nil { log.WithError(err).Error("Failed to parse message") return @@ -273,10 +273,10 @@ func (su *smtpUser) Send(returnPath string, to []string, messageReader io.Reader } if attachedPublicKey != "" { - pkgMessage.AttachPublicKey(parser, attachedPublicKey, attachedPublicKeyName) + pkgMsg.AttachPublicKey(parser, attachedPublicKey, attachedPublicKeyName) } - mimeBody, err := pkgMessage.BuildMIMEBody(parser) + mimeBody, err := pkgMsg.BuildMIMEBody(parser) if err != nil { log.WithError(err).Error("Failed to build message") return diff --git a/internal/store/event_loop_test.go b/internal/store/event_loop_test.go index 139c4e44..bf79bc6f 100644 --- a/internal/store/event_loop_test.go +++ b/internal/store/event_loop_test.go @@ -82,22 +82,92 @@ func TestEventLoopUpdateMessageFromLoop(t *testing.T) { Subject: subject, }) + testEvent(t, m, &pmapi.Event{ + EventID: "event1", + Messages: []*pmapi.EventMessage{{ + EventItem: pmapi.EventItem{ + ID: "msg1", + Action: pmapi.EventUpdate, + }, + Updated: &pmapi.EventMessageUpdated{ + ID: "msg1", + Subject: &newSubject, + }, + }}, + }) + + msg, err := m.store.getMessageFromDB("msg1") + require.NoError(t, err) + require.Equal(t, newSubject, msg.Subject) +} + +func TestEventLoopDeletionNotPaused(t *testing.T) { + m, clear := initMocks(t) + defer clear() + + m.newStoreNoEvents(true, &pmapi.Message{ + ID: "msg1", + Subject: "subject", + LabelIDs: []string{"label"}, + }) + + m.changeNotifier.EXPECT().CanDelete("label").Return(true, func() {}) + m.store.SetChangeNotifier(m.changeNotifier) + + testEvent(t, m, &pmapi.Event{ + EventID: "event1", + Messages: []*pmapi.EventMessage{{ + EventItem: pmapi.EventItem{ + ID: "msg1", + Action: pmapi.EventDelete, + }, + }}, + }) + + _, err := m.store.getMessageFromDB("msg1") + require.Error(t, err) +} + +func TestEventLoopDeletionPaused(t *testing.T) { + m, clear := initMocks(t) + defer clear() + + m.newStoreNoEvents(true, &pmapi.Message{ + ID: "msg1", + Subject: "subject", + LabelIDs: []string{"label"}, + }) + + delay := 5 * time.Second + + m.changeNotifier.EXPECT().CanDelete("label").Return(false, func() { + time.Sleep(delay) + }) + m.changeNotifier.EXPECT().CanDelete("label").Return(true, func() {}) + m.store.SetChangeNotifier(m.changeNotifier) + + start := time.Now() + + testEvent(t, m, &pmapi.Event{ + EventID: "event1", + Messages: []*pmapi.EventMessage{{ + EventItem: pmapi.EventItem{ + ID: "msg1", + Action: pmapi.EventDelete, + }, + }}, + }) + + _, err := m.store.getMessageFromDB("msg1") + require.Error(t, err) + require.True(t, time.Since(start) > delay) +} + +func testEvent(t *testing.T, m *mocksForStore, event *pmapi.Event) { eventReceived := make(chan struct{}) m.client.EXPECT().GetEvent("latestEventID").DoAndReturn(func(eventID string) (*pmapi.Event, error) { defer close(eventReceived) - return &pmapi.Event{ - EventID: "event1", - Messages: []*pmapi.EventMessage{{ - EventItem: pmapi.EventItem{ - ID: "msg1", - Action: pmapi.EventUpdate, - }, - Updated: &pmapi.EventMessageUpdated{ - ID: "msg1", - Subject: &newSubject, - }, - }}, - }, nil + return event, nil }) // Event loop runs in goroutine started during store creation (newStoreNoEvents). @@ -109,10 +179,6 @@ func TestEventLoopUpdateMessageFromLoop(t *testing.T) { case <-time.After(5 * time.Second): require.Fail(t, "latestEventID was not processed") } - - msg, err := m.store.getMessageFromDB("msg1") - require.NoError(t, err) - require.Equal(t, newSubject, msg.Subject) } func TestEventLoopUpdateMessage(t *testing.T) { diff --git a/internal/store/message.go b/internal/store/message.go index 78b889b7..f74bd129 100644 --- a/internal/store/message.go +++ b/internal/store/message.go @@ -20,7 +20,7 @@ package store import ( "net/mail" - backendMessage "github.com/ProtonMail/proton-bridge/pkg/message" + pkgMsg "github.com/ProtonMail/proton-bridge/pkg/message" "github.com/ProtonMail/proton-bridge/pkg/pmapi" bolt "go.etcd.io/bbolt" ) @@ -121,8 +121,8 @@ func (message *Message) SetContentTypeAndHeader(mimeType string, header mail.Hea return message.store.db.Update(txUpdate) } -// SetBodyStructure stores serialized body structure in database -func (message *Message) SetBodyStructure(bs *backendMessage.BodyStructure) error { +// SetBodyStructure stores serialized body structure in database. +func (message *Message) SetBodyStructure(bs *pkgMsg.BodyStructure) error { txUpdate := func(tx *bolt.Tx) error { return message.store.txPutBodyStructure( tx.Bucket(bodystructureBucket), @@ -132,10 +132,10 @@ func (message *Message) SetBodyStructure(bs *backendMessage.BodyStructure) error return message.store.db.Update(txUpdate) } -// GetBodyStructure deserialize body structure from database. If body structure +// GetBodyStructure deserializes body structure from database. If body structure // is not in database it returns nil error and nil body structure. If error // occurs it returns nil body structure. -func (message *Message) GetBodyStructure() (bs *backendMessage.BodyStructure, err error) { +func (message *Message) GetBodyStructure() (bs *pkgMsg.BodyStructure, err error) { txRead := func(tx *bolt.Tx) error { bs, err = message.store.txGetBodyStructure( tx.Bucket(bodystructureBucket), diff --git a/internal/store/store.go b/internal/store/store.go index 1df2541c..cacd9201 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -51,6 +51,8 @@ var ( // Database structure: // * metadata // * {messageID} -> message data (subject, from, to, time, headers, body size, ...) + // * bodystructure + // * {messageID} -> message body structure // * counts // * {mailboxID} -> mailboxCounts: totalOnAPI, unreadOnAPI, labelName, labelColor, labelIsExclusive // * address_info diff --git a/internal/store/store_test.go b/internal/store/store_test.go index 603cc81b..0f2aafe5 100644 --- a/internal/store/store_test.go +++ b/internal/store/store_test.go @@ -103,7 +103,7 @@ func (mocks *mocksForStore) newStoreNoEvents(combinedMode bool, msgs ...*pmapi.M {ID: addrID1, Email: addr1, Type: pmapi.OriginalAddress, Receive: pmapi.CanReceive}, {ID: addrID2, Email: addr2, Type: pmapi.AliasAddress, Receive: pmapi.CanReceive}, }) - mocks.client.EXPECT().ListLabels() + mocks.client.EXPECT().ListLabels().AnyTimes() mocks.client.EXPECT().CountMessages("") // Call to get latest event ID and then to process first event. diff --git a/internal/store/user_message.go b/internal/store/user_message.go index 7798161d..9628657e 100644 --- a/internal/store/user_message.go +++ b/internal/store/user_message.go @@ -27,7 +27,7 @@ import ( "strings" "github.com/ProtonMail/gopenpgp/v2/crypto" - backendMessage "github.com/ProtonMail/proton-bridge/pkg/message" + pkgMsg "github.com/ProtonMail/proton-bridge/pkg/message" "github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -171,7 +171,7 @@ func (store *Store) txPutMessage(metaBucket *bolt.Bucket, onlyMeta *pmapi.Messag return nil } -func (store *Store) txPutBodyStructure(bsBucket *bolt.Bucket, msgID string, bs *backendMessage.BodyStructure) error { +func (store *Store) txPutBodyStructure(bsBucket *bolt.Bucket, msgID string, bs *pkgMsg.BodyStructure) error { raw, err := bs.Serialize() if err != nil { return err @@ -183,12 +183,12 @@ func (store *Store) txPutBodyStructure(bsBucket *bolt.Bucket, msgID string, bs * return nil } -func (store *Store) txGetBodyStructure(bsBucket *bolt.Bucket, msgID string) (*backendMessage.BodyStructure, error) { +func (store *Store) txGetBodyStructure(bsBucket *bolt.Bucket, msgID string) (*pkgMsg.BodyStructure, error) { raw := bsBucket.Get([]byte(msgID)) if len(raw) == 0 { return nil, nil } - return backendMessage.DeserializeBodyStructure(raw) + return pkgMsg.DeserializeBodyStructure(raw) } // createOrUpdateMessageEvent is helper to create only one message with diff --git a/internal/transfer/provider_pmapi_source.go b/internal/transfer/provider_pmapi_source.go index bac7422d..b1e65cd1 100644 --- a/internal/transfer/provider_pmapi_source.go +++ b/internal/transfer/provider_pmapi_source.go @@ -21,7 +21,7 @@ import ( "fmt" "sync" - pkgMessage "github.com/ProtonMail/proton-bridge/pkg/message" + pkgMsg "github.com/ProtonMail/proton-bridge/pkg/message" "github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/pkg/errors" "github.com/sirupsen/logrus" @@ -153,7 +153,7 @@ func (p *PMAPIProvider) exportMessage(rule *Rule, progress *Progress, pmapiMsgID p.timeIt.start("build", msgID) defer p.timeIt.stop("build", msgID) - msgBuilder := pkgMessage.NewBuilder(p.client(), msg) + msgBuilder := pkgMsg.NewBuilder(p.client(), msg) msgBuilder.EncryptedToHTML = false _, body, err := msgBuilder.BuildMessage() if err != nil { diff --git a/internal/transfer/provider_pmapi_target.go b/internal/transfer/provider_pmapi_target.go index 445bcfee..36149e75 100644 --- a/internal/transfer/provider_pmapi_target.go +++ b/internal/transfer/provider_pmapi_target.go @@ -24,7 +24,7 @@ import ( "io/ioutil" "sync" - pkgMessage "github.com/ProtonMail/proton-bridge/pkg/message" + pkgMsg "github.com/ProtonMail/proton-bridge/pkg/message" "github.com/ProtonMail/proton-bridge/pkg/pmapi" "github.com/pkg/errors" ) @@ -246,7 +246,7 @@ func (p *PMAPIProvider) generateImportMsgReq(rules transferRules, progress *Prog func (p *PMAPIProvider) parseMessage(msg Message) (m *pmapi.Message, r []io.Reader, err error) { p.timeIt.start("parse", msg.ID) defer p.timeIt.stop("parse", msg.ID) - message, _, _, attachmentReaders, err := pkgMessage.Parse(bytes.NewBuffer(msg.Body)) + message, _, _, attachmentReaders, err := pkgMsg.Parse(bytes.NewBuffer(msg.Body)) return message, attachmentReaders, err } @@ -254,7 +254,7 @@ func (p *PMAPIProvider) encryptMessage(msg *pmapi.Message, attachmentReaders []i if msg.MIMEType == pmapi.ContentTypeMultipartEncrypted { return []byte(msg.Body), nil } - return pkgMessage.BuildEncrypted(msg, attachmentReaders, p.keyRing) + return pkgMsg.BuildEncrypted(msg, attachmentReaders, p.keyRing) } func computeMessageFlags(labels []string) (flag int64) { diff --git a/internal/users/user.go b/internal/users/user.go index bd0783e5..a70b0745 100644 --- a/internal/users/user.go +++ b/internal/users/user.go @@ -152,7 +152,7 @@ func (u *User) authorizeIfNecessary(emitEvent bool) (err error) { u.log.WithError(err).Error("Could not authorize and unlock user") switch errors.Cause(err) { - case pmapi.ErrUpgradeApplication, pmapi.ErrAPINotReachable: + case pmapi.ErrUpgradeApplication, pmapi.ErrAPINotReachable: // Ignore these errors. default: if errLogout := u.credStorer.Logout(u.userID); errLogout != nil { u.log.WithField("err", errLogout).Error("Could not log user out from credentials store") diff --git a/pkg/pmapi/clientmanager.go b/pkg/pmapi/clientmanager.go index 0b022f67..15eafe17 100644 --- a/pkg/pmapi/clientmanager.go +++ b/pkg/pmapi/clientmanager.go @@ -132,7 +132,9 @@ func (cm *ClientManager) noConnection() { } cm.log.Warn("Connection lost") - cm.config.ConnectionOffHandler() + if cm.config.ConnectionOffHandler != nil { + cm.config.ConnectionOffHandler() + } cm.connectionOff = true go func() { @@ -141,7 +143,9 @@ func (cm *ClientManager) noConnection() { if err := cm.CheckConnection(); err == nil { cm.log.Info("Connection re-established") - cm.config.ConnectionOnHandler() + if cm.config.ConnectionOnHandler != nil { + cm.config.ConnectionOnHandler() + } cm.connectionOff = false return } diff --git a/unreleased.md b/unreleased.md index 89f38f37..88bc7308 100644 --- a/unreleased.md +++ b/unreleased.md @@ -32,9 +32,13 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/) * GODT-854 EXPUNGE and FETCH unilateral responses are returned before OK EXPUNGE or OK STORE, respectively. * GODT-806 Changed GUI dialog on manual update. Added autoupdates checkbox. Simplifyed installation process GUI. * Bump gopenpgp dependency to v2.1.3 for improved memory usage. -* GODT-912 Changed scroll bar behaviour in settings tab +* GODT-912 Changed scroll bar behaviour in settings tab. * GODT-149 Send heartbeat ASAP on each new calendar day. * GODT-792 GODT-908 Cache body structure in order to reduce network traffic. +* GODT-792 Stop IMAP server while no internet connection. +* GODT-792 Cache message size every time to reduce network traffic. +* GODT-792 Cache body structure in order to reduce network traffic. +* GODT-908 Do not unpause event loop if other mailbox is still fetching. ### Removed * GODT-208 Remove deprecated use of BuildNameToCertificate. @@ -47,7 +51,7 @@ Changelog [format](http://keepachangelog.com/en/1.0.0/) * GODT-898 Only set ContentID for inline attachments. * GODT-773 Replace `INTERNALDATE` older than birthday of RFC822 by birthday of RFC822 to not crash Apple Mail. * GODT-927 Avoid to call API with empty label name. -* GODT-732 Fix usage of fontawesome +* GODT-732 Fix usage of fontawesome. * GODT-915 Bump go-imap dependency and remove go-imap-specialuse dependency. * GODT-831 Cancel request of uploading attachment if reading/writing it fails.