Skip to content

Commit

Permalink
feat(GODT-1817): Extend move to not expunge original messages
Browse files Browse the repository at this point in the history
Update connector interface to signal Gluon whether to expunge the
original messages after a move command. This is required so that we can
correctly support the concept of mail labels.
  • Loading branch information
LBeernaertProton committed Jan 5, 2023
1 parent b04fb94 commit 675bf5d
Show file tree
Hide file tree
Showing 8 changed files with 145 additions and 11 deletions.
3 changes: 2 additions & 1 deletion connector/connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ type Connector interface {
RemoveMessagesFromMailbox(ctx context.Context, messageIDs []imap.MessageID, mboxID imap.MailboxID) error

// MoveMessages removes the given messages from one mailbox and adds them to the another mailbox.
MoveMessages(ctx context.Context, messageIDs []imap.MessageID, mboxFromID, mboxToID imap.MailboxID) error
// Returns true if the original messages should be removed from mboxFromID (e.g: Distinguishing between labels and folders).
MoveMessages(ctx context.Context, messageIDs []imap.MessageID, mboxFromID, mboxToID imap.MailboxID) (bool, error)

// MarkMessagesSeen sets the seen value of the given messages.
MarkMessagesSeen(ctx context.Context, messageIDs []imap.MessageID, seen bool) error
Expand Down
4 changes: 2 additions & 2 deletions connector/dummy.go
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ func (conn *Dummy) RemoveMessagesFromMailbox(ctx context.Context, messageIDs []i
return nil
}

func (conn *Dummy) MoveMessages(ctx context.Context, messageIDs []imap.MessageID, mboxFromID, mboxToID imap.MailboxID) error {
func (conn *Dummy) MoveMessages(ctx context.Context, messageIDs []imap.MessageID, mboxFromID, mboxToID imap.MailboxID) (bool, error) {
for _, messageID := range messageIDs {
conn.state.removeMessageFromMailbox(messageID, mboxFromID)
conn.state.addMessageToMailbox(messageID, mboxToID)
Expand All @@ -219,7 +219,7 @@ func (conn *Dummy) MoveMessages(ctx context.Context, messageIDs []imap.MessageID
))
}

return nil
return true, nil
}

func (conn *Dummy) MarkMessagesSeen(ctx context.Context, messageIDs []imap.MessageID, seen bool) error {
Expand Down
2 changes: 1 addition & 1 deletion internal/backend/state_connector_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ func (sc *stateConnectorImpl) MoveMessagesFromMailbox(
messageIDs []imap.MessageID,
mboxFromID imap.MailboxID,
mboxToID imap.MailboxID,
) error {
) (bool, error) {
ctx = sc.newContextWithMetadata(ctx)

return sc.connector.MoveMessages(ctx, messageIDs, mboxFromID, mboxToID)
Expand Down
5 changes: 3 additions & 2 deletions internal/state/actions.go
Original file line number Diff line number Diff line change
Expand Up @@ -532,11 +532,12 @@ func (state *State) actionMoveMessages(

internalIDs, remoteIDs := ids.SplitMessageIDPairSlice(messagesIDsToMove)

if err := state.user.GetRemote().MoveMessagesFromMailbox(ctx, remoteIDs, mboxFromID.RemoteID, mboxToID.RemoteID); err != nil {
shouldRemoveOldMessages, err := state.user.GetRemote().MoveMessagesFromMailbox(ctx, remoteIDs, mboxFromID.RemoteID, mboxToID.RemoteID)
if err != nil {
return nil, err
}

messageUIDs, updates, err := MoveMessagesFromMailbox(ctx, tx, mboxFromID.InternalID, mboxToID.InternalID, internalIDs, state, state.imapLimits)
messageUIDs, updates, err := MoveMessagesFromMailbox(ctx, tx, mboxFromID.InternalID, mboxToID.InternalID, internalIDs, state, state.imapLimits, shouldRemoveOldMessages)
if err != nil {
return nil, err
}
Expand Down
2 changes: 1 addition & 1 deletion internal/state/connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ type Connector interface {
messageIDs []imap.MessageID,
mboxFromID imap.MailboxID,
mboxToID imap.MailboxID,
) error
) (bool, error)

// SetMessagesSeen marks the message with the given ID as seen or unseen.
SetMessagesSeen(ctx context.Context, messageIDs []imap.MessageID, seen bool) error
Expand Down
9 changes: 6 additions & 3 deletions internal/state/updates_mailbox.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ func MoveMessagesFromMailbox(
messageIDs []imap.InternalMessageID,
s *State,
imapLimits limits.IMAP,
removeOldMessages bool,
) ([]db.UIDWithFlags, []Update, error) {
messageCount, uid, err := db.GetMailboxMessageCountAndUID(ctx, tx.Client(), mboxToID)
if err != nil {
Expand All @@ -35,7 +36,7 @@ func MoveMessagesFromMailbox(
return nil, nil, err
}

if mboxFromID != mboxToID {
if mboxFromID != mboxToID && removeOldMessages {
if err := db.RemoveMessagesFromMailbox(ctx, tx, messageIDs, mboxFromID); err != nil {
return nil, nil, err
}
Expand All @@ -57,8 +58,10 @@ func MoveMessagesFromMailbox(
stateUpdates = append(stateUpdates, newExistsStateUpdateWithExists(mboxToID, responders, s))
}

for _, messageID := range messageIDs {
stateUpdates = append(stateUpdates, NewMessageIDAndMailboxIDResponderStateUpdate(messageID, mboxFromID, NewExpunge(messageID)))
if removeOldMessages {
for _, messageID := range messageIDs {
stateUpdates = append(stateUpdates, NewMessageIDAndMailboxIDResponderStateUpdate(messageID, mboxFromID, NewExpunge(messageID)))
}
}

return messageUIDs, stateUpdates, nil
Expand Down
129 changes: 129 additions & 0 deletions tests/move_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
package tests

import (
"context"
"fmt"
"github.com/ProtonMail/gluon/connector"
goimap "github.com/emersion/go-imap"
"github.com/emersion/go-imap/client"
"github.com/stretchr/testify/require"
"testing"
"time"

"github.com/ProtonMail/gluon/imap"
)
Expand Down Expand Up @@ -214,3 +220,126 @@ func TestConcurrency(t *testing.T) {
}
})
}

// disableRemoveFromMailboxConnector fails the first append and panics if move or remove takes place on the
// connector.
type simulateLabelConnector struct {
*connector.Dummy
mboxID imap.MailboxID
}

func (r *simulateLabelConnector) CreateMailbox(ctx context.Context, name []string) (imap.Mailbox, error) {
mbox, err := r.Dummy.CreateMailbox(ctx, name)
if err != nil {
return mbox, err
}

if len(r.mboxID) == 0 {
r.mboxID = mbox.ID
}

return mbox, nil
}

func (r *simulateLabelConnector) MoveMessages(
ctx context.Context,
ids []imap.MessageID,
from imap.MailboxID,
to imap.MailboxID,
) (bool, error) {
if _, err := r.Dummy.MoveMessages(ctx, ids, from, to); err != nil {
return false, err
}

return to != r.mboxID, nil
}

type simulateLabelConnectorBuilder struct{}

func (simulateLabelConnectorBuilder) New(usernames []string, password []byte, period time.Duration, flags, permFlags, attrs imap.FlagSet) Connector {
return &simulateLabelConnector{
Dummy: connector.NewDummy(usernames, password, period, flags, permFlags, attrs),
}
}

func TestMoveLabelBehavior(t *testing.T) {
runOneToOneTestClientWithAuth(t, defaultServerOptions(t, withConnectorBuilder(&simulateLabelConnectorBuilder{})), func(client *client.Client, _ *testSession) {
require.NoError(t, doAppendWithClient(client, "inbox", "To: [email protected]", time.Now()))
require.NoError(t, doAppendWithClient(client, "inbox", "To: [email protected]", time.Now()))
require.NoError(t, doAppendWithClient(client, "inbox", "To: [email protected]", time.Now()))

require.NoError(t, client.Create("mylabel"))

// Move message to label
{
status, err := client.Select("INBOX", false)
require.NoError(t, err)
require.Equal(t, uint32(3), status.Messages)

// Move one message to label
require.NoError(t, client.Move(createSeqSet("1"), "mylabel"))

// Inbox should have 3 messages
status, err = client.Status("INBOX", []goimap.StatusItem{goimap.StatusMessages})
require.NoError(t, err)
require.Equal(t, uint32(3), status.Messages)

// Check all messages are still present
newFetchCommand(t, client).withItems("ENVELOPE").fetch("1:3").
forSeqNum(1, func(builder *validatorBuilder) {
builder.ignoreFlags()
builder.wantEnvelope(func(builder *envelopeValidatorBuilder) {
builder.wantTo("[email protected]")
})
}).
forSeqNum(2, func(builder *validatorBuilder) {
builder.ignoreFlags()
builder.wantEnvelope(func(builder *envelopeValidatorBuilder) {
builder.wantTo("[email protected]")
})
}).
forSeqNum(3, func(builder *validatorBuilder) {
builder.ignoreFlags()
builder.wantEnvelope(func(builder *envelopeValidatorBuilder) {
builder.wantTo("[email protected]")
})
}).
checkAndRequireMessageCount(3)

// Label should have 1 message
status, err = client.Status("mylabel", []goimap.StatusItem{goimap.StatusMessages})
require.NoError(t, err)
require.Equal(t, uint32(1), status.Messages)

}

// Move message to inbox from label
{
status, err := client.Select("mylabel", false)
require.NoError(t, err)
require.Equal(t, uint32(1), status.Messages)

// Check it has the right message
newFetchCommand(t, client).withItems("ENVELOPE").fetch("1").forSeqNum(1, func(builder *validatorBuilder) {
builder.ignoreFlags()
builder.wantEnvelope(func(builder *envelopeValidatorBuilder) {
builder.wantTo("[email protected]")
})
}).checkAndRequireMessageCount(1)

// Move one message to label
require.NoError(t, client.Move(createSeqSet("1"), "INBOX"))

// Inbox should have 3 messages
status, err = client.Status("INBOX", []goimap.StatusItem{goimap.StatusMessages})
require.NoError(t, err)
require.Equal(t, uint32(3), status.Messages)

// Label should have 1 message
status, err = client.Status("mylabel", []goimap.StatusItem{goimap.StatusMessages})
require.NoError(t, err)
require.Equal(t, uint32(0), status.Messages)
}

})
}
2 changes: 1 addition & 1 deletion tests/recovery_mailbox_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ func (r *disableRemoveFromMailboxConnector) MoveMessages(
_ []imap.MessageID,
_ imap.MailboxID,
_ imap.MailboxID,
) error {
) (bool, error) {
panic("Should not be called")
}

Expand Down

0 comments on commit 675bf5d

Please sign in to comment.