From e572d01341719badc124d3c6fb9ed7f0c20a0d53 Mon Sep 17 00:00:00 2001 From: Mechiel Lukkien Date: Wed, 5 Mar 2025 22:28:25 +0100 Subject: [PATCH] Don't allow mailboxes named "." or ".." and normalize names during imports too It only serves to confuse. When exporting such mailboxes in zip files or tar files, extracting will cause trouble. --- imapserver/create_test.go | 12 +++++++++++- import.go | 7 +++++++ store/account.go | 21 +++++++++++++++------ webaccount/import.go | 6 +++--- 4 files changed, 36 insertions(+), 10 deletions(-) diff --git a/imapserver/create_test.go b/imapserver/create_test.go index eab1ef7..7337230 100644 --- a/imapserver/create_test.go +++ b/imapserver/create_test.go @@ -19,12 +19,22 @@ func TestCreate(t *testing.T) { tc.transactf("no", "create inbox") // Already exists and not allowed. ../rfc/9051:1913 tc.transactf("no", "create Inbox") // Idem. + // Don't allow names that can cause trouble when exporting to directories. + tc.transactf("no", "create .") + tc.transactf("no", "create ..") + tc.transactf("no", "create legit/..") + tc.transactf("ok", "create ...") // No special meaning. + // ../rfc/9051:1937 tc.transactf("ok", "create inbox/a/c") tc.xuntagged(imapclient.UntaggedList{Flags: []string{`\Subscribed`}, Separator: '/', Mailbox: "Inbox/a"}, imapclient.UntaggedList{Flags: []string{`\Subscribed`}, Separator: '/', Mailbox: "Inbox/a/c"}) tc2.transactf("ok", "noop") - tc2.xuntagged(imapclient.UntaggedList{Flags: []string{`\Subscribed`}, Separator: '/', Mailbox: "Inbox/a"}, imapclient.UntaggedList{Flags: []string{`\Subscribed`}, Separator: '/', Mailbox: "Inbox/a/c"}) + tc2.xuntagged( + imapclient.UntaggedList{Flags: []string{`\Subscribed`}, Separator: '/', Mailbox: "..."}, + imapclient.UntaggedList{Flags: []string{`\Subscribed`}, Separator: '/', Mailbox: "Inbox/a"}, + imapclient.UntaggedList{Flags: []string{`\Subscribed`}, Separator: '/', Mailbox: "Inbox/a/c"}, + ) tc.transactf("no", "create inbox/a/c") // Exists. diff --git a/import.go b/import.go index 1b650a4..d767ab9 100644 --- a/import.go +++ b/import.go @@ -16,6 +16,8 @@ import ( "strings" "time" + "golang.org/x/text/unicode/norm" + "github.com/mjl-/mox/config" "github.com/mjl-/mox/message" "github.com/mjl-/mox/metrics" @@ -189,6 +191,11 @@ func importctl(ctx context.Context, ctl *ctl, mbox bool) { var mdnewf, mdcurf *os.File var msgreader store.MsgSource + // Ensure normalized form. + mailbox = norm.NFC.String(mailbox) + mailbox, _, err = store.CheckMailboxName(mailbox, true) + ctl.xcheck(err, "checking mailbox name") + // Open account, creating a database file if it doesn't exist yet. It must be known // in the configuration file. a, err := store.OpenAccount(ctl.log, account, false) diff --git a/store/account.go b/store/account.go index 7b4f3a7..0ad62d4 100644 --- a/store/account.go +++ b/store/account.go @@ -2355,6 +2355,8 @@ func (a *Account) Subjectpass(email string) (key string, err error) { // // Modseq is used, and initialized if 0, for created mailboxes. // +// Name must be in normalized form, see CheckMailboxName. +// // Caller must hold account wlock. // Caller must propagate changes if any. func (a *Account) MailboxEnsure(tx *bstore.Tx, name string, subscribe bool, specialUse SpecialUse, modseq *ModSeq) (mb Mailbox, changes []Change, rerr error) { @@ -3341,7 +3343,7 @@ func MailboxID(tx *bstore.Tx, id int64) (Mailbox, error) { // The mailbox is created with special-use flags, with those flags taken away from // other mailboxes if they have them, reflected in the returned changes. // -// Name must be in normalized form. +// Name must be in normalized form, see CheckMailboxName. func (a *Account) MailboxCreate(tx *bstore.Tx, name string, specialUse SpecialUse) (nmb Mailbox, changes []Change, created []string, exists bool, rerr error) { elems := strings.Split(name, "/") var p string @@ -3375,7 +3377,7 @@ func (a *Account) MailboxCreate(tx *bstore.Tx, name string, specialUse SpecialUs // MailboxRename renames mailbox mbsrc to dst, including children of mbsrc, and // adds missing parents for dst. // -// Names must be in normalized form and cannot be Inbox. +// Name must be in normalized form, see CheckMailboxName, and cannot be Inbox. func (a *Account) MailboxRename(tx *bstore.Tx, mbsrc *Mailbox, dst string, modseq *ModSeq) (changes []Change, isInbox, alreadyExists bool, rerr error) { if mbsrc.Name == "Inbox" || dst == "Inbox" { return nil, true, false, fmt.Errorf("inbox cannot be renamed") @@ -3576,8 +3578,8 @@ func (a *Account) MailboxDelete(ctx context.Context, log mlog.Log, tx *bstore.Tx // If name is the inbox, and allowInbox is false, this is indicated with the isInbox return parameter. // For that case, and for other invalid names, an error is returned. func CheckMailboxName(name string, allowInbox bool) (normalizedName string, isInbox bool, rerr error) { - first := strings.SplitN(name, "/", 2)[0] - if strings.EqualFold(first, "inbox") { + t := strings.Split(name, "/") + if strings.EqualFold(t[0], "inbox") { if len(name) == len("inbox") && !allowInbox { return "", true, fmt.Errorf("special mailbox name Inbox not allowed") } @@ -3588,8 +3590,15 @@ func CheckMailboxName(name string, allowInbox bool) (normalizedName string, isIn return "", false, errors.New("non-unicode-normalized mailbox names not allowed") } - if name == "" { - return "", false, errors.New("empty mailbox name") + for _, e := range t { + switch e { + case "": + return "", false, errors.New("empty mailbox name") + case ".": + return "", false, errors.New(`"." not allowed`) + case "..": + return "", false, errors.New(`".." not allowed`) + } } if strings.HasPrefix(name, "/") || strings.HasSuffix(name, "/") || strings.Contains(name, "//") { return "", false, errors.New("bad slashes in mailbox name") diff --git a/webaccount/import.go b/webaccount/import.go index 52fbb4e..e2cfd8b 100644 --- a/webaccount/import.go +++ b/webaccount/import.go @@ -433,10 +433,10 @@ func importMessages(ctx context.Context, log mlog.Log, token string, acc *store. } xensureMailbox := func(name string) *store.Mailbox { + // Ensure name is normalized. name = norm.NFC.String(name) - if strings.ToLower(name) == "inbox" { - name = "Inbox" - } + name, _, err := store.CheckMailboxName(name, true) + ximportcheckf(err, "checking mailbox name") if mb, ok := mailboxNames[name]; ok { return mb