mirror of
https://github.com/ProtonMail/proton-bridge.git
synced 2025-12-16 15:16:44 +00:00
Other: Implement subfolder support
This commit is contained in:
@ -31,6 +31,8 @@ import (
|
|||||||
"github.com/ProtonMail/proton-bridge/v2/internal/vault"
|
"github.com/ProtonMail/proton-bridge/v2/internal/vault"
|
||||||
"github.com/ProtonMail/proton-bridge/v2/pkg/message"
|
"github.com/ProtonMail/proton-bridge/v2/pkg/message"
|
||||||
"github.com/bradenaw/juniper/stream"
|
"github.com/bradenaw/juniper/stream"
|
||||||
|
"github.com/bradenaw/juniper/xslices"
|
||||||
|
"github.com/google/go-cmp/cmp"
|
||||||
"gitlab.protontech.ch/go/liteapi"
|
"gitlab.protontech.ch/go/liteapi"
|
||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
@ -82,7 +84,7 @@ func (conn *imapConnector) Authorize(username string, password []byte) bool {
|
|||||||
|
|
||||||
// GetMailbox returns information about the mailbox with the given ID.
|
// GetMailbox returns information about the mailbox with the given ID.
|
||||||
func (conn *imapConnector) GetMailbox(ctx context.Context, mailboxID imap.MailboxID) (imap.Mailbox, error) {
|
func (conn *imapConnector) GetMailbox(ctx context.Context, mailboxID imap.MailboxID) (imap.Mailbox, error) {
|
||||||
label, err := conn.client.GetLabel(ctx, string(mailboxID), liteapi.LabelTypeLabel, liteapi.LabelTypeFolder)
|
label, err := conn.client.GetLabel(ctx, string(mailboxID), liteapi.LabelTypeLabel, liteapi.LabelTypeFolder, liteapi.LabelTypeSystem)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return imap.Mailbox{}, err
|
return imap.Mailbox{}, err
|
||||||
}
|
}
|
||||||
@ -92,22 +94,64 @@ func (conn *imapConnector) GetMailbox(ctx context.Context, mailboxID imap.Mailbo
|
|||||||
|
|
||||||
// CreateMailbox creates a label with the given name.
|
// CreateMailbox creates a label with the given name.
|
||||||
func (conn *imapConnector) CreateMailbox(ctx context.Context, name []string) (imap.Mailbox, error) {
|
func (conn *imapConnector) CreateMailbox(ctx context.Context, name []string) (imap.Mailbox, error) {
|
||||||
if len(name) != 2 {
|
if len(name) < 2 {
|
||||||
panic("subfolders are unsupported")
|
return imap.Mailbox{}, fmt.Errorf("invalid mailbox name %q", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
var labelType liteapi.LabelType
|
switch name[0] {
|
||||||
|
case folderPrefix:
|
||||||
|
return conn.createFolder(ctx, name[1:])
|
||||||
|
|
||||||
if name[0] == folderPrefix {
|
case labelPrefix:
|
||||||
labelType = liteapi.LabelTypeFolder
|
return conn.createLabel(ctx, name[1:])
|
||||||
} else {
|
|
||||||
labelType = liteapi.LabelTypeLabel
|
default:
|
||||||
|
return imap.Mailbox{}, fmt.Errorf("invalid mailbox name %q", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (conn *imapConnector) createLabel(ctx context.Context, name []string) (imap.Mailbox, error) {
|
||||||
|
if len(name) != 1 {
|
||||||
|
return imap.Mailbox{}, fmt.Errorf("a label cannot have children")
|
||||||
}
|
}
|
||||||
|
|
||||||
label, err := conn.client.CreateLabel(ctx, liteapi.CreateLabelReq{
|
label, err := conn.client.CreateLabel(ctx, liteapi.CreateLabelReq{
|
||||||
Name: name[1:][0],
|
Name: name[0],
|
||||||
Color: "#f66",
|
Color: "#f66",
|
||||||
Type: labelType,
|
Type: liteapi.LabelTypeLabel,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return imap.Mailbox{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return toIMAPMailbox(label, conn.flags, conn.permFlags, conn.attrs), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (conn *imapConnector) createFolder(ctx context.Context, name []string) (imap.Mailbox, error) {
|
||||||
|
var parentID string
|
||||||
|
|
||||||
|
if len(name) > 1 {
|
||||||
|
folders, err := conn.client.GetLabels(ctx, liteapi.LabelTypeFolder)
|
||||||
|
if err != nil {
|
||||||
|
return imap.Mailbox{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
idx := xslices.IndexFunc(folders, func(folder liteapi.Label) bool {
|
||||||
|
return cmp.Equal(folder.Path, name[:len(name)-1])
|
||||||
|
})
|
||||||
|
|
||||||
|
if idx < 0 {
|
||||||
|
return imap.Mailbox{}, fmt.Errorf("parent folder %q does not exist", name[:len(name)-1])
|
||||||
|
}
|
||||||
|
|
||||||
|
parentID = folders[idx].ID
|
||||||
|
}
|
||||||
|
|
||||||
|
label, err := conn.client.CreateLabel(ctx, liteapi.CreateLabelReq{
|
||||||
|
Name: name[len(name)-1],
|
||||||
|
Color: "#f66",
|
||||||
|
Type: liteapi.LabelTypeFolder,
|
||||||
|
ParentID: parentID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return imap.Mailbox{}, err
|
return imap.Mailbox{}, err
|
||||||
@ -117,36 +161,35 @@ func (conn *imapConnector) CreateMailbox(ctx context.Context, name []string) (im
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UpdateMailboxName sets the name of the label with the given ID.
|
// UpdateMailboxName sets the name of the label with the given ID.
|
||||||
func (conn *imapConnector) UpdateMailboxName(ctx context.Context, labelID imap.MailboxID, newName []string) error {
|
func (conn *imapConnector) UpdateMailboxName(ctx context.Context, labelID imap.MailboxID, name []string) error {
|
||||||
if len(newName) != 2 {
|
if len(name) < 2 {
|
||||||
panic("subfolders are unsupported")
|
return fmt.Errorf("invalid mailbox name %q", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
label, err := conn.client.GetLabel(ctx, string(labelID), liteapi.LabelTypeLabel, liteapi.LabelTypeFolder)
|
switch name[0] {
|
||||||
|
case folderPrefix:
|
||||||
|
return conn.updateFolder(ctx, labelID, name[1:])
|
||||||
|
|
||||||
|
case labelPrefix:
|
||||||
|
return conn.updateLabel(ctx, labelID, name[1:])
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("invalid mailbox name %q", name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (conn *imapConnector) updateLabel(ctx context.Context, labelID imap.MailboxID, name []string) error {
|
||||||
|
if len(name) != 1 {
|
||||||
|
return fmt.Errorf("a label cannot have children")
|
||||||
|
}
|
||||||
|
|
||||||
|
label, err := conn.client.GetLabel(ctx, string(labelID), liteapi.LabelTypeLabel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
switch label.Type {
|
|
||||||
case liteapi.LabelTypeFolder:
|
|
||||||
if newName[0] != folderPrefix {
|
|
||||||
return fmt.Errorf("cannot rename folder to label")
|
|
||||||
}
|
|
||||||
|
|
||||||
case liteapi.LabelTypeLabel:
|
|
||||||
if newName[0] != labelPrefix {
|
|
||||||
return fmt.Errorf("cannot rename label to folder")
|
|
||||||
}
|
|
||||||
|
|
||||||
case liteapi.LabelTypeSystem:
|
|
||||||
return fmt.Errorf("cannot rename system label %q", label.Name)
|
|
||||||
|
|
||||||
case liteapi.LabelTypeContactGroup:
|
|
||||||
return fmt.Errorf("cannot rename contact group label %q", label.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := conn.client.UpdateLabel(ctx, label.ID, liteapi.UpdateLabelReq{
|
if _, err := conn.client.UpdateLabel(ctx, label.ID, liteapi.UpdateLabelReq{
|
||||||
Name: newName[1:][0],
|
Name: name[0],
|
||||||
Color: label.Color,
|
Color: label.Color,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
@ -155,6 +198,42 @@ func (conn *imapConnector) UpdateMailboxName(ctx context.Context, labelID imap.M
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (conn *imapConnector) updateFolder(ctx context.Context, labelID imap.MailboxID, name []string) error {
|
||||||
|
var parentID string
|
||||||
|
|
||||||
|
if len(name) > 1 {
|
||||||
|
folders, err := conn.client.GetLabels(ctx, liteapi.LabelTypeFolder)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
idx := xslices.IndexFunc(folders, func(folder liteapi.Label) bool {
|
||||||
|
return cmp.Equal(folder.Path, name[:len(name)-1])
|
||||||
|
})
|
||||||
|
|
||||||
|
if idx < 0 {
|
||||||
|
return fmt.Errorf("parent folder %q does not exist", name[:len(name)-1])
|
||||||
|
}
|
||||||
|
|
||||||
|
parentID = folders[idx].ID
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// DeleteMailbox deletes the label with the given ID.
|
// DeleteMailbox deletes the label with the given ID.
|
||||||
func (conn *imapConnector) DeleteMailbox(ctx context.Context, labelID imap.MailboxID) error {
|
func (conn *imapConnector) DeleteMailbox(ctx context.Context, labelID imap.MailboxID) error {
|
||||||
return conn.client.DeleteLabel(ctx, string(labelID))
|
return conn.client.DeleteLabel(ctx, string(labelID))
|
||||||
@ -171,8 +250,6 @@ func (conn *imapConnector) GetMessage(ctx context.Context, messageID imap.Messag
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CreateMessage creates a new message on the remote.
|
// CreateMessage creates a new message on the remote.
|
||||||
//
|
|
||||||
// nolint:funlen
|
|
||||||
func (conn *imapConnector) CreateMessage(
|
func (conn *imapConnector) CreateMessage(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
mailboxID imap.MailboxID,
|
mailboxID imap.MailboxID,
|
||||||
@ -362,17 +439,15 @@ func toIMAPMessage(message liteapi.MessageMetadata) imap.Message {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func toIMAPMailbox(label liteapi.Label, flags, permFlags, attrs imap.FlagSet) imap.Mailbox {
|
func toIMAPMailbox(label liteapi.Label, flags, permFlags, attrs imap.FlagSet) imap.Mailbox {
|
||||||
var name []string
|
|
||||||
|
|
||||||
if label.Type == liteapi.LabelTypeLabel {
|
if label.Type == liteapi.LabelTypeLabel {
|
||||||
name = append(name, labelPrefix)
|
label.Path = append([]string{labelPrefix}, label.Path...)
|
||||||
} else if label.Type == liteapi.LabelTypeFolder {
|
} else if label.Type == liteapi.LabelTypeFolder {
|
||||||
name = append(name, folderPrefix)
|
label.Path = append([]string{folderPrefix}, label.Path...)
|
||||||
}
|
}
|
||||||
|
|
||||||
return imap.Mailbox{
|
return imap.Mailbox{
|
||||||
ID: imap.MailboxID(label.ID),
|
ID: imap.MailboxID(label.ID),
|
||||||
Name: append(name, label.Name),
|
Name: label.Path,
|
||||||
Flags: flags,
|
Flags: flags,
|
||||||
PermanentFlags: permFlags,
|
PermanentFlags: permFlags,
|
||||||
Attributes: attrs,
|
Attributes: attrs,
|
||||||
|
|||||||
@ -51,3 +51,135 @@ Feature: IMAP create mailbox
|
|||||||
| Labels/l1 |
|
| Labels/l1 |
|
||||||
| Labels/l2 |
|
| Labels/l2 |
|
||||||
| Labels/l3 |
|
| Labels/l3 |
|
||||||
|
|
||||||
|
Scenario: Creating subfolders is possible and they persist after resync
|
||||||
|
When IMAP client "1" creates "Folders/f1/f11"
|
||||||
|
Then it succeeds
|
||||||
|
When IMAP client "1" creates "Folders/f1/f12"
|
||||||
|
Then it succeeds
|
||||||
|
When IMAP client "1" creates "Folders/f2/f21"
|
||||||
|
Then it succeeds
|
||||||
|
When IMAP client "1" creates "Folders/f2/f22"
|
||||||
|
Then it succeeds
|
||||||
|
Then IMAP client "1" sees the following mailbox info:
|
||||||
|
| name |
|
||||||
|
| INBOX |
|
||||||
|
| Drafts |
|
||||||
|
| Sent |
|
||||||
|
| Starred |
|
||||||
|
| Archive |
|
||||||
|
| Spam |
|
||||||
|
| Trash |
|
||||||
|
| All Mail |
|
||||||
|
| Folders |
|
||||||
|
| Folders/f1 |
|
||||||
|
| Folders/f1/f11 |
|
||||||
|
| Folders/f1/f12 |
|
||||||
|
| Folders/f2 |
|
||||||
|
| Folders/f2/f21 |
|
||||||
|
| Folders/f2/f22 |
|
||||||
|
| Labels |
|
||||||
|
| Labels/l1 |
|
||||||
|
| Labels/l2 |
|
||||||
|
When user "user@pm.me" is deleted
|
||||||
|
And the user logs in with username "user@pm.me" and password "password"
|
||||||
|
And user "user@pm.me" finishes syncing
|
||||||
|
And user "user@pm.me" connects and authenticates IMAP client "2"
|
||||||
|
Then IMAP client "2" sees the following mailbox info:
|
||||||
|
| name |
|
||||||
|
| INBOX |
|
||||||
|
| Drafts |
|
||||||
|
| Sent |
|
||||||
|
| Starred |
|
||||||
|
| Archive |
|
||||||
|
| Spam |
|
||||||
|
| Trash |
|
||||||
|
| All Mail |
|
||||||
|
| Folders |
|
||||||
|
| Folders/f1 |
|
||||||
|
| Folders/f1/f11 |
|
||||||
|
| Folders/f1/f12 |
|
||||||
|
| Folders/f2 |
|
||||||
|
| Folders/f2/f21 |
|
||||||
|
| Folders/f2/f22 |
|
||||||
|
| Labels |
|
||||||
|
| Labels/l1 |
|
||||||
|
| Labels/l2 |
|
||||||
|
|
||||||
|
Scenario: Changing folder parent is possible and it persists after resync
|
||||||
|
When IMAP client "1" creates "Folders/f1/f11"
|
||||||
|
Then it succeeds
|
||||||
|
When IMAP client "1" creates "Folders/f1/f12"
|
||||||
|
Then it succeeds
|
||||||
|
When IMAP client "1" creates "Folders/f2/f21"
|
||||||
|
Then it succeeds
|
||||||
|
When IMAP client "1" creates "Folders/f2/f22"
|
||||||
|
Then it succeeds
|
||||||
|
Then IMAP client "1" sees the following mailbox info:
|
||||||
|
| name |
|
||||||
|
| INBOX |
|
||||||
|
| Drafts |
|
||||||
|
| Sent |
|
||||||
|
| Starred |
|
||||||
|
| Archive |
|
||||||
|
| Spam |
|
||||||
|
| Trash |
|
||||||
|
| All Mail |
|
||||||
|
| Folders |
|
||||||
|
| Folders/f1 |
|
||||||
|
| Folders/f1/f11 |
|
||||||
|
| Folders/f1/f12 |
|
||||||
|
| Folders/f2 |
|
||||||
|
| Folders/f2/f21 |
|
||||||
|
| Folders/f2/f22 |
|
||||||
|
| Labels |
|
||||||
|
| Labels/l1 |
|
||||||
|
| Labels/l2 |
|
||||||
|
When IMAP client "1" renames "Folders/f1/f11" to "Folders/f2/f11"
|
||||||
|
Then it succeeds
|
||||||
|
When IMAP client "1" renames "Folders/f1/f12" to "Folders/f2/f12"
|
||||||
|
Then it succeeds
|
||||||
|
Then IMAP client "1" sees the following mailbox info:
|
||||||
|
| name |
|
||||||
|
| INBOX |
|
||||||
|
| Drafts |
|
||||||
|
| Sent |
|
||||||
|
| Starred |
|
||||||
|
| Archive |
|
||||||
|
| Spam |
|
||||||
|
| Trash |
|
||||||
|
| All Mail |
|
||||||
|
| Folders |
|
||||||
|
| Folders/f1 |
|
||||||
|
| Folders/f2 |
|
||||||
|
| Folders/f2/f11 |
|
||||||
|
| Folders/f2/f12 |
|
||||||
|
| Folders/f2/f21 |
|
||||||
|
| Folders/f2/f22 |
|
||||||
|
| Labels |
|
||||||
|
| Labels/l1 |
|
||||||
|
| Labels/l2 |
|
||||||
|
When user "user@pm.me" is deleted
|
||||||
|
And the user logs in with username "user@pm.me" and password "password"
|
||||||
|
And user "user@pm.me" finishes syncing
|
||||||
|
And user "user@pm.me" connects and authenticates IMAP client "2"
|
||||||
|
Then IMAP client "2" sees the following mailbox info:
|
||||||
|
| name |
|
||||||
|
| INBOX |
|
||||||
|
| Drafts |
|
||||||
|
| Sent |
|
||||||
|
| Starred |
|
||||||
|
| Archive |
|
||||||
|
| Spam |
|
||||||
|
| Trash |
|
||||||
|
| All Mail |
|
||||||
|
| Folders |
|
||||||
|
| Folders/f1 |
|
||||||
|
| Folders/f2 |
|
||||||
|
| Folders/f2/f11 |
|
||||||
|
| Folders/f2/f12 |
|
||||||
|
| Folders/f2/f21 |
|
||||||
|
| Folders/f2/f22 |
|
||||||
|
| Labels |
|
||||||
|
| Labels/l1 |
|
||||||
|
| Labels/l2 |
|
||||||
@ -160,7 +160,7 @@ func matchMailboxes(have, want []Mailbox) error {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if !IsSub(want, have) {
|
if !IsSub(want, have) {
|
||||||
return fmt.Errorf("missing messages: %v", want)
|
return fmt.Errorf("missing mailboxes: %v", want)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
Reference in New Issue
Block a user