diff --git a/internal/events/address.go b/internal/events/address.go
new file mode 100644
index 00000000..73658624
--- /dev/null
+++ b/internal/events/address.go
@@ -0,0 +1,42 @@
+// Copyright (c) 2022 Proton AG
+//
+// This file is part of Proton Mail Bridge.
+//
+// Proton Mail 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.
+//
+// Proton Mail 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 Proton Mail Bridge. If not, see .
+
+package events
+
+type UserAddressCreated struct {
+ eventBase
+
+ UserID string
+ AddressID string
+ Email string
+}
+
+type UserAddressUpdated struct {
+ eventBase
+
+ UserID string
+ AddressID string
+ Email string
+}
+
+type UserAddressDeleted struct {
+ eventBase
+
+ UserID string
+ AddressID string
+ Email string
+}
diff --git a/internal/events/label.go b/internal/events/label.go
new file mode 100644
index 00000000..c4710161
--- /dev/null
+++ b/internal/events/label.go
@@ -0,0 +1,42 @@
+// Copyright (c) 2022 Proton AG
+//
+// This file is part of Proton Mail Bridge.
+//
+// Proton Mail 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.
+//
+// Proton Mail 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 Proton Mail Bridge. If not, see .
+
+package events
+
+type UserLabelCreated struct {
+ eventBase
+
+ UserID string
+ LabelID string
+ Name string
+}
+
+type UserLabelUpdated struct {
+ eventBase
+
+ UserID string
+ LabelID string
+ Name string
+}
+
+type UserLabelDeleted struct {
+ eventBase
+
+ UserID string
+ LabelID string
+ Name string
+}
diff --git a/internal/events/user.go b/internal/events/user.go
index 38640aa3..7d7fda2a 100644
--- a/internal/events/user.go
+++ b/internal/events/user.go
@@ -59,30 +59,6 @@ type UserChanged struct {
UserID string
}
-type UserAddressCreated struct {
- eventBase
-
- UserID string
- AddressID string
- Email string
-}
-
-type UserAddressUpdated struct {
- eventBase
-
- UserID string
- AddressID string
- Email string
-}
-
-type UserAddressDeleted struct {
- eventBase
-
- UserID string
- AddressID string
- Email string
-}
-
type AddressModeChanged struct {
eventBase
diff --git a/internal/safe/mutex.go b/internal/safe/mutex.go
index b7102c90..8e21f4b5 100644
--- a/internal/safe/mutex.go
+++ b/internal/safe/mutex.go
@@ -1,3 +1,20 @@
+// Copyright (c) 2022 Proton AG
+//
+// This file is part of Proton Mail Bridge.
+//
+// Proton Mail 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.
+//
+// Proton Mail 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 Proton Mail Bridge. If not, see .
+
package safe
type Mutex interface {
diff --git a/internal/user/events.go b/internal/user/events.go
index 4774ef5c..522d0963 100644
--- a/internal/user/events.go
+++ b/internal/user/events.go
@@ -119,12 +119,6 @@ func (user *User) handleCreateAddressEvent(ctx context.Context, event liteapi.Ad
user.updateCh.Set(event.Address.ID, queue.NewQueuedChannel[imap.Update](0, 0))
}
- user.eventCh.Enqueue(events.UserAddressCreated{
- UserID: user.ID(),
- AddressID: event.Address.ID,
- Email: event.Address.Email,
- })
-
if user.vault.AddressMode() == vault.SplitMode {
if ok, err := user.updateCh.GetErr(event.Address.ID, func(updateCh *queue.QueuedChannel[imap.Update]) error {
return syncLabels(ctx, user.client, updateCh)
@@ -135,14 +129,20 @@ func (user *User) handleCreateAddressEvent(ctx context.Context, event liteapi.Ad
}
}
+ user.eventCh.Enqueue(events.UserAddressCreated{
+ UserID: user.ID(),
+ AddressID: event.Address.ID,
+ Email: event.Address.Email,
+ })
+
return nil
}, &user.apiAddrsLock)
}
func (user *User) handleUpdateAddressEvent(_ context.Context, event liteapi.AddressEvent) error { //nolint:unparam
return safe.LockRet(func() error {
- if _, ok := user.apiAddrs[event.Address.ID]; ok {
- return fmt.Errorf("address %q already exists", event.ID)
+ if _, ok := user.apiAddrs[event.Address.ID]; !ok {
+ return fmt.Errorf("address %q does not exist", event.Address.ID)
}
user.apiAddrs[event.Address.ID] = event.Address
@@ -207,33 +207,70 @@ func (user *User) handleLabelEvents(ctx context.Context, labelEvents []liteapi.L
}
func (user *User) handleCreateLabelEvent(_ context.Context, event liteapi.LabelEvent) error { //nolint:unparam
- user.apiLabels.Set(event.Label.ID, event.Label)
+ return safe.LockRet(func() error {
+ if _, ok := user.apiLabels[event.Label.ID]; ok {
+ return fmt.Errorf("label %q already exists", event.ID)
+ }
- user.updateCh.IterValues(func(updateCh *queue.QueuedChannel[imap.Update]) {
- updateCh.Enqueue(newMailboxCreatedUpdate(imap.MailboxID(event.ID), getMailboxName(event.Label)))
- })
+ user.apiLabels[event.Label.ID] = event.Label
- return nil
+ user.updateCh.IterValues(func(updateCh *queue.QueuedChannel[imap.Update]) {
+ updateCh.Enqueue(newMailboxCreatedUpdate(imap.MailboxID(event.ID), getMailboxName(event.Label)))
+ })
+
+ user.eventCh.Enqueue(events.UserLabelCreated{
+ UserID: user.ID(),
+ LabelID: event.Label.ID,
+ Name: event.Label.Name,
+ })
+
+ return nil
+ }, &user.apiLabelsLock)
}
func (user *User) handleUpdateLabelEvent(_ context.Context, event liteapi.LabelEvent) error { //nolint:unparam
- user.apiLabels.Set(event.Label.ID, event.Label)
+ return safe.LockRet(func() error {
+ if _, ok := user.apiLabels[event.Label.ID]; !ok {
+ return fmt.Errorf("label %q does not exist", event.ID)
+ }
- user.updateCh.IterValues(func(updateCh *queue.QueuedChannel[imap.Update]) {
- updateCh.Enqueue(imap.NewMailboxUpdated(imap.MailboxID(event.ID), getMailboxName(event.Label)))
- })
+ user.apiLabels[event.Label.ID] = event.Label
- return nil
+ user.updateCh.IterValues(func(updateCh *queue.QueuedChannel[imap.Update]) {
+ updateCh.Enqueue(imap.NewMailboxUpdated(imap.MailboxID(event.ID), getMailboxName(event.Label)))
+ })
+
+ user.eventCh.Enqueue(events.UserLabelUpdated{
+ UserID: user.ID(),
+ LabelID: event.Label.ID,
+ Name: event.Label.Name,
+ })
+
+ return nil
+ }, &user.apiLabelsLock)
}
func (user *User) handleDeleteLabelEvent(_ context.Context, event liteapi.LabelEvent) error { //nolint:unparam
- user.apiLabels.Delete(event.Label.ID)
+ return safe.LockRet(func() error {
+ label, ok := user.apiLabels[event.ID]
+ if !ok {
+ return fmt.Errorf("label %q does not exist", event.ID)
+ }
- user.updateCh.IterValues(func(updateCh *queue.QueuedChannel[imap.Update]) {
- updateCh.Enqueue(imap.NewMailboxDeleted(imap.MailboxID(event.ID)))
- })
+ delete(user.apiLabels, event.ID)
- return nil
+ user.updateCh.IterValues(func(updateCh *queue.QueuedChannel[imap.Update]) {
+ updateCh.Enqueue(imap.NewMailboxDeleted(imap.MailboxID(event.ID)))
+ })
+
+ user.eventCh.Enqueue(events.UserLabelDeleted{
+ UserID: user.ID(),
+ LabelID: event.ID,
+ Name: label.Name,
+ })
+
+ return nil
+ }, &user.apiLabelsLock)
}
// handleMessageEvents handles the given message events.
diff --git a/internal/user/imap.go b/internal/user/imap.go
index 9a0e391b..f1310292 100644
--- a/internal/user/imap.go
+++ b/internal/user/imap.go
@@ -31,7 +31,6 @@ import (
"github.com/ProtonMail/proton-bridge/v2/internal/vault"
"github.com/ProtonMail/proton-bridge/v2/pkg/message"
"github.com/bradenaw/juniper/stream"
- "github.com/google/go-cmp/cmp"
"gitlab.protontech.ch/go/liteapi"
"golang.org/x/exp/slices"
)
@@ -83,14 +82,14 @@ func (conn *imapConnector) Authorize(username string, password []byte) bool {
// GetMailbox returns information about the mailbox with the given ID.
func (conn *imapConnector) GetMailbox(ctx context.Context, mailboxID imap.MailboxID) (imap.Mailbox, error) {
- mailbox, ok := safe.MapGetRet(conn.apiLabels, string(mailboxID), func(label liteapi.Label) imap.Mailbox {
- return toIMAPMailbox(label, conn.flags, conn.permFlags, conn.attrs)
- })
- if !ok {
- return imap.Mailbox{}, fmt.Errorf("no such mailbox: %s", mailboxID)
- }
+ return safe.RLockRetErr(func() (imap.Mailbox, error) {
+ mailbox, ok := conn.apiLabels[string(mailboxID)]
+ if !ok {
+ return imap.Mailbox{}, fmt.Errorf("no such mailbox: %s", mailboxID)
+ }
- return mailbox, nil
+ return toIMAPMailbox(mailbox, conn.flags, conn.permFlags, conn.attrs), nil
+ }, &conn.apiLabelsLock)
}
// CreateMailbox creates a label with the given name.
@@ -129,29 +128,37 @@ func (conn *imapConnector) createLabel(ctx context.Context, name []string) (imap
}
func (conn *imapConnector) createFolder(ctx context.Context, name []string) (imap.Mailbox, error) {
- var parentID string
+ return safe.RLockRetErr(func() (imap.Mailbox, error) {
+ var parentID string
- if len(name) > 1 {
- if ok := conn.apiLabels.GetFunc(func(label liteapi.Label) bool {
- return cmp.Equal(label.Path, name[:len(name)-1])
- }, func(label liteapi.Label) {
- parentID = label.ID
- }); !ok {
- return imap.Mailbox{}, fmt.Errorf("parent folder %q does not exist", name[:len(name)-1])
+ if len(name) > 1 {
+ for _, label := range conn.apiLabels {
+ if !slices.Equal(label.Path, name[:len(name)-1]) {
+ continue
+ }
+
+ parentID = label.ID
+
+ break
+ }
+
+ if parentID == "" {
+ return imap.Mailbox{}, fmt.Errorf("parent folder %q does not exist", name[:len(name)-1])
+ }
}
- }
- label, err := conn.client.CreateLabel(ctx, liteapi.CreateLabelReq{
- Name: name[len(name)-1],
- Color: "#f66",
- Type: liteapi.LabelTypeFolder,
- ParentID: parentID,
- })
- if err != nil {
- return imap.Mailbox{}, err
- }
+ label, err := conn.client.CreateLabel(ctx, liteapi.CreateLabelReq{
+ Name: name[len(name)-1],
+ Color: "#f66",
+ Type: liteapi.LabelTypeFolder,
+ ParentID: parentID,
+ })
+ if err != nil {
+ return imap.Mailbox{}, err
+ }
- return toIMAPMailbox(label, conn.flags, conn.permFlags, conn.attrs), nil
+ return toIMAPMailbox(label, conn.flags, conn.permFlags, conn.attrs), nil
+ }, &conn.apiLabelsLock)
}
// UpdateMailboxName sets the name of the label with the given ID.
@@ -193,32 +200,40 @@ func (conn *imapConnector) updateLabel(ctx context.Context, labelID imap.Mailbox
}
func (conn *imapConnector) updateFolder(ctx context.Context, labelID imap.MailboxID, name []string) error {
- var parentID string
+ return safe.RLockRet(func() error {
+ var parentID string
- if len(name) > 1 {
- if ok := conn.apiLabels.GetFunc(func(label liteapi.Label) bool {
- return cmp.Equal(label.Path, name[:len(name)-1])
- }, func(label liteapi.Label) {
- parentID = label.ID
- }); !ok {
- return fmt.Errorf("parent folder %q does not exist", name[:len(name)-1])
+ if len(name) > 1 {
+ for _, label := range conn.apiLabels {
+ if !slices.Equal(label.Path, name[:len(name)-1]) {
+ continue
+ }
+
+ parentID = label.ID
+
+ break
+ }
+
+ if parentID == "" {
+ return fmt.Errorf("parent folder %q does not exist", name[:len(name)-1])
+ }
}
- }
- label, err := conn.client.GetLabel(ctx, string(labelID), liteapi.LabelTypeFolder)
- if err != nil {
- return err
- }
+ label, err := conn.client.GetLabel(ctx, string(labelID), liteapi.LabelTypeFolder)
+ if err != nil {
+ return err
+ }
- if _, err := conn.client.UpdateLabel(ctx, string(labelID), liteapi.UpdateLabelReq{
- Name: name[len(name)-1],
- Color: label.Color,
- ParentID: parentID,
- }); err != nil {
- return err
- }
+ if _, err := conn.client.UpdateLabel(ctx, string(labelID), liteapi.UpdateLabelReq{
+ Name: name[len(name)-1],
+ Color: label.Color,
+ ParentID: parentID,
+ }); err != nil {
+ return err
+ }
- return nil
+ return nil
+ }, &conn.apiLabelsLock)
}
// DeleteMailbox deletes the label with the given ID.
diff --git a/internal/user/user.go b/internal/user/user.go
index f9890f81..bc18bd01 100644
--- a/internal/user/user.go
+++ b/internal/user/user.go
@@ -59,9 +59,11 @@ type User struct {
apiAddrs map[string]liteapi.Address
apiAddrsLock sync.RWMutex
- apiLabels *safe.Map[string, liteapi.Label]
- updateCh *safe.Map[string, *queue.QueuedChannel[imap.Update]]
- sendHash *sendRecorder
+ apiLabels map[string]liteapi.Label
+ apiLabelsLock sync.RWMutex
+
+ updateCh *safe.Map[string, *queue.QueuedChannel[imap.Update]]
+ sendHash *sendRecorder
tasks *xsync.Group
abortable async.Abortable
@@ -138,7 +140,7 @@ func New(
apiUser: apiUser,
apiAddrs: groupBy(apiAddrs, func(addr liteapi.Address) string { return addr.ID }),
- apiLabels: safe.NewMapFrom(groupBy(apiLabels, func(label liteapi.Label) string { return label.ID }), nil),
+ apiLabels: groupBy(apiLabels, func(label liteapi.Label) string { return label.ID }),
updateCh: safe.NewMapFrom(updateCh, nil),
sendHash: newSendRecorder(sendEntryExpiry),