view src/lib-storage/index/maildir/maildir-uidlist.c @ 1947:777da553d1d3 HEAD

Recent-flag should work now
author Timo Sirainen <tss@iki.fi>
date Sat, 01 May 2004 21:30:52 +0300
parents 876bd2849a00
children 2f6e137cdc44
line wrap: on
line source

/* Copyright (C) 2003 Timo Sirainen */

#include "lib.h"
#include "ioloop.h"
#include "buffer.h"
#include "hash.h"
#include "istream.h"
#include "str.h"
#include "file-dotlock.h"
#include "write-full.h"
#include "maildir-storage.h"
#include "maildir-uidlist.h"

#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <utime.h>

/* how many seconds to wait before overriding uidlist.lock */
#define UIDLIST_LOCK_STALE_TIMEOUT (60*5)

#define UIDLIST_IS_LOCKED(uidlist) \
	((uidlist)->lock_fd != -1)

struct maildir_uidlist_rec {
	uint32_t uid;
	uint32_t flags;
	char *filename;
};

struct maildir_uidlist {
	struct index_mailbox *ibox;
	char *fname;
	int lock_fd;

	time_t last_mtime;

	pool_t filename_pool;
	buffer_t *record_buf;
	struct hash_table *files;

	unsigned int version;
	unsigned int uid_validity, next_uid, last_read_uid;
	uint32_t first_recent_uid;
};

struct maildir_uidlist_sync_ctx {
	struct maildir_uidlist *uidlist;

	pool_t filename_pool;
	struct hash_table *files;
	buffer_t *new_record_buf;

	unsigned int new_files:1;
	unsigned int synced:1;
	unsigned int failed:1;
};

struct maildir_uidlist_iter_ctx {
	const struct maildir_uidlist_rec *next, *end;
};

int maildir_uidlist_try_lock(struct maildir_uidlist *uidlist)
{
	const char *path;
	mode_t old_mask;
	int fd;

	if (UIDLIST_IS_LOCKED(uidlist))
		return 1;

	path = t_strconcat(uidlist->ibox->control_dir,
			   "/" MAILDIR_UIDLIST_NAME, NULL);
        old_mask = umask(0777 & ~uidlist->ibox->mail_create_mode);
	fd = file_dotlock_open(path, NULL, 0, 0, UIDLIST_LOCK_STALE_TIMEOUT,
			       NULL, NULL);
	umask(old_mask);
	if (fd == -1) {
		if (errno == EAGAIN)
			return 0;
		mail_storage_set_critical(uidlist->ibox->box.storage,
			"file_dotlock_open(%s) failed: %m", path);
		return -1;
	}

	uidlist->lock_fd = fd;
	return 1;
}

void maildir_uidlist_unlock(struct maildir_uidlist *uidlist)
{
	const char *path;

	if (!UIDLIST_IS_LOCKED(uidlist))
		return;

	path = t_strconcat(uidlist->ibox->control_dir,
			   "/" MAILDIR_UIDLIST_NAME, NULL);
	(void)file_dotlock_delete(path, uidlist->lock_fd);
	uidlist->lock_fd = -1;
}

struct maildir_uidlist *maildir_uidlist_init(struct index_mailbox *ibox)
{
	struct maildir_uidlist *uidlist;

	uidlist = i_new(struct maildir_uidlist, 1);
	uidlist->ibox = ibox;
	uidlist->fname =
		i_strconcat(ibox->control_dir, "/" MAILDIR_UIDLIST_NAME, NULL);
	uidlist->lock_fd = -1;
	uidlist->record_buf =
		buffer_create_dynamic(default_pool, 512, (size_t)-1);
	uidlist->files = hash_create(default_pool, default_pool, 4096,
				     maildir_hash, maildir_cmp);

	uidlist->uid_validity = ioloop_time;
	uidlist->next_uid = 1;

	return uidlist;
}

void maildir_uidlist_deinit(struct maildir_uidlist *uidlist)
{
	i_assert(!UIDLIST_IS_LOCKED(uidlist));

	hash_destroy(uidlist->files);
	if (uidlist->filename_pool != NULL)
		pool_unref(uidlist->filename_pool);

	buffer_free(uidlist->record_buf);
	i_free(uidlist->fname);
	i_free(uidlist);
}

static void
maildir_uidlist_mark_recent(struct maildir_uidlist *uidlist, uint32_t uid)
{
	if (uidlist->first_recent_uid == 0)
		uidlist->first_recent_uid = uid;
	i_assert(uid >= uidlist->first_recent_uid);
}

static int maildir_uidlist_next(struct maildir_uidlist *uidlist,
				const char *line, uint32_t last_uid)
{
        struct maildir_uidlist_rec *rec;
	uint32_t uid, flags;

	uid = flags = 0;
	while (*line >= '0' && *line <= '9') {
		uid = uid*10 + (*line - '0');
		line++;
	}

	if (uid <= last_uid) {
		/* we already have this */
		return 1;
	}

	if (uid == 0 || *line != ' ') {
		/* invalid file */
                mail_storage_set_critical(uidlist->ibox->box.storage,
			"Invalid data in file %s", uidlist->fname);
		return 0;
	}
	if (uid <= uidlist->last_read_uid) {
                mail_storage_set_critical(uidlist->ibox->box.storage,
			"UIDs not ordered in file %s (%u > %u)",
			uidlist->fname, uid, uidlist->last_read_uid);
		return 0;
	}
	if (uid >= uidlist->next_uid) {
                mail_storage_set_critical(uidlist->ibox->box.storage,
			"UID larger than next_uid in file %s (%u >= %u)",
			uidlist->fname, uid, uidlist->next_uid);
		return 0;
	}

	while (*line == ' ') line++;

	flags = 0;
	if (uidlist->version > 1) {
		while (*line != ' ') {
			switch (*line) {
			case 'N':
				flags |= MAILDIR_UIDLIST_REC_FLAG_NEW_DIR;
				break;
			}
			line++;
		}
		while (*line == ' ') line++;
	} else {
		/* old version, have to assume it's in new dir since we
		   don't know */
		flags |= MAILDIR_UIDLIST_REC_FLAG_NEW_DIR;
	}

	if (hash_lookup(uidlist->files, line) != NULL) {
                mail_storage_set_critical(uidlist->ibox->box.storage,
			"Duplicate file in uidlist file %s: %s",
			uidlist->fname, line);
		return 0;
	}

	rec = buffer_append_space_unsafe(uidlist->record_buf, sizeof(*rec));
	rec->uid = uid;
	rec->flags = flags;
	rec->filename = p_strdup(uidlist->filename_pool, line);
	hash_insert(uidlist->files, rec->filename, rec);
	return 1;
}

int maildir_uidlist_update(struct maildir_uidlist *uidlist)
{
	struct mail_storage *storage = uidlist->ibox->box.storage;
	const struct maildir_uidlist_rec *rec;
	const char *line;
	struct istream *input;
	struct stat st;
	uint32_t last_uid;
	size_t size;
	int fd, ret;

	if (uidlist->last_mtime != 0) {
		if (stat(uidlist->fname, &st) < 0) {
			if (errno != ENOENT) {
				mail_storage_set_critical(storage,
					"stat(%s) failed: %m", uidlist->fname);
				return -1;
			}
			return 0;
		}

		if (st.st_mtime == uidlist->last_mtime) {
			/* unchanged */
			return 1;
		}
	}

	fd = open(uidlist->fname, O_RDONLY);
	if (fd == -1) {
		if (errno != ENOENT) {
			mail_storage_set_critical(storage,
				"open(%s) failed: %m", uidlist->fname);
			return -1;
		}
		return 0;
	}

	if (fstat(fd, &st) < 0) {
		mail_storage_set_critical(storage,
			"fstat(%s) failed: %m", uidlist->fname);
		return -1;
	}

	if (uidlist->filename_pool == NULL) {
		uidlist->filename_pool =
			pool_alloconly_create("uidlist filename_pool",
					      nearest_power(st.st_size -
							    st.st_size/8));
	}

	rec = buffer_get_data(uidlist->record_buf, &size);
	last_uid = size == 0 ? 0 : rec[(size / sizeof(*rec))-1].uid;

	uidlist->version = 0;

	input = i_stream_create_file(fd, default_pool, 4096, TRUE);

	/* get header */
	line = i_stream_read_next_line(input);
	if (line == NULL || sscanf(line, "%u %u %u", &uidlist->version,
				   &uidlist->uid_validity,
				   &uidlist->next_uid) != 3 ||
	    uidlist->version < 1 || uidlist->version > 2) {
		/* broken file */
                mail_storage_set_critical(storage,
			"Corrupted header in file %s (version = %u)",
			uidlist->fname, uidlist->version);
		ret = 0;
	} else {
		ret = 1;
		while ((line = i_stream_read_next_line(input)) != NULL) {
			if (!maildir_uidlist_next(uidlist, line, last_uid)) {
				ret = 0;
				break;
			}
		}
	}

	if (ret != 0)
		uidlist->last_mtime = st.st_mtime;
	else {
		(void)unlink(uidlist->fname);
                uidlist->last_mtime = 0;
	}

	i_stream_unref(input);
	return ret;
}

static const struct maildir_uidlist_rec *
maildir_uidlist_lookup_rec(struct maildir_uidlist *uidlist, uint32_t uid,
			   unsigned int *idx_r)
{
	const struct maildir_uidlist_rec *rec;
	unsigned int idx, left_idx, right_idx;
	size_t size;

	i_assert(uidlist->last_mtime != 0);

	rec = buffer_get_data(uidlist->record_buf, &size);
	size /= sizeof(*rec);

	idx = 0;
	left_idx = 0;
	right_idx = size;

	while (left_idx < right_idx) {
		idx = (left_idx + right_idx) / 2;

		if (rec[idx].uid < uid)
			left_idx = idx+1;
		else if (rec[idx].uid > uid)
			right_idx = idx;
		else {
			*idx_r = idx;
			return &rec[idx];
		}
	}

	if (idx > 0) idx--;
	*idx_r = idx;
	return NULL;
}

const char *
maildir_uidlist_lookup(struct maildir_uidlist *uidlist, uint32_t uid,
		       enum maildir_uidlist_rec_flag *flags_r)
{
	const struct maildir_uidlist_rec *rec;
	unsigned int idx;

	rec = maildir_uidlist_lookup_rec(uidlist, uid, &idx);
	if (rec == NULL)
		return NULL;

	*flags_r = rec->flags;
	return rec->filename;
}

int maildir_uidlist_is_recent(struct maildir_uidlist *uidlist, uint32_t uid)
{
	enum maildir_uidlist_rec_flag flags;

	if (uidlist->first_recent_uid == 0 || uid < uidlist->first_recent_uid)
		return FALSE;

	if (maildir_uidlist_lookup(uidlist, uid, &flags) == NULL)
		return FALSE;

	i_assert(uidlist->first_recent_uid != uid ||
		 (flags & MAILDIR_UIDLIST_REC_FLAG_RECENT) != 0);
	return (flags & MAILDIR_UIDLIST_REC_FLAG_RECENT) != 0;
}

uint32_t maildir_uidlist_get_recent_count(struct maildir_uidlist *uidlist)
{
	const struct maildir_uidlist_rec *rec;
	unsigned int idx;
	size_t size;
	uint32_t count;

	if (uidlist->first_recent_uid == 0)
		return 0;

	rec = buffer_get_data(uidlist->record_buf, &size);
	size /= sizeof(*rec);

	maildir_uidlist_lookup(uidlist, uidlist->first_recent_uid, &idx);
	for (count = 0; idx < size; idx++) {
		if ((rec[idx].flags & MAILDIR_UIDLIST_REC_FLAG_RECENT) != 0)
			count++;
	}
	return count;
}

static int maildir_uidlist_rewrite_fd(struct maildir_uidlist *uidlist,
				      const char *temp_path)
{
	struct mail_storage *storage = uidlist->ibox->box.storage;
	struct maildir_uidlist_iter_ctx *iter;
	struct utimbuf ut;
	string_t *str;
	uint32_t uid, flags;
	const char *filename, *flags_str;
	int ret = 0;

        uidlist->version = 2;

	str = t_str_new(4096);
	str_printfa(str, "%u %u %u\n", uidlist->version,
		    uidlist->uid_validity, uidlist->next_uid);

	iter = maildir_uidlist_iter_init(uidlist->ibox->uidlist);
	while (maildir_uidlist_iter_next(iter, &uid, &flags, &filename)) {
		if (str_len(str) + MAX_INT_STRLEN +
		    strlen(filename) + 2 >= 4096) {
			/* flush buffer */
			if (write_full(uidlist->lock_fd,
				       str_data(str), str_len(str)) < 0) {
				mail_storage_set_critical(storage,
					"write_full(%s) failed: %m", temp_path);
				ret = -1;
				break;
			}
			str_truncate(str, 0);
		}

		flags_str = (flags & MAILDIR_UIDLIST_REC_FLAG_NEW_DIR) != 0 ?
			"N" : "-";
		str_printfa(str, "%u %s %s\n", uid, flags_str, filename);
	}
	maildir_uidlist_iter_deinit(iter);

	if (ret < 0)
		return -1;

	if (write_full(uidlist->lock_fd, str_data(str), str_len(str)) < 0) {
		mail_storage_set_critical(storage,
			"write_full(%s) failed: %m", temp_path);
		return -1;
	}

	/* uidlist's mtime must grow every time */
	uidlist->last_mtime = ioloop_time <= uidlist->last_mtime ?
		uidlist->last_mtime + 1 : ioloop_time;
	ut.actime = ioloop_time;
	ut.modtime = uidlist->last_mtime;
	if (utime(temp_path, &ut) < 0) {
		mail_storage_set_critical(storage,
			"utime(%s) failed: %m", temp_path);
		return -1;
	}

	if (fsync(uidlist->lock_fd) < 0) {
		mail_storage_set_critical(storage,
			"fsync(%s) failed: %m", temp_path);
		return -1;
	}

	return 0;
}

static int maildir_uidlist_rewrite(struct maildir_uidlist *uidlist)
{
	struct index_mailbox *ibox = uidlist->ibox;
	const char *temp_path, *db_path;
	int ret;

	i_assert(UIDLIST_IS_LOCKED(uidlist));

	temp_path = t_strconcat(ibox->control_dir,
				"/" MAILDIR_UIDLIST_NAME ".lock", NULL);
	ret = maildir_uidlist_rewrite_fd(uidlist, temp_path);

	if (ret == 0) {
		db_path = t_strconcat(ibox->control_dir,
				      "/" MAILDIR_UIDLIST_NAME, NULL);

		if (file_dotlock_replace(db_path, uidlist->lock_fd,
					 FALSE) <= 0) {
			mail_storage_set_critical(ibox->box.storage,
				"file_dotlock_replace(%s) failed: %m", db_path);
			ret = -1;
		}
	} else {
		(void)close(uidlist->lock_fd);
	}
        uidlist->lock_fd = -1;

	if (ret < 0)
		(void)unlink(temp_path);
	return ret;
}

struct maildir_uidlist_sync_ctx *
maildir_uidlist_sync_init(struct maildir_uidlist *uidlist)
{
	struct maildir_uidlist_sync_ctx *ctx;

	ctx = i_new(struct maildir_uidlist_sync_ctx, 1);
	ctx->uidlist = uidlist;
	ctx->filename_pool =
		pool_alloconly_create("maildir_uidlist_sync", 16384);
	ctx->new_record_buf =
		buffer_create_dynamic(default_pool, 512, (size_t)-1);
	ctx->files = hash_create(default_pool, ctx->filename_pool, 4096,
				 maildir_hash, maildir_cmp);

	if (uidlist->last_mtime == 0) {
		/* uidlist not read yet, do it */
		if (maildir_uidlist_update(uidlist) < 0)
			ctx->failed = TRUE;
	}
	return ctx;
}

int maildir_uidlist_sync_next(struct maildir_uidlist_sync_ctx *ctx,
			      const char *filename,
			      enum maildir_uidlist_rec_flag flags)
{
	struct maildir_uidlist_rec *rec;
	char *fname;
	int ret;

	if (ctx->failed)
		return -1;

	rec = hash_lookup(ctx->files, filename);
	if (rec != NULL) {
		if ((rec->flags & (MAILDIR_UIDLIST_REC_FLAG_NEW_DIR |
				   MAILDIR_UIDLIST_REC_FLAG_MOVED)) == 0) {
			/* possibly duplicate */
			return 0;
		}

		rec->flags &= ~(MAILDIR_UIDLIST_REC_FLAG_NEW_DIR |
				MAILDIR_UIDLIST_REC_FLAG_MOVED);
	} else {
		rec = hash_lookup(ctx->uidlist->files, filename);
		if (rec == NULL && !ctx->synced) {
			/* lock and update uidlist to see if it's just
			   been added */
			ret = maildir_uidlist_try_lock(ctx->uidlist);
			if (ret <= 0) {
				if (ret == 0)
					return 1; // FIXME: does it work right?
				ctx->failed = TRUE;
				return -1;
			}
			if (maildir_uidlist_update(ctx->uidlist) < 0) {
				ctx->failed = TRUE;
				return -1;
			}

			ctx->synced = TRUE;
			rec = hash_lookup(ctx->uidlist->files, filename);
		}

		if (rec == NULL) {
			ctx->new_files = TRUE;
			rec = buffer_append_space_unsafe(ctx->new_record_buf,
							 sizeof(*rec));
			memset(rec, 0, sizeof(*rec));
		}
	}

	rec->flags |= flags;

	fname = p_strdup(ctx->filename_pool, filename);
	if (rec->filename == NULL)
		rec->filename = fname;
	hash_insert(ctx->files, fname, rec);
	return 1;
}

static int maildir_time_cmp(const void *p1, const void *p2)
{
	const struct maildir_uidlist_rec *rec1 = p1, *rec2 = p2;
	const char *s1 = rec1->filename, *s2 = rec2->filename;
	time_t t1 = 0, t2 = 0;

	/* we have to do numeric comparision, strcmp() will break when
	   there's different amount of digits (mostly the 999999999 ->
	   1000000000 change in Sep 9 2001) */
	while (*s1 >= '0' && *s1 <= '9') {
		t1 = t1*10 + (*s1 - '0');
		s1++;
	}
	while (*s2 >= '0' && *s2 <= '9') {
		t2 = t2*10 + (*s2 - '0');
		s2++;
	}

	return t1 < t2 ? -1 : t1 > t2 ? 1 : 0;
}

static void maildir_uidlist_swap(struct maildir_uidlist_sync_ctx *ctx)
{
	struct maildir_uidlist *uidlist = ctx->uidlist;
	struct maildir_uidlist_rec *rec;
	void *key, *value;
	size_t size;
	unsigned int src, dest;

	/* @UNSAFE */

	rec = buffer_get_modifyable_data(uidlist->record_buf, &size);
	size /= sizeof(*rec);

	/* update filename pointers, skip deleted messages */
	for (dest = src = 0; src < size; src++) {
		if (!hash_lookup_full(ctx->files, rec[src].filename,
				      &key, &value))
			continue;

		rec[dest].uid = rec[src].uid;
		rec[dest].flags =
			rec[src].flags & ~MAILDIR_UIDLIST_REC_FLAG_MOVED;
		rec[dest].filename = key;
		if ((rec[dest].flags & MAILDIR_UIDLIST_REC_FLAG_RECENT) != 0) {
			maildir_uidlist_mark_recent(ctx->uidlist,
						    rec[dest].uid);
		}
		dest++;
	}
	buffer_set_used_size(uidlist->record_buf, dest * sizeof(*rec));

	buffer_append_buf(uidlist->record_buf, ctx->new_record_buf,
			  0, (size_t)-1);

	rec = buffer_get_modifyable_data(uidlist->record_buf, &size);
	size /= sizeof(*rec);

	/* sort new files and assign UIDs for them */
	qsort(rec + dest, size - dest, sizeof(*rec), maildir_time_cmp);
	for (; dest < size; dest++) {
		rec[dest].uid = uidlist->next_uid++;
		rec[dest].flags &= ~MAILDIR_UIDLIST_REC_FLAG_MOVED;

		if ((rec[dest].flags & MAILDIR_UIDLIST_REC_FLAG_RECENT) != 0) {
			maildir_uidlist_mark_recent(ctx->uidlist,
						    rec[dest].uid);
		}
	}

	hash_destroy(uidlist->files);
	uidlist->files = ctx->files;
	ctx->files = NULL;

	if (uidlist->filename_pool != NULL)
		pool_unref(uidlist->filename_pool);
	uidlist->filename_pool = ctx->filename_pool;
	ctx->filename_pool = NULL;
}

int maildir_uidlist_sync_deinit(struct maildir_uidlist_sync_ctx *ctx)
{
	int ret;

	if (ctx->failed)
		ret = -1;
	else {
		maildir_uidlist_swap(ctx);
		if (!ctx->new_files)
			ret = 0;
		else
			ret = maildir_uidlist_rewrite(ctx->uidlist);
	}

	if (UIDLIST_IS_LOCKED(ctx->uidlist))
		maildir_uidlist_unlock(ctx->uidlist);

	if (ctx->files != NULL)
		hash_destroy(ctx->files);
	if (ctx->filename_pool != NULL)
		pool_unref(ctx->filename_pool);
	buffer_free(ctx->new_record_buf);
	i_free(ctx);
	return ret;
}

struct maildir_uidlist_iter_ctx *
maildir_uidlist_iter_init(struct maildir_uidlist *uidlist)
{
	struct maildir_uidlist_iter_ctx *ctx;
	size_t size;

	ctx = i_new(struct maildir_uidlist_iter_ctx, 1);
	ctx->next = buffer_get_data(uidlist->record_buf, &size);
	size /= sizeof(*ctx->next);
	ctx->end = ctx->next + size;
	return ctx;
}

int maildir_uidlist_iter_next(struct maildir_uidlist_iter_ctx *ctx,
			      uint32_t *uid_r, uint32_t *flags_r,
			      const char **filename_r)
{
	if (ctx->next == ctx->end)
		return 0;

	*uid_r = ctx->next->uid;
	*flags_r = ctx->next->flags;
	*filename_r = ctx->next->filename;
	ctx->next++;
	return 1;
}

void maildir_uidlist_iter_deinit(struct maildir_uidlist_iter_ctx *ctx)
{
	i_free(ctx);
}