/* Copyright (c) Dovecot authors, see top-level COPYING file */

#include "lib.h"
#include "eacces-error.h"
#include "restrict-access.h"
#include "nfs-workarounds.h"
#include "ipwd.h"
#include "mail-index-private.h"
#include "mbox-storage.h"
#include "istream-raw-mbox.h"
#include "mbox-file.h"
#include "mbox-lock.h"
#include "settings.h"

#include <time.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>

#ifdef HAVE_FLOCK
#  include <sys/file.h>
#endif

/* 0.1 .. 0.2msec */
#define LOCK_RANDOM_USLEEP_TIME (100000 + (unsigned int)i_rand() % 100000)

enum mbox_lock_type {
	MBOX_LOCK_DOTLOCK,
	MBOX_LOCK_DOTLOCK_TRY,
	MBOX_LOCK_FCNTL,
	MBOX_LOCK_FLOCK,
	MBOX_LOCK_LOCKF,

	MBOX_LOCK_COUNT
};

enum mbox_dotlock_op {
	MBOX_DOTLOCK_OP_LOCK,
	MBOX_DOTLOCK_OP_UNLOCK,
	MBOX_DOTLOCK_OP_TOUCH
};

struct mbox_lock_context {
	struct mbox_mailbox *mbox;
	bool locked_status[MBOX_LOCK_COUNT];
	bool checked_file;

	int lock_type;
	bool dotlock_last_stale;
	bool fcntl_locked;
	bool using_privileges;
};

struct mbox_lock_data {
	enum mbox_lock_type type;
	const char *name;
	int (*func)(struct mbox_lock_context *ctx, int lock_type,
		    time_t max_wait_time);
};

static int mbox_lock_dotlock(struct mbox_lock_context *ctx, int lock_type,
			     time_t max_wait_time);
static int mbox_lock_dotlock_try(struct mbox_lock_context *ctx, int lock_type,
				 time_t max_wait_time);
static int mbox_lock_fcntl(struct mbox_lock_context *ctx, int lock_type,
			   time_t max_wait_time);
#ifdef HAVE_FLOCK
static int mbox_lock_flock(struct mbox_lock_context *ctx, int lock_type,
			   time_t max_wait_time);
#else
#  define mbox_lock_flock NULL
#endif
static int mbox_lock_lockf(struct mbox_lock_context *ctx, int lock_type,
			   time_t max_wait_time);

static struct mbox_lock_data lock_data[] = {
	{ MBOX_LOCK_DOTLOCK, "dotlock", mbox_lock_dotlock },
	{ MBOX_LOCK_DOTLOCK_TRY, "dotlock_try", mbox_lock_dotlock_try },
	{ MBOX_LOCK_FCNTL, "fcntl", mbox_lock_fcntl },
	{ MBOX_LOCK_FLOCK, "flock", mbox_lock_flock },
	{ MBOX_LOCK_LOCKF, "lockf", mbox_lock_lockf },
	{ 0, NULL, NULL }
};

static int ATTR_NOWARN_UNUSED_RESULT
mbox_lock_list(struct mbox_lock_context *ctx, int lock_type,
	       time_t max_wait_time, int idx);
static int ATTR_NOWARN_UNUSED_RESULT
mbox_unlock_files(struct mbox_lock_context *ctx);

static void mbox_read_lock_methods(const ARRAY_TYPE(const_string) *list, const char *env,
				   enum mbox_lock_type *locks)
{
	enum mbox_lock_type type;
	const char *const *lock;
	int i, dest;

	for (lock = settings_boollist_get(list), dest = 0; *lock != NULL; lock++) {
		for (type = 0; lock_data[type].name != NULL; type++) {
			if (strcasecmp(*lock, lock_data[type].name) == 0) {
				type = lock_data[type].type;
				break;
			}
		}
		if (lock_data[type].name == NULL)
			i_fatal("%s: Invalid value %s", env, *lock);
		if (lock_data[type].func == NULL) {
			i_fatal("%s: Support for lock type %s "
				"not compiled into binary", env, *lock);
		}

		for (i = 0; i < dest; i++) {
			if (locks[i] == type)
				i_fatal("%s: Duplicated value %s", env, *lock);
		}

		/* @UNSAFE */
		locks[dest++] = type;
	}
	locks[dest] = (enum mbox_lock_type)-1;
}

static void mbox_init_lock_settings(struct mbox_storage *storage)
{
	enum mbox_lock_type read_locks[MBOX_LOCK_COUNT+1];
	enum mbox_lock_type write_locks[MBOX_LOCK_COUNT+1];
	int r, w;

	mbox_read_lock_methods(&storage->set->mbox_read_locks,
			       "mbox_read_locks", read_locks);
	mbox_read_lock_methods(&storage->set->mbox_write_locks,
			       "mbox_write_locks", write_locks);

	/* check that read/write list orders match. write_locks must contain
	   at least read_locks and possibly more. */
	for (r = w = 0; write_locks[w] != (enum mbox_lock_type)-1; w++) {
		if (read_locks[r] == (enum mbox_lock_type)-1)
			break;
		if (read_locks[r] == write_locks[w])
			r++;
	}
	if (read_locks[r] != (enum mbox_lock_type)-1) {
		i_fatal("mbox read/write lock list settings are invalid. "
			"Lock ordering must be the same with both, "
			"and write locks must contain all read locks "
			"(and possibly more)");
	}

	storage->read_locks = p_new(storage->storage.pool,
				    enum mbox_lock_type, MBOX_LOCK_COUNT+1);
	memcpy(storage->read_locks, read_locks,
	       sizeof(*storage->read_locks) * (MBOX_LOCK_COUNT+1));

	storage->write_locks = p_new(storage->storage.pool,
				     enum mbox_lock_type, MBOX_LOCK_COUNT+1);
	memcpy(storage->write_locks, write_locks,
	       sizeof(*storage->write_locks) * (MBOX_LOCK_COUNT+1));

	storage->lock_settings_initialized = TRUE;
}

static int mbox_file_open_latest(struct mbox_lock_context *ctx, int lock_type)
{
	struct mbox_mailbox *mbox = ctx->mbox;
	struct stat st;

	if (ctx->checked_file || lock_type == F_UNLCK)
		return 0;

	if (mbox->mbox_fd != -1) {
		/* we could flush NFS file handle cache here if we wanted to
		   be sure that the file is latest, but mbox files get rarely
		   deleted and the flushing might cause errors (e.g. EBUSY for
		   trying to flush a /var/mail mountpoint) */
		if (nfs_safe_stat(mailbox_get_path(&mbox->box), &st) < 0) {
			if (errno == ENOENT)
				mailbox_set_deleted(&mbox->box);
			else
				mbox_set_syscall_error(mbox, "stat()");
			return -1;
		}

		if (st.st_ino != mbox->mbox_ino ||
		    !CMP_DEV_T(st.st_dev, mbox->mbox_dev))
			mbox_file_close(mbox);
	}

	if (mbox->mbox_fd == -1) {
		if (mbox_file_open(mbox) < 0)
			return -1;
	}

	ctx->checked_file = TRUE;
	return 0;
}

static bool dotlock_callback(unsigned int secs_left, bool stale, void *context)
{
	struct mbox_lock_context *ctx = context;
	enum mbox_lock_type *lock_types;
	int i;

	if (ctx->using_privileges)
		restrict_access_drop_priv_gid();

	if (stale && !ctx->dotlock_last_stale) {
		/* get next index we wish to try locking. it's the one after
		   dotlocking. */
		lock_types = ctx->lock_type == F_WRLCK ||
			(ctx->lock_type == F_UNLCK &&
			 ctx->mbox->mbox_lock_type == F_WRLCK) ?
			ctx->mbox->storage->write_locks :
			ctx->mbox->storage->read_locks;

		for (i = 0; lock_types[i] != (enum mbox_lock_type)-1; i++) {
			if (lock_types[i] == MBOX_LOCK_DOTLOCK)
				break;
		}

		if (lock_types[i] != (enum mbox_lock_type)-1 &&
		    lock_types[i+1] != (enum mbox_lock_type)-1) {
			i++;
			if (mbox_lock_list(ctx, ctx->lock_type, 0, i) <= 0) {
				/* we couldn't get fd lock -
				   it's really locked */
				ctx->dotlock_last_stale = TRUE;
				return FALSE;
			}
			mbox_lock_list(ctx, F_UNLCK, 0, i);
		}
	}
	ctx->dotlock_last_stale = stale;

	index_storage_lock_notify(&ctx->mbox->box, stale ?
				  MAILBOX_LOCK_NOTIFY_MAILBOX_OVERRIDE :
				  MAILBOX_LOCK_NOTIFY_MAILBOX_ABORT,
				  secs_left);
	if (ctx->using_privileges) {
		if (restrict_access_use_priv_gid() < 0) {
			/* shouldn't get here */
			return FALSE;
		}
	}
	return TRUE;
}

static int ATTR_NULL(2) ATTR_NOWARN_UNUSED_RESULT
mbox_dotlock_privileged_op(struct mbox_mailbox *mbox,
			   struct dotlock_settings *set,
			   enum mbox_dotlock_op op)
{
	const char *box_path, *dir, *fname;
	int ret = -1, orig_dir_fd, orig_errno;

	orig_dir_fd = open(".", O_RDONLY);
	if (orig_dir_fd == -1) {
		mailbox_set_critical(&mbox->box, "open(.) failed: %m");
		return -1;
	}

	/* allow dotlocks to be created only for files we can read while we're
	   unprivileged. to make sure there are no race conditions we first
	   have to chdir to the mbox file's directory and then use relative
	   paths. unless this is done, users could:
	    - create *.lock files to any directory writable by the
	      privileged group
	    - DoS other users by dotlocking their mailboxes infinitely
	*/
	box_path = mailbox_get_path(&mbox->box);
	fname = strrchr(box_path, '/');
	if (fname == NULL) {
		/* already relative */
		fname = box_path;
	} else {
		dir = t_strdup_until(box_path, fname);
		if (chdir(dir) < 0) {
			mailbox_set_critical(&mbox->box,
				"chdir(%s) failed: %m", dir);
			i_close_fd(&orig_dir_fd);
			return -1;
		}
		fname++;
	}
	if (op == MBOX_DOTLOCK_OP_LOCK) {
		if (i_faccessat2(AT_FDCWD, fname, R_OK, AT_EACCESS) < 0) {
			mailbox_set_critical(&mbox->box,
				"access(%s) failed: %m", box_path);
			i_close_fd(&orig_dir_fd);
			return -1;
		}
	}

	if (restrict_access_use_priv_gid() < 0) {
		i_close_fd(&orig_dir_fd);
		return -1;
	}

	switch (op) {
	case MBOX_DOTLOCK_OP_LOCK:
		/* we're now privileged - avoid doing as much as possible */
		ret = file_dotlock_create(set, fname, 0, &mbox->mbox_dotlock);
		if (ret > 0)
			mbox->mbox_used_privileges = TRUE;
		else if (ret < 0 && ENOACCESS(errno)) {
			const char *errmsg =
				eacces_error_get_creating("file_dotlock_create",
							  fname);
			mailbox_set_critical(&mbox->box, "%s", errmsg);
		} else {
			mbox_set_syscall_error(mbox, "file_dotlock_create()");
		}
		break;
	case MBOX_DOTLOCK_OP_UNLOCK:
		/* we're now privileged - avoid doing as much as possible */
		ret = file_dotlock_delete(&mbox->mbox_dotlock);
		if (ret < 0)
			mbox_set_syscall_error(mbox, "file_dotlock_delete()");
		mbox->mbox_used_privileges = FALSE;
		break;
	case MBOX_DOTLOCK_OP_TOUCH:
		ret = file_dotlock_touch(mbox->mbox_dotlock);
		if (ret < 0)
			mbox_set_syscall_error(mbox, "file_dotlock_touch()");
		break;
	}

	orig_errno = errno;
	restrict_access_drop_priv_gid();

	if (fchdir(orig_dir_fd) < 0) {
		mailbox_set_critical(&mbox->box, "fchdir() failed: %m");
	}
	i_close_fd(&orig_dir_fd);
	errno = orig_errno;
	return ret;
}

static void
mbox_dotlock_log_eacces_error(struct mbox_mailbox *mbox, const char *path)
{
	const char *dir, *errmsg, *name;
	struct stat st;
	struct group group;
	int orig_errno = errno;

	errmsg = eacces_error_get_creating("file_dotlock_create", path);
	dir = strrchr(path, '/');
	dir = dir == NULL ? "." : t_strdup_until(path, dir);
	/* allow privileged locking for
	   a) user's own INBOX,
	   b) another user's shared INBOX, and
	   c) anything called INBOX (in inbox=no namespace) */
	if (!mbox->box.inbox_any && strcmp(mbox->box.name, "INBOX") != 0) {
		mailbox_set_critical(&mbox->box,
			"%s (not INBOX -> no privileged locking)", errmsg);
	} else if (!mbox->mbox_privileged_locking) {
		dir = mailbox_list_get_root_forced(mbox->box.list,
						   MAILBOX_LIST_PATH_TYPE_DIR);
		mailbox_set_critical(&mbox->box,
			"%s (under root dir %s -> no privileged locking)",
			errmsg, dir);
	} else if (stat(dir, &st) == 0 &&
		   (st.st_mode & 02) == 0 && /* not world-writable */
		   (st.st_mode & 020) != 0) { /* group-writable */
		if (i_getgrgid(st.st_gid, &group) <= 0)
			name = dec2str(st.st_gid);
		else
			name = group.gr_name;
		mailbox_set_critical(&mbox->box,
			"%s (set mail_privileged_group=%s)", errmsg, name);
	} else {
		mailbox_set_critical(&mbox->box,
			"%s (nonstandard permissions in %s)", errmsg, dir);
	}
	errno = orig_errno;
}

static int
mbox_lock_dotlock_int(struct mbox_lock_context *ctx, int lock_type, bool try)
{
	struct mbox_mailbox *mbox = ctx->mbox;
	struct dotlock_settings set;
	int ret;

	if (lock_type == F_UNLCK) {
		if (!mbox->mbox_dotlocked)
			return 1;

		if (!mbox->mbox_used_privileges) {
			if (file_dotlock_delete(&mbox->mbox_dotlock) <= 0) {
				mbox_set_syscall_error(mbox,
						       "file_dotlock_delete()");
			}
		} else {
			ctx->using_privileges = TRUE;
			mbox_dotlock_privileged_op(mbox, NULL,
						   MBOX_DOTLOCK_OP_UNLOCK);
			ctx->using_privileges = FALSE;
		}
		mbox->mbox_dotlocked = FALSE;
		return 1;
	}

	if (mbox->mbox_dotlocked)
		return 1;

	ctx->dotlock_last_stale = TRUE;

	i_zero(&set);
	set.use_excl_lock = mbox->storage->storage.set->dotlock_use_excl;
	set.nfs_flush = mbox->storage->storage.set->mail_nfs_storage;
	set.timeout = mail_storage_get_lock_timeout(&mbox->storage->storage,
				mbox->storage->set->mbox_lock_timeout);
	set.stale_timeout = mbox->storage->set->mbox_dotlock_change_timeout;
	set.callback = dotlock_callback;
	set.context = ctx;

	ret = file_dotlock_create(&set, mailbox_get_path(&mbox->box), 0,
				  &mbox->mbox_dotlock);
	if (ret >= 0) {
		/* success / timeout */
	} else if (ENOACCESS(errno) && restrict_access_have_priv_gid() &&
		   mbox->mbox_privileged_locking) {
		/* try again, this time with extra privileges */
		ret = mbox_dotlock_privileged_op(mbox, &set,
						 MBOX_DOTLOCK_OP_LOCK);
	} else if (ENOACCESS(errno))
		mbox_dotlock_log_eacces_error(mbox, mailbox_get_path(&mbox->box));
	else
		mbox_set_syscall_error(mbox, "file_dotlock_create()");

	if (ret < 0) {
		if ((ENOSPACE(errno) || ENOACCESS(errno)) && try)
			return 1;
		return -1;
	}
	if (ret == 0) {
		mail_storage_set_error(&mbox->storage->storage,
			MAIL_ERROR_TEMP, MAIL_ERRSTR_LOCK_TIMEOUT);
		return 0;
	}
	mbox->mbox_dotlocked = TRUE;

	if (mbox_file_open_latest(ctx, lock_type) < 0)
		return -1;
	return 1;
}

static int mbox_lock_dotlock(struct mbox_lock_context *ctx, int lock_type,
			     time_t max_wait_time ATTR_UNUSED)
{
	return mbox_lock_dotlock_int(ctx, lock_type, FALSE);
}

static int mbox_lock_dotlock_try(struct mbox_lock_context *ctx, int lock_type,
				 time_t max_wait_time ATTR_UNUSED)
{
	return mbox_lock_dotlock_int(ctx, lock_type, TRUE);
}

#ifdef HAVE_FLOCK
static int mbox_lock_flock(struct mbox_lock_context *ctx, int lock_type,
			   time_t max_wait_time)
{
	time_t now;
	unsigned int next_alarm;

	if (mbox_file_open_latest(ctx, lock_type) < 0)
		return -1;

	if (lock_type == F_UNLCK && ctx->mbox->mbox_fd == -1)
		return 1;

	if (lock_type == F_WRLCK)
		lock_type = LOCK_EX;
	else if (lock_type == F_RDLCK)
		lock_type = LOCK_SH;
	else
		lock_type = LOCK_UN;

	if (max_wait_time == 0) {
		/* usually we're waiting here, but if we came from
		   mbox_lock_dotlock(), we just want to try locking */
		lock_type |= LOCK_NB;
	} else {
		now = time(NULL);
		if (now >= max_wait_time)
			alarm(1);
		else
			alarm(I_MIN(max_wait_time - now, 5));
	}

	while (flock(ctx->mbox->mbox_fd, lock_type) < 0) {
		if (errno != EINTR) {
			if (errno == EWOULDBLOCK && max_wait_time == 0) {
				/* non-blocking lock trying failed */
				return 0;
			}
			alarm(0);
			mbox_set_syscall_error(ctx->mbox, "flock()");
			return -1;
		}

		now = time(NULL);
		if (now >= max_wait_time) {
			alarm(0);
			return 0;
		}

		/* notify locks once every 5 seconds.
		   try to use rounded values. */
		next_alarm = (max_wait_time - now) % 5;
		if (next_alarm == 0)
			next_alarm = 5;
		alarm(next_alarm);

		index_storage_lock_notify(&ctx->mbox->box,
					  MAILBOX_LOCK_NOTIFY_MAILBOX_ABORT,
					  max_wait_time - now);
	}

	alarm(0);
	return 1;
}
#endif

static int mbox_lock_lockf(struct mbox_lock_context *ctx, int lock_type,
			   time_t max_wait_time)
{
	time_t now;
	unsigned int next_alarm;

	if (mbox_file_open_latest(ctx, lock_type) < 0)
		return -1;

	if (lock_type == F_UNLCK && ctx->mbox->mbox_fd == -1)
		return 1;

	if (lock_type == F_UNLCK)
		lock_type = F_ULOCK;
	else if (max_wait_time == 0) {
		/* usually we're waiting here, but if we came from
		   mbox_lock_dotlock(), we just want to try locking */
		lock_type = F_TLOCK;
	} else {
		now = time(NULL);
		if (now >= max_wait_time)
			alarm(1);
		else
			alarm(I_MIN(max_wait_time - now, 5));
		lock_type = F_LOCK;
	}

	while (lockf(ctx->mbox->mbox_fd, lock_type, 0) < 0) {
		if (errno != EINTR) {
			if ((ENOACCESS(errno) || errno == EAGAIN) &&
			    max_wait_time == 0) {
				/* non-blocking lock trying failed */
				return 0;
			}
			alarm(0);
			mbox_set_syscall_error(ctx->mbox, "lockf()");
			return -1;
		}

		now = time(NULL);
		if (now >= max_wait_time) {
			alarm(0);
			return 0;
		}

		/* notify locks once every 5 seconds.
		   try to use rounded values. */
		next_alarm = (max_wait_time - now) % 5;
		if (next_alarm == 0)
			next_alarm = 5;
		alarm(next_alarm);

		index_storage_lock_notify(&ctx->mbox->box,
					  MAILBOX_LOCK_NOTIFY_MAILBOX_ABORT,
					  max_wait_time - now);
	}

	alarm(0);
	return 1;
}

static int mbox_lock_fcntl(struct mbox_lock_context *ctx, int lock_type,
			   time_t max_wait_time)
{
	struct flock fl;
	time_t now;
	unsigned int next_alarm;
	int wait_type;

	if (mbox_file_open_latest(ctx, lock_type) < 0)
		return -1;

	if (lock_type == F_UNLCK && ctx->mbox->mbox_fd == -1)
		return 1;

	i_zero(&fl);
	fl.l_type = lock_type;
	fl.l_whence = SEEK_SET;
	fl.l_start = 0;
	fl.l_len = 0;

	if (max_wait_time == 0) {
		/* usually we're waiting here, but if we came from
		   mbox_lock_dotlock(), we just want to try locking */
		wait_type = F_SETLK;
	} else {
		wait_type = F_SETLKW;
		now = time(NULL);
		if (now >= max_wait_time)
			alarm(1);
		else
			alarm(I_MIN(max_wait_time - now, 5));
	}

	while (fcntl(ctx->mbox->mbox_fd, wait_type, &fl) < 0) {
		if (errno != EINTR) {
			if ((ENOACCESS(errno) || errno == EAGAIN) &&
			    wait_type == F_SETLK) {
				/* non-blocking lock trying failed */
				return 0;
			}
			alarm(0);
			if (errno != EACCES) {
				mbox_set_syscall_error(ctx->mbox, "fcntl()");
				return -1;
			}
			mailbox_set_critical(&ctx->mbox->box,
				"fcntl() failed with mbox file %s: "
				"File is locked by another process (EACCES)",
				mailbox_get_path(&ctx->mbox->box));
			return -1;
		}

		now = time(NULL);
		if (now >= max_wait_time) {
			alarm(0);
			return 0;
		}

		/* notify locks once every 5 seconds.
		   try to use rounded values. */
		next_alarm = (max_wait_time - now) % 5;
		if (next_alarm == 0)
			next_alarm = 5;
		alarm(next_alarm);

		index_storage_lock_notify(&ctx->mbox->box,
					  MAILBOX_LOCK_NOTIFY_MAILBOX_ABORT,
					  max_wait_time - now);
	}

	alarm(0);
	ctx->fcntl_locked = TRUE;
	return 1;
}

static int ATTR_NOWARN_UNUSED_RESULT
mbox_lock_list(struct mbox_lock_context *ctx, int lock_type,
	       time_t max_wait_time, int idx)
{
	enum mbox_lock_type *lock_types;
	enum mbox_lock_type type;
	int i, ret = 0;
	bool locked_status;

	ctx->lock_type = lock_type;

	lock_types = lock_type == F_WRLCK ||
		(lock_type == F_UNLCK && ctx->mbox->mbox_lock_type == F_WRLCK) ?
		ctx->mbox->storage->write_locks :
		ctx->mbox->storage->read_locks;
	for (i = idx; lock_types[i] != (enum mbox_lock_type)-1; i++) {
		type = lock_types[i];
		locked_status = lock_type != F_UNLCK;

		if (ctx->locked_status[type] == locked_status)
			continue;
		ctx->locked_status[type] = locked_status;

		ret = lock_data[type].func(ctx, lock_type, max_wait_time);
		if (ret <= 0)
			break;
	}
	return ret;
}

static int mbox_update_locking(struct mbox_mailbox *mbox, int lock_type,
			       bool *fcntl_locked_r)
{
	struct mbox_lock_context ctx;
	time_t max_wait_time;
	int ret, i;
	bool drop_locks;

	*fcntl_locked_r = FALSE;

	index_storage_lock_notify_reset(&mbox->box);

	if (!mbox->storage->lock_settings_initialized)
		mbox_init_lock_settings(mbox->storage);

	if (mbox->mbox_fd == -1 && mbox->mbox_file_stream != NULL) {
		/* read-only mbox stream. no need to lock. */
		i_assert(mbox_is_backend_readonly(mbox));
		mbox->mbox_lock_type = lock_type;
		return 1;
	}

	max_wait_time = time(NULL) +
		mail_storage_get_lock_timeout(&mbox->storage->storage,
			mbox->storage->set->mbox_lock_timeout);

	i_zero(&ctx);
	ctx.mbox = mbox;

	if (mbox->mbox_lock_type == F_WRLCK) {
		/* dropping to shared lock. first drop those that we
		   don't remove completely. */
		const enum mbox_lock_type *read_locks =
			mbox->storage->read_locks;

		for (i = 0; i < MBOX_LOCK_COUNT; i++)
			ctx.locked_status[i] = TRUE;
		for (i = 0; read_locks[i] != (enum mbox_lock_type)-1; i++)
			ctx.locked_status[read_locks[i]] = FALSE;
		drop_locks = TRUE;
	} else {
		drop_locks = FALSE;
	}

	mbox->mbox_lock_type = lock_type;
	ret = mbox_lock_list(&ctx, lock_type, max_wait_time, 0);
	if (ret <= 0) {
		if (!drop_locks)
			mbox_unlock_files(&ctx);
		if (ret == 0) {
			mail_storage_set_error(&mbox->storage->storage,
				MAIL_ERROR_TEMP, MAIL_ERRSTR_LOCK_TIMEOUT);
		}
		return ret;
	}

	if (drop_locks) {
		/* dropping to shared lock: drop the locks that are only
		   in write list */
		const enum mbox_lock_type *read_locks =
			mbox->storage->read_locks;
		const enum mbox_lock_type *write_locks =
			mbox->storage->write_locks;

		memset(ctx.locked_status, 0, sizeof(ctx.locked_status));
		for (i = 0; write_locks[i] != (enum mbox_lock_type)-1; i++)
			ctx.locked_status[write_locks[i]] = TRUE;
		for (i = 0; read_locks[i] != (enum mbox_lock_type)-1; i++)
			ctx.locked_status[read_locks[i]] = FALSE;

		mbox->mbox_lock_type = F_WRLCK;
		mbox_lock_list(&ctx, F_UNLCK, 0, 0);
		mbox->mbox_lock_type = F_RDLCK;
	}

	*fcntl_locked_r = ctx.fcntl_locked;
	return 1;
}

int mbox_lock(struct mbox_mailbox *mbox, int lock_type,
	      unsigned int *lock_id_r)
{
	const char *path = mailbox_get_path(&mbox->box);
	int mbox_fd = mbox->mbox_fd;
	bool fcntl_locked;
	int ret;

	if (lock_type == F_RDLCK && mbox->external_transactions > 0 &&
	    mbox->mbox_lock_type != F_RDLCK) {
		/* we have a transaction open that is going to save mails
		   and apparently also wants to read from the same mailbox
		   (copy, move, catenate). we need to write lock the mailbox,
		   since we can't later upgrade a read lock to write lock. */
		lock_type = F_WRLCK;
	}

	/* allow only unlock -> shared/exclusive or exclusive -> shared */
	i_assert(lock_type == F_RDLCK || lock_type == F_WRLCK);
	i_assert(lock_type == F_RDLCK || mbox->mbox_lock_type != F_RDLCK);

	if (mbox->mbox_lock_type == F_UNLCK) {
		ret = mbox_update_locking(mbox, lock_type, &fcntl_locked);
		if (ret <= 0)
			return ret;

		if (mbox->storage->storage.set->mail_nfs_storage) {
			if (fcntl_locked) {
				nfs_flush_attr_cache_fd_locked(path, mbox_fd);
				nfs_flush_read_cache_locked(path, mbox_fd);
			} else {
				nfs_flush_attr_cache_unlocked(path);
				nfs_flush_read_cache_unlocked(path, mbox_fd);
			}
		}

		mbox->mbox_lock_id += 2;
	}

	if (lock_type == F_RDLCK) {
		mbox->mbox_shared_locks++;
		*lock_id_r = mbox->mbox_lock_id;
	} else {
		mbox->mbox_excl_locks++;
		*lock_id_r = mbox->mbox_lock_id + 1;
	}
	if (mbox->mbox_stream != NULL)
		istream_raw_mbox_set_locked(mbox->mbox_stream);
	return 1;
}

static int mbox_unlock_files(struct mbox_lock_context *ctx)
{
	int ret = 0;

	if (mbox_lock_list(ctx, F_UNLCK, 0, 0) < 0)
		ret = -1;

	ctx->mbox->mbox_lock_id += 2;
	ctx->mbox->mbox_lock_type = F_UNLCK;
	return ret;
}

int mbox_unlock(struct mbox_mailbox *mbox, unsigned int lock_id)
{
	struct mbox_lock_context ctx;
	bool fcntl_locked;
	int i;

	i_assert(mbox->mbox_lock_id == (lock_id & ~1U));

	if ((lock_id & 1) != 0) {
		/* dropping exclusive lock */
		i_assert(mbox->mbox_excl_locks > 0);
		if (--mbox->mbox_excl_locks > 0)
			return 0;
		if (mbox->mbox_shared_locks > 0) {
			/* drop to shared lock */
			if (mbox_update_locking(mbox, F_RDLCK,
						&fcntl_locked) < 0)
				return -1;
			return 0;
		}
	} else {
		/* dropping shared lock */
		i_assert(mbox->mbox_shared_locks > 0);
		if (--mbox->mbox_shared_locks > 0)
			return 0;
		if (mbox->mbox_excl_locks > 0)
			return 0;
	}
	/* all locks gone */

	/* make sure we don't read the stream while unlocked */
	if (mbox->mbox_stream != NULL)
		istream_raw_mbox_set_unlocked(mbox->mbox_stream);

	i_zero(&ctx);
	ctx.mbox = mbox;

	for (i = 0; i < MBOX_LOCK_COUNT; i++)
		ctx.locked_status[i] = TRUE;

	return mbox_unlock_files(&ctx);
}

unsigned int mbox_get_cur_lock_id(struct mbox_mailbox *mbox)
{
	return mbox->mbox_lock_id +
		(mbox->mbox_excl_locks > 0 ? 1 : 0);
}

void mbox_dotlock_touch(struct mbox_mailbox *mbox)
{
	if (mbox->mbox_dotlock == NULL)
		return;

	if (!mbox->mbox_used_privileges)
		(void)file_dotlock_touch(mbox->mbox_dotlock);
	else {
		mbox_dotlock_privileged_op(mbox, NULL,
						 MBOX_DOTLOCK_OP_TOUCH);
	}
}
