forked from Silverfish/proton-bridge
Tests and final touches
This commit is contained in:
6
Makefile
6
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
|
||||
|
||||
|
||||
2
go.sum
2
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=
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -15,8 +15,8 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// 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;"
|
||||
|
||||
8
internal/imap/cache/cache.go
vendored
8
internal/imap/cache/cache.go
vendored
@ -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()
|
||||
|
||||
|
||||
4
internal/imap/cache/cache_test.go
vendored
4
internal/imap/cache/cache_test.go
vendored
@ -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) {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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,20 +138,14 @@ 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 {
|
||||
// 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]
|
||||
|
||||
log.Info("IMAP server stopped")
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
65
internal/imap/server_test.go
Normal file
65
internal/imap/server_test.go
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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))
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
60
internal/imap/updates_test.go
Normal file
60
internal/imap/updates_test.go
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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)
|
||||
}
|
||||
@ -15,8 +15,8 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// 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;"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -82,10 +82,7 @@ func TestEventLoopUpdateMessageFromLoop(t *testing.T) {
|
||||
Subject: subject,
|
||||
})
|
||||
|
||||
eventReceived := make(chan struct{})
|
||||
m.client.EXPECT().GetEvent("latestEventID").DoAndReturn(func(eventID string) (*pmapi.Event, error) {
|
||||
defer close(eventReceived)
|
||||
return &pmapi.Event{
|
||||
testEvent(t, m, &pmapi.Event{
|
||||
EventID: "event1",
|
||||
Messages: []*pmapi.EventMessage{{
|
||||
EventItem: pmapi.EventItem{
|
||||
@ -97,7 +94,80 @@ func TestEventLoopUpdateMessageFromLoop(t *testing.T) {
|
||||
Subject: &newSubject,
|
||||
},
|
||||
}},
|
||||
}, nil
|
||||
})
|
||||
|
||||
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 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) {
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -132,7 +132,9 @@ func (cm *ClientManager) noConnection() {
|
||||
}
|
||||
|
||||
cm.log.Warn("Connection lost")
|
||||
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")
|
||||
if cm.config.ConnectionOnHandler != nil {
|
||||
cm.config.ConnectionOnHandler()
|
||||
}
|
||||
cm.connectionOff = false
|
||||
return
|
||||
}
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user