feat(GODT-1264): creation and visibility of the 'Scheduled' system label.

feat(GODT-1264): typo in error message
feat(GODT-1264): fix split mode broken by previous commit.
This commit is contained in:
Xavier Michelon
2023-02-09 16:25:21 +01:00
parent 8b9265ad96
commit 0823d393ed
7 changed files with 100 additions and 9 deletions

2
go.mod
View File

@ -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.20230206162331-cf36d870802b github.com/ProtonMail/gluon v0.14.2-0.20230207142445-9f98ae47a031
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.4.0 github.com/ProtonMail/go-proton-api v0.4.0
github.com/ProtonMail/go-rfc5322 v0.11.0 github.com/ProtonMail/go-rfc5322 v0.11.0

4
go.sum
View File

@ -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.20230206162331-cf36d870802b h1:v/XwH5Em8gFSpJQErhSCt0XAsIxojFxgrVcfPUEWH7I= github.com/ProtonMail/gluon v0.14.2-0.20230207142445-9f98ae47a031 h1:QxHAyLmGqBtxTybgDwXwqvcA8auTcTGo8ivcEh2IIr8=
github.com/ProtonMail/gluon v0.14.2-0.20230206162331-cf36d870802b/go.mod h1:HYHr7hG7LPWI1S50M8NfHRb1kYi5B+Yu4/N/H+y+JUY= github.com/ProtonMail/gluon v0.14.2-0.20230207142445-9f98ae47a031/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=

View File

@ -770,7 +770,7 @@ func withBridge(
}) })
} }
func waitForEvent[T any](t *testing.T, eventCh <-chan events.Event, wantEvent T) { func waitForEvent[T any](t *testing.T, eventCh <-chan events.Event, _ T) {
t.Helper() t.Helper()
for event := range eventCh { for event := range eventCh {

View File

@ -24,6 +24,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings"
"sync/atomic" "sync/atomic"
"testing" "testing"
"time" "time"
@ -43,6 +44,8 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
const scheduled = "Scheduled"
func TestBridge_Sync(t *testing.T) { func TestBridge_Sync(t *testing.T) {
numMsg := 1 << 8 numMsg := 1 << 8
@ -468,3 +471,45 @@ func countBytesRead(ctl *proton.NetCtl, fn func()) uint64 {
return read return read
} }
func TestBridge_ScheduledLabel(t *testing.T) {
withEnv(t, func(ctx context.Context, s *server.Server, netCtl *proton.NetCtl, locator bridge.Locator, storeKey []byte) {
hasScheduledSystemLabel := func(imapClient *client.Client) bool {
return xslices.Any(clientList(imapClient), func(mailboxInfo *imap.MailboxInfo) bool { return mailboxInfo.Name == scheduled })
}
_, _, err := s.CreateUser(username, password)
require.NoError(t, err)
withBridge(ctx, t, s.GetHostURL(), netCtl, locator, storeKey, func(b *bridge.Bridge, _ *bridge.Mocks) {
// Perform initial sync
syncCh, done := chToType[events.Event, events.SyncFinished](b.GetEvents(events.SyncFinished{}))
defer done()
userID, err := b.LoginFull(ctx, username, password, nil, nil)
require.NoError(t, err)
require.Equal(t, userID, (<-syncCh).UserID)
// connect an IMAP client
info, err := b.GetUserInfo(userID)
require.NoError(t, err)
imapClient, err := client.Dial(fmt.Sprintf("%v:%v", constants.Host, b.GetIMAPPort()))
require.NoError(t, err)
require.NoError(t, imapClient.Login(info.Addresses[0], string(info.BridgePass)))
defer func() { _ = imapClient.Logout() }()
// Scheduled mailbox is empty. It's not listed.
require.False(t, hasScheduledSystemLabel(imapClient))
// Add a message to the Schedule mailbox. It's now listed.
require.NoError(t, imapClient.Append(scheduled, []string{}, time.Now(), strings.NewReader("To: no_reply@pm.me")))
require.True(t, hasScheduledSystemLabel(imapClient))
// delete message from Scheduled. The mailbox is now empty and not listed anymore
_, err = imapClient.Select(scheduled, false)
require.NoError(t, err)
require.NoError(t, clientStore(imapClient, 1, 1, true, imap.FormatFlagsOp(imap.AddFlags, true), imap.DeletedFlag))
require.NoError(t, imapClient.Expunge(nil))
require.False(t, hasScheduledSystemLabel(imapClient))
})
})
}

View File

@ -505,9 +505,20 @@ func (conn *imapConnector) GetUpdates() <-chan imap.Update {
}, conn.updateChLock) }, conn.updateChLock)
} }
// IsMailboxVisible returns whether this mailbox should be visible over IMAP. // GetMailboxVisibility returns the visibility of a mailbox over IMAP.
func (conn *imapConnector) IsMailboxVisible(_ context.Context, mailboxID imap.MailboxID) bool { func (conn *imapConnector) GetMailboxVisibility(_ context.Context, mailboxID imap.MailboxID) imap.MailboxVisibility {
return atomic.LoadUint32(&conn.showAllMail) != 0 || mailboxID != proton.AllMailLabel switch mailboxID {
case proton.AllMailLabel:
if atomic.LoadUint32(&conn.showAllMail) != 0 {
return imap.Visible
}
return imap.Hidden
case proton.AllScheduledLabel:
return imap.HiddenIfEmpty
default:
return imap.Visible
}
} }
// Close the connector will no longer be used and all resources should be closed/released. // Close the connector will no longer be used and all resources should be closed/released.

View File

@ -43,7 +43,31 @@ import (
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
) )
// doSync begins syncing the users data. // syncSystemLabels ensures that system labels are all known to gluon.
func (user *User) syncSystemLabels(ctx context.Context) error {
return safe.RLockRet(func() error {
var updates []imap.Update
for _, label := range xslices.Filter(maps.Values(user.apiLabels), func(label proton.Label) bool { return label.Type == proton.LabelTypeSystem }) {
if !wantLabel(label) {
continue
}
for _, updateCh := range xslices.Unique(maps.Values(user.updateCh)) {
update := newSystemMailboxCreatedUpdate(imap.MailboxID(label.ID), label.Name)
updateCh.Enqueue(update)
updates = append(updates, update)
}
}
if err := waitOnIMAPUpdates(ctx, updates); err != nil {
return fmt.Errorf("could not sync system labels: %w", err)
}
return nil
}, user.apiUserLock, user.apiAddrsLock, user.apiLabelsLock, user.updateChLock)
}
// doSync begins syncing the user's data.
// It first ensures the latest event ID is known; if not, it fetches it. // It first ensures the latest event ID is known; if not, it fetches it.
// It sends a SyncStarted event and then either SyncFinished or SyncFailed // It sends a SyncStarted event and then either SyncFinished or SyncFailed
// depending on whether the sync was successful. // depending on whether the sync was successful.
@ -658,6 +682,9 @@ func newSystemMailboxCreatedUpdate(labelID imap.MailboxID, labelName string) *im
case proton.StarredLabel: case proton.StarredLabel:
attrs = attrs.Add(imap.AttrFlagged) attrs = attrs.Add(imap.AttrFlagged)
case proton.AllScheduledLabel:
labelName = "Scheduled" // API actual name is "All Scheduled"
} }
return imap.NewMailboxCreated(imap.Mailbox{ return imap.NewMailboxCreated(imap.Mailbox{
@ -720,6 +747,9 @@ func wantLabel(label proton.Label) bool {
case proton.StarredLabel: case proton.StarredLabel:
return true return true
case proton.AllScheduledLabel:
return true
default: default:
return false return false
} }

View File

@ -190,7 +190,12 @@ func New(
// Sync the user. // Sync the user.
user.syncAbort.Do(ctx, func(ctx context.Context) { user.syncAbort.Do(ctx, func(ctx context.Context) {
if user.vault.SyncStatus().IsComplete() { if user.vault.SyncStatus().IsComplete() {
user.log.Info("Sync already complete, skipping") user.log.Info("Sync already complete, only system label will be updated")
if err := user.syncSystemLabels(ctx); err != nil {
user.log.WithError(err).Error("Failed to update system labels")
return
}
user.log.Info("System label update complete, starting API event stream")
return return
} }