]> git.ipfire.org Git - thirdparty/dovecot/core.git/commitdiff
lib-storage: mailbox_name_hdr_encode/decode - Distinguish INBOX from <ns prefix>...
authorTimo Sirainen <timo.sirainen@open-xchange.com>
Thu, 30 Apr 2026 14:41:33 +0000 (14:41 +0000)
committerTimo Sirainen <timo.sirainen@open-xchange.com>
Thu, 7 May 2026 20:22:22 +0000 (20:22 +0000)
The box-name index header stored the storage name with per-part unescape,
which collapses <escape>49NBOX to "INBOX". On rebuild from box-name headers,
this made <ns prefix>/INBOX indistinguishable from the regular INBOX, so
list index rebuild restored <ns prefix>/INBOX as INBOX.

Append a backwards compatible flag byte to the box-name header after two
trailing NUL bytes, with MAILBOX_NAME_HDR_FLAG_INBOX_INBOX bit signaling
that the on-disk "INBOX" name actually refers to <ns prefix>/INBOX. The
flag byte is only emitted when at least one bit is set, so old headers
(no flag byte) round-trip unchanged. The decoder strips trailing NUL
padding added by lib-index, then detects the flag suffix unambiguously:
two consecutive NULs at the end never occur in the old format because
the encoder splits on the hierarchy separator and real storage names
contain no empty hierarchy parts.

Old Dovecot versions reading a new-format header will parse the flag
byte as a one-byte trailing hierarchy component. This is acceptable
because old Dovecot never wrote <ns prefix>/INBOX in the first place.

src/lib-storage/list/mail-storage-list-index-rebuild.c
src/lib-storage/list/mailbox-list-index-backend.c
src/lib-storage/list/mailbox-list-index.c
src/lib-storage/list/mailbox-list-index.h

index 78fd0c07847f90dffc80ccb052bed381dc68b240..fa73cf171d9f0d03038f65c9003756bd9e6877ce 100644 (file)
@@ -99,7 +99,7 @@ mail_storage_list_index_rebuild_unlock_lists(struct mail_storage_list_index_rebu
 
 static bool try_get_mailbox_name(struct mail_storage_list_index_rebuild_ctx *ctx,
                                 struct mailbox_list *list, const char *path,
-                                const char **name_r)
+                                const char **name_r, uint8_t *flags_r)
 {
        struct mail_index *index =
                mail_index_alloc(ctx->storage->event, path, MAIL_INDEX_PREFIX);
@@ -107,6 +107,7 @@ static bool try_get_mailbox_name(struct mail_storage_list_index_rebuild_ctx *ctx
        uint32_t box_name_hdr_ext_id;
        bool ret = FALSE;
        int rc;
+       *flags_r = 0;
        if ((rc = mail_index_open(index, MAIL_INDEX_OPEN_FLAG_READONLY)) > 0) {
                if (mail_index_ext_lookup(index, "box-name", &box_name_hdr_ext_id)) {
                        view = mail_index_view_open(index);
@@ -115,7 +116,8 @@ static bool try_get_mailbox_name(struct mail_storage_list_index_rebuild_ctx *ctx
                        mail_index_get_header_ext(view, box_name_hdr_ext_id,
                                                  &name_hdr, &name_hdr_size);
                        *name_r = mailbox_name_hdr_decode_storage_name(list,
-                                                       name_hdr, name_hdr_size);
+                                                       name_hdr, name_hdr_size,
+                                                       flags_r);
                        ret = TRUE;
                        mail_index_view_close(&view);
                } else {
@@ -134,7 +136,8 @@ static bool try_get_mailbox_name(struct mail_storage_list_index_rebuild_ctx *ctx
 }
 
 static const char *get_box_name(struct mail_storage_list_index_rebuild_ctx *ctx,
-                               struct mail_storage_list_index_rebuild_mailbox *box)
+                               struct mail_storage_list_index_rebuild_mailbox *box,
+                               uint8_t *flags_r)
 {
        const char *path =
                t_strdup_printf("%s/%s",
@@ -142,7 +145,8 @@ static const char *get_box_name(struct mail_storage_list_index_rebuild_ctx *ctx,
                                guid_128_to_string(box->guid));
        const char *box_name;
 
-       if (try_get_mailbox_name(ctx, box->list, path, &box_name)) {
+       *flags_r = 0;
+       if (try_get_mailbox_name(ctx, box->list, path, &box_name, flags_r)) {
                e_debug(ctx->storage->event, "Found '%s' from storage %s",
                        box_name, path);
        } else {
@@ -455,13 +459,18 @@ static int mail_storage_list_index_add_missing(struct mail_storage_list_index_re
                   fallback to trying to find the box-name header from the
                   mailbox's index. */
                const char *name = box->storage_name;
+               uint8_t name_hdr_flags = 0;
                if (name == NULL)
-                       name = get_box_name(ctx, box);
+                       name = get_box_name(ctx, box, &name_hdr_flags);
 
-               /* Differentiate between INBOX and <ns prefix>/INBOX */
+               /* Differentiate between INBOX and <ns prefix>/INBOX. The flag
+                  bit lets us recover <ns prefix>/INBOX from a box-name header
+                  where the on-disk name is just "INBOX". */
                const char *orig_vname;
-               if ((box->list->ns->flags & NAMESPACE_FLAG_INBOX_USER) != 0 &&
-                   strcmp(name, "INBOX") == 0)
+               if ((name_hdr_flags & MAILBOX_NAME_HDR_FLAG_INBOX_INBOX) != 0)
+                       orig_vname = t_strconcat(box->list->ns->prefix, "INBOX", NULL);
+               else if ((box->list->ns->flags & NAMESPACE_FLAG_INBOX_USER) != 0 &&
+                        strcmp(name, "INBOX") == 0)
                        orig_vname = "INBOX";
                else
                        orig_vname = t_strconcat(box->list->ns->prefix, name, NULL);
index 0d1201cd6dee8be8314518282224913656389873..8aba465aa976d8b53d10f12c2bcb91dd96a680fd 100644 (file)
@@ -592,7 +592,7 @@ static int index_list_mailbox_open(struct mailbox *box)
                /* Mailbox name is corrupted. Rename it to the previous name. */
                const char *newname =
                        mailbox_name_hdr_decode_storage_name(
-                               box->list, name_hdr, name_hdr_size);
+                               box->list, name_hdr, name_hdr_size, NULL);
                index_list_rename_corrupted(box, newname);
        }
        return 0;
index 1bfc1a7b8dd00a6a9c7d00072d63a25a1da0c41b..2fe6106c22d303e410f7685e2f9ff4b81cea094a 100644 (file)
@@ -580,10 +580,20 @@ const unsigned char *
 mailbox_name_hdr_encode(struct mailbox_list *list, const char *storage_name,
                        size_t *name_len_r)
 {
+       struct mailbox_list_index *ilist = INDEX_LIST_CONTEXT(list);
        const char sep[] = {
                mailbox_list_get_hierarchy_sep(list),
                '\0'
        };
+       uint8_t flags = 0;
+
+       /* The on-disk name loses the <escape>49NBOX -> "INBOX" distinction
+          after per-part unescape below, so detect <ns prefix>/INBOX from the
+          storage_name before splitting and remember it as a flag bit. */
+       if (ilist != NULL && ilist->inbox_inbox_storage_name != NULL &&
+           strcmp(storage_name, ilist->inbox_inbox_storage_name) == 0)
+               flags |= MAILBOX_NAME_HDR_FLAG_INBOX_INBOX;
+
        /* NOTE: The stored mailbox name may be UTF-8 or mUTF-7 depending on
           mailbox_list_utf8 setting. Ideally it would be UTF-8 always.
           However, the name is stored unescaped. */
@@ -603,6 +613,21 @@ mailbox_name_hdr_encode(struct mailbox_list *list, const char *storage_name,
                str_append_c(str, '\0');
                str_append(str, name_parts[i]);
        }
+       if (flags != 0) {
+               /* Backwards compatible extension: append two NUL bytes after
+                  the last name part and one byte of flags. The double NUL
+                  distinguishes the new format unambiguously - old format
+                  never produces two consecutive trailing NULs, since real
+                  mailbox storage names contain no empty hierarchy parts. Old
+                  Dovecot versions don't know about the flag byte and will
+                  parse it as an extra trailing single-byte hierarchy
+                  component, so the decoded name on old code paths becomes
+                  slightly wrong. This is acceptable because old code never
+                  wrote <ns prefix>/INBOX in the first place. */
+               str_append_c(str, '\0');
+               str_append_c(str, '\0');
+               str_append_c(str, flags);
+       }
        *name_len_r = str_len(str);
        return str_data(str);
 }
@@ -610,10 +635,34 @@ mailbox_name_hdr_encode(struct mailbox_list *list, const char *storage_name,
 const char *
 mailbox_name_hdr_decode_storage_name(struct mailbox_list *list,
                                     const unsigned char *name_hdr,
-                                    size_t name_hdr_size)
+                                    size_t name_hdr_size,
+                                    uint8_t *flags_r)
 {
        ARRAY_TYPE(const_string) raw_parts;
        const char *raw_part;
+       uint8_t flags = 0;
+
+       /* lib-index may grow the header with trailing zero padding, so strip
+          any trailing NULs first. The flag byte (if present) is non-zero by
+          construction, so it survives the strip. */
+       while (name_hdr_size > 0 && name_hdr[name_hdr_size-1] == '\0')
+               name_hdr_size--;
+
+       /* New-format header ends with "<name parts>\0\0<nonzero flag byte>".
+          Old-format headers never produce two consecutive trailing NULs
+          because the encoder splits storage names on the hierarchy
+          separator and real storage names contain no empty parts. So
+          data[size-3]=='\0' && data[size-2]=='\0' unambiguously marks the
+          new format - treat the trailing byte as flags even if some bits
+          are unknown to this Dovecot version, so unknown future flag bits
+          don't get misparsed as a trailing hierarchy component. */
+       if (name_hdr_size >= 3 && name_hdr[name_hdr_size-3] == '\0' &&
+           name_hdr[name_hdr_size-2] == '\0') {
+               flags = name_hdr[name_hdr_size-1];
+               name_hdr_size -= 3;
+       }
+       if (flags_r != NULL)
+               *flags_r = flags;
 
        /* NOTE: The stored mailbox name may be UTF-8 or mUTF-7 depending on
           mailbox_list_utf8 setting. Ideally it would be UTF-8 always.
index 5c76c38365484d93e1c9c01eb58c1811dbc254ae..e6cb6373cb99ce6278bccb9e4fa2caf330088395 100644 (file)
@@ -195,15 +195,28 @@ void mailbox_list_get_escaped_mailbox_name(struct mailbox_list *list,
                                           const struct mailbox_list_index_node *node,
                                           string_t *escaped_name);
 
+/* Flags appended to box-name header after the mailbox name's trailing NUL.
+   Old Dovecot versions wrote no trailing NUL and no flag byte. The flag byte
+   is only present when at least one bit is set, so a 0 byte never encodes
+   "no flags" - that case is encoded by omitting the trailing NUL+flag. */
+enum mailbox_name_hdr_flags {
+       /* The mailbox is <ns prefix>/INBOX (encoded in the header as just
+          "INBOX" so that old Dovecot versions still see a sensible name). */
+       MAILBOX_NAME_HDR_FLAG_INBOX_INBOX = 0x01,
+};
+
 /* Return mailbox name encoded into box-name header. */
 const unsigned char *
 mailbox_name_hdr_encode(struct mailbox_list *list, const char *storage_name,
                        size_t *name_len_r);
-/* Return mailbox name decoded from box-name header. */
+/* Return mailbox name decoded from box-name header. If flags_r is non-NULL,
+   it is set to the decoded flag bits (0 if the header is in the old format
+   without a flag byte). */
 const char *
 mailbox_name_hdr_decode_storage_name(struct mailbox_list *list,
                                     const unsigned char *name_hdr,
-                                    size_t name_hdr_size);
+                                    size_t name_hdr_size,
+                                    uint8_t *flags_r);
 
 int mailbox_list_index_index_open(struct mailbox_list *list);
 bool mailbox_list_index_need_refresh(struct mailbox_list_index *ilist,