forked from Silverfish/proton-bridge
feat(GODT-2289): UIDValidity as Timestamp
Update UIDValidity to be timestamp with the number of seconds since the 1st of February 2023. This avoids the problem where we lose the last UIDValidity value due to the vault being missing/corrupted/deleted.
This commit is contained in:
2
go.mod
2
go.mod
@ -5,7 +5,7 @@ go 1.18
|
|||||||
require (
|
require (
|
||||||
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
|
github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557
|
||||||
github.com/Masterminds/semver/v3 v3.1.1
|
github.com/Masterminds/semver/v3 v3.1.1
|
||||||
github.com/ProtonMail/gluon v0.14.2-0.20230130104154-2c64e59b8f54
|
github.com/ProtonMail/gluon v0.14.2-0.20230201115538-18e0b89693fc
|
||||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a
|
||||||
github.com/ProtonMail/go-proton-api v0.3.1-0.20230130144605-4b05f1e5c427
|
github.com/ProtonMail/go-proton-api v0.3.1-0.20230130144605-4b05f1e5c427
|
||||||
github.com/ProtonMail/go-rfc5322 v0.11.0
|
github.com/ProtonMail/go-rfc5322 v0.11.0
|
||||||
|
|||||||
4
go.sum
4
go.sum
@ -28,8 +28,8 @@ github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf h1:yc9daCCYUefEs
|
|||||||
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
|
github.com/ProtonMail/bcrypt v0.0.0-20211005172633-e235017c1baf/go.mod h1:o0ESU9p83twszAU8LBeJKFAAMX14tISa0yk4Oo5TOqo=
|
||||||
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
|
github.com/ProtonMail/docker-credential-helpers v1.1.0 h1:+kvUIpwWcbtP3WFv5sSvkFn/XLzSqPOB5AAthuk9xPk=
|
||||||
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
|
github.com/ProtonMail/docker-credential-helpers v1.1.0/go.mod h1:mK0aBveCxhnQ756AmaTfXMZDeULvheYVhF/MWMErN5g=
|
||||||
github.com/ProtonMail/gluon v0.14.2-0.20230130104154-2c64e59b8f54 h1:uUg8CDiYTMlbvGijzoN0fb72vwDJD7hMjgNTbmAHxRc=
|
github.com/ProtonMail/gluon v0.14.2-0.20230201115538-18e0b89693fc h1:q7sX422Eu9H97v2sLRPmPFi8yBJtNwQRzVN9DSmBHvc=
|
||||||
github.com/ProtonMail/gluon v0.14.2-0.20230130104154-2c64e59b8f54/go.mod h1:HYHr7hG7LPWI1S50M8NfHRb1kYi5B+Yu4/N/H+y+JUY=
|
github.com/ProtonMail/gluon v0.14.2-0.20230201115538-18e0b89693fc/go.mod h1:HYHr7hG7LPWI1S50M8NfHRb1kYi5B+Yu4/N/H+y+JUY=
|
||||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
|
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a h1:D+aZah+k14Gn6kmL7eKxoo/4Dr/lK3ChBcwce2+SQP4=
|
||||||
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
|
github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a/go.mod h1:oTGdE7/DlWIr23G0IKW3OXK9wZ5Hw1GGiaJFccTvZi4=
|
||||||
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
|
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
|
"github.com/ProtonMail/gluon/imap"
|
||||||
"github.com/ProtonMail/go-autostart"
|
"github.com/ProtonMail/go-autostart"
|
||||||
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
"github.com/ProtonMail/gopenpgp/v2/crypto"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||||
@ -110,6 +111,7 @@ func withBridge( //nolint:funlen
|
|||||||
// Crash and report stuff
|
// Crash and report stuff
|
||||||
crashHandler,
|
crashHandler,
|
||||||
reporter,
|
reporter,
|
||||||
|
imap.DefaultEpochUIDValidityGenerator(),
|
||||||
|
|
||||||
// The logging stuff.
|
// The logging stuff.
|
||||||
c.String(flagLogIMAP) == "client" || c.String(flagLogIMAP) == "all",
|
c.String(flagLogIMAP) == "client" || c.String(flagLogIMAP) == "all",
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import (
|
|||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
"github.com/ProtonMail/gluon"
|
"github.com/ProtonMail/gluon"
|
||||||
imapEvents "github.com/ProtonMail/gluon/events"
|
imapEvents "github.com/ProtonMail/gluon/events"
|
||||||
|
"github.com/ProtonMail/gluon/imap"
|
||||||
"github.com/ProtonMail/gluon/reporter"
|
"github.com/ProtonMail/gluon/reporter"
|
||||||
"github.com/ProtonMail/gluon/watcher"
|
"github.com/ProtonMail/gluon/watcher"
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
@ -122,6 +123,8 @@ type Bridge struct {
|
|||||||
|
|
||||||
// goUpdate triggers a check/install of updates.
|
// goUpdate triggers a check/install of updates.
|
||||||
goUpdate func()
|
goUpdate func()
|
||||||
|
|
||||||
|
uidValidityGenerator imap.UIDValidityGenerator
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new bridge.
|
// New creates a new bridge.
|
||||||
@ -140,6 +143,7 @@ func New( //nolint:funlen
|
|||||||
proxyCtl ProxyController, // the DoH controller
|
proxyCtl ProxyController, // the DoH controller
|
||||||
crashHandler async.PanicHandler,
|
crashHandler async.PanicHandler,
|
||||||
reporter reporter.Reporter,
|
reporter reporter.Reporter,
|
||||||
|
uidValidityGenerator imap.UIDValidityGenerator,
|
||||||
|
|
||||||
logIMAPClient, logIMAPServer bool, // whether to log IMAP client/server activity
|
logIMAPClient, logIMAPServer bool, // whether to log IMAP client/server activity
|
||||||
logSMTP bool, // whether to log SMTP activity
|
logSMTP bool, // whether to log SMTP activity
|
||||||
@ -169,6 +173,7 @@ func New( //nolint:funlen
|
|||||||
api,
|
api,
|
||||||
identifier,
|
identifier,
|
||||||
proxyCtl,
|
proxyCtl,
|
||||||
|
uidValidityGenerator,
|
||||||
logIMAPClient, logIMAPServer, logSMTP,
|
logIMAPClient, logIMAPServer, logSMTP,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -214,6 +219,7 @@ func newBridge(
|
|||||||
api *proton.Manager,
|
api *proton.Manager,
|
||||||
identifier Identifier,
|
identifier Identifier,
|
||||||
proxyCtl ProxyController,
|
proxyCtl ProxyController,
|
||||||
|
uidValidityGenerator imap.UIDValidityGenerator,
|
||||||
|
|
||||||
logIMAPClient, logIMAPServer, logSMTP bool,
|
logIMAPClient, logIMAPServer, logSMTP bool,
|
||||||
) (*Bridge, error) {
|
) (*Bridge, error) {
|
||||||
@ -252,6 +258,7 @@ func newBridge(
|
|||||||
logIMAPServer,
|
logIMAPServer,
|
||||||
imapEventCh,
|
imapEventCh,
|
||||||
tasks,
|
tasks,
|
||||||
|
uidValidityGenerator,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create IMAP server: %w", err)
|
return nil, fmt.Errorf("failed to create IMAP server: %w", err)
|
||||||
@ -298,6 +305,8 @@ func newBridge(
|
|||||||
lastVersion: lastVersion,
|
lastVersion: lastVersion,
|
||||||
|
|
||||||
tasks: tasks,
|
tasks: tasks,
|
||||||
|
|
||||||
|
uidValidityGenerator: uidValidityGenerator,
|
||||||
}
|
}
|
||||||
|
|
||||||
bridge.smtpServer = newSMTPServer(bridge, tlsConfig, logSMTP)
|
bridge.smtpServer = newSMTPServer(bridge, tlsConfig, logSMTP)
|
||||||
|
|||||||
@ -29,6 +29,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
|
"github.com/ProtonMail/gluon/imap"
|
||||||
"github.com/ProtonMail/go-proton-api"
|
"github.com/ProtonMail/go-proton-api"
|
||||||
"github.com/ProtonMail/go-proton-api/server"
|
"github.com/ProtonMail/go-proton-api/server"
|
||||||
"github.com/ProtonMail/go-proton-api/server/backend"
|
"github.com/ProtonMail/go-proton-api/server/backend"
|
||||||
@ -594,6 +595,9 @@ func withMocks(t *testing.T, tests func(*bridge.Mocks)) {
|
|||||||
tests(mocks)
|
tests(mocks)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Needs to be global to survive bridge shutdown/startup in unit tests as they happen to fast.
|
||||||
|
var testUIDValidityGenerator = imap.DefaultEpochUIDValidityGenerator()
|
||||||
|
|
||||||
// withBridge creates a new bridge which points to the given API URL and uses the given keychain, and closes it when done.
|
// withBridge creates a new bridge which points to the given API URL and uses the given keychain, and closes it when done.
|
||||||
func withBridgeNoMocks(
|
func withBridgeNoMocks(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
@ -639,6 +643,7 @@ func withBridgeNoMocks(
|
|||||||
mocks.ProxyCtl,
|
mocks.ProxyCtl,
|
||||||
mocks.CrashHandler,
|
mocks.CrashHandler,
|
||||||
mocks.Reporter,
|
mocks.Reporter,
|
||||||
|
testUIDValidityGenerator,
|
||||||
|
|
||||||
// The logging stuff.
|
// The logging stuff.
|
||||||
os.Getenv("BRIDGE_LOG_IMAP_CLIENT") == "1",
|
os.Getenv("BRIDGE_LOG_IMAP_CLIENT") == "1",
|
||||||
|
|||||||
@ -28,6 +28,7 @@ import (
|
|||||||
"github.com/Masterminds/semver/v3"
|
"github.com/Masterminds/semver/v3"
|
||||||
"github.com/ProtonMail/gluon"
|
"github.com/ProtonMail/gluon"
|
||||||
imapEvents "github.com/ProtonMail/gluon/events"
|
imapEvents "github.com/ProtonMail/gluon/events"
|
||||||
|
"github.com/ProtonMail/gluon/imap"
|
||||||
"github.com/ProtonMail/gluon/reporter"
|
"github.com/ProtonMail/gluon/reporter"
|
||||||
"github.com/ProtonMail/gluon/store"
|
"github.com/ProtonMail/gluon/store"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/async"
|
"github.com/ProtonMail/proton-bridge/v3/internal/async"
|
||||||
@ -254,6 +255,7 @@ func newIMAPServer(
|
|||||||
logClient, logServer bool,
|
logClient, logServer bool,
|
||||||
eventCh chan<- imapEvents.Event,
|
eventCh chan<- imapEvents.Event,
|
||||||
tasks *async.Group,
|
tasks *async.Group,
|
||||||
|
uidValidityGenerator imap.UIDValidityGenerator,
|
||||||
) (*gluon.Server, error) {
|
) (*gluon.Server, error) {
|
||||||
gluonCacheDir = ApplyGluonCachePathSuffix(gluonCacheDir)
|
gluonCacheDir = ApplyGluonCachePathSuffix(gluonCacheDir)
|
||||||
gluonConfigDir = ApplyGluonConfigPathSuffix(gluonConfigDir)
|
gluonConfigDir = ApplyGluonConfigPathSuffix(gluonConfigDir)
|
||||||
@ -297,6 +299,7 @@ func newIMAPServer(
|
|||||||
gluon.WithLogger(imapClientLog, imapServerLog),
|
gluon.WithLogger(imapClientLog, imapServerLog),
|
||||||
getGluonVersionInfo(version),
|
getGluonVersionInfo(version),
|
||||||
gluon.WithReporter(reporter),
|
gluon.WithReporter(reporter),
|
||||||
|
gluon.WithUIDValidityGenerator(uidValidityGenerator),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@ -57,6 +57,7 @@ func TestBridge_Refresh(t *testing.T) {
|
|||||||
require.Equal(t, userID, (<-syncCh).UserID)
|
require.Equal(t, userID, (<-syncCh).UserID)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
var uidValidities = make(map[string]uint32, len(names))
|
||||||
// If we then connect an IMAP client, it should see all the labels with UID validity of 1.
|
// If we then connect an IMAP client, it should see all the labels with UID validity of 1.
|
||||||
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, mocks *bridge.Mocks) {
|
||||||
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).AnyTimes()
|
mocks.Reporter.EXPECT().ReportMessageWithContext(gomock.Any(), gomock.Any()).AnyTimes()
|
||||||
@ -73,7 +74,7 @@ func TestBridge_Refresh(t *testing.T) {
|
|||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
status, err := client.Select("Folders/"+name, false)
|
status, err := client.Select("Folders/"+name, false)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, uint32(1000), status.UidValidity)
|
uidValidities[name] = status.UidValidity
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -106,7 +107,7 @@ func TestBridge_Refresh(t *testing.T) {
|
|||||||
for _, name := range names {
|
for _, name := range names {
|
||||||
status, err := client.Select("Folders/"+name, false)
|
status, err := client.Select("Folders/"+name, false)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, uint32(1001), status.UidValidity)
|
require.Greater(t, status.UidValidity, uidValidities[name])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -163,6 +163,7 @@ func (bridge *Bridge) SetGluonDir(ctx context.Context, newGluonDir string) error
|
|||||||
bridge.logIMAPServer,
|
bridge.logIMAPServer,
|
||||||
bridge.imapEventCh,
|
bridge.imapEventCh,
|
||||||
bridge.tasks,
|
bridge.tasks,
|
||||||
|
bridge.uidValidityGenerator,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Errorf("failed to create new IMAP server: %w", err))
|
panic(fmt.Errorf("failed to create new IMAP server: %w", err))
|
||||||
|
|||||||
@ -477,16 +477,6 @@ func (conn *imapConnector) GetUpdates() <-chan imap.Update {
|
|||||||
}, conn.updateChLock)
|
}, conn.updateChLock)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUIDValidity returns the default UID validity for this user.
|
|
||||||
func (conn *imapConnector) GetUIDValidity() imap.UID {
|
|
||||||
return conn.vault.GetUIDValidity(conn.addrID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetUIDValidity sets the default UID validity for this user.
|
|
||||||
func (conn *imapConnector) SetUIDValidity(validity imap.UID) error {
|
|
||||||
return conn.vault.SetUIDValidity(conn.addrID, validity)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsMailboxVisible returns whether this mailbox should be visible over IMAP.
|
// IsMailboxVisible returns whether this mailbox should be visible over IMAP.
|
||||||
func (conn *imapConnector) IsMailboxVisible(_ context.Context, mailboxID imap.MailboxID) bool {
|
func (conn *imapConnector) IsMailboxVisible(_ context.Context, mailboxID imap.MailboxID) bool {
|
||||||
return atomic.LoadUint32(&conn.showAllMail) != 0 || mailboxID != proton.AllMailLabel
|
return atomic.LoadUint32(&conn.showAllMail) != 0 || mailboxID != proton.AllMailLabel
|
||||||
|
|||||||
@ -17,8 +17,6 @@
|
|||||||
|
|
||||||
package vault
|
package vault
|
||||||
|
|
||||||
import "github.com/ProtonMail/gluon/imap"
|
|
||||||
|
|
||||||
// UserData holds information about a single bridge user.
|
// UserData holds information about a single bridge user.
|
||||||
// The user may or may not be logged in.
|
// The user may or may not be logged in.
|
||||||
type UserData struct {
|
type UserData struct {
|
||||||
@ -28,7 +26,6 @@ type UserData struct {
|
|||||||
|
|
||||||
GluonKey []byte
|
GluonKey []byte
|
||||||
GluonIDs map[string]string
|
GluonIDs map[string]string
|
||||||
UIDValidity map[string]imap.UID
|
|
||||||
BridgePass []byte // raw token represented as byte slice (needs to be encoded)
|
BridgePass []byte // raw token represented as byte slice (needs to be encoded)
|
||||||
AddressMode AddressMode
|
AddressMode AddressMode
|
||||||
|
|
||||||
@ -79,7 +76,6 @@ func newDefaultUser(userID, username, primaryEmail, authUID, authRef string, key
|
|||||||
|
|
||||||
GluonKey: newRandomToken(32),
|
GluonKey: newRandomToken(32),
|
||||||
GluonIDs: make(map[string]string),
|
GluonIDs: make(map[string]string),
|
||||||
UIDValidity: make(map[string]imap.UID),
|
|
||||||
BridgePass: newRandomToken(16),
|
BridgePass: newRandomToken(16),
|
||||||
AddressMode: CombinedMode,
|
AddressMode: CombinedMode,
|
||||||
|
|
||||||
|
|||||||
@ -20,7 +20,6 @@ package vault
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/ProtonMail/gluon/imap"
|
|
||||||
"github.com/bradenaw/juniper/xslices"
|
"github.com/bradenaw/juniper/xslices"
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
@ -81,24 +80,6 @@ func (user *User) RemoveGluonID(addrID, gluonID string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (user *User) GetUIDValidity(addrID string) imap.UID {
|
|
||||||
if validity, ok := user.vault.getUser(user.userID).UIDValidity[addrID]; ok {
|
|
||||||
return validity
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := user.SetUIDValidity(addrID, 1000); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return user.GetUIDValidity(addrID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (user *User) SetUIDValidity(addrID string, validity imap.UID) error {
|
|
||||||
return user.vault.modUser(user.userID, func(data *UserData) {
|
|
||||||
data.UIDValidity[addrID] = validity
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddressMode returns the user's address mode.
|
// AddressMode returns the user's address mode.
|
||||||
func (user *User) AddressMode() AddressMode {
|
func (user *User) AddressMode() AddressMode {
|
||||||
return user.vault.getUser(user.userID).AddressMode
|
return user.vault.getUser(user.userID).AddressMode
|
||||||
@ -208,10 +189,6 @@ func (user *User) ClearSyncStatus() error {
|
|||||||
data.SyncStatus = SyncStatus{}
|
data.SyncStatus = SyncStatus{}
|
||||||
|
|
||||||
data.EventID = ""
|
data.EventID = ""
|
||||||
|
|
||||||
for addrID := range data.UIDValidity {
|
|
||||||
data.UIDValidity[addrID]++
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -29,6 +29,7 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/ProtonMail/gluon/imap"
|
||||||
"github.com/ProtonMail/gluon/queue"
|
"github.com/ProtonMail/gluon/queue"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
"github.com/ProtonMail/proton-bridge/v3/internal/bridge"
|
||||||
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
"github.com/ProtonMail/proton-bridge/v3/internal/constants"
|
||||||
@ -160,6 +161,7 @@ func (t *testCtx) initBridge() (<-chan events.Event, error) {
|
|||||||
t.mocks.ProxyCtl,
|
t.mocks.ProxyCtl,
|
||||||
t.mocks.CrashHandler,
|
t.mocks.CrashHandler,
|
||||||
t.reporter,
|
t.reporter,
|
||||||
|
imap.DefaultEpochUIDValidityGenerator(),
|
||||||
|
|
||||||
// Logging stuff
|
// Logging stuff
|
||||||
logIMAP,
|
logIMAP,
|
||||||
|
|||||||
Reference in New Issue
Block a user