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