From 9390cb64b48328f162cf978d5ac11b3033686214 Mon Sep 17 00:00:00 2001 From: Leander Beernaert Date: Fri, 6 Jan 2023 10:55:31 +0100 Subject: [PATCH] GODT-1817: Restore move related feature tests Gluon updated to latest dev commit, required for feature. Checks from move_local_folder.feature are implemented in Gluon. --- COPYING_NOTES.md | 1 - go.mod | 3 +- go.sum | 11 ++- internal/user/imap.go | 56 ++++++++--- tests/_features/imap/message/move.feature | 98 ------------------- .../imap/message/move_local_folder.feature | 80 --------------- .../imap/message/move_without_support.feature | 77 --------------- tests/bdd_test.go | 4 +- tests/features/imap/message/move.feature | 85 ++++++++++++++++ .../imap/message/move_without_support.feature | 6 +- tests/imap_test.go | 91 ++++++++++++++--- 11 files changed, 215 insertions(+), 297 deletions(-) delete mode 100644 tests/_features/imap/message/move.feature delete mode 100644 tests/_features/imap/message/move_local_folder.feature delete mode 100644 tests/_features/imap/message/move_without_support.feature create mode 100644 tests/features/imap/message/move.feature diff --git a/COPYING_NOTES.md b/COPYING_NOTES.md index 3782fd42..2269bf30 100644 --- a/COPYING_NOTES.md +++ b/COPYING_NOTES.md @@ -132,7 +132,6 @@ Proton Mail Bridge includes the following 3rd party software: gopkg.in/yaml.v2 gopkg.in/yaml.v3 * [docker-credential-helpers](https://github.com/ProtonMail/docker-credential-helpers) available under [license](https://github.com/ProtonMail/docker-credential-helpers/blob/master/LICENSE) -* [go-imap](https://github.com/ProtonMail/go-imap) available under [license](https://github.com/ProtonMail/go-imap/blob/master/LICENSE) * [go-message](https://github.com/ProtonMail/go-message) available under [license](https://github.com/ProtonMail/go-message/blob/master/LICENSE) * [go-keychain](https://github.com/cuthix/go-keychain) available under [license](https://github.com/cuthix/go-keychain/blob/master/LICENSE) diff --git a/go.mod b/go.mod index 09529c82..c9bb4a7a 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.18 require ( github.com/0xAX/notificator v0.0.0-20220220101646-ee9b8921e557 github.com/Masterminds/semver/v3 v3.1.1 - github.com/ProtonMail/gluon v0.14.2-0.20221220184532-b04fb948e367 + github.com/ProtonMail/gluon v0.14.2-0.20230106095250-7e99ea4da61e github.com/ProtonMail/go-autostart v0.0.0-20210130080809-00ed301c8e9a github.com/ProtonMail/go-proton-api v0.2.4-0.20230103140323-680d85d1c3f0 github.com/ProtonMail/go-rfc5322 v0.11.0 @@ -120,7 +120,6 @@ require ( replace ( github.com/docker/docker-credential-helpers => github.com/ProtonMail/docker-credential-helpers v1.1.0 - github.com/emersion/go-imap => github.com/ProtonMail/go-imap v0.0.0-20201228133358-4db68cea0cac github.com/emersion/go-message => github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753 github.com/keybase/go-keychain => github.com/cuthix/go-keychain v0.0.0-20220405075754-31e7cee908fe ) diff --git a/go.sum b/go.sum index 02395de4..ee4362ef 100644 --- a/go.sum +++ b/go.sum @@ -28,16 +28,16 @@ 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/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/gluon v0.14.2-0.20221220184532-b04fb948e367 h1:oYEXHIRCqK3RG0nErhhZ6cpqRTCZmd1QYtmHastjWB8= -github.com/ProtonMail/gluon v0.14.2-0.20221220184532-b04fb948e367/go.mod h1:z2AxLIiBCT1K+0OBHyaDI7AEaO5qI6/BEC2TE42vs4Q= +github.com/ProtonMail/gluon v0.14.2-0.20230105101243-675bf5daf3e6 h1:pGom2w5ncRNZf8+Z3IL4DQyTCI2UK8jhb6zkQVbVrLg= +github.com/ProtonMail/gluon v0.14.2-0.20230105101243-675bf5daf3e6/go.mod h1:z2AxLIiBCT1K+0OBHyaDI7AEaO5qI6/BEC2TE42vs4Q= +github.com/ProtonMail/gluon v0.14.2-0.20230106095250-7e99ea4da61e h1://xRNjGTAMXw2U91MtqPc4krUtxQmt2+4z1oYrBaOWU= +github.com/ProtonMail/gluon v0.14.2-0.20230106095250-7e99ea4da61e/go.mod h1:z2AxLIiBCT1K+0OBHyaDI7AEaO5qI6/BEC2TE42vs4Q= 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-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= github.com/ProtonMail/go-crypto v0.0.0-20220822140716-1678d6eb0cbe/go.mod h1:UBYPn8k0D56RtnR8RFQMjmh4KrZzWJ5o7Z9SYjossQ8= github.com/ProtonMail/go-crypto v0.0.0-20220824120805-4b6e5c587895 h1:NsReiLpErIPzRrnogAXYwSoU7txA977LjDGrbkewJbg= github.com/ProtonMail/go-crypto v0.0.0-20220824120805-4b6e5c587895/go.mod h1:UBYPn8k0D56RtnR8RFQMjmh4KrZzWJ5o7Z9SYjossQ8= -github.com/ProtonMail/go-imap v0.0.0-20201228133358-4db68cea0cac h1:2xU3QncAiS/W3UlWZTkbNKW5WkLzk6Egl1T0xX+sbjs= -github.com/ProtonMail/go-imap v0.0.0-20201228133358-4db68cea0cac/go.mod h1:yKASt+C3ZiDAiCSssxg9caIckWF/JG7ZQTO7GAmvicU= github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753 h1:I8IsYA297x0QLU80G5I6aLYUu3JYNSpo8j5fkXtFDW0= github.com/ProtonMail/go-message v0.0.0-20210611055058-fabeff2ec753/go.mod h1:NBAn21zgCJ/52WLDyed18YvYFm5tEoeDauubFqLokM4= github.com/ProtonMail/go-mime v0.0.0-20220302105931-303f85f7fe0f/go.mod h1:NYt+V3/4rEeDuaev/zw1zCq8uqVEuPHzDPo3OZrlGJ4= @@ -122,9 +122,10 @@ github.com/elastic/go-sysinfo v1.8.1 h1:4Yhj+HdV6WjbCRgGdZpPJ8lZQlXZLKDAeIkmQ/VR github.com/elastic/go-sysinfo v1.8.1/go.mod h1:JfllUnzoQV/JRYymbH3dO1yggI3mV2oTKSXsDHM+uIM= github.com/elastic/go-windows v1.0.1 h1:AlYZOldA+UJ0/2nBuqWdo90GFCgG9xuyw9SYzGUtJm0= github.com/elastic/go-windows v1.0.1/go.mod h1:FoVvqWSun28vaDQPbj2Elfc0JahhPB7WQEGa3c814Ss= +github.com/emersion/go-imap v1.2.1-0.20220429085312-746087b7a317 h1:i0cBrdFLm8A/3hWEjn/BwdXLBplFJoZtu63p7bjrmaI= +github.com/emersion/go-imap v1.2.1-0.20220429085312-746087b7a317/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY= github.com/emersion/go-imap-id v0.0.0-20190926060100-f94a56b9ecde h1:43mBoVwooyLm1+1YVf5nvn1pSFWhw7rOpcrp1Jg/qk0= github.com/emersion/go-imap-id v0.0.0-20190926060100-f94a56b9ecde/go.mod h1:sPwp0FFboaK/bxsrUz1lNrDMUCsZUsKC5YuM4uRVRVs= -github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k= github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead h1:fI1Jck0vUrXT8bnphprS1EoVRe2Q5CKCX8iDlpqjQ/Y= github.com/emersion/go-sasl v0.0.0-20220912192320-0145f2c60ead/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= diff --git a/internal/user/imap.go b/internal/user/imap.go index f215f0b5..29261d94 100644 --- a/internal/user/imap.go +++ b/internal/user/imap.go @@ -112,16 +112,20 @@ func (conn *imapConnector) createLabel(ctx context.Context, name []string) (imap return imap.Mailbox{}, fmt.Errorf("a label cannot have children") } - label, err := conn.client.CreateLabel(ctx, proton.CreateLabelReq{ - Name: name[0], - Color: "#f66", - Type: proton.LabelTypeLabel, - }) - if err != nil { - return imap.Mailbox{}, err - } + return safe.LockRetErr(func() (imap.Mailbox, error) { + label, err := conn.client.CreateLabel(ctx, proton.CreateLabelReq{ + Name: name[0], + Color: "#f66", + Type: proton.LabelTypeLabel, + }) + if err != nil { + return imap.Mailbox{}, err + } - return toIMAPMailbox(label, conn.flags, conn.permFlags, conn.attrs), nil + conn.apiLabels[label.ID] = label + + return toIMAPMailbox(label, conn.flags, conn.permFlags, conn.attrs), nil + }, conn.apiLabelsLock) } func (conn *imapConnector) createFolder(ctx context.Context, name []string) (imap.Mailbox, error) { @@ -368,18 +372,42 @@ func (conn *imapConnector) RemoveMessagesFromMailbox(ctx context.Context, messag } // MoveMessages removes the given messages from one label and adds them to the other label. -func (conn *imapConnector) MoveMessages(ctx context.Context, messageIDs []imap.MessageID, labelFromID imap.MailboxID, labelToID imap.MailboxID) error { +func (conn *imapConnector) MoveMessages(ctx context.Context, messageIDs []imap.MessageID, labelFromID imap.MailboxID, labelToID imap.MailboxID) (bool, error) { defer conn.goPollAPIEvents(false) + if (labelFromID == proton.InboxLabel && labelToID == proton.SentLabel) || + (labelFromID == proton.SentLabel && labelToID == proton.InboxLabel) { + return false, fmt.Errorf("not allowed") + } + + shouldExpungeOldLocation := func() bool { + conn.apiLabelsLock.RLock() + defer conn.apiLabelsLock.RUnlock() + + var result bool + + if v, ok := conn.apiLabels[string(labelFromID)]; ok && v.Type == proton.LabelTypeLabel { + result = result || true + } + + if v, ok := conn.apiLabels[string(labelToID)]; ok && v.Type == proton.LabelTypeFolder { + result = result || true + } + + return result + }() + if err := conn.client.LabelMessages(ctx, mapTo[imap.MessageID, string](messageIDs), string(labelToID)); err != nil { - return fmt.Errorf("labeling messages: %w", err) + return false, fmt.Errorf("labeling messages: %w", err) } - if err := conn.client.UnlabelMessages(ctx, mapTo[imap.MessageID, string](messageIDs), string(labelFromID)); err != nil { - return fmt.Errorf("unlabeling messages: %w", err) + if shouldExpungeOldLocation { + if err := conn.client.UnlabelMessages(ctx, mapTo[imap.MessageID, string](messageIDs), string(labelFromID)); err != nil { + return false, fmt.Errorf("unlabeling messages: %w", err) + } } - return nil + return shouldExpungeOldLocation, nil } // MarkMessagesSeen sets the seen value of the given messages. diff --git a/tests/_features/imap/message/move.feature b/tests/_features/imap/message/move.feature deleted file mode 100644 index e2d27532..00000000 --- a/tests/_features/imap/message/move.feature +++ /dev/null @@ -1,98 +0,0 @@ -Feature: IMAP move messages - Background: - Given there is connected user "user" - And there is "user" with mailbox "Folders/folder" - And there is "user" with mailbox "Labels/label" - And there is "user" with mailbox "Labels/label2" - And there are messages in mailbox "INBOX" for "user" - | from | to | subject | body | - | john.doe@mail.com | user@pm.me | foo | hello | - | jane.doe@mail.com | name@pm.me | bar | world | - And there are messages in mailbox "Sent" for "user" - | from | to | subject | body | - | john.doe@mail.com | user@pm.me | response | hello | - And there are messages in mailbox "Labels/label2" for "user" - | from | to | subject | body | - | john.doe@mail.com | user@pm.me | baz | hello | - And there is IMAP client logged in as "user" - - Scenario: Move message from inbox (folder) to folder - Given there is IMAP client selected in "INBOX" - When IMAP client moves message seq "1" to "Folders/folder" - Then IMAP response is "OK" - And mailbox "INBOX" for "user" has messages - | from | to | subject | - | jane.doe@mail.com | name@pm.me | bar | - And mailbox "Folders/folder" for "user" has messages - | from | to | subject | - | john.doe@mail.com | user@pm.me | foo | - And API endpoint "PUT /mail/v4/messages/label" is called - And API endpoint "PUT /mail/v4/messages/unlabel" is not called - - Scenario: Move all messages from inbox to folder - Given there is IMAP client selected in "INBOX" - When IMAP client moves message seq "1:*" to "Folders/folder" - Then IMAP response is "OK" - And mailbox "INBOX" for "user" has 0 messages - And mailbox "Folders/folder" for "user" has messages - | from | to | subject | - | john.doe@mail.com | user@pm.me | foo | - | jane.doe@mail.com | name@pm.me | bar | - And API endpoint "PUT /mail/v4/messages/label" is called - And API endpoint "PUT /mail/v4/messages/unlabel" is not called - - Scenario: Move message from folder to label (keeps in folder) - Given there is IMAP client selected in "INBOX" - When IMAP client moves message seq "1" to "Labels/label" - Then IMAP response is "OK" - And mailbox "INBOX" for "user" has messages - | from | to | subject | - | john.doe@mail.com | user@pm.me | foo | - | jane.doe@mail.com | name@pm.me | bar | - And mailbox "Labels/label" for "user" has messages - | from | to | subject | - | john.doe@mail.com | user@pm.me | foo | - And API endpoint "PUT /mail/v4/messages/label" is called - And API endpoint "PUT /mail/v4/messages/unlabel" is not called - - Scenario: Move message from label to folder - Given there is IMAP client selected in "Labels/label2" - When IMAP client moves message seq "1" to "Folders/folder" - Then IMAP response is "OK" - And mailbox "Labels/label2" for "user" has 0 messages - And mailbox "Folders/folder" for "user" has messages - | from | to | subject | - | john.doe@mail.com | user@pm.me | baz | - And API endpoint "PUT /mail/v4/messages/label" is called - And API endpoint "PUT /mail/v4/messages/unlabel" is called - - Scenario: Move message from label to label - Given there is IMAP client selected in "Labels/label2" - When IMAP client moves message seq "1" to "Labels/label" - Then IMAP response is "OK" - And mailbox "Labels/label2" for "user" has 0 messages - And mailbox "Labels/label" for "user" has messages - | from | to | subject | - | john.doe@mail.com | user@pm.me | baz | - And API endpoint "PUT /mail/v4/messages/label" is called - And API endpoint "PUT /mail/v4/messages/unlabel" is called - - Scenario: Move message from All Mail is not possible - Given there is IMAP client selected in "All Mail" - When IMAP client moves message seq "1" to "Folders/folder" - Then IMAP response is "NO move from All Mail is not allowed" - And mailbox "All Mail" for "user" has messages - | from | to | subject | - | john.doe@mail.com | user@pm.me | foo | - | jane.doe@mail.com | name@pm.me | bar | - And mailbox "Folders/folder" for "user" has 0 messages - - Scenario: Move message from Inbox to Sent is not possible - Given there is IMAP client selected in "INBOX" - When IMAP client moves message seq "1" to "Sent" - Then IMAP response is "move from Inbox to Sent is not allowed" - - Scenario: Move message from Sent to Inbox is not possible - Given there is IMAP client selected in "Sent" - When IMAP client moves message seq "1" to "INBOX" - Then IMAP response is "move from Sent to Inbox is not allowed" diff --git a/tests/_features/imap/message/move_local_folder.feature b/tests/_features/imap/message/move_local_folder.feature deleted file mode 100644 index da35023c..00000000 --- a/tests/_features/imap/message/move_local_folder.feature +++ /dev/null @@ -1,80 +0,0 @@ -# IMAP clients can move message to local folder (setting \Deleted flag) -# and then move it back (IMAP client does not remember the message, -# so instead removing the flag it imports duplicate message). -# Regular IMAP server would keep the message twice and later EXPUNGE would -# not delete the message (EXPUNGE would delete the original message and -# the new duplicate one would stay). Both Bridge and API detects duplicates; -# therefore we need to remove \Deleted flag if IMAP client re-imports. -Feature: IMAP move message out to and back from local folder - Background: - Given there is connected user "user" - Given there is IMAP client logged in as "user" - And there is IMAP client selected in "INBOX" - - Scenario: Mark message as deleted and re-append again - When IMAP client imports message to "INBOX" - """ - From: - To: - Subject: foo - Date: Mon, 02 Jan 2006 15:04:05 +0000 - Message-Id: - Received: by 2002:0:0:0:0:0:0:0 with SMTP id 0123456789abcdef; Wed, 30 Dec 2020 01:23:45 0000 - - hello - """ - Then IMAP response is "OK" - When IMAP client marks message seq "1" as deleted - Then IMAP response is "OK" - When IMAP client imports message to "INBOX" - """ - From: - To: - Subject: foo - Date: Mon, 02 Jan 2006 15:04:05 +0000 - Message-Id: - Received: by 2002:0:0:0:0:0:0:0 with SMTP id 0123456789abcdef; Wed, 30 Dec 2020 01:23:45 0000 - - hello - """ - Then IMAP response is "OK" - And mailbox "INBOX" for "user" has 1 message - And mailbox "INBOX" for "user" has messages - | from | to | subject | deleted | - | john.doe@mail.com | user@pm.me | foo | false | - - # We cannot control ID generation on API. - @ignore-live - Scenario: Mark internal message as deleted and re-append again - # Each message has different subject so if the ID generations on fake API - # changes, test will fail because not even external ID mechanism will work. - When IMAP client imports message to "INBOX" - """ - From: - To: - Subject: foo - Date: Mon, 02 Jan 2006 15:04:05 +0000 - Received: by 2002:0:0:0:0:0:0:0 with SMTP id 0123456789abcdef; Wed, 30 Dec 2020 01:23:45 0000 - - hello - """ - Then IMAP response is "OK" - When IMAP client marks message seq "1" as deleted - Then IMAP response is "OK" - # Fake API generates for the first message simple ID 1. - When IMAP client imports message to "INBOX" - """ - From: - To: - Subject: bar - Date: Mon, 02 Jan 2006 15:04:05 +0000 - X-Pm-Internal-Id: 1 - Received: by 2002:0:0:0:0:0:0:0 with SMTP id 0123456789abcdef; Wed, 30 Dec 2020 01:23:45 0000 - - hello - """ - Then IMAP response is "OK" - And mailbox "INBOX" for "user" has 1 message - And mailbox "INBOX" for "user" has messages - | from | to | subject | deleted | - | john.doe@mail.com | user@pm.me | foo | false | diff --git a/tests/_features/imap/message/move_without_support.feature b/tests/_features/imap/message/move_without_support.feature deleted file mode 100644 index 36386223..00000000 --- a/tests/_features/imap/message/move_without_support.feature +++ /dev/null @@ -1,77 +0,0 @@ -Feature: IMAP move messages by append and delete (without MOVE support, e.g., Outlook) - Background: - Given there is connected user "user" - And there is "user" with mailbox "Folders/mbox" - And there is IMAP client "source" logged in as "user" - And there is IMAP client "target" logged in as "user" - - Scenario Outline: Move message from to by - Given there are messages in mailbox "" for "user" - | id | from | to | subject | body | - | 1 | sndr1@pm.me | rcvr1@pm.me | subj1 | body1 | - | 2 | sndr2@pm.me | rcvr2@pm.me | subj2 | body2 | - And there is IMAP client "source" selected in "" - And there is IMAP client "target" selected in "" - When IMAP clients "source" and "target" move message seq "2" of "user" to "" by - Then IMAP response to "source" is "OK" - Then IMAP response to "target" is "OK" - And mailbox "" for "user" has 1 messages - And mailbox "" for "user" has messages - | from | to | subject | - | sndr2@pm.me | rcvr2@pm.me | subj2 | - And mailbox "" for "user" has 1 messages - And mailbox "" for "user" has messages - | from | to | subject | - | sndr1@pm.me | rcvr1@pm.me | subj1 | - Examples: - | srcMailbox | dstMailbox | order | - | Trash | INBOX | APPEND DELETE EXPUNGE | - | Spam | INBOX | APPEND DELETE EXPUNGE | - | INBOX | Archive | APPEND DELETE EXPUNGE | - | INBOX | Folders/mbox | APPEND DELETE EXPUNGE | - | INBOX | Spam | APPEND DELETE EXPUNGE | - | INBOX | Trash | APPEND DELETE EXPUNGE | - | Trash | INBOX | DELETE APPEND EXPUNGE | - | Spam | INBOX | DELETE APPEND EXPUNGE | - | INBOX | Archive | DELETE APPEND EXPUNGE | - | INBOX | Folders/mbox | DELETE APPEND EXPUNGE | - | INBOX | Spam | DELETE APPEND EXPUNGE | - | INBOX | Trash | DELETE APPEND EXPUNGE | - | Trash | INBOX | DELETE EXPUNGE APPEND | - | Spam | INBOX | DELETE EXPUNGE APPEND | - | INBOX | Archive | DELETE EXPUNGE APPEND | - | INBOX | Folders/mbox | DELETE EXPUNGE APPEND | - | INBOX | Spam | DELETE EXPUNGE APPEND | - | INBOX | Trash | DELETE EXPUNGE APPEND | - - Scenario Outline: Move message from to All Mail by - Given there are messages in mailbox "" for "user" - | id | from | to | subject | body | - | 1 | john.doe@mail.com | user@pm.me | subj1 | body1 | - | 2 | john.doe@mail.com | name@pm.me | subj2 | body2 | - And there is IMAP client "source" selected in "" - And there is IMAP client "target" selected in "All Mail" - When IMAP clients "source" and "target" move message seq "2" of "user" to "All Mail" by - Then IMAP response to "source" is "OK" - Then IMAP response to "target" is "OK" - And mailbox "" for "user" has messages - | from | to | subject | - | john.doe@mail.com | user@pm.me | subj1 | - And mailbox "All Mail" for "user" has messages - | from | to | subject | - | john.doe@mail.com | user@pm.me | subj1 | - | john.doe@mail.com | name@pm.me | subj2 | - Examples: - | mailbox | order | - | INBOX | APPEND DELETE EXPUNGE | - | Archive | APPEND DELETE EXPUNGE | - | Trash | APPEND DELETE EXPUNGE | - | Spam | APPEND DELETE EXPUNGE | - | INBOX | DELETE APPEND EXPUNGE | - | Archive | DELETE APPEND EXPUNGE | - | Trash | DELETE APPEND EXPUNGE | - | Spam | DELETE APPEND EXPUNGE | - | INBOX | DELETE EXPUNGE APPEND | - | Archive | DELETE EXPUNGE APPEND | - | Trash | DELETE EXPUNGE APPEND | - | Spam | DELETE EXPUNGE APPEND | diff --git a/tests/bdd_test.go b/tests/bdd_test.go index 600b046c..5072f7f8 100644 --- a/tests/bdd_test.go +++ b/tests/bdd_test.go @@ -188,6 +188,8 @@ func TestFeatures(testingT *testing.T) { ctx.Step(`^IMAP client "([^"]*)" selects "([^"]*)"$`, s.imapClientSelectsMailbox) ctx.Step(`^IMAP client "([^"]*)" copies the message with subject "([^"]*)" from "([^"]*)" to "([^"]*)"$`, s.imapClientCopiesTheMessageWithSubjectFromTo) ctx.Step(`^IMAP client "([^"]*)" copies all messages from "([^"]*)" to "([^"]*)"$`, s.imapClientCopiesAllMessagesFromTo) + ctx.Step(`^IMAP client "([^"]*)" moves the message with subject "([^"]*)" from "([^"]*)" to "([^"]*)"$`, s.imapClientMovesTheMessageWithSubjectFromTo) + ctx.Step(`^IMAP client "([^"]*)" moves all messages from "([^"]*)" to "([^"]*)"$`, s.imapClientMovesAllMessagesFromTo) ctx.Step(`^IMAP client "([^"]*)" sees the following messages in "([^"]*)":$`, s.imapClientSeesTheFollowingMessagesInMailbox) ctx.Step(`^IMAP client "([^"]*)" eventually sees the following messages in "([^"]*)":$`, s.imapClientEventuallySeesTheFollowingMessagesInMailbox) ctx.Step(`^IMAP client "([^"]*)" sees (\d+) messages in "([^"]*)"$`, s.imapClientSeesMessagesInMailbox) @@ -201,7 +203,7 @@ func TestFeatures(testingT *testing.T) { ctx.Step(`^IMAP client "([^"]*)" appends the following message to "([^"]*)":$`, s.imapClientAppendsTheFollowingMessageToMailbox) ctx.Step(`^IMAP client "([^"]*)" appends the following messages to "([^"]*)":$`, s.imapClientAppendsTheFollowingMessagesToMailbox) ctx.Step(`^IMAP client "([^"]*)" appends "([^"]*)" to "([^"]*)"$`, s.imapClientAppendsToMailbox) - ctx.Step(`^IMAP clients "([^"]*)" and "([^"]*)" move message seq "([^"]*)" of "([^"]*)" to "([^"]*)" by ([^"]*) ([^"]*) ([^"]*)`, s.imapClientsMoveMessageSeqOfUserFromToByOrderedOperations) + ctx.Step(`^IMAP clients "([^"]*)" and "([^"]*)" move message with subject "([^"]*)" of "([^"]*)" to "([^"]*)" by ([^"]*) ([^"]*) ([^"]*)`, s.imapClientsMoveMessageWithSubjectUserFromToByOrderedOperations) ctx.Step(`^IMAP client "([^"]*)" sees header "([^"]*)" in message with subject "([^"]*)" in "([^"]*)"$`, s.imapClientSeesHeaderInMessageWithSubject) // ==== SMTP ==== diff --git a/tests/features/imap/message/move.feature b/tests/features/imap/message/move.feature new file mode 100644 index 00000000..f98cca25 --- /dev/null +++ b/tests/features/imap/message/move.feature @@ -0,0 +1,85 @@ +Feature: IMAP move messages + Background: + Given there exists an account with username "[user:user]" and password "password" + And the account "[user:user]" has the following custom mailboxes: + | name | type | + | mbox | folder | + | label | label | + | label2 | label | + And the address "[user:user]@[domain]" of account "[user:user]" has the following messages in "Inbox": + | from | to | subject | unread | + | john.doe@mail.com | [user:user]@[domain] | foo | false | + | jane.doe@mail.com | name@[domain] | bar | true | + And the address "[user:user]@[domain]" of account "[user:user]" has the following messages in "Labels/label2": + | from | to | subject | unread | + | john.doe@mail.com | [user:user]@[domain] | baz | false | + And the address "[user:user]@[domain]" of account "[user:user]" has the following messages in "Sent": + | from | to | subject | unread | + | john.doe@mail.com | [user:user]@[domain] | bax | false | + And bridge starts + And the user logs in with username "[user:user]" and password "password" + And user "[user:user]" finishes syncing + And user "[user:user]" connects and authenticates IMAP client "1" + + Scenario: Move message from folder to label (keeps in folder) + When IMAP client "1" moves the message with subject "foo" from "INBOX" to "Labels/label" + And it succeeds + And IMAP client "1" sees the following messages in "INBOX": + | from | to | subject | unread | + | john.doe@mail.com | [user:user]@[domain] | foo | false | + | jane.doe@mail.com | name@[domain] | bar | true | + And IMAP client "1" sees the following messages in "Labels/label": + | from | to | subject | unread | + | john.doe@mail.com | [user:user]@[domain] | foo | false | + + # This test covers a limitation of Gluon where we are not able to ensure the messages moved to a label via append + # expunge are preserved in the original folder. + Scenario: Move message from folder to label with append expunge does not keep message in origin folder + When user "[user:user]" connects and authenticates IMAP client "source" + And user "[user:user]" connects and authenticates IMAP client "target" + And IMAP client "source" selects "INBOX" + And IMAP client "target" selects "Labels/label" + And IMAP clients "source" and "target" move message with subject "foo" of "[user:user]" to "Labels/label" by APPEND DELETE EXPUNGE + And it succeeds + Then IMAP client "source" sees the following messages in "INBOX": + | from | to | subject | unread | + | jane.doe@mail.com | name@[domain] | bar | true | + And IMAP client "target" sees the following messages in "Labels/label": + | from | to | subject | unread | + | john.doe@mail.com | [user:user]@[domain] | foo | false | + + Scenario: Move message from label to folder + When IMAP client "1" moves the message with subject "baz" from "Labels/label2" to "Folders/mbox" + And it succeeds + And IMAP client "1" sees the following messages in "Folders/mbox": + | from | to | subject | unread | + | john.doe@mail.com | [user:user]@[domain] | baz | false | + And IMAP client "1" sees 0 messages in "Labels/label2" + + Scenario: Move message from label to label + When IMAP client "1" moves the message with subject "baz" from "Labels/label2" to "Labels/label" + And it succeeds + And IMAP client "1" sees the following messages in "Labels/label": + | from | to | subject | unread | + | john.doe@mail.com | [user:user]@[domain] | baz | false | + And IMAP client "1" sees 0 messages in "Labels/label2" + + Scenario: Move message from All Mail is not possible + When IMAP client "1" moves the message with subject "baz" from "All Mail" to "Folders/folder" + Then it fails + And IMAP client "1" sees the following messages in "All Mail": + | from | to | subject | unread | + | john.doe@mail.com | [user:user]@[domain] | foo | false | + | jane.doe@mail.com | name@[domain] | bar | true | + | john.doe@mail.com | [user:user]@[domain] | baz | false | + | john.doe@mail.com | [user:user]@[domain] | bax | false | + + Scenario: Move message from Inbox to Sent is not possible + Given test skips reporter checks + When IMAP client "1" moves the message with subject "bar" from "Inbox" to "Sent" + Then it fails + + Scenario: Move message from Sent to Inbox is not possible + Given test skips reporter checks + When IMAP client "1" moves the message with subject "bax" from "Sent" to "Inbox" + Then it fails \ No newline at end of file diff --git a/tests/features/imap/message/move_without_support.feature b/tests/features/imap/message/move_without_support.feature index a19ae566..bdf583b7 100644 --- a/tests/features/imap/message/move_without_support.feature +++ b/tests/features/imap/message/move_without_support.feature @@ -33,13 +33,13 @@ Feature: IMAP move messages by append and delete (without MOVE support, e.g., Ou Then it succeeds And IMAP client "source" selects "" And IMAP client "target" selects "" - When IMAP clients "source" and "target" move message seq "2" of "[user:user]" to "" by + When IMAP clients "source" and "target" move message with subject "subj2" of "[user:user]" to "" by And IMAP client "source" sees 1 messages in "" And IMAP client "source" sees the following messages in "": | from | to | subject | | sndr1@[domain] | rcvr1@[domain] | subj1 | - And IMAP client "target" sees 1 messages in "" - And IMAP client "target" sees the following messages in "": + And IMAP client "target" eventually sees 1 messages in "" + And IMAP client "target" eventually sees the following messages in "": | from | to | subject | | sndr2@[domain] | rcvr2@[domain] | subj2 | Examples: diff --git a/tests/imap_test.go b/tests/imap_test.go index 84870b85..26a427a0 100644 --- a/tests/imap_test.go +++ b/tests/imap_test.go @@ -23,7 +23,6 @@ import ( "io" "os" "path/filepath" - "strconv" "strings" "time" @@ -288,6 +287,31 @@ func (s *scenario) imapClientSeesTheFollowingMessagesInMailbox(clientID, mailbox return matchMessages(haveMessages, wantMessages) } +func (s *scenario) imapClientMovesTheMessageWithSubjectFromTo(clientID, subject, from, to string) error { + _, client := s.t.getIMAPClient(clientID) + + uid, err := clientGetUIDBySubject(client, from, subject) + if err != nil { + return err + } + + if err := clientMove(client, from, to, uid); err != nil { + s.t.pushError(err) + } + + return nil +} + +func (s *scenario) imapClientMovesAllMessagesFromTo(clientID, from, to string) error { + _, client := s.t.getIMAPClient(clientID) + + if err := clientMove(client, from, to); err != nil { + s.t.pushError(err) + } + + return nil +} + func (s *scenario) imapClientEventuallySeesTheFollowingMessagesInMailbox(clientID, mailbox string, table *godog.Table) error { return eventually(func() error { err := s.imapClientSeesTheFollowingMessagesInMailbox(clientID, mailbox, table) @@ -433,16 +457,11 @@ func (s *scenario) imapClientAppendsToMailbox(clientID string, file, mailbox str return nil } -func (s *scenario) imapClientsMoveMessageSeqOfUserFromToByOrderedOperations(sourceIMAPClient, targetIMAPClient, messageSeq, bddUserID, targetMailboxName, op1, op2, op3 string) error { +func (s *scenario) imapClientsMoveMessageWithSubjectUserFromToByOrderedOperations(sourceIMAPClient, targetIMAPClient, messageSubject, bddUserID, targetMailboxName, op1, op2, op3 string) error { // call NOOP to prevent unilateral updates in following FETCH _, sourceClient := s.t.getIMAPClient(sourceIMAPClient) _, targetClient := s.t.getIMAPClient(targetIMAPClient) - sequenceID, err := strconv.Atoi(messageSeq) - if err != nil { - return err - } - if err := sourceClient.Noop(); err != nil { return err } @@ -451,8 +470,13 @@ func (s *scenario) imapClientsMoveMessageSeqOfUserFromToByOrderedOperations(sour return err } + uid, err := clientGetUIDBySubject(sourceClient, sourceClient.Mailbox().Name, messageSubject) + if err != nil { + return err + } + // get the original message - messages, err := clientFetchSequence(sourceClient, messageSeq) + messages, err := clientFetchSequence(sourceClient, fmt.Sprintf("%v", uid), true) if err != nil { return err } @@ -486,7 +510,7 @@ func (s *scenario) imapClientsMoveMessageSeqOfUserFromToByOrderedOperations(sour targetErr = targetClient.Append(targetMailboxName, flags, time.Now(), bytes.NewReader(literal)) case "DELETE": - if _, err := clientStore(sourceClient, sequenceID, sequenceID, false, imap.FormatFlagsOp(imap.AddFlags, true), imap.DeletedFlag); err != nil { + if _, err := clientStore(sourceClient, int(uid), int(uid), true, imap.FormatFlagsOp(imap.AddFlags, true), imap.DeletedFlag); err != nil { storeErr = err } case "EXPUNGE": @@ -606,7 +630,7 @@ func clientFetch(client *client.Client, mailbox string) ([]*imap.Message, error) return iterator.Collect(iterator.Chan(resCh)), nil } -func clientFetchSequence(client *client.Client, sequenceSet string) ([]*imap.Message, error) { +func clientFetchSequence(client *client.Client, sequenceSet string, isUID bool) ([]*imap.Message, error) { seqSet, err := imap.ParseSeqSet(sequenceSet) if err != nil { return nil, err @@ -615,12 +639,22 @@ func clientFetchSequence(client *client.Client, sequenceSet string) ([]*imap.Mes resCh := make(chan *imap.Message) go func() { - if err := client.Fetch( - seqSet, - []imap.FetchItem{imap.FetchFlags, imap.FetchEnvelope, imap.FetchUid, "BODY.PEEK[]"}, - resCh, - ); err != nil { - panic(err) + if isUID { + if err := client.UidFetch( + seqSet, + []imap.FetchItem{imap.FetchFlags, imap.FetchEnvelope, imap.FetchUid, "BODY.PEEK[]"}, + resCh, + ); err != nil { + panic(err) + } + } else { + if err := client.Fetch( + seqSet, + []imap.FetchItem{imap.FetchFlags, imap.FetchEnvelope, imap.FetchUid, "BODY.PEEK[]"}, + resCh, + ); err != nil { + panic(err) + } } }() @@ -652,6 +686,31 @@ func clientCopy(client *client.Client, from, to string, uid ...uint32) error { return client.UidCopy(seqset, to) } +func clientMove(client *client.Client, from, to string, uid ...uint32) error { + status, err := client.Select(from, false) + if err != nil { + return err + } + + if status.Messages == 0 { + return fmt.Errorf("expected %v to have messages, but it doesn't", from) + } + + var seqset *imap.SeqSet + + if len(uid) == 0 { + seqset = &imap.SeqSet{Set: []imap.Seq{{Start: 1, Stop: status.Messages}}} + } else { + seqset = &imap.SeqSet{} + + for _, uid := range uid { + seqset.AddNum(uid) + } + } + + return client.UidMove(seqset, to) +} + func clientStore(client *client.Client, from, to int, isUID bool, item imap.StoreItem, flags ...string) ([]*imap.Message, error) { //nolint:unparam resCh := make(chan *imap.Message)