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),