]> git.ipfire.org Git - thirdparty/dovecot/core.git/commitdiff
Maildir syncing updates: it's now faster, it handles read-only folders, it
authorTimo Sirainen <tss@iki.fi>
Sun, 18 May 2003 12:26:06 +0000 (15:26 +0300)
committerTimo Sirainen <tss@iki.fi>
Sun, 18 May 2003 12:26:06 +0000 (15:26 +0300)
handles many out of disk space conditions (uidlist doesn't) and all commands
finally handle the condition when maildir filename was renamed after
our syncing but before we had the chance to open it.

--HG--
branch : HEAD

12 files changed:
src/lib-index/mail-index.h
src/lib-index/maildir/Makefile.am
src/lib-index/maildir/maildir-build.c
src/lib-index/maildir/maildir-expunge.c [new file with mode: 0644]
src/lib-index/maildir/maildir-index.c
src/lib-index/maildir/maildir-index.h
src/lib-index/maildir/maildir-open.c
src/lib-index/maildir/maildir-sync.c
src/lib-index/maildir/maildir-uidlist.c
src/lib-index/maildir/maildir-update-flags.c [new file with mode: 0644]
src/lib-storage/index/maildir/maildir-copy.c
src/lib-storage/index/maildir/maildir-expunge.c

index 21ef3efc43b9b6ec573d456dc4d097a7ec711062..c009f3c72904e504e91495db654b42b32e4b6509 100644 (file)
@@ -69,9 +69,12 @@ enum mail_index_mail_flag {
        INDEX_MAIL_FLAG_BINARY_HEADER   = 0x0001,
        INDEX_MAIL_FLAG_BINARY_BODY     = 0x0002,
 
-       /* Currently this means with mbox format that message flags have
-          been changed in index, but not written into mbox file yet. */
-       INDEX_MAIL_FLAG_DIRTY           = 0x0004
+       /* Mail flags have been changed in index, but not written into
+          actual mailbox yet. */
+       INDEX_MAIL_FLAG_DIRTY           = 0x0004,
+
+       /* Maildir: Mail file is in new/ dir instead of cur/ */
+       INDEX_MAIL_FLAG_MAILDIR_NEW     = 0x0008
 };
 
 enum mail_lock_type {
@@ -394,8 +397,11 @@ struct mail_index {
        ino_t mbox_ino;
 
        /* last maildir sync: */
-       time_t uidlist_mtime;
+       time_t last_new_mtime, last_cur_mtime, last_uidlist_mtime;
+       time_t maildir_cur_dirty;
        int maildir_lock_fd;
+       pool_t new_filename_pool;
+       struct hash_table *new_filenames;
 
        int fd; /* opened index file */
        char *error; /* last error message */
@@ -428,6 +434,8 @@ struct mail_index {
        unsigned int allow_new_custom_flags:1;
        unsigned int mailbox_readonly:1;
        unsigned int mailbox_lock_timeout:1;
+       unsigned int maildir_keep_new:1;
+       unsigned int maildir_have_new:1;
 };
 
 #ifdef DEV_T_STRUCT
@@ -443,7 +451,8 @@ struct mail_index {
        0, 0, 0, 0, 0, 0, { 0, 0, 0 }, 0, 0, \
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, \
        0, 0, 0, 0, 0, 0, 0, 0, 0, 0, \
-       0, 0, 0, 0, 0, 0, 0, 0
+       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, \
+       0, 0, 0, 0, 0
 #endif
 
 /* defaults - same as above but prefixed with mail_index_. */
index 76ecc343fd96c0dd0dec860b3c918494c9194852..adc20f4b1aaca8fe94a7f057f3bc61a00e885ff6 100644 (file)
@@ -10,10 +10,12 @@ libindex_maildir_a_SOURCES = \
        maildir-index.c \
        maildir-build.c \
        maildir-clean.c \
+       maildir-expunge.c \
        maildir-open.c \
        maildir-rebuild.c \
        maildir-sync.c \
-       maildir-uidlist.c
+       maildir-uidlist.c \
+       maildir-update-flags.c
 
 noinst_HEADERS = \
        maildir-index.h \
index 5590a7d327a59f3aa9cc947b6d1c5f34e3164e16..6198c62bd2d69d3aedc500a659e009b1f25118f6 100644 (file)
@@ -31,7 +31,7 @@ static int maildir_record_update(struct mail_index *index,
 }
 
 static int maildir_index_append_fd(struct mail_index *index,
-                                  int fd, const char *fname)
+                                  int fd, const char *fname, int new_dir)
 {
        struct mail_index_record *rec;
        struct mail_index_update *update;
@@ -79,6 +79,8 @@ static int maildir_index_append_fd(struct mail_index *index,
        }
 
        /* set the location */
+       if (new_dir)
+               rec->index_flags |= INDEX_MAIL_FLAG_MAILDIR_NEW;
        index->update_field(update, DATA_FIELD_LOCATION, fname,
                            MAILDIR_LOCATION_EXTRA_SPACE);
 
@@ -94,7 +96,7 @@ static int maildir_index_append_fd(struct mail_index *index,
 }
 
 int maildir_index_append_file(struct mail_index *index, const char *dir,
-                             const char *fname)
+                             const char *fname, int new_dir)
 {
        const char *path;
        int fd, ret;
@@ -103,22 +105,25 @@ int maildir_index_append_file(struct mail_index *index, const char *dir,
 
        if ((index->header->cache_fields & ~DATA_FIELD_LOCATION) == 0) {
                /* nothing cached, don't bother opening the file */
-               return maildir_index_append_fd(index, -1, fname);
+               return maildir_index_append_fd(index, -1, fname, new_dir);
        }
 
        path = t_strconcat(dir, "/", fname, NULL);
        fd = open(path, O_RDONLY);
        if (fd == -1) {
-               /* open() failed - treat it as error unless the error was
-                  "file doesn't exist" in which case someone just managed
-                  to delete it before we saw it */
-               if (errno == EEXIST)
-                       return TRUE;
+               if (errno == ENOENT) {
+                       /* it's not found because it's deleted or renamed.
+                          don't try to handle any error cases here, just
+                          save the thing and let the syncing handle it
+                          later */
+                       return maildir_index_append_fd(index, -1,
+                                                      fname, new_dir);
+               }
 
                return index_file_set_syscall_error(index, path, "open()");
        }
 
-       ret = maildir_index_append_fd(index, fd, fname);
+       ret = maildir_index_append_fd(index, fd, fname, new_dir);
        if (close(fd) < 0)
                return index_file_set_syscall_error(index, path, "close()");
        return ret;
diff --git a/src/lib-index/maildir/maildir-expunge.c b/src/lib-index/maildir/maildir-expunge.c
new file mode 100644 (file)
index 0000000..cdc2cad
--- /dev/null
@@ -0,0 +1,75 @@
+/* Copyright (C) 2002 Timo Sirainen */
+
+#include "lib.h"
+#include "maildir-index.h"
+#include "mail-index-util.h"
+
+#include <unistd.h>
+
+static int maildir_expunge_mail_file(struct mail_index *index,
+                                    struct mail_index_record *rec,
+                                    const char **fname)
+{
+       const char *path;
+
+       *fname = maildir_get_location(index, rec);
+       if (*fname == NULL)
+               return -1;
+
+       if ((rec->index_flags & INDEX_MAIL_FLAG_MAILDIR_NEW) != 0) {
+               /* probably in new/ dir */
+               path = t_strconcat(index->mailbox_path, "/new/", *fname, NULL);
+               if (unlink(path) == 0)
+                       return 1;
+
+               if (errno == EACCES)
+                       return -1;
+               if (errno != ENOENT) {
+                       index_set_error(index, "unlink(%s) failed: %m", path);
+                       return -1;
+               }
+       }
+
+       path = t_strconcat(index->mailbox_path, "/cur/", *fname, NULL);
+       if (unlink(path) == 0)
+               return 1;
+
+       if (errno == EACCES)
+               return -1;
+
+       if (errno != ENOENT) {
+               index_set_error(index, "unlink(%s) failed: %m", path);
+               return -1;
+       }
+
+       return 0;
+}
+
+int maildir_expunge_mail(struct mail_index *index,
+                        struct mail_index_record *rec)
+{
+       const char *fname;
+       int i, ret, found;
+
+       for (i = 0;; i++) {
+               ret = maildir_expunge_mail_file(index, rec, &fname);
+               if (ret > 0)
+                       return TRUE;
+               if (ret < 0)
+                       return FALSE;
+
+               if (i == 10) {
+                       index_set_error(index, "Filename keeps changing, "
+                                       "expunge failed: %s", fname);
+                       return FALSE;
+               }
+
+               if (!maildir_index_sync_readonly(index, fname, &found))
+                       return FALSE;
+
+               if (!found) {
+                       /* syncing didn't find it, it's already deleted */
+                       return TRUE;
+               }
+       }
+}
index cf39f3e70895162d1a1a4282b00aa7082e867723..f671b06211bde0a18b4766fd86b08de63411d79a 100644 (file)
@@ -2,6 +2,7 @@
 
 #include "lib.h"
 #include "ioloop.h"
+#include "hash.h"
 #include "hostpid.h"
 #include "str.h"
 #include "maildir-index.h"
@@ -25,13 +26,20 @@ static int maildir_index_open(struct mail_index *index,
 const char *maildir_get_location(struct mail_index *index,
                                 struct mail_index_record *rec)
 {
-       const char *fname;
+       const char *fname, *new_fname;
 
        fname = index->lookup_field(index, rec, DATA_FIELD_LOCATION);
        if (fname == NULL) {
                index_data_set_corrupted(index->data,
                        "Missing location field for record %u", rec->uid);
        }
+
+       if (index->new_filenames != NULL) {
+               new_fname = hash_lookup(index->new_filenames, fname);
+               if (new_fname != NULL)
+                       return new_fname;
+       }
+
        return fname;
 }
 
@@ -225,9 +233,15 @@ maildir_index_alloc(const char *maildir, const char *index_dir,
 
 static void maildir_index_free(struct mail_index *index)
 {
+       if (index->new_filenames != NULL)
+               hash_destroy(index->new_filenames);
+       if (index->new_filename_pool != NULL)
+               pool_unref(index->new_filename_pool);
+
        mail_index_close(index);
        i_free(index->dir);
        i_free(index->mailbox_path);
+       i_free(index->control_dir);
        i_free(index);
 }
 
@@ -256,53 +270,6 @@ static time_t maildir_get_internal_date(struct mail_index *index,
        return st.st_mtime;
 }
 
-static int maildir_index_update_flags(struct mail_index *index,
-                                     struct mail_index_record *rec,
-                                     unsigned int seq, enum mail_flags flags,
-                                     int external_change)
-{
-       struct mail_index_update *update;
-       const char *old_fname, *new_fname;
-       const char *old_path, *new_path;
-
-       /* we need to update the flags in the file name */
-       old_fname = maildir_get_location(index, rec);
-       if (old_fname == NULL)
-               return FALSE;
-
-       new_fname = maildir_filename_set_flags(old_fname, flags);
-
-       if (strcmp(old_fname, new_fname) != 0) {
-               old_path = t_strconcat(index->mailbox_path,
-                                      "/cur/", old_fname, NULL);
-               new_path = t_strconcat(index->mailbox_path,
-                                      "/cur/", new_fname, NULL);
-
-               /* minor problem: new_path is overwritten if it exists.. */
-               if (rename(old_path, new_path) < 0) {
-                       if (ENOSPACE(errno))
-                               index->nodiskspace = TRUE;
-
-                       index_set_error(index, "maildir flags update: "
-                                       "rename(%s, %s) failed: %m",
-                                       old_path, new_path);
-                       return FALSE;
-               }
-
-               /* update the filename in index */
-               update = index->update_begin(index, rec);
-               index->update_field(update, DATA_FIELD_LOCATION, new_fname, 0);
-
-               if (!index->update_end(update))
-                       return FALSE;
-       }
-
-       if (!mail_index_update_flags(index, rec, seq, flags, external_change))
-               return FALSE;
-
-       return TRUE;
-}
-
 struct mail_index maildir_index = {
        maildir_index_open,
        maildir_index_free,
index cce7b7127f5f5048c97dec53faa45b34d0b67ecb..d21c4e44f20c7c1c24465a91ea32418f45797ca3 100644 (file)
@@ -25,16 +25,24 @@ const char *maildir_filename_set_flags(const char *fname,
                                       enum mail_flags flags);
 
 int maildir_index_rebuild(struct mail_index *index);
+int maildir_index_sync_readonly(struct mail_index *index,
+                               const char *fname, int *found);
 int maildir_index_sync(struct mail_index *index,
                       enum mail_lock_type lock_type, int *changes);
 
 int maildir_index_append_file(struct mail_index *index, const char *dir,
-                             const char *fname);
+                             const char *fname, int new_dir);
+int maildir_index_update_flags(struct mail_index *index,
+                              struct mail_index_record *rec, unsigned int seq,
+                              enum mail_flags flags, int external_change);
 
 struct istream *maildir_open_mail(struct mail_index *index,
                                  struct mail_index_record *rec,
                                  time_t *internal_date, int *deleted);
 
+int maildir_expunge_mail(struct mail_index *index,
+                        struct mail_index_record *rec);
+
 void maildir_clean_tmp(const char *dir);
 
 #endif
index cd09c74b1096c553b62a5b2213e450d3e56b6e10..c60e23fd55dd74c383fc75952405a149199d61cd 100644 (file)
 #include <fcntl.h>
 #include <sys/stat.h>
 
+static int maildir_open_mail_file(struct mail_index *index,
+                                 struct mail_index_record *rec,
+                                 const char **fname, int *deleted)
+{
+       const char *path;
+       int fd = -1;
+
+       *fname = maildir_get_location(index, rec);
+       if (*fname == NULL)
+               return -1;
+
+       if ((rec->index_flags & INDEX_MAIL_FLAG_MAILDIR_NEW) != 0) {
+               /* probably in new/ dir */
+               path = t_strconcat(index->mailbox_path, "/new/", *fname, NULL);
+               fd = open(path, O_RDONLY);
+               if (fd == -1 && errno != ENOENT) {
+                       index_set_error(index, "open(%s) failed: %m", path);
+                       return -1;
+               }
+       }
+
+       if (fd == -1) {
+               path = t_strconcat(index->mailbox_path, "/cur/", *fname, NULL);
+               fd = open(path, O_RDONLY);
+               if (fd == -1) {
+                       if (errno == ENOENT) {
+                               *deleted = TRUE;
+                               return -1;
+                       }
+
+                       index_set_error(index, "open(%s) failed: %m", path);
+                       return -1;
+               }
+       }
+
+       return fd;
+}
+
 struct istream *maildir_open_mail(struct mail_index *index,
                                  struct mail_index_record *rec,
                                  time_t *internal_date, int *deleted)
 {
        struct stat st;
-       const char *fname, *path;
-       int fd;
+       const char *fname;
+       int i, found, fd;
 
        i_assert(index->lock_type != MAIL_LOCK_UNLOCK);
 
@@ -26,20 +64,24 @@ struct istream *maildir_open_mail(struct mail_index *index,
        if (index->inconsistent)
                return NULL;
 
-       fname = maildir_get_location(index, rec);
-       if (fname == NULL)
-               return NULL;
+       fd = maildir_open_mail_file(index, rec, &fname, deleted);
+       for (i = 0; fd == -1 && *deleted && i < 10; i++) {
+               /* file is either renamed or deleted. sync the maildir and
+                  see which one. if file appears to be renamed constantly,
+                  don't try to open it more than 10 times. */
+               if (!maildir_index_sync_readonly(index, fname, &found)) {
+                       *deleted = FALSE;
+                       return NULL;
+               }
 
-       path = t_strconcat(index->mailbox_path, "/cur/", fname, NULL);
-       fd = open(path, O_RDONLY);
-       if (fd == -1) {
-               if (errno == ENOENT) {
-                       *deleted = TRUE;
+               if (!found) {
+                       /* syncing didn't find it, it's deleted */
                        return NULL;
                }
 
-               index_set_error(index, "Error opening mail file %s: %m", path);
-               return NULL;
+               fd = maildir_open_mail_file(index, rec, &fname, deleted);
+               if (fd == -1)
+                       return NULL;
        }
 
        if (internal_date != NULL) {
index e7ad1570f1e8bcf4671caaf72b45512604167641..d2d12faa713e591064ba2815ed2d8fb9688fe853 100644 (file)
@@ -1,7 +1,170 @@
 /* Copyright (C) 2002-2003 Timo Sirainen */
 
+/*
+   Here's a description of how we handle Maildir synchronization and
+   it's problems:
+
+   We want to be as efficient as we can. The most efficient way to
+   check if changes have occured is to stat() the new/ and cur/
+   directories and uidlist file - if their mtimes haven't changed,
+   there's no changes and we don't need to do anything.
+
+   Problem 1: Multiple changes can happen within a single second -
+   nothing guarantees that once we synced it, someone else didn't just
+   then make a modification. Such modifications wouldn't get noticed
+   until a new modification occured later.
+
+   Problem 2: Syncing cur/ directory is much more costly than syncing
+   new/. Moving mails from new/ to cur/ will always change mtime of
+   cur/ causing us to sync it as well.
+
+   Problem 3: We may not be able to move mail from new/ to cur/
+   because we're out of quota, or simply because we're accessing a
+   read-only mailbox.
+
+
+   MAILDIR_SYNC_SECS
+   -----------------
+
+   Several checks below use MAILDIR_SYNC_SECS, which should be maximum
+   clock drift between all computers accessing the maildir (eg. via
+   NFS), rounded up to next second. Our default is 1 second, since
+   everyone should be using NTP.
+
+   Note that setting it to 0 works only if there's only one computer
+   accessing the maildir. It's practically impossible to make two
+   clocks _exactly_ synchronized.
+
+   It might be possible to only use file server's clock by looking at
+   the atime field, but I don't know how well that would actually work.
+
+   cur directory
+   -------------
+
+   We have maildir_cur_dirty variable which is set to cur/ directory's
+   mtime when it's >= time() - MAILDIR_SYNC_SECS and we _think_ we have
+   synchronized the directory.
+
+   When maildir_cur_dirty is non-zero, we don't synchronize the cur/
+   directory until
+
+      a) cur/'s mtime changes
+      b) opening a mail fails with ENOENT
+      c) time() > maildir_cur_dirty + MAILDIR_SYNC_SECS
+
+   This allows us to modify the maildir multiple times without having
+   to sync it at every change. The sync will eventually be done to
+   make sure we didn't miss any external changes.
+
+   The maildir_cur_dirty is set when:
+
+      - we change message flags
+      - we expunge messages
+      - we move mail from new/ to cur/
+      - we sync cur/ directory and it's mtime is
+        >= time() - MAILDIR_SYNC_SECS
+
+   It's unset when we do the final syncing, ie. when mtime is
+   older than time() - MAILDIR_SYNC_SECS.
+
+   new directory
+   -------------
+
+   If new/'s mtime is >= time() - MAILDIR_SYNC_SECS, always synchronize
+   it. maildir_cur_dirty-like feature might save us a few syncs, but
+   that might break a client which saves a mail in one connection and
+   tries to fetch it in another one. new/ directory is almost always
+   empty, so syncing it should be very fast anyway. Actually this can
+   still happen if we sync only new/ dir while another client is also
+   moving mails from it to cur/ - it takes us a while to see them.
+   That's pretty unlikely to happen however, and only way to fix it
+   would be to always synchronize cur/ after new/.
+
+   Normally we move all mails from new/ to cur/ whenever we sync it. If
+   it's not possible for some reason, we set maildir_have_new flag on
+   which instructs synchronization to check files in new/ directory as
+   well. maildir_keep_new flag is also set which instructs syncing to
+   not even try to move mails to cur/ anymore.
+
+   If client tries to change a flag for message in new/, we try to
+   rename() it into cur/. If it's successful, we clear the
+   maildir_keep_new flag so at next sync we'll try to move all of them
+   to cur/. When all of them have been moved, maildir_have_new flag is
+   cleared as well. Expunges will also clear maildir_keep_new flag.
+
+   If rename() still fails because of ENOSPC or EDQUOT, we still save
+   the flag changes in index with dirty-flag on. When moving the mail
+   to cur/ directory, or when we notice it's already moved there, we
+   apply the flag changes to the filename, rename it and remove the
+   dirty flag. If there's dirty flags, this should be tried every time
+   after expunge or when closing the mailbox.
+
+   broken clients
+   --------------
+
+   Originally the middle identifier in Maildir filename was specified
+   only as <process id>_<delivery counter>. That however created a
+   problem with randomized PIDs which made it possible that the same
+   PID was reused within one second.
+
+   So if within one second a mail was delivered, MUA moved it to cur/
+   and another mail was delivered by a new process using same PID as
+   the first one, we likely ended up overwriting the first mail when
+   the second mail was moved over it.
+
+   Nowadays everyone should be giving a bit more specific identifier,
+   for example include microseconds in it which Dovecot does.
+
+   There's a simple way to prevent this from happening in some cases:
+   Don't move the mail from new/ to cur/ if it's mtime is >= time() -
+   MAILDIR_SYNC_SECS. The second delivery's link() call then fails
+   because the file is already in new/, and it will then use a
+   different filename. There's a few problems with this however:
+
+      - it requires extra stat() call which is unneeded extra I/O
+      - another MUA might still move the mail to cur/
+      - if first file's flags are modified by either Dovecot or another
+        MUA, it's moved to cur/ (you _could_ just do the dirty-flagging
+       but that'd be ugly)
+
+   Because this is useful only for very few people and it requires
+   extra I/O, I decided not to implement this. It should be however
+   quite easy to do since we need to be able to deal with files in new/
+   in any case.
+
+   It's also possible to never accidentally overwrite a mail by using
+   link() + unlink() rather than rename(). This however isn't very
+   good idea as it introduces potential race conditions when multiple
+   clients are accessing the mailbox:
+
+   Trying to move the same mail from new/ to cur/ at the same time:
+
+      a) Client 1 uses slightly different filename than client 2,
+         for example one sets read-flag on but the other doesn't.
+        You have the same mail duplicated now.
+
+      b) Client 3 sees the mail between Client 1's and 2's link() calls
+         and changes it's flag. You have the same mail duplicated now.
+
+   And it gets worse when they're unlink()ing in cur/ directory:
+
+      c) Client 1 changes mails's flag and client 2 changes it back
+         between 1's link() and unlink(). The mail is now expunged.
+
+      d) If you try to deal with the duplicates by unlink()ing another
+         one of them, you might end up unlinking both of them.
+
+   So, what should we do then if we notice a duplicate? First of all,
+   it might not be a duplicate at all, readdir() might have just
+   returned it twice because it was just renamed. What we should do is
+   create a completely new base name for it and rename() it to that.
+   If the call fails with ENOENT, it only means that it wasn't a
+   duplicate after all.
+*/
+
 #include "lib.h"
 #include "buffer.h"
+#include "istream.h"
 #include "hash.h"
 #include "ioloop.h"
 #include "str.h"
 #include <utime.h>
 #include <sys/stat.h>
 
+#define MAILDIR_SYNC_SECS 1
+
 enum maildir_file_action {
        MAILDIR_FILE_ACTION_EXPUNGE,
-       MAILDIR_FILE_ACTION_UPDATE_FLAGS,
+        MAILDIR_FILE_ACTION_UPDATE_FLAGS,
        MAILDIR_FILE_ACTION_UPDATE_CONTENT,
        MAILDIR_FILE_ACTION_NEW,
-       MAILDIR_FILE_ACTION_NONE
+       MAILDIR_FILE_ACTION_NONE,
+
+       MAILDIR_FILE_FLAG_NEWDIR = 0x1000
 };
 
 struct maildir_hash_context {
@@ -37,27 +204,103 @@ struct maildir_hash_rec {
        struct mail_index_record *rec;
        enum maildir_file_action action;
 };
+#define ACTION(hash) ((hash)->action & 0x0f)
+
+struct maildir_sync_context {
+        struct mail_index *index;
+       const char *new_dir, *cur_dir;
+
+       pool_t pool;
+       struct hash_table *files;
+       unsigned int new_count;
+
+       DIR *new_dirp;
+       struct dirent *new_dent;
+
+       struct maildir_uidlist *uidlist;
+       unsigned int readonly_check:1;
+       unsigned int flag_updates:1;
+};
+
+/* a char* hash function from ASU -- from glib */
+static unsigned int maildir_hash(const void *p)
+{
+        const unsigned char *s = p;
+       unsigned int g, h = 0;
+
+       while (*s != ':' && *s != '\0') {
+               h = (h << 4) + *s;
+               if ((g = h & 0xf0000000UL)) {
+                       h = h ^ (g >> 24);
+                       h = h ^ g;
+               }
+               s++;
+       }
+
+       return h;
+}
+
+static int maildir_cmp(const void *p1, const void *p2)
+{
+       const char *s1 = p1, *s2 = p2;
+
+       while (*s1 == *s2 && *s1 != ':' && *s1 != '\0') {
+               s1++; s2++;
+       }
+       if ((*s1 == '\0' || *s1 == ':') &&
+           (*s2 == '\0' || *s2 == '\0'))
+               return 0;
+       return *s1 - *s2;
+}
 
-static int maildir_update_filename(struct mail_index *index,
+static void maildir_update_filename_memory(struct mail_index *index,
+                                          const char *fname)
+{
+       char *new_fname;
+
+       if (index->new_filename_pool == NULL) {
+               index->new_filename_pool =
+                       pool_alloconly_create("Maildir fname", 10240);
+       }
+       if (index->new_filenames == NULL) {
+               index->new_filenames =
+                       hash_create(system_pool, index->new_filename_pool, 0,
+                                   maildir_hash, maildir_cmp);
+       }
+
+       new_fname = p_strdup(index->new_filename_pool, fname);
+       hash_insert(index->new_filenames, new_fname, new_fname);
+}
+
+static int maildir_update_filename(struct maildir_sync_context *ctx,
                                   struct mail_index_record *rec,
                                   const char *new_fname)
 {
        struct mail_index_update *update;
 
-       update = index->update_begin(index, rec);
-       index->update_field(update, DATA_FIELD_LOCATION, new_fname, 0);
-       return index->update_end(update);
+       if (ctx->index->lock_type != MAIL_LOCK_EXCLUSIVE) {
+               maildir_update_filename_memory(ctx->index, new_fname);
+               return TRUE;
+       }
+
+       update = ctx->index->update_begin(ctx->index, rec);
+       ctx->index->update_field(update, DATA_FIELD_LOCATION, new_fname, 0);
+       return ctx->index->update_end(update);
 }
 
-static int maildir_update_flags(struct mail_index *index,
+static int maildir_update_flags(struct maildir_sync_context *ctx,
                                struct mail_index_record *rec,
                                unsigned int seq, const char *new_fname)
 {
        enum mail_flags flags;
 
+       if (ctx->index->lock_type != MAIL_LOCK_EXCLUSIVE)
+               return TRUE;
+
        flags = maildir_filename_get_flags(new_fname, rec->msg_flags);
        if (flags != rec->msg_flags) {
-               if (!index->update_flags(index, rec, seq, flags, TRUE))
+               if (!ctx->index->update_flags(ctx->index, rec,
+                                             seq, flags, TRUE))
                        return FALSE;
        }
 
@@ -105,57 +348,26 @@ static int is_file_content_changed(struct mail_index *index,
        return FALSE;
 }
 
-/* a char* hash function from ASU -- from glib */
-static unsigned int maildir_hash(const void *p)
-{
-        const unsigned char *s = p;
-       unsigned int g, h = 0;
-
-       while (*s != ':' && *s != '\0') {
-               h = (h << 4) + *s;
-               if ((g = h & 0xf0000000UL)) {
-                       h = h ^ (g >> 24);
-                       h = h ^ g;
-               }
-               s++;
-       }
-
-       return h;
-}
-
-static int maildir_cmp(const void *p1, const void *p2)
-{
-       const char *s1 = p1, *s2 = p2;
-
-       while (*s1 == *s2 && *s1 != ':' && *s1 != '\0') {
-               s1++; s2++;
-       }
-       if ((*s1 == '\0' || *s1 == ':') &&
-           (*s2 == '\0' || *s2 == '\0'))
-               return 0;
-       return *s1 - *s2;
-}
-
 static void uidlist_hash_get_filenames(void *key, void *value, void *context)
 {
        buffer_t *buf = context;
        struct maildir_hash_rec *hash_rec = value;
 
-       if (hash_rec->action == MAILDIR_FILE_ACTION_NEW)
+       if (ACTION(hash_rec) == MAILDIR_FILE_ACTION_NEW)
                buffer_append(buf, (const void *) &key, sizeof(key));
 }
 
-static int maildir_sync_uidlist(struct mail_index *index, const char *dir,
-                               struct maildir_uidlist *uidlist,
-                               struct hash_table *files, pool_t pool,
-                               unsigned int new_count)
+static int maildir_sync_uidlist(struct maildir_sync_context *ctx)
 {
+       struct mail_index *index = ctx->index;
+        struct maildir_uidlist *uidlist = ctx->uidlist;
        struct mail_index_record *rec;
        struct maildir_hash_rec *hash_rec;
         struct maildir_uidlist_rec uid_rec;
-       const char *fname, **new_files;
+       const char *fname, **new_files, *dir;
        void *orig_key, *orig_value;
-       unsigned int seq, uid, last_uid, i;
+       unsigned int seq, uid, last_uid, i, new_flag;
+       int new_dir;
        buffer_t *buf;
 
         seq = 0;
@@ -179,52 +391,53 @@ static int maildir_sync_uidlist(struct mail_index *index, const char *dir,
                }
 
                fname = maildir_get_location(index, rec);
-               if (fname == NULL) {
-                       hash_destroy(files);
+               if (fname == NULL)
                        return FALSE;
-               }
 
-               hash_rec = hash_lookup(files, fname);
+               hash_rec = hash_lookup(ctx->files, fname);
                if (hash_rec == NULL) {
-                       index_set_corrupted(index, "Unexpectedly lost file "
-                                           "%s from hash", fname);
+                       index_set_corrupted(index,
+                               "Unexpectedly lost file %s from hash", fname);
                        return FALSE;
                }
 
                if (uid_rec.uid == uid &&
                    maildir_cmp(fname, uid_rec.filename) != 0) {
-                       index_set_corrupted(index, "Filename mismatch for UID "
-                                           "%u: %s vs %s", uid, fname,
-                                           uid_rec.filename);
+                       index_set_corrupted(index,
+                               "Filename mismatch for UID %u: %s vs %s",
+                               uid, fname, uid_rec.filename);
                        return FALSE;
                }
 
                if (uid_rec.uid > uid &&
-                   (hash_rec->action == MAILDIR_FILE_ACTION_UPDATE_FLAGS ||
-                    hash_rec->action == MAILDIR_FILE_ACTION_NONE)) {
+                   (ACTION(hash_rec) == MAILDIR_FILE_ACTION_UPDATE_FLAGS ||
+                    ACTION(hash_rec) == MAILDIR_FILE_ACTION_NONE)) {
                        /* it's UID has changed */
-                       hash_rec->action = MAILDIR_FILE_ACTION_UPDATE_CONTENT;
+                       hash_rec->action = MAILDIR_FILE_ACTION_UPDATE_CONTENT |
+                               (hash_rec->action & MAILDIR_FILE_FLAG_NEWDIR);
 
                        /* make sure filename is not invalidated by expunge */
-                       hash_insert(files, p_strdup(pool, fname), hash_rec);
+                       hash_insert(ctx->files, p_strdup(ctx->pool, fname),
+                                   hash_rec);
                }
 
-               switch (hash_rec->action) {
+               switch (ACTION(hash_rec)) {
                case MAILDIR_FILE_ACTION_EXPUNGE:
                        if (!index->expunge(index, rec, seq, TRUE))
                                return FALSE;
                        seq--;
                        break;
                case MAILDIR_FILE_ACTION_UPDATE_FLAGS:
-                       if (!maildir_update_flags(index, rec, seq, fname))
+                       if (!maildir_update_flags(ctx, rec, seq, fname))
                                return FALSE;
                        break;
                case MAILDIR_FILE_ACTION_UPDATE_CONTENT:
                        if (!index->expunge(index, rec, seq, TRUE))
                                return FALSE;
                        seq--;
-                       hash_rec->action = MAILDIR_FILE_ACTION_NEW;
-                       new_count++;
+                       hash_rec->action = MAILDIR_FILE_ACTION_NEW |
+                               (hash_rec->action & MAILDIR_FILE_FLAG_NEWDIR);
+                       ctx->new_count++;
                        break;
                case MAILDIR_FILE_ACTION_NONE:
                        break;
@@ -246,17 +459,17 @@ static int maildir_sync_uidlist(struct mail_index *index, const char *dir,
                return FALSE;
        }
 
-       /* if there's mails with UIDs in uidlist, write them */
+       /* if there's new mails which are already in uidlist, get them */
        last_uid = 0;
        while (uid_rec.uid != 0) {
-               if (!hash_lookup_full(files, uid_rec.filename,
+               if (!hash_lookup_full(ctx->files, uid_rec.filename,
                                      &orig_key, &orig_value)) {
                        /* expunged */
                        if (uidlist != NULL)
                                uidlist->rewrite = TRUE;
                } else {
                        hash_rec = orig_value;
-                       i_assert(hash_rec->action == MAILDIR_FILE_ACTION_NEW);
+                       i_assert(ACTION(hash_rec) == MAILDIR_FILE_ACTION_NEW);
 
                        /* make sure we set the same UID for it. */
                        if (index->header->next_uid > uid_rec.uid) {
@@ -269,10 +482,16 @@ static int maildir_sync_uidlist(struct mail_index *index, const char *dir,
                        }
                        index->header->next_uid = uid_rec.uid;
 
-                        hash_rec->action = MAILDIR_FILE_ACTION_NONE;
-                       new_count--;
+                       new_flag = hash_rec->action & MAILDIR_FILE_FLAG_NEWDIR;
+                       hash_rec->action = MAILDIR_FILE_ACTION_NONE | new_flag;
+                       ctx->new_count--;
 
-                       if (!maildir_index_append_file(index, dir, orig_key))
+                       if (new_flag != 0)
+                               ctx->index->maildir_have_new = TRUE;
+                       dir = new_flag != 0 ? ctx->new_dir : ctx->cur_dir;
+
+                       if (!maildir_index_append_file(index, dir, orig_key,
+                                                      new_flag != 0))
                                return FALSE;
                }
 
@@ -280,7 +499,7 @@ static int maildir_sync_uidlist(struct mail_index *index, const char *dir,
                        return FALSE;
        }
 
-       if (new_count == 0 || !INDEX_IS_UIDLIST_LOCKED(index)) {
+       if (ctx->new_count == 0 || !INDEX_IS_UIDLIST_LOCKED(index)) {
                /* all done (or can't do it since we don't have lock) */
                return TRUE;
        }
@@ -290,40 +509,49 @@ static int maildir_sync_uidlist(struct mail_index *index, const char *dir,
 
        /* then there's the completely new mails. sort them by the filename
           so we should get them to same order as they were created. */
-       buf = buffer_create_static_hard(pool, new_count * sizeof(const char *));
-       hash_foreach(files, uidlist_hash_get_filenames, buf);
-       i_assert(buffer_get_used_size(buf) / sizeof(const char *) <= new_count);
+       buf = buffer_create_static_hard(ctx->pool,
+                                       ctx->new_count * sizeof(const char *));
+       hash_foreach(ctx->files, uidlist_hash_get_filenames, buf);
+       i_assert(buffer_get_used_size(buf) / sizeof(const char *) <=
+                ctx->new_count);
 
        new_files = buffer_get_modifyable_data(buf, NULL);
-       qsort(new_files, new_count, sizeof(const char *),
+       qsort(new_files, ctx->new_count, sizeof(const char *),
              (int (*)(const void *, const void *)) strcmp);
 
-       /* and finally write */
-       for (i = 0; i < new_count; i++) {
-               if (!maildir_index_append_file(index, dir, new_files[i]))
+       if (!index->maildir_keep_new) {
+               dir = ctx->cur_dir;
+               new_dir = FALSE;
+       } else {
+               /* this is actually slightly wrong, because we don't really
+                  know if some of the new messages are in cur/ already.
+                  we could know that by saving it into buffer, but that'd
+                  require extra memory. luckily it doesn't really matter if
+                  we say it's in new/, but it's actually in cur/. we have
+                  to deal with such case anyway since another client might
+                  have just moved it. */
+               dir = ctx->new_dir;
+               new_dir = TRUE;
+               ctx->index->maildir_have_new = TRUE;
+       }
+
+       for (i = 0; i < ctx->new_count; i++) {
+               if (!maildir_index_append_file(index, dir,
+                                              new_files[i], new_dir))
                        return FALSE;
        }
 
        return TRUE;
 }
 
-static int maildir_index_sync_dir(struct mail_index *index, const char *dir,
-                                 struct maildir_uidlist *uidlist)
+static int maildir_index_full_sync_init(struct maildir_sync_context *ctx)
 {
-       pool_t pool;
-       struct hash_table *files;
+       struct mail_index *index = ctx->index;
        struct mail_index_record *rec;
        struct maildir_hash_rec *hash_rec;
-       DIR *dirp;
-       struct dirent *d;
        const char *fname;
-       void *orig_key, *orig_value;
-       unsigned int new_count;
        size_t size;
-       int failed, check_content_changes;
-
-       i_assert(dir != NULL);
-       i_assert(index->lock_type == MAIL_LOCK_EXCLUSIVE);
+       int have_new;
 
        if (index->header->messages_count >= INT_MAX/32) {
                index_set_corrupted(index, "Header says %u messages",
@@ -334,44 +562,59 @@ static int maildir_index_sync_dir(struct mail_index *index, const char *dir,
        /* read current messages in index into hash */
        size = nearest_power(index->header->messages_count *
                             sizeof(struct maildir_hash_rec) + 1024);
-       pool = pool_alloconly_create("maildir sync", I_MAX(size, 16384));
-       files = hash_create(default_pool, pool, index->header->messages_count*2,
-                           maildir_hash, maildir_cmp);
+       ctx->pool = pool_alloconly_create("maildir sync", I_MAX(size, 16384));
+       ctx->files = hash_create(default_pool, ctx->pool,
+                                index->header->messages_count * 2,
+                                maildir_hash, maildir_cmp);
+
+       have_new = FALSE;
 
        rec = index->lookup(index, 1);
        while (rec != NULL) {
                fname = maildir_get_location(index, rec);
-               if (fname == NULL) {
-                       hash_destroy(files);
+               if (fname == NULL)
                        return FALSE;
-               }
-               hash_rec = p_new(pool, struct maildir_hash_rec, 1);
+
+               if ((rec->index_flags & INDEX_MAIL_FLAG_MAILDIR_NEW) != 0)
+                       have_new = TRUE;
+
+               hash_rec = p_new(ctx->pool, struct maildir_hash_rec, 1);
                hash_rec->rec = rec;
                hash_rec->action = MAILDIR_FILE_ACTION_EXPUNGE;
-               hash_insert(files, (void *) fname, hash_rec);
+               hash_insert(ctx->files, (void *) fname, hash_rec);
 
                rec = index->next(index, rec);
        }
 
+       index->maildir_have_new = have_new;
+       return TRUE;
+}
+
+static int maildir_index_full_sync_dir(struct maildir_sync_context *ctx,
+                                      const char *dir, int new_dir,
+                                      DIR *dirp, struct dirent *d)
+{
+       struct maildir_hash_rec *hash_rec;
+       void *orig_key, *orig_value;
+       int check_content_changes, newflag;
+
+       newflag = new_dir ? MAILDIR_FILE_FLAG_NEWDIR : 0;
+
        /* Do we want to check changes in file contents? This slows down
           things as we need to do extra stat() for all files. */
-       check_content_changes = getenv("MAILDIR_CHECK_CONTENT_CHANGES") != NULL;
+       check_content_changes = !ctx->readonly_check &&
+               getenv("MAILDIR_CHECK_CONTENT_CHANGES") != NULL;
 
-       dirp = opendir(dir);
-       if (dirp == NULL)
-               return index_file_set_syscall_error(index, dir, "opendir()");
-
-       new_count = 0; failed = FALSE;
-       while ((d = readdir(dirp)) != NULL) {
+       do {
                if (d->d_name[0] == '.')
                        continue;
 
-               if (!hash_lookup_full(files, d->d_name,
+               if (!hash_lookup_full(ctx->files, d->d_name,
                                      &orig_key, &orig_value)) {
-                       hash_rec = p_new(pool, struct maildir_hash_rec, 1);
+                       hash_rec = p_new(ctx->pool, struct maildir_hash_rec, 1);
                } else {
                        hash_rec = orig_value;
-                       if (hash_rec->action != MAILDIR_FILE_ACTION_EXPUNGE) {
+                       if (ACTION(hash_rec) != MAILDIR_FILE_ACTION_EXPUNGE) {
                                /* FIXME: duplicate */
                                continue;
                        }
@@ -379,273 +622,352 @@ static int maildir_index_sync_dir(struct mail_index *index, const char *dir,
 
                if (hash_rec->rec == NULL) {
                        /* new message */
-                       new_count++;
-                       hash_rec->action = MAILDIR_FILE_ACTION_NEW;
-                       hash_insert(files, p_strdup(pool, d->d_name), hash_rec);
-               } else if (check_content_changes &&
-                          is_file_content_changed(index, rec,
-                                                  dir, d->d_name)) {
+                       if (ctx->readonly_check)
+                               continue;
+
+                       ctx->new_count++;
+                       hash_rec->action = MAILDIR_FILE_ACTION_NEW | newflag;
+                       hash_insert(ctx->files, p_strdup(ctx->pool, d->d_name),
+                                   hash_rec);
+                       continue;
+               }
+
+               if (!new_dir && (hash_rec->rec->index_flags &
+                                INDEX_MAIL_FLAG_MAILDIR_NEW) != 0 &&
+                   ctx->index->lock_type == MAIL_LOCK_EXCLUSIVE) {
+                       /* mail was indexed in new/ but it has been
+                          moved to cur/ later */
+                       hash_rec->rec->index_flags &=
+                               ~INDEX_MAIL_FLAG_MAILDIR_NEW;
+               }
+
+               if (check_content_changes &&
+                   is_file_content_changed(ctx->index, hash_rec->rec,
+                                           dir, d->d_name)) {
                        /* file content changed, treat it as new message */
-                       hash_rec->action = MAILDIR_FILE_ACTION_UPDATE_CONTENT;
+                       hash_rec->action =
+                               MAILDIR_FILE_ACTION_UPDATE_CONTENT | newflag;
 
                        /* make sure filename is not invalidated by expunge
                           later. the file name may have changed also. */
-                       hash_insert(files, p_strdup(pool, d->d_name), hash_rec);
+                       hash_insert(ctx->files, p_strdup(ctx->pool, d->d_name),
+                                   hash_rec);
                } else if (strcmp(orig_key, d->d_name) != 0) {
-                       /* update filename now, flags later */
-                       hash_rec->action =  MAILDIR_FILE_ACTION_UPDATE_FLAGS;
-                       if (!maildir_update_filename(index, hash_rec->rec,
-                                                    d->d_name)) {
-                               failed = TRUE;
-                               break;
-                       }
+                       /* update file name now. flags later because we don't
+                          know it's sequence number */
+                       hash_rec->action =
+                               MAILDIR_FILE_ACTION_UPDATE_FLAGS | newflag;
+                       if (!maildir_update_filename(ctx, hash_rec->rec,
+                                                    d->d_name))
+                               return FALSE;
+                        ctx->flag_updates = TRUE;
                } else {
-                       hash_rec->action =  MAILDIR_FILE_ACTION_NONE;
+                       hash_rec->action = MAILDIR_FILE_ACTION_NONE | newflag;
                }
-       }
+       } while ((d = readdir(dirp)) != NULL);
 
-       if (closedir(dirp) < 0)
-               index_file_set_syscall_error(index, dir, "closedir()");
-
-       if (!failed) {
-               failed = !maildir_sync_uidlist(index, dir, uidlist,
-                                              files, pool, new_count);
-       }
-       hash_destroy(files);
-       pool_unref(pool);
-       return !failed;
+       return TRUE;
 }
 
-static int maildir_new_scan_first_file(struct mail_index *index,
-                                      const char *dir, DIR **dirp,
-                                      struct dirent **d)
+static int maildir_new_scan_first_file(struct maildir_sync_context *ctx)
 {
-       *dirp = opendir(dir);
-       if (*dirp == NULL)
-               return index_file_set_syscall_error(index, dir, "opendir()");
+       DIR *dirp;
+       struct dirent *d;
+
+       dirp = opendir(ctx->new_dir);
+       if (dirp == NULL) {
+               return index_file_set_syscall_error(ctx->index, ctx->new_dir,
+                                                   "opendir()");
+       }
 
        /* find first file */
-       while ((*d = readdir(*dirp)) != NULL) {
-               if ((*d)->d_name[0] != '.')
+       while ((d = readdir(dirp)) != NULL) {
+               if (d->d_name[0] != '.')
                        break;
        }
 
-       if (*d == NULL) {
-               if (closedir(*dirp) < 0)
-                       index_file_set_syscall_error(index, dir, "closedir()");
-               *dirp = NULL;
+       if (d == NULL) {
+               if (closedir(dirp) < 0) {
+                       index_file_set_syscall_error(ctx->index, ctx->new_dir,
+                                                    "closedir()");
+               }
+       } else {
+               ctx->new_dirp = dirp;
+               ctx->new_dent = d;
        }
 
        return TRUE;
 }
 
-static int maildir_sync_new(struct mail_index *index,
-                           const char *source_dir, const char *dest_dir,
-                           DIR *dirp, struct dirent *d, int append_index)
+static int maildir_index_full_sync_dirs(struct maildir_sync_context *ctx)
 {
-       const char *final_dir;
-       string_t *sourcepath, *destpath;
+       DIR *dirp;
        int failed;
 
-       i_assert(index->maildir_lock_fd != -1);
-       i_assert(index->lock_type != MAIL_LOCK_SHARED);
+       if (ctx->index->maildir_have_new || ctx->index->maildir_keep_new) {
+               if (ctx->new_dirp == NULL) {
+                       if (!maildir_new_scan_first_file(ctx))
+                               return FALSE;
+               }
+
+               if (ctx->new_dent != NULL) {
+                       if (!maildir_index_full_sync_dir(ctx, ctx->new_dir,
+                                                        TRUE, ctx->new_dirp,
+                                                        ctx->new_dent))
+                               return FALSE;
+               }
+       }
+
+       dirp = opendir(ctx->cur_dir);
+       if (dirp == NULL) {
+               return index_file_set_syscall_error(ctx->index, ctx->cur_dir,
+                                                   "opendir()");
+       }
+
+       failed = !maildir_index_full_sync_dir(ctx, ctx->cur_dir, FALSE,
+                                             dirp, readdir(dirp));
+
+       if (closedir(dirp) < 0) {
+               return index_file_set_syscall_error(ctx->index, ctx->cur_dir,
+                                                   "closedir()");
+       }
+
+       return !failed;
+}
+
+static int maildir_sync_new_dir(struct maildir_sync_context *ctx,
+                               int move_to_cur, int append_index)
+{
+       struct dirent *d;
+       string_t *sourcepath, *destpath;
+       const char *final_dir;
+
+       d = ctx->new_dent;
+       ctx->new_dent = NULL;
 
        sourcepath = t_str_new(PATH_MAX);
        destpath = t_str_new(PATH_MAX);
 
-       final_dir = dest_dir != NULL ? dest_dir : source_dir;
+       final_dir = move_to_cur ? ctx->cur_dir : ctx->new_dir;
 
-       failed = FALSE;
-       for (; d != NULL && !failed; d = readdir(dirp)) {
+       do {
                if (d->d_name[0] == '.')
                        continue;
 
-               if (dest_dir != NULL) {
-                       /* rename() has the problem that it might overwrite
-                          some mails, but that happens only with a broken
-                          client that has created non-unique base name.
-
-                          Alternative would be link() + unlink(), but that's
-                          racy when multiple clients try to move the mail from
-                          new/ to cur/:
+               str_truncate(sourcepath, 0);
+               str_printfa(sourcepath, "%s/%s", ctx->new_dir, d->d_name);
 
-                          a) One of the clients uses slightly different
-                          filename (eg. sets flags)
-
-                          b) Third client changes mail's flag between
-                          client1's unlink() and client2's link() calls.
-
-                          Checking first if file exists with stat() is pretty
-                          useless as well. It requires that we also stat the
-                          file in new/, to make sure that the dest file isn't
-                          actually the same file which someone _just_ had
-                          rename()d. */
-                       str_truncate(sourcepath, 0);
+               if (move_to_cur) {
                        str_truncate(destpath, 0);
-
-                       str_printfa(sourcepath, "%s/%s", source_dir, d->d_name);
-                       str_printfa(destpath, "%s/%s", dest_dir, d->d_name);
+                       str_printfa(destpath, "%s/%s", ctx->cur_dir, d->d_name);
 
                        if (rename(str_c(sourcepath), str_c(destpath)) < 0 &&
                            errno != ENOENT) {
-                               index_set_error(index, "maildir build: "
+                               if (ENOSPACE(errno))
+                                       ctx->index->nodiskspace = TRUE;
+                               else if (errno == EACCES)
+                                       ctx->index->mailbox_readonly = TRUE;
+                               else {
+                                       index_set_error(ctx->index,
                                                "rename(%s, %s) failed: %m",
                                                str_c(sourcepath),
                                                str_c(destpath));
-                               failed = TRUE;
-                               break;
+                                       return FALSE;
+                               }
+
+                               ctx->index->maildir_keep_new = TRUE;
+                               if (!append_index) {
+                                       ctx->new_dent = d;
+                                       return TRUE;
+                               }
+
+                               /* continue by keeping them in new/ dir */
+                               final_dir = ctx->new_dir;
+                               move_to_cur = FALSE;
                        }
                }
 
                if (append_index) {
+                       if (!move_to_cur)
+                               ctx->index->maildir_have_new = TRUE;
+
                        t_push();
-                       failed = !maildir_index_append_file(index, final_dir,
-                                                           d->d_name);
+                       if (!maildir_index_append_file(ctx->index, final_dir,
+                                                      d->d_name,
+                                                      !move_to_cur)) {
+                               t_pop();
+                               return FALSE;
+                       }
                        t_pop();
                }
-       }
+       } while ((d = readdir(ctx->new_dirp)) != NULL);
 
-       return !failed;
+       return TRUE;
 }
-static int maildir_index_lock_and_sync(struct mail_index *index, int *changes,
-                                      DIR *new_dirp, struct dirent *new_dent,
-                                      struct maildir_uidlist **uidlist_r)
+
+static int maildir_index_sync_context(struct maildir_sync_context *ctx,
+                                     int *changes)
+
 {
-       struct stat st, std;
-       struct utimbuf ut;
-       struct maildir_uidlist *uidlist;
-       const char *uidlist_path, *cur_dir, *new_dir;
-       time_t index_mtime, uidlist_mtime;
-       int cur_changed;
+        struct mail_index *index = ctx->index;
+       struct stat st;
+       const char *uidlist_path;
+       time_t uidlist_mtime, new_mtime, cur_mtime;
+       int new_changed, full_sync;
 
-       *uidlist_r = uidlist = NULL;
+       uidlist_path = t_strconcat(index->control_dir,
+                                  "/" MAILDIR_UIDLIST_NAME, NULL);
 
        if (index->fd == -1) {
                /* anon-mmaped */
-               index_mtime = index->file_sync_stamp;
+               index->last_cur_mtime = index->file_sync_stamp;
        } else {
+               /* FIXME: we should save it in index's header */
                if (fstat(index->fd, &st) < 0)
                        return index_set_syscall_error(index, "fstat()");
-               index_mtime = st.st_mtime;
+               index->last_cur_mtime = st.st_mtime;
        }
 
-        cur_dir = t_strconcat(index->mailbox_path, "/cur", NULL);
-       if (stat(cur_dir, &std) < 0)
-               return index_file_set_syscall_error(index, cur_dir, "stat()");
+       if (stat(ctx->new_dir, &st) < 0) {
+               index_file_set_syscall_error(index, ctx->new_dir, "stat()");
+               return FALSE;
+       }
+       new_mtime = st.st_mtime;
 
-       uidlist_path = t_strconcat(index->mailbox_path,
-                                  "/" MAILDIR_UIDLIST_NAME, NULL);
-       if (stat(uidlist_path, &st) < 0) {
+       if (stat(ctx->cur_dir, &st) < 0) {
+               index_file_set_syscall_error(index, ctx->cur_dir, "stat()");
+               return FALSE;
+       }
+       cur_mtime = st.st_mtime;
+
+       if (new_mtime == index->last_new_mtime &&
+           new_mtime < ioloop_time - MAILDIR_SYNC_SECS)
+               new_changed = FALSE;
+       else {
+               if (!maildir_new_scan_first_file(ctx))
+                       return FALSE;
+               new_changed = ctx->new_dent != NULL;
+       }
+
+       if (cur_mtime != index->last_cur_mtime ||
+           (index->maildir_cur_dirty != 0 &&
+            index->maildir_cur_dirty < ioloop_time - MAILDIR_SYNC_SECS)) {
+               /* cur/ changed, or delayed cur/ check */
+               full_sync = TRUE;
+       } else if (stat(uidlist_path, &st) < 0) {
                if (errno != ENOENT) {
                        return index_file_set_syscall_error(index, uidlist_path,
                                                            "stat()");
                }
 
-               memset(&st, 0, sizeof(st));
-               cur_changed = TRUE;
-                uidlist_mtime = 0;
+               /* have to create uidlist */
+               full_sync = TRUE;
+       } else if (st.st_mtime != index->last_uidlist_mtime) {
+               /* uidlist changed */
+               full_sync = TRUE;
+       } else if (!new_changed) {
+               /* no changes to anything */
+               return TRUE;
        } else {
-               /* FIXME: save mtime into index header, so we don't
-                  have to read it every time mailbox is opened */
-                uidlist_mtime = st.st_mtime;
-               cur_changed = index_mtime != std.st_mtime ||
-                       index->uidlist_mtime != uidlist_mtime;
+               full_sync = index->maildir_have_new;
        }
 
-       if (new_dirp != NULL || cur_changed) {
-               if (maildir_uidlist_try_lock(index) < 0)
-                       return FALSE;
-
-               /* we may or may not have succeeded. if we didn't,
-                  just continue by syncing with existing uidlist file */
-
-               if (!cur_changed && !INDEX_IS_UIDLIST_LOCKED(index)) {
-                       /* just new mails in new/ dir, we can't sync them
-                          if we can't get the lock. */
-                       return TRUE;
-               }
+       if (maildir_uidlist_try_lock(index) < 0)
+               return FALSE;
 
-               if (!index->set_lock(index, MAIL_LOCK_EXCLUSIVE))
-                       return FALSE;
+       /* we may or may not have succeeded. if we didn't,
+          just continue by syncing with existing uidlist file */
 
-               *uidlist_r = uidlist = maildir_uidlist_open(index);
-               if (uidlist != NULL &&
-                   uidlist->uid_validity != index->header->uid_validity) {
-                       /* uidvalidity changed */
-                       if (!index->rebuilding && index->opened) {
-                               index_set_corrupted(index,
-                                       "UIDVALIDITY changed in uidlist");
-                               return FALSE;
-                       }
+       if (!full_sync && !INDEX_IS_UIDLIST_LOCKED(index)) {
+               /* just new mails in new/ dir, we can't sync them
+                  if we can't get the lock. */
+               return TRUE;
+       }
 
-                       if (!index->rebuilding) {
-                               index->set_flags |= MAIL_INDEX_FLAG_REBUILD;
-                               return FALSE;
-                       }
+       if (!index->set_lock(index, MAIL_LOCK_EXCLUSIVE))
+               return FALSE;
 
-                       index->header->uid_validity = uidlist->uid_validity;
-                       i_assert(index->header->next_uid == 1);
+       ctx->uidlist = maildir_uidlist_open(index);
+       if (ctx->uidlist != NULL &&
+           ctx->uidlist->uid_validity != index->header->uid_validity) {
+               /* uidvalidity changed */
+               if (!index->rebuilding && index->opened) {
+                       index_set_corrupted(index,
+                                           "UIDVALIDITY changed in uidlist");
+                       return FALSE;
                }
 
-               if (uidlist != NULL &&
-                   index->header->next_uid > uidlist->next_uid) {
-                       index_set_corrupted(index, "index.next_uid (%u) > "
-                                           "uidlist.next_uid (%u)",
-                                           index->header->next_uid,
-                                           uidlist->next_uid);
+               if (!index->rebuilding) {
+                       index->set_flags |= MAIL_INDEX_FLAG_REBUILD;
                        return FALSE;
                }
 
-               if (changes != NULL)
-                       *changes = TRUE;
+               index->header->uid_validity = ctx->uidlist->uid_validity;
+               i_assert(index->header->next_uid == 1);
        }
 
-       /* move mail from new/ to cur/ */
-       if (new_dirp != NULL && INDEX_IS_UIDLIST_LOCKED(index)) {
-               new_dir = t_strconcat(index->mailbox_path, "/new", NULL);
-               if (!maildir_sync_new(index, new_dir, cur_dir,
-                                     new_dirp, new_dent, FALSE))
-                       return FALSE;
+       if (ctx->uidlist != NULL) {
+               /* get uidlist's mtime again now that we have opened the file.
+                  it might have changed from out last check. */
+               if (fstat(i_stream_get_fd(ctx->uidlist->input), &st) < 0) {
+                       return index_file_set_syscall_error(index, uidlist_path,
+                                                           "fstat()");
+               }
+               uidlist_mtime = st.st_mtime;
+       }
 
-               if (uidlist != NULL)
-                       uidlist->rewrite = TRUE;
+       if (ctx->uidlist != NULL &&
+           index->header->next_uid > ctx->uidlist->next_uid) {
+               index_set_corrupted(index, "index.next_uid (%u) > "
+                                   "uidlist.next_uid (%u)",
+                                   index->header->next_uid,
+                                   ctx->uidlist->next_uid);
+               return FALSE;
+       }
 
-               /* set cur/ directory's timestamp into past to make sure we
-                  notice if new mail is moved there. FIXME: is this such a
-                  good idea? Works fine only as long as others don't try
-                  the same thing.. But alternative is to wait a second or
-                  potentially lose some mails. */
-               ut.actime = ut.modtime = ioloop_time-60;
-               if (utime(cur_dir, &ut) < 0) {
-                       index_file_set_syscall_error(index, cur_dir, "utime()");
-                       return FALSE;
-               }
+       if (changes != NULL)
+               *changes = TRUE;
 
-               /* We have to always scan the cur/ directory to make
-                  sure we don't miss any mails some other non-Dovecot
-                  client may have moved there. FIXME: in Dovecot-only setup
-                  we could just skip checking cur/ changes. In that case the
-                  new/ dir has to be be synced after uidlist is first synced
-                  or there could be some problems with conflicting UIDs */
-               cur_changed = TRUE;
+       if (new_changed && full_sync &&
+           INDEX_IS_UIDLIST_LOCKED(index) && !index->maildir_keep_new) {
+               /* we'll be syncing cur/ anyway, so move the mails from
+                  new/ to cur/ first. */
+               if (!maildir_sync_new_dir(ctx, TRUE, FALSE))
+                       return FALSE;
 
-               /* set the cur/ directory's timestamp */
-               std.st_mtime = ut.modtime;
+               /* new_dent is non-NULL only if rename() failed because there
+                  was no disk space */
+               new_changed = ctx->new_dent != NULL;
        }
 
-       if (cur_changed) {
-               if (!maildir_index_sync_dir(index, cur_dir, uidlist))
+       if (full_sync) {
+               if (!maildir_index_full_sync_init(ctx) ||
+                   !maildir_index_full_sync_dirs(ctx) ||
+                   !maildir_sync_uidlist(ctx))
+                       return FALSE;
+               new_changed = !INDEX_IS_UIDLIST_LOCKED(index);
+       } else if (new_changed) {
+               i_assert(INDEX_IS_UIDLIST_LOCKED(index));
+
+               if (!maildir_sync_new_dir(ctx, !index->maildir_keep_new, TRUE))
                        return FALSE;
+               new_changed = FALSE;
+
+               /* this will set maildir_cur_dirty. it may actually be
+                  different from cur/'s mtime if we're unlucky, but that
+                  doesn't really matter and it's not worth the extra stat() */
+               cur_mtime = time(NULL);
        }
 
-       if (uidlist != NULL && uidlist->next_uid > index->header->next_uid)
-               index->header->next_uid = uidlist->next_uid;
+       if (ctx->uidlist != NULL &&
+           ctx->uidlist->next_uid > index->header->next_uid)
+               index->header->next_uid = ctx->uidlist->next_uid;
 
-       if ((new_dirp != NULL || cur_changed) &&
-           (uidlist == NULL || uidlist->rewrite)) {
+       if (ctx->uidlist == NULL || ctx->uidlist->rewrite) {
                if (!INDEX_IS_UIDLIST_LOCKED(index)) {
                        /* there's more new mails, but we need .lock file to
                           be able to sync them. */
+                       index->last_uidlist_mtime = uidlist_mtime;
                        return TRUE;
                }
 
@@ -653,56 +975,183 @@ static int maildir_index_lock_and_sync(struct mail_index *index, int *changes,
                        return FALSE;
        }
 
-       /* uidlist file synced */
-       index->uidlist_mtime = uidlist_mtime;
+       if (cur_mtime < ioloop_time - MAILDIR_SYNC_SECS)
+               index->maildir_cur_dirty = 0;
+       else
+               index->maildir_cur_dirty = cur_mtime;
+
+       index->last_uidlist_mtime = uidlist_mtime;
+       index->last_cur_mtime = cur_mtime;
+       if (!new_changed)
+               index->last_new_mtime = new_mtime;
 
        /* update sync stamp */
-       index->file_sync_stamp = std.st_mtime;
+       index->file_sync_stamp = cur_mtime;
 
-       if (index->lock_type == MAIL_LOCK_UNLOCK && !index->anon_mmap) {
-               /* no changes to index, we need to update it's timestamp
-                  ourself to get it changed */
-               ut.actime = ioloop_time;
-               ut.modtime = index->file_sync_stamp;
-               if (utime(index->filepath, &ut) < 0)
-                       return index_set_syscall_error(index, "utime()");
+       return TRUE;
+}
+
+static int maildir_index_sync_readonly_flags(struct maildir_sync_context *ctx)
+{
+       struct mail_index *index = ctx->index;
+       struct mail_index_record *rec;
+       struct maildir_hash_rec *hash_rec;
+       const char *fname;
+       unsigned int seq;
+
+       if (index->lock_type != MAIL_LOCK_EXCLUSIVE || !ctx->flag_updates)
+               return TRUE;
+
+       rec = index->lookup(index, 1); seq = 1;
+       while (rec != NULL) {
+               fname = maildir_get_location(index, rec);
+               if (fname == NULL)
+                       return FALSE;
+
+               hash_rec = hash_lookup(ctx->files, fname);
+               if (hash_rec == NULL) {
+                       index_set_corrupted(index,
+                               "Unexpectedly lost file %s from hash", fname);
+                       return FALSE;
+               }
+
+               if (ACTION(hash_rec) == MAILDIR_FILE_ACTION_UPDATE_FLAGS) {
+                       if (!maildir_update_flags(ctx, rec, seq, fname))
+                               return FALSE;
+               }
+
+               rec = index->next(index, rec); seq++;
        }
 
        return TRUE;
 }
 
-int maildir_index_sync(struct mail_index *index,
-                      enum mail_lock_type data_lock_type __attr_unused__,
-                      int *changes)
+static int maildir_index_sync_context_readonly(struct maildir_sync_context *ctx)
 {
-        struct maildir_uidlist *uidlist;
-       DIR *new_dirp;
-       struct dirent *new_dent;
-       const char *new_dir;
-       int ret;
-
-       i_assert(index->lock_type != MAIL_LOCK_SHARED);
+       struct mail_index *index = ctx->index;
+       struct stat st;
+       int cur_changed;
 
-       if (changes != NULL)
-               *changes = FALSE;
+       i_assert(index->lock_type != MAIL_LOCK_UNLOCK);
 
-       new_dir = t_strconcat(index->mailbox_path, "/new", NULL);
-       if (!maildir_new_scan_first_file(index, new_dir, &new_dirp, &new_dent))
+       if (stat(ctx->cur_dir, &st) < 0) {
+               index_file_set_syscall_error(index, ctx->cur_dir, "stat()");
                return FALSE;
+       }
 
-       ret = maildir_index_lock_and_sync(index, changes, new_dirp, new_dent,
-                                         &uidlist);
+       cur_changed = st.st_mtime != index->last_cur_mtime ||
+               index->maildir_cur_dirty != 0;
 
-       if (uidlist != NULL)
-               maildir_uidlist_close(uidlist);
+       if (!cur_changed) {
+               if (!index->maildir_have_new) {
+                       /* no changes */
+                       return TRUE;
+               }
+
+               if (stat(ctx->new_dir, &st) < 0) {
+                       return index_file_set_syscall_error(index, ctx->new_dir,
+                                                           "stat()");
+               }
+               if (st.st_mtime == index->last_new_mtime &&
+                   st.st_mtime < ioloop_time - MAILDIR_SYNC_SECS) {
+                       /* no changes */
+                       return TRUE;
+               }
+
+               if (!maildir_new_scan_first_file(ctx))
+                       return FALSE;
+       }
+
+       /* ok, something's changed. check only changes in file names. */
+
+       /* if we can get exclusive lock, we can update the index
+          directly. but don't rely on it. */
+       (void)index->try_lock(index, MAIL_LOCK_EXCLUSIVE);
 
-       if (new_dirp != NULL) {
-               if (closedir(new_dirp) < 0) {
-                       index_file_set_syscall_error(index, new_dir,
+       if (!maildir_index_full_sync_init(ctx) ||
+           !maildir_index_full_sync_dirs(ctx) ||
+           !maildir_index_sync_readonly_flags(ctx))
+               return FALSE;
+
+       return TRUE;
+}
+
+static void maildir_index_sync_deinit(struct maildir_sync_context *ctx)
+{
+       if (ctx->uidlist != NULL)
+               maildir_uidlist_close(ctx->uidlist);
+       if (ctx->files != NULL)
+               hash_destroy(ctx->files);
+       if (ctx->pool != NULL)
+               pool_unref(ctx->pool);
+
+       if (ctx->new_dirp != NULL) {
+               if (closedir(ctx->new_dirp) < 0) {
+                       index_file_set_syscall_error(ctx->index, ctx->new_dir,
                                                     "closedir()");
                }
        }
 
-       maildir_uidlist_unlock(index);
+       maildir_uidlist_unlock(ctx->index);
+}
+
+static struct maildir_sync_context *
+maildir_sync_context_new(struct mail_index *index)
+{
+        struct maildir_sync_context *ctx;
+
+       if (index->new_filenames != NULL) {
+               hash_destroy(index->new_filenames);
+               index->new_filenames = NULL;
+       }
+
+       if (index->new_filename_pool != NULL)
+               p_clear(index->new_filename_pool);
+
+       ctx = t_new(struct maildir_sync_context, 1);
+       ctx->index = index;
+       ctx->new_dir = t_strconcat(index->mailbox_path, "/new", NULL);
+       ctx->cur_dir = t_strconcat(index->mailbox_path, "/cur", NULL);
+       return ctx;
+}
+
+int maildir_index_sync_readonly(struct mail_index *index,
+                               const char *fname, int *found)
+{
+        struct maildir_sync_context *ctx;
+       struct maildir_hash_rec *hash_rec;
+       int ret;
+
+       ctx = maildir_sync_context_new(index);
+       ctx->readonly_check = TRUE;
+
+       ret = maildir_index_sync_context_readonly(ctx);
+
+       if (!ret)
+               *found = FALSE;
+       else {
+               hash_rec = hash_lookup(ctx->files, fname);
+               *found = hash_rec != NULL &&
+                       hash_rec->action != MAILDIR_FILE_ACTION_EXPUNGE;
+       }
+       maildir_index_sync_deinit(ctx);
+       return ret;
+}
+
+int maildir_index_sync(struct mail_index *index,
+                      enum mail_lock_type data_lock_type __attr_unused__,
+                      int *changes)
+{
+        struct maildir_sync_context *ctx;
+       int ret;
+
+       i_assert(index->lock_type != MAIL_LOCK_SHARED);
+
+       if (changes != NULL)
+               *changes = FALSE;
+
+       ctx = maildir_sync_context_new(index);
+       ret = maildir_index_sync_context(ctx, changes);
+        maildir_index_sync_deinit(ctx);
        return ret;
 }
index 29a66db5121e20b26d29aec6e63b02c11be4ff88..8862ea6ecfc05dcc63478bbc5e5184bc6703e9a6 100644 (file)
@@ -83,7 +83,7 @@ void maildir_uidlist_unlock(struct mail_index *index)
 struct maildir_uidlist *maildir_uidlist_open(struct mail_index *index)
 {
        const char *path, *line;
-        struct maildir_uidlist *uidlist;
+       struct maildir_uidlist *uidlist;
        unsigned int version;
        int fd;
 
diff --git a/src/lib-index/maildir/maildir-update-flags.c b/src/lib-index/maildir/maildir-update-flags.c
new file mode 100644 (file)
index 0000000..7b9405e
--- /dev/null
@@ -0,0 +1,141 @@
+/* Copyright (C) 2002 Timo Sirainen */
+
+#include "lib.h"
+#include "maildir-index.h"
+#include "mail-index-util.h"
+
+#include <stdio.h>
+
+static int handle_error(struct mail_index *index,
+                       const char *path, const char *new_path)
+{
+       if (errno == ENOENT)
+               return 0;
+
+       if (ENOSPACE(errno)) {
+               index->nodiskspace = TRUE;
+               return -2;
+       }
+
+       if (errno == EPERM)
+               index->mailbox_readonly = TRUE;
+       else {
+               index_set_error(index, "rename(%s, %s) failed: %m",
+                               path, new_path);
+       }
+
+       return -1;
+}
+
+static int maildir_rename_mail_file(struct mail_index *index,
+                                   struct mail_index_record *rec,
+                                   const char *old_fname, const char *new_path)
+{
+       const char *path;
+
+       if ((rec->index_flags & INDEX_MAIL_FLAG_MAILDIR_NEW) != 0) {
+               /* probably in new/ dir */
+               path = t_strconcat(index->mailbox_path, "/new/",
+                                  old_fname, NULL);
+               if (rename(path, new_path) == 0)
+                       return 1;
+
+               if (errno != ENOENT)
+                       return handle_error(index, path, new_path);
+       }
+
+       path = t_strconcat(index->mailbox_path, "/cur/", old_fname, NULL);
+       if (rename(path, new_path) == 0)
+               return 1;
+
+       return handle_error(index, path, new_path);
+}
+
+static int maildir_rename_mail(struct mail_index *index,
+                              struct mail_index_record *rec,
+                              enum mail_flags flags, const char **new_fname_r)
+{
+       const char *old_fname, *new_fname, *new_path;
+       int i, ret, found;
+
+       new_fname = new_path = NULL;
+
+       i = 0;
+       do {
+               /* we need to update the flags in the file name */
+               old_fname = maildir_get_location(index, rec);
+               if (old_fname == NULL)
+                       return FALSE;
+
+               if (new_path == NULL) {
+                       new_fname = maildir_filename_set_flags(old_fname,
+                                                              flags);
+                        *new_fname_r = new_fname;
+                       new_path = t_strconcat(index->mailbox_path,
+                                              "/cur/", new_fname, NULL);
+               }
+
+               if (strcmp(old_fname, new_fname) == 0)
+                       ret = 1;
+               else {
+                       ret = maildir_rename_mail_file(index, rec, old_fname,
+                                                      new_path);
+                       if (ret == -1)
+                               return FALSE;
+
+                       if (ret == 1 && index->maildir_keep_new) {
+                               /* looks like we have some more space again,
+                                  see if we could move mails from new/ to
+                                  cur/ again */
+                               index->maildir_keep_new = FALSE;
+                       }
+
+               }
+               if (ret == 0) {
+                       if (!maildir_index_sync_readonly(index, old_fname,
+                                                        &found))
+                               return FALSE;
+                       if (!found)
+                               break;
+               }
+
+               i++;
+       } while (i < 10 && ret == 0);
+
+       if (ret != 1) {
+               /* we couldn't actually rename() the file now.
+                  leave it's flags dirty so they get changed later. */
+               rec->index_flags |= INDEX_MAIL_FLAG_DIRTY;
+       }
+       return TRUE;
+}
+
+int maildir_index_update_flags(struct mail_index *index,
+                              struct mail_index_record *rec, unsigned int seq,
+                              enum mail_flags flags, int external_change)
+{
+       struct mail_index_update *update;
+       const char *new_fname;
+       int ret;
+
+       t_push();
+       if (!maildir_rename_mail(index, rec, flags, &new_fname)) {
+               t_pop();
+               return FALSE;
+       }
+
+       /* update the filename in index */
+       update = index->update_begin(index, rec);
+       index->update_field(update, DATA_FIELD_LOCATION, new_fname, 0);
+
+       if (!index->update_end(update))
+               ret = FALSE;
+       else if (!mail_index_update_flags(index, rec, seq, flags,
+                                         external_change))
+               ret = FALSE;
+       else
+               ret = TRUE;
+       t_pop();
+
+       return ret;
+}
index 4de83d3e27f626667a85e2fdc964aec0f754c000..a17ddb299d6dea174cfb878a7127aec6891c7e35 100644 (file)
@@ -16,6 +16,54 @@ struct rollback {
        const char *fname;
 };
 
+static int maildir_hardlink_file(struct mail_index *index,
+                                struct mail_index_record *rec,
+                                const char **fname, const char *new_path)
+{
+       const char *path;
+
+       *fname = maildir_get_location(index, rec);
+       if (*fname == NULL)
+               return -1;
+
+       if ((rec->index_flags & INDEX_MAIL_FLAG_MAILDIR_NEW) != 0) {
+               /* probably in new/ dir */
+               path = t_strconcat(index->mailbox_path, "/new/", *fname, NULL);
+               if (link(path, new_path) == 0)
+                       return 1;
+
+               if (ENOSPACE(errno)) {
+                       index->nodiskspace = TRUE;
+                       return -1;
+               }
+               if (errno == EACCES || errno == EXDEV)
+                       return -1;
+               if (errno != ENOENT) {
+                       index_set_error(index, "link(%s, %s) failed: %m",
+                                       path, new_path);
+                       return -1;
+               }
+       }
+
+       path = t_strconcat(index->mailbox_path, "/cur/", *fname, NULL);
+       if (link(path, new_path) == 0)
+               return 1;
+
+       if (ENOSPACE(errno)) {
+               index->nodiskspace = TRUE;
+               return -1;
+       }
+       if (errno == EACCES || errno == EXDEV)
+               return -1;
+       if (errno != ENOENT) {
+               index_set_error(index, "link(%s, %s) failed: %m",
+                               path, new_path);
+               return -1;
+       }
+
+       return 0;
+}
+
 static int hardlink_messageset(struct messageset_context *ctx,
                               struct index_mailbox *src,
                               struct index_mailbox *dest)
@@ -27,7 +75,7 @@ static int hardlink_messageset(struct messageset_context *ctx,
        enum mail_flags flags;
        const char **custom_flags;
        const char *fname, *src_path, *dest_fname, *dest_path;
-       int ret;
+       int ret, i, found;
 
        pool = pool_alloconly_create("hard copy rollbacks", 2048);
        rollbacks = NULL;
@@ -35,7 +83,7 @@ static int hardlink_messageset(struct messageset_context *ctx,
        custom_flags = mail_custom_flags_list_get(index->custom_flags);
 
        ret = 1;
-       while ((mail = index_messageset_next(ctx)) != NULL) {
+       while (ret > 0 && (mail = index_messageset_next(ctx)) != NULL) {
                flags = mail->rec->msg_flags;
                if (!index_mailbox_fix_custom_flags(dest, &flags,
                                                    custom_flags,
@@ -45,16 +93,6 @@ static int hardlink_messageset(struct messageset_context *ctx,
                }
 
                /* link the file */
-               fname = index->lookup_field(index, mail->rec,
-                                           DATA_FIELD_LOCATION);
-               if (fname == NULL) {
-                       index_set_corrupted(index,
-                               "Missing location field for record %u",
-                               mail->rec->uid);
-                       ret = -1;
-                       break;
-               }
-
                t_push();
                src_path = t_strconcat(index->mailbox_path, "/cur/",
                                       fname, NULL);
@@ -64,23 +102,31 @@ static int hardlink_messageset(struct messageset_context *ctx,
                dest_path = t_strconcat(dest->index->mailbox_path, "/new/",
                                        dest_fname, NULL);
 
-               if (link(src_path, dest_path) == 0) {
-                       rb = p_new(pool, struct rollback, 1);
-                       rb->fname = p_strdup(pool, dest_fname);
-                       rb->next = rollbacks;
-                       rollbacks = rb;
-               } else {
-                       if (errno != EXDEV) {
-                               mail_storage_set_critical(src->box.storage,
-                                       "link(%s, %s) failed: %m",
-                                       src_path, dest_path);
-                               t_pop();
-                               ret = -1;
+               for (i = 0;; i++) {
+                       ret = maildir_hardlink_file(index, mail->rec, &fname,
+                                                   dest_path);
+                       if (ret > 0) {
+                               rb = p_new(pool, struct rollback, 1);
+                               rb->fname = p_strdup(pool, dest_fname);
+                               rb->next = rollbacks;
+                               rollbacks = rb;
                                break;
                        }
-                       t_pop();
-                       ret = 0;
-                       break;
+                       if (ret < 0)
+                               break;
+
+                       if (i == 10) {
+                                mail_storage_set_error(src->box.storage,
+                                       "File name keeps changing, "
+                                       "copy failed");
+                               break;
+                       }
+
+                       if (!maildir_index_sync_readonly(index, fname, &found))
+                               break;
+
+                       if (!found)
+                               break;
                }
                t_pop();
        }
@@ -103,17 +149,20 @@ static int copy_with_hardlinks(struct index_mailbox *src,
                               const char *messageset, int uidset)
 {
         struct messageset_context *ctx;
-       int ret, ret2;
+       int exdev, ret, ret2;
 
        if (!index_storage_sync_and_lock(src, TRUE, MAIL_LOCK_SHARED))
                return -1;
 
        ctx = index_messageset_init(src, messageset, uidset, FALSE);
        ret = hardlink_messageset(ctx, src, dest);
+       exdev = ret < 0 && errno == EXDEV;
        ret2 = index_messageset_deinit(ctx);
+
        if (ret2 < 0)
                ret = -1;
-       else if (ret2 == 0) {
+
+       if (ret == 0 || ret2 == 0) {
                mail_storage_set_error(src->box.storage,
                        "Some of the requested messages no longer exist.");
                ret = -1;
@@ -121,7 +170,7 @@ static int copy_with_hardlinks(struct index_mailbox *src,
 
        (void)index_storage_lock(src, MAIL_LOCK_UNLOCK);
 
-       return ret;
+       return exdev ? 0 : ret;
 }
 
 int maildir_storage_copy(struct mailbox *box, struct mailbox *destbox,
index 021f2b24fd1bbd3c27dc2f1ac2275acb06955d9a..5550baf705186e36fa8ff56ef05195b8a490cd14 100644 (file)
@@ -1,55 +1,33 @@
 /* Copyright (C) 2002 Timo Sirainen */
 
 #include "lib.h"
+#include "maildir-index.h"
 #include "maildir-storage.h"
 
-#include <unistd.h>
-
-static int expunge_msg(struct index_mailbox *ibox,
-                      struct mail_index_record *rec)
-{
-       const char *fname;
-       char path[PATH_MAX];
-
-       fname = ibox->index->lookup_field(ibox->index, rec,
-                                         DATA_FIELD_LOCATION);
-       if (fname != NULL) {
-               if (str_ppath(path, sizeof(path),
-                             ibox->index->mailbox_path, "cur/", fname) < 0) {
-                       mail_storage_set_critical(ibox->box.storage,
-                                                 "Filename too long: %s",
-                                                 fname);
-                       return FALSE;
-               }
-
-               if (unlink(path) < 0) {
-                       /* if it didn't exist, someone just had either
-                          deleted it or changed it's flags */
-                       mail_storage_set_error(ibox->box.storage,
-                                              "unlink() failed for "
-                                              "message file %s: %m", path);
-                       return FALSE;
-               }
-       }
-
-       return TRUE;
-}
-
 int maildir_expunge_locked(struct index_mailbox *ibox, int notify)
 {
        struct mail_index_record *rec;
        unsigned int seq;
+       int ret, no_permission = FALSE;
 
        if (!index_expunge_seek_first(ibox, &seq, &rec))
                return FALSE;
 
        while (rec != NULL) {
                if (rec->msg_flags & MAIL_DELETED) {
-                       if (!expunge_msg(ibox, rec))
-                               return FALSE;
-
-                       if (!index_expunge_mail(ibox, rec, seq, notify))
-                               return FALSE;
+                       t_push();
+                       ret = maildir_expunge_mail(ibox->index, rec);
+                       t_pop();
+
+                       if (!ret) {
+                               if (errno != EACCES)
+                                       return FALSE;
+                               no_permission = TRUE;
+                               seq++;
+                       } else {
+                               if (!index_expunge_mail(ibox, rec, seq, notify))
+                                       return FALSE;
+                       }
                } else {
                        seq++;
                }
@@ -57,5 +35,11 @@ int maildir_expunge_locked(struct index_mailbox *ibox, int notify)
                rec = ibox->index->next(ibox->index, rec);
        }
 
+       if (no_permission) {
+               ibox->box.storage->callbacks->notify_no(&ibox->box,
+                       "We didn't have permission to expunge all the mails",
+                       ibox->box.storage->callback_context);
+       }
+
        return TRUE;
 }