<commit_before>/* Copyright (c) 2011 Dovecot authors, see the included COPYING file */

#include "lib.h"
#include "ioloop.h"
#include "imap-arg.h"
#include "imap-util.h"
#include "imapc-client.h"
#include "imapc-mail.h"
#include "imapc-msgmap.h"
#include "imapc-sync.h"
#include "imapc-storage.h"

#define NOTIFY_DELAY_MSECS 500

static void imapc_mailbox_set_corrupted(struct imapc_mailbox *mbox,
					const char *reason, ...)
{
	va_list va;

	va_start(va, reason);
	i_error("imapc: Mailbox '%s' state corrupted: %s",
		mbox->box.name, t_strdup_vprintf(reason, va));
	va_end(va);

	if (!mbox->initial_sync_done) {
		/* we failed during initial sync. need to rebuild indexes if
		   we want to get this fixed */
		mail_index_mark_corrupted(mbox->box.index);
	} else {
		/* maybe the remote server is buggy and has become confused.
		   try reconnecting. */
	}
	imapc_client_mailbox_disconnect(mbox->client_box);
}

static struct mail_index_view *
imapc_mailbox_get_sync_view(struct imapc_mailbox *mbox)
{
	if (mbox->sync_view == NULL)
		mbox->sync_view = mail_index_view_open(mbox->box.index);
	return mbox->sync_view;
}

static void imapc_mailbox_init_delayed_trans(struct imapc_mailbox *mbox)
{
	if (mbox->delayed_sync_trans != NULL)
		return;

	mbox->delayed_sync_trans =
		mail_index_transaction_begin(imapc_mailbox_get_sync_view(mbox),
					MAIL_INDEX_TRANSACTION_FLAG_EXTERNAL);
	mbox->delayed_sync_view =
		mail_index_transaction_open_updated_view(mbox->delayed_sync_trans);
}

static int imapc_mailbox_commit_delayed_expunges(struct imapc_mailbox *mbox)
{
	struct mail_index_view *view = imapc_mailbox_get_sync_view(mbox);
	struct mail_index_transaction *trans;
	const uint32_t *uidp;
	uint32_t lseq;
	int ret;

	trans = mail_index_transaction_begin(view,
			MAIL_INDEX_TRANSACTION_FLAG_EXTERNAL);
	array_foreach(&mbox->delayed_expunged_uids, uidp) {
		if (mail_index_lookup_seq(view, *uidp, &lseq))
			mail_index_expunge(trans, lseq);
	}
	array_clear(&mbox->delayed_expunged_uids);
	ret = mail_index_transaction_commit(&trans);
	if (ret < 0)
		mail_storage_set_index_error(&mbox->box);
	return ret;
}

int imapc_mailbox_commit_delayed_trans(struct imapc_mailbox *mbox,
				       bool *changes_r)
{
	int ret = 0;

	*changes_r = FALSE;

	if (mbox->delayed_sync_view != NULL)
		mail_index_view_close(&mbox->delayed_sync_view);
	if (mbox->delayed_sync_trans != NULL) {
		if (mail_index_transaction_commit(&mbox->delayed_sync_trans) < 0) {
			mail_storage_set_index_error(&mbox->box);
			ret = -1;
		}
		*changes_r = TRUE;
	}
	if (mbox->sync_view != NULL)
		mail_index_view_close(&mbox->sync_view);

	if (array_count(&mbox->delayed_expunged_uids) > 0) {
		/* delayed expunges - commit them now in a separate
		   transaction */
		if (imapc_mailbox_commit_delayed_expunges(mbox) < 0)
			ret = -1;
	}
	return ret;
}

static void imapc_mailbox_idle_timeout(struct imapc_mailbox *mbox)
{
	timeout_remove(&mbox->to_idle_delay);
	if (mbox->box.notify_callback != NULL)
		mbox->box.notify_callback(&mbox->box, mbox->box.notify_context);
}

static void imapc_mailbox_idle_notify(struct imapc_mailbox *mbox)
{
	struct ioloop *old_ioloop = current_ioloop;

	if (mbox->box.notify_callback != NULL &&
	    mbox->to_idle_delay == NULL) {
		io_loop_set_current(mbox->storage->root_ioloop);
		mbox->to_idle_delay =
			timeout_add(NOTIFY_DELAY_MSECS,
				    imapc_mailbox_idle_timeout, mbox);
		io_loop_set_current(old_ioloop);
	}
}

static void
imapc_untagged_exists(const struct imapc_untagged_reply *reply,
		      struct imapc_mailbox *mbox)
{
	struct mail_index_view *view = mbox->delayed_sync_view;
	uint32_t exists_count = reply->num;
	const struct mail_index_header *hdr;

	if (mbox == NULL)
		return;

	if (view == NULL)
		view = imapc_mailbox_get_sync_view(mbox);

	if (mbox->selecting) {
		/* We don't know the latest flags, refresh them. */
		mbox->sync_fetch_first_uid = 1;
	} else if (mbox->sync_fetch_first_uid != 1) {
		hdr = mail_index_get_header(view);
		mbox->sync_fetch_first_uid = hdr->next_uid;
	}
	mbox->exists_count = exists_count;
	imapc_mailbox_idle_notify(mbox);
}

static bool keywords_are_equal(struct mail_keywords *kw,
			       const ARRAY_TYPE(keyword_indexes) *kw_arr)
{
	const unsigned int *kw_idx;
	unsigned int i, j, count;

	kw_idx = array_get(kw_arr, &count);
	if (count != kw->count)
		return FALSE;

	/* there are normally only a few keywords, so O(n^2) is fine */
	for (i = 0; i < count; i++) {
		for (j = 0; j < count; j++) {
			if (kw->idx[i] == kw_idx[j])
				break;
		}
		if (j == count)
			return FALSE;
	}
	return TRUE;
}

static int
imapc_mailbox_msgmap_update(struct imapc_mailbox *mbox,
			    uint32_t rseq, uint32_t fetch_uid,
			    uint32_t *lseq_r, uint32_t *uid_r)
{
	struct imapc_msgmap *msgmap;
	uint32_t uid, msg_count, rseq2;

	*lseq_r = 0;
	*uid_r = uid = fetch_uid;

	if (rseq > mbox->exists_count) {
		/* Receiving a FETCH for a message that EXISTS hasn't
		   announced yet. MS Exchange has a bug where our UID FETCH
		   request sometimes sends replies where sequences are above
		   EXISTS value, but their UIDs are for existing messages.
		   We'll just ignore these replies. */
		return 0;
	}
	if (rseq < mbox->prev_skipped_rseq &&
	    fetch_uid > mbox->prev_skipped_uid) {
		/* This was the initial attempt at catching the above
		   MS Exchange bug, but the above one appears to catch all
		   these cases. But keep it here just in case. */
		imapc_mailbox_set_corrupted(mbox,
			"FETCH sequence/UID order is mixed "
			"(seq=%u,%u vs uid=%u,%u)",
			mbox->prev_skipped_rseq, rseq,
			mbox->prev_skipped_uid, fetch_uid);
		return -1;
	}

	msgmap = imapc_client_mailbox_get_msgmap(mbox->client_box);
	msg_count = imapc_msgmap_count(msgmap);
	if (rseq <= msg_count) {
		uid = imapc_msgmap_rseq_to_uid(msgmap, rseq);
		if (uid != fetch_uid && fetch_uid != 0) {
			imapc_mailbox_set_corrupted(mbox,
				"FETCH UID mismatch (%u != %u)",
				fetch_uid, uid);
			return -1;
		}
	} else if (fetch_uid == 0 || rseq != msg_count+1) {
		/* probably a flag update for a message we haven't yet
		   received our initial UID FETCH for. we should get
		   another one. */
		if (fetch_uid == 0)
			return 0;

		if (imapc_msgmap_uid_to_rseq(msgmap, fetch_uid, &rseq2)) {
			imapc_mailbox_set_corrupted(mbox,
				"FETCH returned wrong sequence for UID %u "
				"(%u != %u)", fetch_uid, rseq, rseq2);
			return -1;
		}
		mbox->prev_skipped_rseq = rseq;
		mbox->prev_skipped_uid = fetch_uid;
	} else if (fetch_uid < imapc_msgmap_uidnext(msgmap)) {
		imapc_mailbox_set_corrupted(mbox,
			"Expunged message reappeared (uid=%u < next_uid=%u)",
			fetch_uid, imapc_msgmap_uidnext(msgmap));
		return -1;
	} else {
		/* newly seen message */
		imapc_msgmap_append(msgmap, rseq, uid);
		if (uid < mbox->min_append_uid) {
			/* message is already added to index */
		} else if (mbox->syncing) {
			mail_index_append(mbox->delayed_sync_trans,
					  uid, lseq_r);
			mbox->min_append_uid = uid + 1;
		}
	}
	return 0;
}

static void imapc_untagged_fetch(const struct imapc_untagged_reply *reply,
				 struct imapc_mailbox *mbox)
{
	uint32_t lseq, rseq = reply->num;
	struct imapc_mail *const *mailp;
	const struct imap_arg *list, *flags_list;
	const char *atom;
	const struct mail_index_record *rec = NULL;
	enum mail_flags flags;
	uint32_t fetch_uid, uid;
	unsigned int i, j;
	ARRAY_TYPE(const_string) keywords = ARRAY_INIT;
	bool seen_flags = FALSE;

	if (mbox == NULL || rseq == 0 || !imap_arg_get_list(reply->args, &list))
		return;

	fetch_uid = 0; flags = 0;
	for (i = 0; list[i].type != IMAP_ARG_EOL; i += 2) {
		if (!imap_arg_get_atom(&list[i], &atom))
			return;

		if (strcasecmp(atom, "UID") == 0) {
			if (!imap_arg_get_atom(&list[i+1], &atom) ||
			    str_to_uint32(atom, &fetch_uid) < 0)
				return;
		} else if (strcasecmp(atom, "FLAGS") == 0) {
			if (!imap_arg_get_list(&list[i+1], &flags_list))
				return;

			t_array_init(&keywords, 8);
			seen_flags = TRUE;
			for (j = 0; flags_list[j].type != IMAP_ARG_EOL; j++) {
				if (!imap_arg_get_atom(&flags_list[j], &atom))
					return;
				if (atom[0] == '\\')
					flags |= imap_parse_system_flag(atom);
				else {
					/* keyword */
					array_append(&keywords, &atom, 1);
				}
			}
		}
	}
	/* FIXME: need to do something about recent flags */
	flags &= ~MAIL_RECENT;

	imapc_mailbox_init_delayed_trans(mbox);
	if (imapc_mailbox_msgmap_update(mbox, rseq, fetch_uid,
					&lseq, &uid) < 0 || uid == 0)
		return;

	/* if this is a reply to some FETCH request, update the mail's fields */
	array_foreach(&mbox->fetch_mails, mailp) {
		struct imapc_mail *mail = *mailp;

		if (mail->imail.mail.mail.uid == uid)
			imapc_mail_fetch_update(mail, reply, list);
	}

	if (lseq == 0) {
		if (!mail_index_lookup_seq(mbox->delayed_sync_view,
					   uid, &lseq)) {
			/* already expunged by another session */
			return;
		}
		rec = mail_index_lookup(mbox->delayed_sync_view, lseq);
	}

	if (rseq == mbox->sync_next_rseq) {
		/* we're doing the initial full sync of mails. expunge any
		   mails that no longer exist. */
		i_assert(mbox->syncing);

		while (mbox->sync_next_lseq < lseq) {
			mail_index_expunge(mbox->delayed_sync_trans,
					   mbox->sync_next_lseq);
			mbox->sync_next_lseq++;
		}
		i_assert(lseq == mbox->sync_next_lseq);
		mbox->sync_next_rseq++;
		mbox->sync_next_lseq++;
	}

	if (seen_flags && (rec == NULL || rec->flags != flags)) {
		mail_index_update_flags(mbox->delayed_sync_trans, lseq,
					MODIFY_REPLACE, flags);
	}
	if (seen_flags) T_BEGIN {
		ARRAY_TYPE(keyword_indexes) old_kws;
		struct mail_keywords *kw;

		t_array_init(&old_kws, 8);
		mail_index_lookup_keywords(mbox->delayed_sync_view, lseq,
					   &old_kws);

		(void)array_append_space(&keywords);
		kw = mail_index_keywords_create(mbox->box.index,
						array_idx(&keywords, 0));
		if (!keywords_are_equal(kw, &old_kws)) {
			mail_index_update_keywords(mbox->delayed_sync_trans,
						   lseq, MODIFY_REPLACE, kw);
		}
		mail_index_keywords_unref(&kw);
	} T_END;
	imapc_mailbox_idle_notify(mbox);
}

static void imapc_untagged_expunge(const struct imapc_untagged_reply *reply,
				   struct imapc_mailbox *mbox)
{
	struct imapc_msgmap *msgmap;
	uint32_t lseq, uid, rseq = reply->num;
	
	if (mbox == NULL || rseq == 0)
		return;

	mbox->prev_skipped_rseq = 0;
	mbox->prev_skipped_uid = 0;

	if (mbox->exists_count == 0) {
		imapc_mailbox_set_corrupted(mbox,
			"EXPUNGE received for empty mailbox");
		return;
	}
	mbox->exists_count--;

	msgmap = imapc_client_mailbox_get_msgmap(mbox->client_box);
	if (rseq > imapc_msgmap_count(msgmap)) {
		/* we haven't even seen this message yet */
		return;
	}
	uid = imapc_msgmap_rseq_to_uid(msgmap, rseq);
	imapc_msgmap_expunge(msgmap, rseq);

	imapc_mailbox_init_delayed_trans(mbox);
	if (mail_index_lookup_seq(mbox->sync_view, uid, &lseq))
		mail_index_expunge(mbox->delayed_sync_trans, lseq);
	else if (mail_index_lookup_seq(mbox->delayed_sync_view, uid, &lseq)) {
		/* this message exists only in this transaction. lib-index
		   can't currently handle expunging anything except the last
		   appended message in a transaction, and fixing it would be
		   quite a lot of trouble. so instead we'll just delay doing
		   this expunge until after the current transaction has been
		   committed. */
		array_append(&mbox->delayed_expunged_uids, &uid, 1);
	} else {
		/* already expunged by another session */
	}
	imapc_mailbox_idle_notify(mbox);
}

static void
imapc_resp_text_uidvalidity(const struct imapc_untagged_reply *reply,
			    struct imapc_mailbox *mbox)
{
	uint32_t uid_validity;

	if (mbox == NULL || reply->resp_text_value == NULL ||
	    str_to_uint32(reply->resp_text_value, &uid_validity) < 0)
		return;

	if (mbox->sync_uid_validity != uid_validity) {
		mbox->sync_uid_validity = uid_validity;
		imapc_mail_cache_free(&mbox->prev_mail_cache);
	}
}

static void
imapc_resp_text_uidnext(const struct imapc_untagged_reply *reply,
			struct imapc_mailbox *mbox)
{
	uint32_t uid_next;

	if (mbox == NULL || reply->resp_text_value == NULL ||
	    str_to_uint32(reply->resp_text_value, &uid_next) < 0)
		return;

	mbox->sync_uid_next = uid_next;
}

static void
imapc_resp_text_permanentflags(const struct imapc_untagged_reply *reply,
			       struct imapc_mailbox *mbox)
{
	const struct imap_arg *flags_args, *arg;
	const char *flag;

	i_assert(reply->args[0].type == IMAP_ARG_ATOM);

	if (mbox == NULL || !imap_arg_get_list(&reply->args[1], &flags_args))
		return;

	mbox->permanent_flags = 0;
	array_clear(&mbox->permanent_keywords);
	mbox->box.disallow_new_keywords = TRUE;

	for (arg = flags_args; arg->type != IMAP_ARG_EOL; arg++) {
		if (!imap_arg_get_atom(arg, &flag))
			continue;

		if (strcmp(flag, "\\*") == 0)
			mbox->box.disallow_new_keywords = FALSE;
		else if (*flag == '\\')
			mbox->permanent_flags |= imap_parse_system_flag(flag);
		else {
			/* this wastes some memory when called multiple times,
			   but that should happen quite rarely */
			flag = p_strdup(mbox->box.pool, flag);
			array_append(&mbox->permanent_keywords, &flag, 1);
		}
	}
	/* NULL-terminate it */
	(void)array_append_space(&mbox->permanent_keywords);
	array_delete(&mbox->permanent_keywords,
		     array_count(&mbox->permanent_keywords)-1, 1);
}

void imapc_mailbox_register_untagged(struct imapc_mailbox *mbox,
				     const char *key,
				     imapc_mailbox_callback_t *callback)
{
	struct imapc_mailbox_event_callback *cb;

	cb = array_append_space(&mbox->untagged_callbacks);
	cb->name = p_strdup(mbox->box.pool, key);
	cb->callback = callback;
}

void imapc_mailbox_register_resp_text(struct imapc_mailbox *mbox,
				      const char *key,
				      imapc_mailbox_callback_t *callback)
{
	struct imapc_mailbox_event_callback *cb;

	cb = array_append_space(&mbox->resp_text_callbacks);
	cb->name = p_strdup(mbox->box.pool, key);
	cb->callback = callback;
}

void imapc_mailbox_register_callbacks(struct imapc_mailbox *mbox)
{
	imapc_mailbox_register_untagged(mbox, "EXISTS",
					imapc_untagged_exists);
	imapc_mailbox_register_untagged(mbox, "FETCH",
					imapc_untagged_fetch);
	imapc_mailbox_register_untagged(mbox, "EXPUNGE",
					imapc_untagged_expunge);
	imapc_mailbox_register_resp_text(mbox, "UIDVALIDITY",
					 imapc_resp_text_uidvalidity);
	imapc_mailbox_register_resp_text(mbox, "UIDNEXT",
					 imapc_resp_text_uidnext);
	imapc_mailbox_register_resp_text(mbox, "PERMANENTFLAGS",
					 imapc_resp_text_permanentflags);
}
<commit_msg>imapc: Detect when message has been unexpectedly expunged from index.<commit_after>/* Copyright (c) 2011 Dovecot authors, see the included COPYING file */

#include "lib.h"
#include "ioloop.h"
#include "imap-arg.h"
#include "imap-util.h"
#include "imapc-client.h"
#include "imapc-mail.h"
#include "imapc-msgmap.h"
#include "imapc-sync.h"
#include "imapc-storage.h"

#define NOTIFY_DELAY_MSECS 500

static void imapc_mailbox_set_corrupted(struct imapc_mailbox *mbox,
					const char *reason, ...)
{
	va_list va;

	va_start(va, reason);
	i_error("imapc: Mailbox '%s' state corrupted: %s",
		mbox->box.name, t_strdup_vprintf(reason, va));
	va_end(va);

	if (!mbox->initial_sync_done) {
		/* we failed during initial sync. need to rebuild indexes if
		   we want to get this fixed */
		mail_index_mark_corrupted(mbox->box.index);
	} else {
		/* maybe the remote server is buggy and has become confused.
		   try reconnecting. */
	}
	imapc_client_mailbox_disconnect(mbox->client_box);
}

static struct mail_index_view *
imapc_mailbox_get_sync_view(struct imapc_mailbox *mbox)
{
	if (mbox->sync_view == NULL)
		mbox->sync_view = mail_index_view_open(mbox->box.index);
	return mbox->sync_view;
}

static void imapc_mailbox_init_delayed_trans(struct imapc_mailbox *mbox)
{
	if (mbox->delayed_sync_trans != NULL)
		return;

	mbox->delayed_sync_trans =
		mail_index_transaction_begin(imapc_mailbox_get_sync_view(mbox),
					MAIL_INDEX_TRANSACTION_FLAG_EXTERNAL);
	mbox->delayed_sync_view =
		mail_index_transaction_open_updated_view(mbox->delayed_sync_trans);
}

static int imapc_mailbox_commit_delayed_expunges(struct imapc_mailbox *mbox)
{
	struct mail_index_view *view = imapc_mailbox_get_sync_view(mbox);
	struct mail_index_transaction *trans;
	const uint32_t *uidp;
	uint32_t lseq;
	int ret;

	trans = mail_index_transaction_begin(view,
			MAIL_INDEX_TRANSACTION_FLAG_EXTERNAL);
	array_foreach(&mbox->delayed_expunged_uids, uidp) {
		if (mail_index_lookup_seq(view, *uidp, &lseq))
			mail_index_expunge(trans, lseq);
	}
	array_clear(&mbox->delayed_expunged_uids);
	ret = mail_index_transaction_commit(&trans);
	if (ret < 0)
		mail_storage_set_index_error(&mbox->box);
	return ret;
}

int imapc_mailbox_commit_delayed_trans(struct imapc_mailbox *mbox,
				       bool *changes_r)
{
	int ret = 0;

	*changes_r = FALSE;

	if (mbox->delayed_sync_view != NULL)
		mail_index_view_close(&mbox->delayed_sync_view);
	if (mbox->delayed_sync_trans != NULL) {
		if (mail_index_transaction_commit(&mbox->delayed_sync_trans) < 0) {
			mail_storage_set_index_error(&mbox->box);
			ret = -1;
		}
		*changes_r = TRUE;
	}
	if (mbox->sync_view != NULL)
		mail_index_view_close(&mbox->sync_view);

	if (array_count(&mbox->delayed_expunged_uids) > 0) {
		/* delayed expunges - commit them now in a separate
		   transaction */
		if (imapc_mailbox_commit_delayed_expunges(mbox) < 0)
			ret = -1;
	}
	return ret;
}

static void imapc_mailbox_idle_timeout(struct imapc_mailbox *mbox)
{
	timeout_remove(&mbox->to_idle_delay);
	if (mbox->box.notify_callback != NULL)
		mbox->box.notify_callback(&mbox->box, mbox->box.notify_context);
}

static void imapc_mailbox_idle_notify(struct imapc_mailbox *mbox)
{
	struct ioloop *old_ioloop = current_ioloop;

	if (mbox->box.notify_callback != NULL &&
	    mbox->to_idle_delay == NULL) {
		io_loop_set_current(mbox->storage->root_ioloop);
		mbox->to_idle_delay =
			timeout_add(NOTIFY_DELAY_MSECS,
				    imapc_mailbox_idle_timeout, mbox);
		io_loop_set_current(old_ioloop);
	}
}

static void
imapc_untagged_exists(const struct imapc_untagged_reply *reply,
		      struct imapc_mailbox *mbox)
{
	struct mail_index_view *view = mbox->delayed_sync_view;
	uint32_t exists_count = reply->num;
	const struct mail_index_header *hdr;

	if (mbox == NULL)
		return;

	if (view == NULL)
		view = imapc_mailbox_get_sync_view(mbox);

	if (mbox->selecting) {
		/* We don't know the latest flags, refresh them. */
		mbox->sync_fetch_first_uid = 1;
	} else if (mbox->sync_fetch_first_uid != 1) {
		hdr = mail_index_get_header(view);
		mbox->sync_fetch_first_uid = hdr->next_uid;
	}
	mbox->exists_count = exists_count;
	imapc_mailbox_idle_notify(mbox);
}

static bool keywords_are_equal(struct mail_keywords *kw,
			       const ARRAY_TYPE(keyword_indexes) *kw_arr)
{
	const unsigned int *kw_idx;
	unsigned int i, j, count;

	kw_idx = array_get(kw_arr, &count);
	if (count != kw->count)
		return FALSE;

	/* there are normally only a few keywords, so O(n^2) is fine */
	for (i = 0; i < count; i++) {
		for (j = 0; j < count; j++) {
			if (kw->idx[i] == kw_idx[j])
				break;
		}
		if (j == count)
			return FALSE;
	}
	return TRUE;
}

static int
imapc_mailbox_msgmap_update(struct imapc_mailbox *mbox,
			    uint32_t rseq, uint32_t fetch_uid,
			    uint32_t *lseq_r, uint32_t *uid_r)
{
	struct imapc_msgmap *msgmap;
	uint32_t uid, msg_count, rseq2;

	*lseq_r = 0;
	*uid_r = uid = fetch_uid;

	if (rseq > mbox->exists_count) {
		/* Receiving a FETCH for a message that EXISTS hasn't
		   announced yet. MS Exchange has a bug where our UID FETCH
		   request sometimes sends replies where sequences are above
		   EXISTS value, but their UIDs are for existing messages.
		   We'll just ignore these replies. */
		return 0;
	}
	if (rseq < mbox->prev_skipped_rseq &&
	    fetch_uid > mbox->prev_skipped_uid) {
		/* This was the initial attempt at catching the above
		   MS Exchange bug, but the above one appears to catch all
		   these cases. But keep it here just in case. */
		imapc_mailbox_set_corrupted(mbox,
			"FETCH sequence/UID order is mixed "
			"(seq=%u,%u vs uid=%u,%u)",
			mbox->prev_skipped_rseq, rseq,
			mbox->prev_skipped_uid, fetch_uid);
		return -1;
	}

	msgmap = imapc_client_mailbox_get_msgmap(mbox->client_box);
	msg_count = imapc_msgmap_count(msgmap);
	if (rseq <= msg_count) {
		uid = imapc_msgmap_rseq_to_uid(msgmap, rseq);
		if (uid != fetch_uid && fetch_uid != 0) {
			imapc_mailbox_set_corrupted(mbox,
				"FETCH UID mismatch (%u != %u)",
				fetch_uid, uid);
			return -1;
		}
	} else if (fetch_uid == 0 || rseq != msg_count+1) {
		/* probably a flag update for a message we haven't yet
		   received our initial UID FETCH for. we should get
		   another one. */
		if (fetch_uid == 0)
			return 0;

		if (imapc_msgmap_uid_to_rseq(msgmap, fetch_uid, &rseq2)) {
			imapc_mailbox_set_corrupted(mbox,
				"FETCH returned wrong sequence for UID %u "
				"(%u != %u)", fetch_uid, rseq, rseq2);
			return -1;
		}
		mbox->prev_skipped_rseq = rseq;
		mbox->prev_skipped_uid = fetch_uid;
	} else if (fetch_uid < imapc_msgmap_uidnext(msgmap)) {
		imapc_mailbox_set_corrupted(mbox,
			"Expunged message reappeared in session "
			"(uid=%u < next_uid=%u)",
			fetch_uid, imapc_msgmap_uidnext(msgmap));
		return -1;
	} else {
		/* newly seen message */
		imapc_msgmap_append(msgmap, rseq, uid);
		if (uid < mbox->min_append_uid) {
			/* message is already added to index */
			if (!mbox->initial_sync_done &&
			    !mail_index_lookup_seq(mbox->delayed_sync_view,
						   uid, lseq_r)) {
				imapc_mailbox_set_corrupted(mbox,
					"Expunged message reappeared "
					"(uid=%u < next_uid=%u)",
					uid, mbox->min_append_uid);
				return -1;
			}
		} else if (mbox->syncing) {
			mail_index_append(mbox->delayed_sync_trans,
					  uid, lseq_r);
			mbox->min_append_uid = uid + 1;
		}
	}
	return 0;
}

static void imapc_untagged_fetch(const struct imapc_untagged_reply *reply,
				 struct imapc_mailbox *mbox)
{
	uint32_t lseq, rseq = reply->num;
	struct imapc_mail *const *mailp;
	const struct imap_arg *list, *flags_list;
	const char *atom;
	const struct mail_index_record *rec = NULL;
	enum mail_flags flags;
	uint32_t fetch_uid, uid;
	unsigned int i, j;
	ARRAY_TYPE(const_string) keywords = ARRAY_INIT;
	bool seen_flags = FALSE;

	if (mbox == NULL || rseq == 0 || !imap_arg_get_list(reply->args, &list))
		return;

	fetch_uid = 0; flags = 0;
	for (i = 0; list[i].type != IMAP_ARG_EOL; i += 2) {
		if (!imap_arg_get_atom(&list[i], &atom))
			return;

		if (strcasecmp(atom, "UID") == 0) {
			if (!imap_arg_get_atom(&list[i+1], &atom) ||
			    str_to_uint32(atom, &fetch_uid) < 0)
				return;
		} else if (strcasecmp(atom, "FLAGS") == 0) {
			if (!imap_arg_get_list(&list[i+1], &flags_list))
				return;

			t_array_init(&keywords, 8);
			seen_flags = TRUE;
			for (j = 0; flags_list[j].type != IMAP_ARG_EOL; j++) {
				if (!imap_arg_get_atom(&flags_list[j], &atom))
					return;
				if (atom[0] == '\\')
					flags |= imap_parse_system_flag(atom);
				else {
					/* keyword */
					array_append(&keywords, &atom, 1);
				}
			}
		}
	}
	/* FIXME: need to do something about recent flags */
	flags &= ~MAIL_RECENT;

	imapc_mailbox_init_delayed_trans(mbox);
	if (imapc_mailbox_msgmap_update(mbox, rseq, fetch_uid,
					&lseq, &uid) < 0 || uid == 0)
		return;

	/* if this is a reply to some FETCH request, update the mail's fields */
	array_foreach(&mbox->fetch_mails, mailp) {
		struct imapc_mail *mail = *mailp;

		if (mail->imail.mail.mail.uid == uid)
			imapc_mail_fetch_update(mail, reply, list);
	}

	if (lseq == 0) {
		if (!mail_index_lookup_seq(mbox->delayed_sync_view,
					   uid, &lseq)) {
			/* already expunged by another session */
			return;
		}
		rec = mail_index_lookup(mbox->delayed_sync_view, lseq);
	}

	if (rseq == mbox->sync_next_rseq) {
		/* we're doing the initial full sync of mails. expunge any
		   mails that no longer exist. */
		i_assert(mbox->syncing);

		while (mbox->sync_next_lseq < lseq) {
			mail_index_expunge(mbox->delayed_sync_trans,
					   mbox->sync_next_lseq);
			mbox->sync_next_lseq++;
		}
		i_assert(lseq == mbox->sync_next_lseq);
		mbox->sync_next_rseq++;
		mbox->sync_next_lseq++;
	}

	if (seen_flags && (rec == NULL || rec->flags != flags)) {
		mail_index_update_flags(mbox->delayed_sync_trans, lseq,
					MODIFY_REPLACE, flags);
	}
	if (seen_flags) T_BEGIN {
		ARRAY_TYPE(keyword_indexes) old_kws;
		struct mail_keywords *kw;

		t_array_init(&old_kws, 8);
		mail_index_lookup_keywords(mbox->delayed_sync_view, lseq,
					   &old_kws);

		(void)array_append_space(&keywords);
		kw = mail_index_keywords_create(mbox->box.index,
						array_idx(&keywords, 0));
		if (!keywords_are_equal(kw, &old_kws)) {
			mail_index_update_keywords(mbox->delayed_sync_trans,
						   lseq, MODIFY_REPLACE, kw);
		}
		mail_index_keywords_unref(&kw);
	} T_END;
	imapc_mailbox_idle_notify(mbox);
}

static void imapc_untagged_expunge(const struct imapc_untagged_reply *reply,
				   struct imapc_mailbox *mbox)
{
	struct imapc_msgmap *msgmap;
	uint32_t lseq, uid, rseq = reply->num;
	
	if (mbox == NULL || rseq == 0)
		return;

	mbox->prev_skipped_rseq = 0;
	mbox->prev_skipped_uid = 0;

	if (mbox->exists_count == 0) {
		imapc_mailbox_set_corrupted(mbox,
			"EXPUNGE received for empty mailbox");
		return;
	}
	mbox->exists_count--;

	msgmap = imapc_client_mailbox_get_msgmap(mbox->client_box);
	if (rseq > imapc_msgmap_count(msgmap)) {
		/* we haven't even seen this message yet */
		return;
	}
	uid = imapc_msgmap_rseq_to_uid(msgmap, rseq);
	imapc_msgmap_expunge(msgmap, rseq);

	imapc_mailbox_init_delayed_trans(mbox);
	if (mail_index_lookup_seq(mbox->sync_view, uid, &lseq))
		mail_index_expunge(mbox->delayed_sync_trans, lseq);
	else if (mail_index_lookup_seq(mbox->delayed_sync_view, uid, &lseq)) {
		/* this message exists only in this transaction. lib-index
		   can't currently handle expunging anything except the last
		   appended message in a transaction, and fixing it would be
		   quite a lot of trouble. so instead we'll just delay doing
		   this expunge until after the current transaction has been
		   committed. */
		array_append(&mbox->delayed_expunged_uids, &uid, 1);
	} else {
		/* already expunged by another session */
	}
	imapc_mailbox_idle_notify(mbox);
}

static void
imapc_resp_text_uidvalidity(const struct imapc_untagged_reply *reply,
			    struct imapc_mailbox *mbox)
{
	uint32_t uid_validity;

	if (mbox == NULL || reply->resp_text_value == NULL ||
	    str_to_uint32(reply->resp_text_value, &uid_validity) < 0)
		return;

	if (mbox->sync_uid_validity != uid_validity) {
		mbox->sync_uid_validity = uid_validity;
		imapc_mail_cache_free(&mbox->prev_mail_cache);
	}
}

static void
imapc_resp_text_uidnext(const struct imapc_untagged_reply *reply,
			struct imapc_mailbox *mbox)
{
	uint32_t uid_next;

	if (mbox == NULL || reply->resp_text_value == NULL ||
	    str_to_uint32(reply->resp_text_value, &uid_next) < 0)
		return;

	mbox->sync_uid_next = uid_next;
}

static void
imapc_resp_text_permanentflags(const struct imapc_untagged_reply *reply,
			       struct imapc_mailbox *mbox)
{
	const struct imap_arg *flags_args, *arg;
	const char *flag;

	i_assert(reply->args[0].type == IMAP_ARG_ATOM);

	if (mbox == NULL || !imap_arg_get_list(&reply->args[1], &flags_args))
		return;

	mbox->permanent_flags = 0;
	array_clear(&mbox->permanent_keywords);
	mbox->box.disallow_new_keywords = TRUE;

	for (arg = flags_args; arg->type != IMAP_ARG_EOL; arg++) {
		if (!imap_arg_get_atom(arg, &flag))
			continue;

		if (strcmp(flag, "\\*") == 0)
			mbox->box.disallow_new_keywords = FALSE;
		else if (*flag == '\\')
			mbox->permanent_flags |= imap_parse_system_flag(flag);
		else {
			/* this wastes some memory when called multiple times,
			   but that should happen quite rarely */
			flag = p_strdup(mbox->box.pool, flag);
			array_append(&mbox->permanent_keywords, &flag, 1);
		}
	}
	/* NULL-terminate it */
	(void)array_append_space(&mbox->permanent_keywords);
	array_delete(&mbox->permanent_keywords,
		     array_count(&mbox->permanent_keywords)-1, 1);
}

void imapc_mailbox_register_untagged(struct imapc_mailbox *mbox,
				     const char *key,
				     imapc_mailbox_callback_t *callback)
{
	struct imapc_mailbox_event_callback *cb;

	cb = array_append_space(&mbox->untagged_callbacks);
	cb->name = p_strdup(mbox->box.pool, key);
	cb->callback = callback;
}

void imapc_mailbox_register_resp_text(struct imapc_mailbox *mbox,
				      const char *key,
				      imapc_mailbox_callback_t *callback)
{
	struct imapc_mailbox_event_callback *cb;

	cb = array_append_space(&mbox->resp_text_callbacks);
	cb->name = p_strdup(mbox->box.pool, key);
	cb->callback = callback;
}

void imapc_mailbox_register_callbacks(struct imapc_mailbox *mbox)
{
	imapc_mailbox_register_untagged(mbox, "EXISTS",
					imapc_untagged_exists);
	imapc_mailbox_register_untagged(mbox, "FETCH",
					imapc_untagged_fetch);
	imapc_mailbox_register_untagged(mbox, "EXPUNGE",
					imapc_untagged_expunge);
	imapc_mailbox_register_resp_text(mbox, "UIDVALIDITY",
					 imapc_resp_text_uidvalidity);
	imapc_mailbox_register_resp_text(mbox, "UIDNEXT",
					 imapc_resp_text_uidnext);
	imapc_mailbox_register_resp_text(mbox, "PERMANENTFLAGS",
					 imapc_resp_text_permanentflags);
}

{'fraction_non_alphanumeric': 0.12004261461268816, 'fraction_numerical': 0.00690769125025775, 'mean_word_length': 4.426892950391645, 'pattern_counts': {'":': 0, '<': 34, '<?xml version=': 0, '>': 242, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7649249', 'n_tokens_mistral': 12342, 'n_tokens_neox': 11169, 'n_words': 2300}