maildir-sync.c revision b37634f5bf23ff8c72b88ef6966fd5c730017419
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen/* Copyright (C) 2002-2003 Timo Sirainen */
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen Here's a description of how we handle Maildir synchronization and
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen it's problems:
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen We want to be as efficient as we can. The most efficient way to
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen check if changes have occured is to stat() the new/ and cur/
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen directories and uidlist file - if their mtimes haven't changed,
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen there's no changes and we don't need to do anything.
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen Problem 1: Multiple changes can happen within a single second -
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen nothing guarantees that once we synced it, someone else didn't just
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen then make a modification. Such modifications wouldn't get noticed
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen until a new modification occured later.
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen Problem 2: Syncing cur/ directory is much more costly than syncing
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen new/. Moving mails from new/ to cur/ will always change mtime of
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen cur/ causing us to sync it as well.
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen Problem 3: We may not be able to move mail from new/ to cur/
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen because we're out of quota, or simply because we're accessing a
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen read-only mailbox.
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen MAILDIR_SYNC_SECS
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen -----------------
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen Several checks below use MAILDIR_SYNC_SECS, which should be maximum
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen clock drift between all computers accessing the maildir (eg. via
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen NFS), rounded up to next second. Our default is 1 second, since
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen everyone should be using NTP.
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen Note that setting it to 0 works only if there's only one computer
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen accessing the maildir. It's practically impossible to make two
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen clocks _exactly_ synchronized.
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen It might be possible to only use file server's clock by looking at
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen the atime field, but I don't know how well that would actually work.
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen cur directory
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen -------------
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen We have maildir_cur_dirty variable which is set to cur/ directory's
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen mtime when it's >= time() - MAILDIR_SYNC_SECS and we _think_ we have
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen synchronized the directory.
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen When maildir_cur_dirty is non-zero, we don't synchronize the cur/
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen directory until
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen a) cur/'s mtime changes
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen b) opening a mail fails with ENOENT
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen c) time() > maildir_cur_dirty + MAILDIR_SYNC_SECS
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen This allows us to modify the maildir multiple times without having
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen to sync it at every change. The sync will eventually be done to
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen make sure we didn't miss any external changes.
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen The maildir_cur_dirty is set when:
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen - we change message flags
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen - we expunge messages
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen - we move mail from new/ to cur/
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen - we sync cur/ directory and it's mtime is
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen >= time() - MAILDIR_SYNC_SECS
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen It's unset when we do the final syncing, ie. when mtime is
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen older than time() - MAILDIR_SYNC_SECS.
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen new directory
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen -------------
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen If new/'s mtime is >= time() - MAILDIR_SYNC_SECS, always synchronize
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen it. maildir_cur_dirty-like feature might save us a few syncs, but
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen that might break a client which saves a mail in one connection and
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen tries to fetch it in another one. new/ directory is almost always
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen empty, so syncing it should be very fast anyway. Actually this can
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen still happen if we sync only new/ dir while another client is also
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen moving mails from it to cur/ - it takes us a while to see them.
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen That's pretty unlikely to happen however, and only way to fix it
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen would be to always synchronize cur/ after new/.
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen Normally we move all mails from new/ to cur/ whenever we sync it. If
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen it's not possible for some reason, we set maildir_have_new flag on
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen which instructs synchronization to check files in new/ directory as
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen well. maildir_keep_new flag is also set which instructs syncing to
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen not even try to move mails to cur/ anymore.
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen If client tries to change a flag for message in new/, we try to
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen rename() it into cur/. If it's successful, we clear the
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen maildir_keep_new flag so at next sync we'll try to move all of them
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen to cur/. When all of them have been moved, maildir_have_new flag is
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen cleared as well. Expunges will also clear maildir_keep_new flag.
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen If rename() still fails because of ENOSPC or EDQUOT, we still save
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen the flag changes in index with dirty-flag on. When moving the mail
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen to cur/ directory, or when we notice it's already moved there, we
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen apply the flag changes to the filename, rename it and remove the
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen dirty flag. If there's dirty flags, this should be tried every time
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen after expunge or when closing the mailbox.
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen This file contains UID <-> filename mappings. It's updated only when
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen new mail arrives, so it may contain filenames that have already been
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen deleted. Updating is done by getting uidlist.lock file, writing the
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen whole uidlist into it and rename()ing it over the old uidlist. This
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen means there's no need to lock the file for reading.
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen Whenever uidlist is rewritten, it's mtime must be larger than the old
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen one's. Use utime() before rename() if needed.
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen Only time you have to read this file is when assigning new UIDs for
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen messages, to see if they already have UIDs. If file's mtime hasn't
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen changed, you don't have to do even that.
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen broken clients
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen --------------
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen Originally the middle identifier in Maildir filename was specified
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen only as <process id>_<delivery counter>. That however created a
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen problem with randomized PIDs which made it possible that the same
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen PID was reused within one second.
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen So if within one second a mail was delivered, MUA moved it to cur/
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen and another mail was delivered by a new process using same PID as
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen the first one, we likely ended up overwriting the first mail when
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen the second mail was moved over it.
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen Nowadays everyone should be giving a bit more specific identifier,
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen for example include microseconds in it which Dovecot does.
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen There's a simple way to prevent this from happening in some cases:
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen Don't move the mail from new/ to cur/ if it's mtime (it's included
7f7be2cbf68f8a202a688d5bc50f82483d461643Timo Sirainen in beginning of file name, so stat() isn't needed) is >= time() -
#include "lib.h"
#include "buffer.h"
#include "istream.h"
#include "hash.h"
#include "ioloop.h"
#include "str.h"
#include "maildir-index.h"
#include "maildir-uidlist.h"
#include "mail-index-util.h"
#include "mail-cache.h"
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <dirent.h>
#include <utime.h>
enum maildir_file_action {
struct maildir_hash_context {
int failed;
struct maildir_hash_rec {
struct maildir_sync_context {
unsigned int new_count;
static unsigned int maildir_hash(const void *p)
return TRUE;
return FALSE;
return TRUE;
const char *path;
return TRUE;
return FALSE;
return TRUE;
return TRUE;
return TRUE;
return FALSE;
return FALSE;
return TRUE;
s1++;
s2++;
int new_dir;
return FALSE;
return TRUE;
return FALSE;
return FALSE;
return FALSE;
return FALSE;
return FALSE;
return FALSE;
switch (action) {
case MAILDIR_FILE_ACTION_NEW:
return FALSE;
MAILDIR_FILE_FLAG_NEWDIR) != 0;
return FALSE;
case MAILDIR_FILE_ACTION_NONE:
return FALSE;
i_unreached();
return FALSE;
if (skip_next)
seq++;
return FALSE;
return FALSE;
last_uid = 0;
return FALSE;
if (new_flag != 0)
return FALSE;
return FALSE;
return TRUE;
int only_new)
const char *fname;
return FALSE;
return FALSE;
if (new_dir)
return FALSE;
return TRUE;
t_push();
t_pop();
return ret;
case MAILDIR_FILE_ACTION_NONE:
int newflag;
return FALSE;
if (new_dir)
hash_rec);
hash_rec);
return TRUE;
struct dirent *d;
if (d == NULL) {
return TRUE;
int failed;
return FALSE;
return FALSE;
return !failed;
return FALSE;
return FALSE;
return FALSE;
return FALSE;
return TRUE;
struct dirent *d;
const char *final_dir;
if (append_index) {
return FALSE;
return FALSE;
return TRUE;
return FALSE;
if (move_to_cur) {
return FALSE;
if (!append_index) {
return TRUE;
if (append_index) {
if (!move_to_cur)
t_push();
!move_to_cur)) {
t_pop();
return FALSE;
t_pop();
return TRUE;
return FALSE;
return FALSE;
return FALSE;
return FALSE;
return TRUE;
int *changes)
return FALSE;
return FALSE;
return FALSE;
return FALSE;
return FALSE;
return FALSE;
return FALSE;
return TRUE;
const char *fname;
unsigned int seq;
return TRUE;
return FALSE;
return FALSE;
return FALSE;
return FALSE;
return TRUE;
int cur_changed;
return FALSE;
if (!cur_changed) {
return TRUE;
return TRUE;
return FALSE;
return FALSE;
return TRUE;
static struct maildir_sync_context *
return ctx;
int ret;
return ret;
int *changes)
int ret;
if (minimal_sync)
return TRUE;
return ret;