Main index
==========

The main index can be used to quickly look up messages' UIDs, flags, keywords
and extension-specific data, such as cache file or mbox file offsets.

Reading, writing and locking
----------------------------

Reading 'dovecot.index' file requires locking, unfortunately. Shared read
locking is done using the standard index locking method specified in
'lock_method' setting ('lock_method' parameter for 'mail_index_open()').

Writing to index files requires transaction log to be exclusively locked first.
This way the index locking only has to worry about existing read locks. The
locking works by first trying to lock the index with the standard locking
method, but if it couldn't acquire the lock in two seconds, it'll fallback to
copying the index file to a temporary file, and when unlocking it'll 'rename()'
the temporary file over the 'dovecot.index' file. Note that this is safe only
because of the exclusive transaction log lock. This way the writers are never
blocked by readers who are allowed to keep the shared lock as long as they
want.

The copy-locking is used always when doing anything that could corrupt the
index file if it crashed in the middle of an operation. For example if the
header or record size changes, or if messages are expunged. New messages can be
appended however, because the message count in the header is updated last.
Expunging the last messages would probably be safe also (because only the
header needs updating), but it's not done currently.

The index file should never be directly modified. Everything should go through
the transaction log, and the only time the index needs to be write-locked is
when transactions are written to it.

Currently the index file is updated whenever the backend mailbox is
synchronized. This isn't necessary, because an old index file can be updated
using the transaction log. In future there could be some smarter decisions
about when writing to the index isn't worth the extra disk writes.

Header
------

---%<-------------------------------------------------------------------------
struct mail_index_header {
        uint8_t major_version;
        uint8_t minor_version;

        uint16_t base_header_size;
        uint32_t header_size;
        uint32_t record_size;

        uint8_t compat_flags;
        uint8_t unused[3];

        uint32_t indexid;
        uint32_t flags;

        uint32_t uid_validity;
        uint32_t next_uid;

        uint32_t messages_count;
        uint32_t unused_old_recent_messages_count;
        uint32_t seen_messages_count;
        uint32_t deleted_messages_count;

        uint32_t first_recent_uid;
        uint32_t first_unseen_uid_lowwater;
        uint32_t first_deleted_uid_lowwater;

        uint32_t log_file_seq;
        uint32_t log_file_tail_offset;
        uint32_t log_file_head_offset;

        uint64_t unused_old_sync_size;
        uint32_t unused_old_sync_stamp;

        uint32_t day_stamp;
        uint32_t day_first_uid[8];
}
---%<-------------------------------------------------------------------------

Fields that won't change without recreating the index:

major_version:
  If this doesn't match 'MAIL_INDEX_MAJOR_VERSION', don't try to read the
  index. Dovecot recreates the index file then.

minor_version:
  If this doesn't match 'MAIL_INDEX_MINOR_VERSION' there are some backwards
  compatible changes in the index file (typically header fields). Try to
  preserve the headers and the minor version when updating the index file.

base_header_size:
  Extension headers begin after the base headers. This is normally the same as
  'sizeof(struct mail_index_header)'.

header_size:
  Records begin after base and extension headers.

record_size:
  Size of each record and its extensions. Initially the same as 'sizeof(struct
  mail_index_record)'.

compat_flags:
  Currently there is just one compatibility flag:
  'MAIL_INDEX_COMPAT_LITTLE_ENDIAN'. Dovecot doesn't try to bother to read
  different endianess files, they're simply recreated.

indexid:
  Unique index file ID. This is used to make sure that the main index,
  transaction log and cache file are all part of the same index.

Header flags:

MAIL_INDEX_HDR_FLAG_CORRUPTED:
  Set whenever the index file is found to be corrupted. If the reader notices
  this flag, it shouldn't try to continue using the index.

MAIL_INDEX_HDR_FLAG_HAVE_DIRTY:
  This index has records with 'MAIL_INDEX_MAIL_FLAG_DIRTY' flag set.

MAIL_INDEX_HDR_FLAG_FSCK:
  Call 'mail_index_fsck()' as soon as possible. This flag isn't actually set
  anywhere currently.

Message UIDs and counters:

uid_validity:
  IMAP UIDVALIDITY field. Initially can be 0, but after it's set we don't
  currently try to even handle the case of UIDVALIDITY changing. It's done by
  marking the index file corrupted and recreating it. That's a bit ugly, but
  typically the UIDVALIDITY never changes.

next_uid:
  UID given to the next appended message. Only increases.

messages_count:
  Number of records in the index file.

recent_messages_count:
  Number of records with 'MAIL_RECENT' flag set.

seen_messages_count:
  Number of records with 'MAIL_SEEN' flag set.

deleted_messages_count:
  Number of records with 'MAIL_DELETED' flag set.

first_recent_uid_lowwater:
  There are no UIDs lower than this with 'MAIL_RECENT' flag set.

first_unseen_uid_lowwater:
  There are no UIDs lower than this *without* 'MAIL_SEEN' flag set.

first_deleted_uid_lowwater:
  There are no UIDs lower than this with 'MAIL_DELETE' flag set.

The lowwater fields are used to optimize searching messages with/without a
specific flag.

Fields related to syncing:

log_file_seq:
  Log file the log_*_offset fields point to.

log_file_int_offset, log_file_ext_offset:
  All the internal/external transactions before this offset in the log file are
  synced to the index. External transactions are synced more often than
  internal, so 'log_file_int_offset' <= 'log_file_ext_offset'.

sync_size, sync_stamp:
  Used by the mailbox backends to store their synchronization information. Some
  day these should be removed and replaced with extension headers.

Then there are day fields:

day_stamp:
  UNIX timestamp to the beginning of the day when new records were last added
  to the index file.

day_first_uid[8]:
  These fields are updated when 'day_stamp' < today. The [0..6] are first moved
  to [1..7], then [0] is set to the first appended UID. So they contain the
  first UID of the day for last 8 days when messages were appended.

The 'day_first_uid[]' fields are used by cache file compression to decide when
to drop 'MAIL_CACHE_DECISION_TEMP' data.

Extension headers
-----------------

After the base header comes a list of extensions and their headers. The first
extension begins from 'mail_index_header.base_header_size' offset. The second
begins after the first one's 'data[]' and so on. The extensions always begin
64bit aligned however, so you may need to skip a few bytes always. Read the
extensions as long as the offset is smaller than
'mail_index_header.header_size'.

---%<-------------------------------------------------------------------------
struct mail_index_ext_header {
        uint32_t hdr_size; /* size of data[] */
        uint32_t reset_id;
        uint16_t record_offset;
        uint16_t record_size;
        uint16_t record_align;
        uint16_t name_size;
        /* unsigned char name[name_size] */
        /* unsigned char data[hdr_size] (starting 64bit aligned) */
};
---%<-------------------------------------------------------------------------

'reset_id', record offset, size and alignment is explained in
<Design.Indexes.TransactionLog.txt>'s 'struct mail_transaction_ext_intro'.

Records
-------

There are 'hdr.messages_count' records in the file. Each record contains at
least two fields: Record UID and flags. The UID is always increasing for the
records, so it's possible to find a record by its UID with binary search. The
record size is specified by 'mail_index_header.record_size'.

The flags are a combination of 'enum mail_flags' and 'enum
mail_index_mail_flags'. There exists only one index flag currently:
'MAIL_INDEX_MAIL_FLAG_DIRTY'. If a record has this flag set, it means that the
mailbox syncing code should ignore the flag in the mailbox and use the flag in
the index file instead. This is used for example with mbox and
'mbox_lazy_writes=yes'. It also allows having modifiable flags for read-only
mailboxes.

The rest data is stored in record extensions.

Keywords
--------

The keywords are stored in record extensions, but for better performance and
lower disk space usage in transaction logs, they are quite tightly integrated
to the index file code.

The list of keywords is stored in "keywords" extension header:

---%<-------------------------------------------------------------------------
struct mail_index_keyword_header {
        uint32_t keywords_count;
        /* struct mail_index_keyword_header_rec[] */
        /* char name[][] */
};
struct mail_index_keyword_header_rec {
        uint32_t unused; /* for backwards compatibility */
        uint32_t name_offset; /* relative to beginning of name[] */
};
---%<-------------------------------------------------------------------------

The unused field originally contained 'count' field, but while writing this
documentation I noticed it's not actually used anywhere. Apparently it was
added there accidentally. It'll be removed in later versions.

So there exists 'keywords_count' keywords, each listed in a NUL-terminated
string beginning from 'name_offset'.

Since crashing in the middle of updating the keywords list pretty much breaks
the keywords, adding new keywords causes the index file to be always copied to
a temporary file and be replaced.

The keywords in the records are stored in a "keywords" extension bitfield. So
the nth bit in the bitfield points to the nth keyword listed in the header.

It's not currently possible to safely remove existing keywords.

Extensions
----------

The extensions only specify their wanted size and alignment, the index file
syncing code is free to assign any offset inside the record to them. The
extensions may be reordered at any time.

Dovecot's current extension ordering code works pretty well, but it's not
perfect. If the extension size isn't the same as its alignment, it may create
larger records than necessary. This will be fixed later.

The records size is always divisible by the maximum alignment requirement. This
isn't strictly necessary either, so it could be fixed later as well.

(This file was created from the wiki on 2019-06-19 12:42)
