// SPDX-License-Identifier: GPL-2.0-only WITH linking exception
// SPDX-FileCopyrightText: 2020–2026 grommunio GmbH
// This file is part of Gromox.
/* 
 * collection of functions for handling the imap command
 */ 
#ifdef HAVE_CONFIG_H
#	include "config.h"
#endif
#include <algorithm>
#include <cassert>
#include <cerrno>
#include <cstdio>
#include <cstring>
#include <fcntl.h>
#include <map>
#include <memory>
#include <set>
#include <span>
#include <string>
#include <unistd.h>
#include <utility>
#include <vector>
#include <fmt/core.h>
#include <libHX/ctype_helper.h>
#include <libHX/io.h>
#include <libHX/scope.hpp>
#include <libHX/string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <gromox/defs.h>
#include <gromox/exmdb_client.hpp>
#include <gromox/exmdb_rpc.hpp>
#include <gromox/fileio.h>
#include <gromox/json.hpp>
#include <gromox/mail.hpp>
#include <gromox/mail_func.hpp>
#include <gromox/mapi_types.hpp>
#include <gromox/midb.hpp>
#include <gromox/midb_agent.hpp>
#include <gromox/mjson.hpp>
#include <gromox/mysql_adaptor.hpp>
#include <gromox/range_set.hpp>
#include <gromox/simple_tree.hpp>
#include <gromox/textmaps.hpp>
#include <gromox/util.hpp>
#include <gromox/xarray2.hpp>
#include "imap.hpp"
#define MAX_DIGLEN		256*1024

/*
 *
 * The inbox name, "INBOX", is specified as case-insensitive, but most code in
 * here does not handle folder names like "inbox/foo/bar", i.e. subordinates of
 * inbox where inbox is not exactly spelled "INBOX". Blech.
 *
 */

using namespace std::string_literals;
using namespace gromox;
using LLU = unsigned long long;
using mdi_list = std::vector<std::string>; /* message data item (RFC 3501 §6.4.5) */

namespace {

struct dir_tree {
	struct cmp {
		inline bool operator()(const std::string &a, const std::string &b) const { return strcasecmp(a.c_str(), b.c_str()) < 0; }
	};

	dir_tree() = default;

	template<typename Vec> void load_from_memfile(Vec &&pfile) try
	{
		for (auto &&[id, orig] : std::forward<Vec>(pfile)) {
			auto curr = this;
			constexpr bool inplace_edit = std::is_rvalue_reference<Vec>::value;
			using ref_or_copy = typename std::conditional<inplace_edit, std::string &, std::string>::type;
			ref_or_copy name{orig};
			char *saveptr = nullptr;

			for (auto token = strtok_r(name.data(), "/", &saveptr);
			     token != nullptr;
			     token = strtok_r(nullptr, "/", &saveptr)) {
				auto [it, added] = curr->entries.try_emplace(token);
				curr = &it->second;
			}
		}
	} catch (const std::bad_alloc &) {
		mlog(LV_ERR, "E-2903: ENOMEM");
	}

	const dir_tree *match(const char *path) const;
	dir_tree *match(const char *path) { return const_cast<dir_tree *>(const_cast<const dir_tree *>(this)->match(path)); }
	bool has_children() const { return entries.size() > 0; }

	std::map<std::string, dir_tree, cmp> entries;
};

enum {
	TYPE_WILDS = 1,
	TYPE_WILDP
};

struct builtin_folder {
	uint64_t fid;
	const char *use_flags;
};

}

/* RFC 6154 does not document \Inbox, but Thunderbird evaluates it. */
/* RFC 6154 says \Junk, but Thunderbird evaluates \Spam */
static constexpr const builtin_folder g_folder_list[] = {
	{PRIVATE_FID_INBOX, "\\Inbox"},
	{PRIVATE_FID_DRAFT, "\\Drafts"},
	{PRIVATE_FID_SENT_ITEMS, "\\Sent"},
	{PRIVATE_FID_DELETED_ITEMS, "\\Trash"},
	{PRIVATE_FID_JUNK, "\\Junk \\Spam"},
};

const dir_tree *dir_tree::match(const char *path) const
{
	auto curr = this;
	if (*path == '\0')
		return curr;
	auto len = strlen(path);
	if (len >= 4096)
		return NULL;
	std::string temp_path(path, len);
	char *saveptr = nullptr;
	for (auto token = strtok_r(temp_path.data(), "/", &saveptr);
	     token != nullptr;
	     token = strtok_r(nullptr, "/", &saveptr)) {
		auto it = curr->entries.find(token);
		if (it == curr->entries.cend())
			return NULL;
		curr = &it->second;
	}
	return curr;
}

/**
 * Collect (full_name, has_children) for every node in the subtree, so a tree
 * built from subscribed names exposes the synthesized intermediate ancestors.
 */
static void dir_tree_collect(const dir_tree &node, const std::string &prefix,
    std::vector<std::pair<std::string, bool>> &out)
{
	for (const auto &[token, child] : node.entries) {
		auto full = prefix.empty() ? token : prefix + "/" + token;
		out.emplace_back(full, child.has_children());
		dir_tree_collect(child, full, out);
	}
}

static const builtin_folder *special_folder(uint64_t fid)
{
	for (const auto &s : g_folder_list)
		if (fid == s.fid)
			return &s;
	return nullptr;
}

/**
 * @list:    rangeset to inspect
 * @num:     number to test for
 * @max_uid: meaning of the star when found in @list
 */
static bool iseq_contains(const imap_seq_list &list,
	unsigned int num, unsigned int max_uid)
{
	auto i = std::lower_bound(list.cbegin(), list.cend(), num,
	         [](const range_node<uint32_t> &rn, uint32_t vv) { return rn.hi < vv; });
	if (i == list.cend())
		return false;
	return i->lo <= num && num <= i->hi && num <= max_uid;
}

static std::string quote_encode(const char *u7)
{
	std::unique_ptr<char[], stdlib_delete> q(HX_strquote(u7, HXQUOTE_DQUOTE, nullptr));
	return "\""s + q.get() + "\"";
}

static std::string quote_encode(const std::string &u7)
{
	return quote_encode(u7.c_str());
}

static bool icp_parse_fetch_args(mdi_list &plist, bool *pb_detail,
    bool *pb_simple, bool *pb_data, char *string,
    std::vector<std::string> &argv) try
{
	static constexpr const char *kw1[] = {"ALL", "FAST", "FULL"};
	static constexpr const char *kw2[] = {
		"BODY", "BODYSTRUCTURE", "ENVELOPE", "FLAGS", "INTERNALDATE",
		"RFC822", "RFC822.HEADER", "RFC822.SIZE", "RFC822.TEXT", "UID",
	};
	auto contained_in = +[](const char *kw, std::span<const char * const> list) {
		return std::binary_search(list.begin(), list.end(), kw, [](const char *a, const char *b) {
			return strcasecmp(a, b) < 0;
		});
	};

	if ('(' == string[0]) {
		if (string[strlen(string)-1] != ')')
			return FALSE;
		if (parse_imap_args(&string[1], strlen(string) - 2, argv) < 0)
			return false;
	} else {
		if (parse_imap_args(string, strlen(string), argv) < 0)
			return false;
	}
	if (argv.size() < 1)
		return FALSE;

	bool b_macro = false;
	plist.emplace_back("UID");
	for (const auto &arg_s : argv) {
		auto arg = arg_s.c_str();

		if (std::any_of(plist.cbegin(), plist.cend(),
		    [&](const std::string &e) { return strcasecmp(e.c_str(), arg) == 0; }))
			/* weed out duplicates */
			continue;
		if (contained_in(arg, {kw1, std::size(kw1)})) {
			b_macro = TRUE;
			plist.emplace_back(arg);
		} else if (contained_in(arg, {kw2, std::size(kw2)})) {
			plist.emplace_back(arg);
		} else if (strncasecmp(arg, "BODY[", 5) == 0 ||
		    strncasecmp(arg, "BODY.PEEK[", 10) == 0) {
			const char *pend = strchr(arg, ']');
			if (pend == nullptr)
				return FALSE;
			const char *ptr = strchr(arg, '[') + 1;
			const char *last_ptr = ptr;
			if (strncasecmp(ptr, "MIME", 4) == 0)
				return FALSE;
			while (']' != *ptr) {
				if ('.' == *ptr) {
					size_t len = ptr - last_ptr, j = 0;
					if (len == 0)
						return FALSE;
					for (j = 0; j < len; ++j)
						if (!HX_isdigit(last_ptr[j]))
							break;
					if (j < len)
						break;
					last_ptr = ptr + 1;
				}
				ptr ++;
			}
			
			size_t len = pend - last_ptr;
			if ((len == 0 && *last_ptr == '.') || len >= 1024)
				return FALSE;
			char buff[1024], temp_buff[1024];
			std::vector<std::string> tmp_argv1;
			memcpy(buff, last_ptr, len);
			buff[len] = '\0';
			if (0 != len &&
				0 != strcasecmp(buff, "HEADER") &&
				0 != strcasecmp(buff, "TEXT") &&
				0 != strcasecmp(buff, "MIME") &&
				0 != strncasecmp(buff, "HEADER.FIELDS ", 14) &&
				0 != strncasecmp(buff, "HEADER.FIELDS.NOT ", 18)) {
				for (size_t j = 0; j < len; ++j)
					if (!HX_isdigit(buff[j]))
						return FALSE;
			} else if (0 == strncasecmp(buff, "HEADER.FIELDS ", 14)) {
				memcpy(temp_buff, buff + 14, strlen(buff) - 14);
				int result;
				if ('(' == buff[14]) {
					if (buff[strlen(buff)-1] != ')')
						return FALSE;
					result = parse_imap_args(&temp_buff[1], strlen(buff) - 16, tmp_argv1);
				} else {
					result = parse_imap_args(temp_buff, strlen(buff) - 14, tmp_argv1);
				}
				if (result < 1)
					return FALSE;
			} else if (0 == strncasecmp(buff, "HEADER.FIELDS.NOT ", 18)) {
				memcpy(temp_buff, buff + 18, strlen(buff) - 18);
				int result;
				if ('(' == buff[18]) {
					if (buff[strlen(buff)-1] != ')')
						return FALSE;
					result = parse_imap_args(&temp_buff[1], strlen(buff) - 20, tmp_argv1);
				} else {
					result = parse_imap_args(temp_buff, strlen(buff) - 18, tmp_argv1);
				}
				if (result < 1)
					return FALSE;
			}
			ptr = pend + 1;
			const char *ptr1 = nullptr;
			if ('\0' != *ptr) {
				pend = strchr(ptr + 1, '>');
				if (*ptr != '<' || pend == nullptr || pend[1] != '\0')
					return FALSE;
				ptr ++;
				size_t count = 0;
				last_ptr = ptr;
				while ('>' != *ptr) {
					if (HX_isdigit(*ptr)) {
						/* do nothing */
					} else if ('.' == *ptr) {
						ptr1 = ptr;
						count ++;
					} else {
						return FALSE;
					}
					ptr ++;
				}
				if (count > 1)
					return FALSE;
				if ((count == 1 && ptr1 == last_ptr) || ptr1 == pend - 1)
					return FALSE;
			}
			plist.emplace_back(arg);
		} else {
			return FALSE;
		}
	}
	if (argv.size() > 1 && b_macro)
		return FALSE;
	/* full load the mail digests from MIDB */
	*pb_detail = FALSE;
	bool want_flags = false;
	*pb_simple = FALSE;
	/* stream object contain file information */
	*pb_data = FALSE;
	for (size_t i = 0; i < plist.size(); ++i) {
		auto kw = plist[i].c_str();
		bool is_all  = strcasecmp(kw, "ALL") == 0;
		bool is_full = strcasecmp(kw, "FULL") == 0;
		if (is_all || is_full || strcasecmp(kw, "FAST") == 0) {
			/*
			 * emplace_back may realloc plist and invalidate kw, so
			 * decide everything off the local bools below.
			 */
			plist.emplace_back("INTERNALDATE");
			plist.emplace_back("RFC822.SIZE");
			if (is_all || is_full) {
				plist.emplace_back("ENVELOPE");
				if (is_full)
					plist.emplace_back("BODY");
			}
			*pb_detail = TRUE;
		} else if (strcasecmp(kw, "RFC822") == 0 ||
		    strcasecmp(kw, "RFC822.HEADER") == 0 ||
		    strcasecmp(kw, "RFC822.TEXT") == 0) {
			*pb_data = TRUE;
			*pb_detail = TRUE;
		} else if (strcasecmp(kw, "BODY") == 0 ||
		    strcasecmp(kw, "BODYSTRUCTURE") == 0 ||
		    strcasecmp(kw, "ENVELOPE") == 0 ||
		    strcasecmp(kw, "INTERNALDATE") == 0 ||
		    strcasecmp(kw, "RFC822.SIZE") == 0) {
			*pb_detail = TRUE;
		} else if (strcasecmp(kw, "FLAGS") == 0) {
			want_flags = true;
		} else if (strncasecmp(kw, "BODY[", 5) == 0 ||
		    strncasecmp(kw, "BODY.PEEK[", 10) == 0) {
			if (strcasestr(kw, "FIELDS") == nullptr)
				*pb_data = TRUE;
			*pb_detail = TRUE;
		}
	}
	if (want_flags && !*pb_detail)
		*pb_simple = TRUE;
	/* move to front (UID goes in front of plist) */
	for (const auto kw : {"RFC822.TEXT", "RFC822.HEADER", "ENVELOPE", "RFC822.SIZE", "INTERNALDATE", "FLAGS", "UID"})
		std::stable_partition(plist.begin(), plist.end(),
			[kw](const std::string &e) { return strcasecmp(e.c_str(), kw) == 0; });
	/* move to back */
	for (const auto kw : {"BODY", "BODYSTRUCTURE", "RFC822"})
		std::stable_partition(plist.begin(), plist.end(),
			[kw](const std::string &e) { return strcasecmp(e.c_str(), kw) != 0; });
	return TRUE;
} catch (const std::bad_alloc &) {
	mlog(LV_ERR, "E-2131: ENOMEM");
	return false;
}

static std::string icp_convert_flags_string(int flag_bits,
    const std::string &keywords = "", bool rev2 = false)
{
	std::string out = "(";
	if (!rev2 && flag_bits & FLAG_RECENT)
		out += "\\Recent ";
	if (flag_bits & FLAG_ANSWERED)
		out += "\\Answered ";
	if (flag_bits & FLAG_FLAGGED)
		out += "\\Flagged ";
	if (flag_bits & FLAG_DELETED)
		out += "\\Deleted ";
	if (flag_bits & FLAG_SEEN)
		out += "\\Seen ";
	if (flag_bits & FLAG_DRAFT)
		out += "\\Draft ";
	if (flag_bits & FLAG_FORWARDED)
		out += "$Forwarded ";
	if (!keywords.empty()) {
		/* keywords is a space-separated list of custom IMAP atoms */
		out += keywords;
		out += ' ';
	}
	if (out.size() > 1)
		/* There is more than just the opening parenthesis, so there must be a space */
		out.back() = ')';
	else
		out += ")";
	return out;
}

/*
 * System flags advertised in untagged `* FLAGS` lines. Keep this identical to
 * the SELECT/EXAMINE response in icp_selex().
 */
#define IMAP_FLAGS_SYSTEM "\\Answered \\Flagged \\Deleted \\Seen \\Draft $Forwarded"

/**
 * Produce a keyword pre-announcement line (RFC 3501 §7.2.6).
 *
 * @kw_space: string with space-separated atoms
 *
 * Tokenizes @kw_space and records any token not yet announced to this session.
 * If the announced set grew, emit one `* FLAGS (system flags + the full
 * announced set)` line. The line only contains plain atoms, never "\*".
 */
std::string icp_make_kwannounce_line(imap_context &ctx, std::string_view kw_space)
{
	bool grew = false;
	for (size_t pos = 0; pos < kw_space.size(); ) {
		auto sp = kw_space.find(' ', pos);
		if (sp == std::string_view::npos)
			sp = kw_space.size();
		if (sp > pos) {
			std::string tok(kw_space.substr(pos, sp - pos));
			if (std::find(ctx.announced_keywords.cbegin(),
			    ctx.announced_keywords.cend(), tok) ==
			    ctx.announced_keywords.cend()) {
				ctx.announced_keywords.emplace_back(std::move(tok));
				grew = true;
			}
		}
		pos = sp + 1;
	}
	if (!grew)
		return "";
	std::string line = "* FLAGS (" IMAP_FLAGS_SYSTEM;
	for (const auto &k : ctx.announced_keywords)
		line += " " + k;
	line += ")\r\n";
	return line;
}

static int icp_match_field(mjson_io &io, const char *cmd_tag,
    const char *file_path, size_t offset, size_t length, bool b_not,
    const char *tags, size_t offset1, ssize_t length1, std::string &value) try
{
	auto pbody = strchr(cmd_tag, '[');
	if (length > 128 * 1024)
		return -1;
	auto eml_content = io.get_substr(file_path, offset, length);
	if (!eml_content.has_value())
		return -1;
	auto &buff = eml_content.value();

	char temp_buff[1024];
	std::vector<std::string> argv;
	gx_strlcpy(temp_buff, tags, std::size(temp_buff)); 
	if (tags[0] == '(')
		parse_imap_args(&temp_buff[1], strlen(tags) - 2, argv);
	else
		parse_imap_args(temp_buff, strlen(tags), argv);

	size_t len, buff_len = 0;
	std::string buff1;
	bool b_hit = false;
	MIME_FIELD mime_field;
	while ((len = parse_mime_field(&buff[buff_len], length - buff_len,
	       &mime_field)) != 0) {
		b_hit = FALSE;
		for (size_t i = 0; i < argv.size(); ++i) {
			if (strcasecmp(argv[i].c_str(), mime_field.name.c_str()) != 0)
				continue;
			if (!b_not) {
				buff1 += std::string_view(&buff[buff_len], len);
				break;
			}
			b_hit = TRUE;
		}
		if (b_not && !b_hit)
			buff1 += std::string_view(&buff[buff_len], len);
		buff_len += len;
	}
	buff1 += "\r\n";
	const auto len1 = buff1.size();
	if (length1 == -1)
		length1 = len1;
	if (offset1 >= len1) {
		value += "BODY"s + pbody + " NIL";
	} else {
		if (offset1 + length1 > len1)
			length1 = len1 - offset1;
		value += "BODY"s + pbody;
		value += " {" + std::to_string(length1) + "}\r\n";
		value += std::string_view(buff1).substr(offset1);
	}
	return 0;
} catch (const std::bad_alloc &) {
	return -1;
}

static int pstruct_null(MJSON *pjson,
    const std::string &cmd_tag, std::string &buf, const char *pbody,
    const char *temp_id, size_t offset, ssize_t length,
    const char *storage_path)
{
	auto pmime = pjson->get_mime(temp_id);
	/* Non-[MIME-IMB] messages, and non-multipart
	   [MIME-IMB] messages with no encapsulated
	   message, only have a part 1
	*/
	if (pmime == nullptr && strcmp(temp_id, "1") == 0)
		pmime = pjson->get_mime("");
	if (pmime == nullptr) {
		buf += "BODY"s + pbody + " NIL";
		return 0;
	}
	size_t part_length = 0, temp_len = 0;
	if (0 == strcmp(temp_id, "")) {
		part_length = pmime->get_entire_length();
		temp_len = pmime->get_head_offset();
	} else {
		part_length = pmime->get_content_length();
		temp_len = pmime->get_content_offset();
	}
	if (length == -1)
		length = part_length;
	if (offset >= part_length) {
		buf += "BODY"s + pbody + " NIL";
		return 0;
	}
	if (offset + length > part_length)
		length = part_length - offset;
	if (storage_path == nullptr)
		buf += fmt::format("BODY{} <<{{file}}{}|{}|{}\r\n",
		       pbody, pjson->get_mail_filename(),
		       temp_len + offset, length);
	else
		buf += fmt::format("BODY{} <<{{rfc822}}{}/{}|{}|{}\r\n",
		       pbody, storage_path,
		       pjson->get_mail_filename(),
		       temp_len + offset, length);
	return 0;
}

static int pstruct_mime(MJSON *pjson,
    const std::string &cmd_tag, std::string &buf, const char *pbody,
    const char *temp_id, const char *data_item, size_t offset, ssize_t length,
    const char *storage_path)
{
	if ((strcasecmp(data_item, "MIME") == 0 && *temp_id == '\0') ||
	    (strcasecmp(data_item, "HEADER") == 0 && *temp_id != '\0')) {
		buf += "BODY"s + pbody + " NIL";
		return 0;
	}
	auto pmime = pjson->get_mime(temp_id);
	if (pmime == nullptr) {
		buf += "BODY"s + pbody + " NIL";
		return 0;
	}
	size_t head_length = pmime->get_head_length();
	if (length == -1)
		length = head_length;
	if (offset >= head_length) {
		buf += "BODY"s + pbody + " NIL";
		return 0;
	}
	if (offset + length > head_length)
		length = head_length - offset;
	if (storage_path == nullptr)
		buf += fmt::format("BODY{} <<{{file}}{}|{}|{}\r\n",
		       pbody, pjson->get_mail_filename(),
		       pmime->get_head_offset() + offset, length);
	else
		buf += fmt::format("BODY{} <<{{rfc822}}{}/{}|{}|{}\r\n",
		       pbody, storage_path,
		       pjson->get_mail_filename(),
		       pmime->get_head_offset() + offset, length);
	return 0;
}

static int pstruct_text(MJSON *pjson,
    const std::string &cmd_tag, std::string &buf, const char *pbody,
    const char *temp_id, size_t offset, ssize_t length,
    const char *storage_path)
{
	if (*temp_id != '\0') {
		buf += "BODY"s + pbody + " NIL";
		return 0;
	}
	auto pmime = pjson->get_mime(temp_id);
	if (pmime == nullptr) {
		buf += "BODY"s + pbody + " NIL";
		return 0;
	}
	size_t ct_length = pmime->get_content_length();
	if (length == -1)
		length = ct_length;
	if (offset >= ct_length) {
		buf += "BODY"s + pbody + " NIL";
		return 0;
	}
	if (offset + length > ct_length)
		length = ct_length - offset;
	if (storage_path == nullptr)
		buf += fmt::format("BODY{} <<{{file}}{}|{}|{}\r\n",
		       pbody, pjson->get_mail_filename(),
		       pmime->get_content_offset() + offset, length);
	else
		buf += fmt::format("BODY{} <<{{rfc822}}{}/{}|{}|{}\r\n",
		       pbody, storage_path,
		       pjson->get_mail_filename(),
		       pmime->get_content_offset() + offset, length);
	return 0;
}

static int pstruct_else(imap_context &ctx, MJSON *pjson,
    const std::string &cmd_tag, std::string &buf, const char *pbody,
    const char *temp_id, const char *data_item, size_t offset, ssize_t length,
    const char *storage_path)
{
	auto b_not = strncasecmp(data_item, "HEADER.FIELDS ", 14) != 0;
	data_item += b_not ? 18 : 14;
	auto pmime = pjson->get_mime(temp_id);
	if (pmime == nullptr) {
		buf += "BODY"s + pbody + " NIL";
		return 0;
	}
	std::string eml_path;
	if (storage_path == nullptr) {
		eml_path = ctx.maildir + "/eml/"s + pjson->get_mail_filename();
		if (!ctx.io_actor.exists(eml_path)) {
			std::string content;
			if (exmdb_client->imapfile_read(ctx.maildir, "eml",
			    pjson->get_mail_filename(), &content))
				ctx.io_actor.place(eml_path, std::move(content), true);
		}
	} else {
		eml_path = ctx.maildir + "/tmp/imap.rfc822/"s + storage_path + "/" + pjson->get_mail_filename();
	}
	std::string b2;
	int len = icp_match_field(ctx.io_actor, cmd_tag.c_str(), eml_path.c_str(),
	          pmime->get_head_offset(), pmime->get_head_length(),
	          b_not, data_item, offset, length, b2);
	if (len == -1)
		buf += "BODY"s + pbody + " NIL";
	else
		buf += std::move(b2);
	return 0;
}

static int icp_print_structure(imap_context &ctx, MJSON *pjson,
    const std::string &cmd_tag, std::string &buf, const char *pbody,
    const char *temp_id, const char *data_item, size_t offset, ssize_t length,
    const char *storage_path) try
{
	if (data_item == nullptr)
		return pstruct_null(pjson, cmd_tag, buf,
		       pbody, temp_id, offset, length, storage_path);
	if (strcasecmp(data_item, "MIME") == 0 ||
	    strcasecmp(data_item, "HEADER") == 0)
		return pstruct_mime(pjson, cmd_tag, buf,
		       pbody, temp_id, data_item, offset, length, storage_path);
	if (strcasecmp(data_item, "TEXT") == 0)
		return pstruct_text(pjson, cmd_tag, buf,
		       pbody, temp_id, offset, length, storage_path);
	if (strcmp(temp_id, "") != 0) {
		buf += "BODY"s + pbody + " NIL";
		return 0;
	}
	return pstruct_else(ctx, pjson, cmd_tag, buf, pbody,
	       temp_id, data_item, offset, length, storage_path);
} catch (const std::bad_alloc &) {
	mlog(LV_ERR, "E-1465: ENOMEM");
	return -1;
}

/***
 * @sv: the part that is within square brackets in e.g.
 *     "BODY[1234.HEADER]" "BODY[HEADER.FIELDS]" etc.
 *
 * Splits @sv into the MIME part ID (the leading run of digits and dots, e.g.
 * "1.2"; empty for the top-level message) and the inner data item (e.g.
 * "HEADER", "TEXT", "HEADER.FIELDS (...)"). The data item comes back empty
 * when @sv denotes a whole part ("" or "1").
 */
static std::pair<std::string, std::string> split_DI(std::string_view sv)
{
	size_t i = 0;
	while (i < sv.size() && (sv[i] == '.' || HX_isdigit(sv[i])))
		++i;
	if (i >= sv.size())
		/* no data item, just a part path (e.g. "" or "1") */
		return {std::string(sv), std::string()};
	/* @i is at the data item; drop the '.' separating it from the part ID */
	size_t idlen = i > 0 && sv[i-1] == '.' ? i - 1 : i;
	return {std::string(sv.substr(0, idlen)), std::string(sv.substr(i))};
}

static int icp_process_fetch_item(imap_context &ctx,
    bool b_data, MITEM *pitem, std::string_view digest_str,
    int item_id, mdi_list &pitem_list) try
{
	auto pcontext = &ctx;
	int errnum;
	MJSON mjson;
	std::string buf;

	if (pitem->flag_bits & FLAG_LOADED) {
		auto eml_path = std::string(pcontext->maildir) + "/eml";
		Json::Value digest;
		if (!str_to_json(digest_str, digest)) {
			mlog(LV_ERR, "E-2321: load_from_json %s/%s oopsied1", ctx.maildir, ctx.mid.c_str());
			return 1923;
		} else if (!mjson.load_from_json(digest)) {
			mlog(LV_ERR, "E-2322: load_from_json %s/%s oopsied2", ctx.maildir, ctx.mid.c_str());
			return 1923;
		}
		mjson.path = std::move(eml_path);
	}
	auto deferred_eml_load = [&]() {
		if (!(pitem->flag_bits & FLAG_LOADED))
			return;
		auto eml_file = mjson.path + "/"s + pitem->mid;
		if (!ctx.io_actor.exists(eml_file)) {
			std::string content;
			if (exmdb_client->imapfile_read(ctx.maildir, "eml", pitem->mid, &content))
				ctx.io_actor.place(eml_file, std::move(content), true);
		}
	};

	bool b_first = false;
	buf = "* " + std::to_string(item_id) + " FETCH (";
	for (auto &kwss : pitem_list) {
		if (!b_first)
			b_first = TRUE;
		else
			buf += ' ';
		auto kw = kwss.data();
		if (strcasecmp(kw, "BODY") == 0) {
			buf += "BODY ";
			if (mjson.has_rfc822_part()) {
				deferred_eml_load();
				auto rfc_path = std::string(pcontext->maildir) + "/tmp/imap.rfc822";
				if (rfc_path.size() <= 0 ||
				    !mjson.rfc822_build(ctx.io_actor, rfc_path.c_str()))
					goto FETCH_BODY_SIMPLE;
				std::string b2;
				auto len = mjson.rfc822_fetch(ctx.io_actor, rfc_path.c_str(),
				           pcontext->defcharset, false, b2);
				if (len == -1)
					goto FETCH_BODY_SIMPLE;
				buf += std::move(b2);
			} else {
 FETCH_BODY_SIMPLE:
				std::string b2;
				auto len = mjson.fetch_structure(ctx.io_actor,
				           ctx.defcharset, false, b2);
				if (len == -1)
					buf += "NIL";
				else
					buf += std::move(b2);
			}
		} else if (strcasecmp(kw, "BODYSTRUCTURE") == 0) {
			buf += "BODYSTRUCTURE ";
			if (mjson.has_rfc822_part()) {
				deferred_eml_load();
				auto rfc_path = std::string(pcontext->maildir) + "/tmp/imap.rfc822";
				if (rfc_path.size() <= 0 ||
				    !mjson.rfc822_build(ctx.io_actor, rfc_path.c_str()))
					goto FETCH_BODYSTRUCTURE_SIMPLE;
				std::string b2;
				auto len = mjson.rfc822_fetch(ctx.io_actor, rfc_path.c_str(),
				           pcontext->defcharset, TRUE, b2);
				if (len == -1)
					goto FETCH_BODYSTRUCTURE_SIMPLE;
				buf += std::move(b2);
			} else {
 FETCH_BODYSTRUCTURE_SIMPLE:
				std::string b2;
				auto len = mjson.fetch_structure(ctx.io_actor,
				           ctx.defcharset, TRUE, b2);
				if (len == -1)
					buf += "NIL";
				else
					buf += std::move(b2);
			}
		} else if (strcasecmp(kw, "ENVELOPE") == 0) {
			buf += "ENVELOPE ";
			std::string b2;
			auto len = mjson.fetch_envelope(pcontext->defcharset, b2);
			if (len == -1)
				buf += "NIL";
			else
				buf += std::move(b2);
		} else if (strcasecmp(kw, "FLAGS") == 0) {
			auto line = icp_make_kwannounce_line(ctx, pitem->keywords);
			if (line.size() > 0)
				ctx.stream.write(line.c_str(), line.size());
			auto fs = icp_convert_flags_string(pitem->flag_bits, pitem->keywords, ctx.enabled_rev2);
			buf += "FLAGS ";
			buf += std::move(fs);
		} else if (strcasecmp(kw, "INTERNALDATE") == 0) {
			time_t tmp_time;
			struct tm tmp_tm;

			if (!parse_rfc822_timestamp(mjson.get_mail_received(), &tmp_time))
				tmp_time = strtol(mjson.get_mail_filename(), nullptr, 0);
			memset(&tmp_tm, 0, sizeof(tmp_tm));
			gmtime_r(&tmp_time, &tmp_tm);
			char b2[80];
			strftime(b2, std::size(b2), "INTERNALDATE \"%d-%b-%Y %T +0000\"", &tmp_tm);
			buf += b2;
		} else if (strcasecmp(kw, "RFC822") == 0) {
			buf += fmt::format("RFC822 <<{{file}}{}|0|{}\r\n",
			       mjson.get_mail_filename(),
			       mjson.get_mail_length());
			if (!pcontext->b_readonly &&
			    !(pitem->flag_bits & FLAG_SEEN)) {
				midb_agent::set_flags(pcontext->maildir,
					pcontext->selected_folder, pitem->mid,
					FLAG_SEEN, nullptr, &errnum);
				pitem->flag_bits |= FLAG_SEEN;
				imap_parser_bcast_flags(*pcontext, pitem->uid, bcastfl::include_self);
			}
		} else if (strcasecmp(kw, "RFC822.HEADER") == 0) {
			auto pmime = mjson.get_mime("");
			if (pmime != nullptr)
				buf += fmt::format("RFC822.HEADER <<{{file}}{}|0|{}\r\n",
				       mjson.get_mail_filename(),
				       pmime->get_head_length());
			else
				buf += "RFC822.HEADER NIL";
		} else if (strcasecmp(kw, "RFC822.SIZE") == 0) {
			buf += "RFC822.SIZE ";
			buf += std::to_string(mjson.get_mail_length());
		} else if (strcasecmp(kw, "RFC822.TEXT") == 0) {
			auto pmime = mjson.get_mime("");
			size_t ct_length = pmime != nullptr ? pmime->get_content_length() : 0;
			if (pmime != nullptr)
				buf += fmt::format("RFC822.TEXT <<{{file}}{}|{}|{}\r\n",
				       mjson.get_mail_filename(),
				       pmime->get_content_offset(),
				       ct_length);
			else
				buf += "RFC822.TEXT NIL";
			if (!pcontext->b_readonly &&
			    !(pitem->flag_bits & FLAG_SEEN)) {
				midb_agent::set_flags(pcontext->maildir,
					pcontext->selected_folder, pitem->mid,
					FLAG_SEEN, nullptr, &errnum);
				pitem->flag_bits |= FLAG_SEEN;
				imap_parser_bcast_flags(*pcontext, pitem->uid, bcastfl::include_self);
			}
		} else if (strcasecmp(kw, "UID") == 0) {
			buf += "UID ";
			buf += std::to_string(pitem->uid);
		} else if (strncasecmp(kw, "BODY[", 5) == 0 ||
		    strncasecmp(kw, "BODY.PEEK[", 10) == 0) {
			auto pbody = strchr(kw, '[');
			assert(pbody != nullptr);
			auto pend = strchr(pbody + 1, ']');
			if (pend == nullptr)
				return 1800;
			size_t offset = 0, length = -1;
			if (pend[1] == '<') {
				offset = strtol(pend + 2, nullptr, 0);
				auto pdot = strchr(pend + 2, '.');
				if (NULL != pdot) {
					length = strtol(pdot + 1, nullptr, 0);
					/* trim the length information for response tag */
					pdot[0] = '>';
					pdot[1] = '\0';
				}
			}
			auto len = pend - (pbody + 1);
			auto [temp_id, data_item] = split_DI(std::string_view(&pbody[1], len));
			const char *ptr = data_item.empty() ? nullptr : data_item.c_str();
			/*
			 * The .MIME header of a message/rfc822 sub-part lives
			 * in the enclosing message, not in the encapsulated
			 * one. When the outer mjson already has that part
			 * (e.g. BODY[2.MIME] where part 2 is message/rfc822),
			 * resolve it against the outer mjson directly.
			 * Otherwise rfc822_get() is told (want_mime) to
			 * descend only as far as the message that contains the
			 * part and leave the part id for the final ".MIME"
			 * lookup.
			 */
			bool want_mime = ptr != nullptr && strcasecmp(ptr, "MIME") == 0;
			bool flag_mime_on_outer = want_mime &&
				mjson.get_mime(temp_id.c_str()) != nullptr;
			if (temp_id.size() > 0 && !flag_mime_on_outer &&
			    mjson.has_rfc822_part()) {
				deferred_eml_load();
				auto rfc_path = std::string(pcontext->maildir) + "/tmp/imap.rfc822";
				if (rfc_path.size() > 0 &&
				    mjson.rfc822_build(ctx.io_actor, rfc_path.c_str())) {
					MJSON temp_mjson;
					char mjson_id[64], final_id[64];
					if (mjson.rfc822_get(ctx.io_actor,
					    &temp_mjson, rfc_path.c_str(),
					    temp_id.c_str(), mjson_id, final_id, want_mime))
						len = icp_print_structure(ctx,
						      &temp_mjson, kwss.c_str(), buf,
						      pbody, final_id, ptr, offset, length,
						      mjson.get_mail_filename());
					else
						len = icp_print_structure(ctx,
						      &mjson, kwss.c_str(), buf,
						      pbody, temp_id.c_str(), ptr, offset, length, nullptr);
				} else {
					len = icp_print_structure(ctx, &mjson, kwss, buf,
					      pbody, temp_id.c_str(), ptr, offset, length, nullptr);
				}
			} else {
				len = icp_print_structure(ctx, &mjson, kwss, buf,
				      pbody, temp_id.c_str(), ptr,
				      offset, length, nullptr);
			}
			if (len < 0)
				return 1918;
			if (!pcontext->b_readonly &&
			    !(pitem->flag_bits & FLAG_SEEN) &&
			    strncasecmp(kw, "BODY[", 5) == 0) {
				midb_agent::set_flags(pcontext->maildir,
					pcontext->selected_folder, pitem->mid,
					FLAG_SEEN, nullptr, &errnum);
				pitem->flag_bits |= FLAG_SEEN;
				imap_parser_bcast_flags(*pcontext, pitem->uid, bcastfl::include_self);
			}
		}
	}
	buf += ")\r\n";
	if (pcontext->stream.write(buf.data(), buf.size()) != STREAM_WRITE_OK)
		return 1922;
	/*
	 * The response for this message is now buffered in ctx.stream (message
	 * bodies as <<{file}>>/<<{rfc822}>> placeholders). The top-level .eml
	 * scratch copies cached while assembling it are no longer needed — the
	 * wrdat writer re-reads them from storage — so drop them before moving
	 * to the next message. Without this, "FETCH 1:* ..." over a large
	 * folder would hold every message body resident until the whole
	 * response had been streamed out.
	 */
	ctx.io_actor.drop_reconstructible();
	if (!pcontext->b_readonly && pitem->flag_bits & FLAG_RECENT) {
		pitem->flag_bits &= ~FLAG_RECENT;
		if (!(pitem->flag_bits & FLAG_SEEN)) {
			midb_agent::unset_flags(pcontext->maildir,
				pcontext->selected_folder, pitem->mid,
				FLAG_RECENT, nullptr, &errnum);
			imap_parser_bcast_flags(*pcontext, pitem->uid);
		}
	}
	return 0;
} catch (const std::bad_alloc &) {
	mlog(LV_ERR, "E-1464: ENOMEM");
	return 1918;
}

/**
 * Split STORE flag atoms into the standard IMAP system flags (returned as a
 * bitmask) and custom keywords (returned verbatim). The pseudo-keyword "\*" is
 * not settable and is rejected. Returns false on an unparsable system flag
 * (e.g. some unknown '\'-prefixed atom).
 */
static bool icp_classify_store_flags(const std::vector<std::string> &atoms,
    int &flag_bits, std::vector<std::string> &kw_list)
{
	for (const auto &kw_s : atoms) {
		auto keyword = kw_s.c_str();
		if (strcasecmp(keyword, "\\Answered") == 0)
			flag_bits |= FLAG_ANSWERED;
		else if (strcasecmp(keyword, "\\Flagged") == 0)
			flag_bits |= FLAG_FLAGGED;
		else if (strcasecmp(keyword, "\\Deleted") == 0)
			flag_bits |= FLAG_DELETED;
		else if (strcasecmp(keyword, "\\Seen") == 0)
			flag_bits |= FLAG_SEEN;
		else if (strcasecmp(keyword, "\\Draft") == 0)
			flag_bits |= FLAG_DRAFT;
		else if (strcasecmp(keyword, "\\Recent") == 0)
			flag_bits |= FLAG_RECENT;
		else if (strcasecmp(keyword, "$Forwarded") == 0)
			flag_bits |= FLAG_FORWARDED;
		else if (strcmp(keyword, "\\*") == 0)
			/* "\*" denotes "any keyword permitted", not settable */
			return false;
		else if (keyword[0] == '\\')
			/* unknown system flag */
			return false;
		else if (!kw_s.empty())
			/* custom keyword (e.g. $keyword1, NIL, ...) */
			kw_list.emplace_back(kw_s);
	}
	return true;
}

static std::string icp_join_keywords(const std::vector<std::string> &kw_list)
{
	std::string out;
	for (const auto &k : kw_list) {
		if (!out.empty())
			out += ' ';
		out += k;
	}
	return out;
}

/**
 * Retrieve the current custom keyword set for a single message (by seqid) from
 * midb. Used to compute the resulting set for +FLAGS/-FLAGS, since the simple
 * fetch path does not carry keywords.
 */
static std::string icp_get_keywords(imap_context &ctx, int id)
{
	imap_seq_list seq;
	seq.insert(id, id);
	XARRAY xa;
	int errnum = 0;
	if (midb_agent::fetch_detail_uid(ctx.maildir, ctx.selected_folder,
	    seq, &xa, &errnum) != MIDB_RESULT_OK)
		return "";
	auto item = xa.get_item(0);
	return item != nullptr ? item->keywords : "";
}

static void icp_store_flags(const char *cmd, const std::string &mid,
    int id, unsigned int uid, unsigned int flag_bits,
    const std::vector<std::string> &kw_list, imap_context &ctx)
{
	auto pcontext = &ctx;
	int errnum;
	char buff[1024];
	int string_length;
	std::string kw_result; /* the resulting keyword set, for the FETCH echo */
	
	string_length = 0;
	if (0 == strcasecmp(cmd, "FLAGS") ||
		0 == strcasecmp(cmd, "FLAGS.SILENT")) {
		midb_agent::unset_flags(pcontext->maildir, pcontext->selected_folder,
			mid, FLAG_SETTABLE, nullptr, &errnum);
		midb_agent::set_flags(pcontext->maildir, pcontext->selected_folder,
			mid, flag_bits, nullptr, &errnum);
		/* FLAGS replaces the keyword set wholesale (empty clears it). */
		kw_result = icp_join_keywords(kw_list);
		midb_agent::set_keywords(pcontext->maildir, pcontext->selected_folder,
			mid, kw_result, &errnum);
		if (0 == strcasecmp(cmd, "FLAGS")) {
			auto fs = icp_convert_flags_string(flag_bits, kw_result, ctx.enabled_rev2);
			if (uid != 0)
				string_length = gx_snprintf(buff, std::size(buff),
					"* %d FETCH (FLAGS %s UID %d)\r\n",
					id, fs.c_str(), uid);
			else
				string_length = gx_snprintf(buff, std::size(buff),
					"* %d FETCH (FLAGS %s)\r\n",
					id, fs.c_str());
		}
	} else if (0 == strcasecmp(cmd, "+FLAGS") ||
		0 == strcasecmp(cmd, "+FLAGS.SILENT")) {
		midb_agent::set_flags(pcontext->maildir, pcontext->selected_folder,
			mid, flag_bits, nullptr, &errnum);
		if (!kw_list.empty()) {
			/* union of current and requested keywords */
			auto cur = icp_get_keywords(ctx, id);
			std::vector<std::string> merged;
			for (size_t pos = 0; pos < cur.size(); ) {
				auto sp = cur.find(' ', pos);
				if (sp == std::string::npos)
					sp = cur.size();
				if (sp > pos)
					merged.emplace_back(cur.substr(pos, sp - pos));
				pos = sp + 1;
			}
			for (const auto &k : kw_list)
				if (std::find(merged.cbegin(), merged.cend(), k) == merged.cend())
					merged.emplace_back(k);
			kw_result = icp_join_keywords(merged);
			midb_agent::set_keywords(pcontext->maildir,
				pcontext->selected_folder, mid, kw_result, &errnum);
		}
		if (0 == strcasecmp(cmd, "+FLAGS") && 
			MIDB_RESULT_OK == midb_agent::get_flags(pcontext->maildir,
		    pcontext->selected_folder, mid, &flag_bits, &errnum)) {
			if (kw_result.empty())
				kw_result = icp_get_keywords(ctx, id);
			auto fs = icp_convert_flags_string(flag_bits, kw_result, ctx.enabled_rev2);
			if (uid != 0)
				string_length = gx_snprintf(buff, std::size(buff),
					"* %d FETCH (FLAGS %s UID %d)\r\n",
					id, fs.c_str(), uid);
			else
				string_length = gx_snprintf(buff, std::size(buff),
					"* %d FETCH (FLAGS %s)\r\n",
					id, fs.c_str());
		}
	} else if (0 == strcasecmp(cmd, "-FLAGS") ||
		0 == strcasecmp(cmd, "-FLAGS.SILENT")) {
		midb_agent::unset_flags(pcontext->maildir, pcontext->selected_folder,
			mid, flag_bits, nullptr, &errnum);
		if (!kw_list.empty()) {
			/* current minus requested keywords */
			auto cur = icp_get_keywords(ctx, id);
			std::vector<std::string> kept;
			for (size_t pos = 0; pos < cur.size(); ) {
				auto sp = cur.find(' ', pos);
				if (sp == std::string::npos)
					sp = cur.size();
				if (sp > pos) {
					auto tok = cur.substr(pos, sp - pos);
					if (std::find(kw_list.cbegin(), kw_list.cend(), tok) == kw_list.cend())
						kept.emplace_back(std::move(tok));
				}
				pos = sp + 1;
			}
			kw_result = icp_join_keywords(kept);
			midb_agent::set_keywords(pcontext->maildir,
				pcontext->selected_folder, mid, kw_result, &errnum);
		}
		if (0 == strcasecmp(cmd, "-FLAGS") &&
			MIDB_RESULT_OK == midb_agent::get_flags(pcontext->maildir,
		    pcontext->selected_folder, mid, &flag_bits, &errnum)) {
			if (kw_result.empty() && kw_list.empty())
				kw_result = icp_get_keywords(ctx, id);
			auto fs = icp_convert_flags_string(flag_bits, kw_result, ctx.enabled_rev2);
			if (uid != 0)
				string_length = gx_snprintf(buff, std::size(buff),
					"* %d FETCH (FLAGS %s UID %d)\r\n",
					id, fs.c_str(), uid);
			else
				string_length = gx_snprintf(buff, std::size(buff),
					"* %d FETCH (FLAGS %s)\r\n",
					id, fs.c_str());
		}
	}
	if (string_length != 0) {
		auto line = icp_make_kwannounce_line(*pcontext, kw_result);
		if (line.size() > 0)
			imap_parser_safe_write(pcontext, line.c_str(), line.size());
		imap_parser_safe_write(pcontext, buff, string_length);
	}
}

static bool icp_convert_imaptime(const char *str_time, time_t *ptime)
{
	struct tm tmp_tm{};
	auto str_zone = strptime(str_time, "%d-%b-%Y %T ", &tmp_tm);
	if (str_zone == nullptr)
		return FALSE;
	int min_west = 0;
	if (!simple_zone_to_minwest(str_zone, &min_west, nullptr))
		return false;
	*ptime = timegm(&tmp_tm) + 60 * min_west;
	return TRUE;
}

static bool icp_wildcard_match(const char *folder, const char *mask)
{
	while (true) {
		if (*folder == '\0' && *mask == '\0')
			return true;
		if (*mask != '*' && *mask != '%') {
			if (HX_toupper(*folder) != HX_toupper(*mask))
				return false;
			++folder;
			++mask;
			continue;
		}
		/* Find longest match for wildcards */
		auto span = *mask == '*' ? strlen(folder) : strcspn(folder, "/");
		++mask;
		while (true) {
			if (icp_wildcard_match(&folder[span], mask))
				return true;
			if (span-- == 0)
				break;
		}
		return false;
	}
}

/**
 * See sysfolder_to_imapfolder for some notes.
 */
static bool icp_imapfolder_to_sysfolder(const char *imap_folder,
    std::string &sys_folder, bool utf8) try
{
	std::string t;
	if (utf8) {
		/* rev2 mailbox names are UTF-8 */
		t = imap_folder;
	} else {
		t.resize(strlen(imap_folder));
		if (mutf7_to_utf8(imap_folder, strlen(imap_folder), t.data(),
		    t.size() + 1) < 0)
			return false;
		t.resize(strlen(t.c_str()));
	}
	if (t.size() > 0 && t.back() == '/')
		t.pop_back();
	if (strncasecmp(t.c_str(), "inbox", 5) == 0 &&
	    (t[5] == '\0' || t[5] == '/'))
		memcpy(t.data(), "INBOX", 5);
	sys_folder = base64_encode(t);
	return TRUE;
} catch (const std::bad_alloc &) {
	mlog(LV_ERR, "E-2332: ENOMEM");
	return false;
}

static bool icp_sysfolder_to_imapfolder(const enum_folder_t &sys_folder,
    std::string &imap_folder, bool utf8) try
{
	if (sys_folder.first == PRIVATE_FID_INBOX) {
		imap_folder = "INBOX";
		return TRUE;
	}
	auto t = base64_decode(sys_folder.second);
	if (t.empty())
		return FALSE;
	if (utf8) {
		imap_folder = std::move(t);
		return TRUE;
	}
	imap_folder.resize(utf8_to_mb_len(t.c_str()));
	if (utf8_to_mutf7(t.c_str(), t.size(), imap_folder.data(), imap_folder.size() + 1) <= 0)
		return FALSE;
	imap_folder.resize(strlen(imap_folder.c_str()));
	return TRUE;
} catch (const std::bad_alloc &) {
	mlog(LV_ERR, "E-2331: ENOMEM");
	return false;
}

static void icp_convert_folderlist(std::vector<enum_folder_t> &pfile, bool utf8) try
{
	std::string o;

	for (auto &e : pfile)
		if (icp_sysfolder_to_imapfolder(e, o, utf8))
			e.second = std::move(o);
} catch (const std::bad_alloc &) {
	mlog(LV_ERR, "E-1814: ENOMEM");
}

static std::string flagbits_to_s(bool seen, bool answ, bool flagged, bool draft,
    bool del, bool fwd)
{
	std::string s = "(";
	if (seen)    s += midb_flag::seen;
	if (answ)    s += midb_flag::answered;
	if (flagged) s += midb_flag::flagged;
	if (draft)   s += midb_flag::unsent;
	if (del)     s += midb_flag::deleted;
	if (fwd)     s += midb_flag::forwarded;
	s += ')';
	return s;
}

int icp_capability(std::span<std::string> argv, imap_context &ctx) try
{
	auto pcontext = &ctx;
	if (pcontext->proto_stat == iproto_stat::select)
		imap_parser_echo_modify(pcontext, NULL);
	/* IMAP_CODE_2170001: OK CAPABILITY completed */
	char ext_str[256];
	capability_list(ext_str, std::size(ext_str), pcontext);
	auto buf = fmt::format("* CAPABILITY {}\r\n{} {}",
	           ext_str, argv[0], resource_get_imap_code(1701, 1));
	imap_parser_safe_write(pcontext, buf.c_str(), buf.size());
	return DISPATCH_CONTINUE;
} catch (const std::bad_alloc &) {
	mlog(LV_ERR, "E-2425: ENOMEM");
	return 1918;
}

/**
 * This is for the ENABLE command (RFC 5161/9051), which is only valid once
 * authenticated. It enables the capabilities the server recognises and
 * silently ignores the rest. (An `ENABLE QRESYNC` against a server without
 * QRESYNC still succeeds with an empty `* ENABLED` line.) The only capability
 * Gromox acts on is IMAP4rev2.
 */
int icp_enable(std::span<std::string> argv, imap_context &ctx) try
{
	if (!ctx.is_authed())
		return 1804;
	if (argv.size() < 3)
		return 1800;
	std::string enabled;
	for (size_t i = 2; i < argv.size(); ++i) {
		if (strcasecmp(argv[i].c_str(), "IMAP4REV2") == 0) {
			ctx.enabled_rev2 = true;
			enabled += " IMAP4rev2";
		}
		/* unrecognised capabilities are ignored, never an error */
	}
	auto buf = fmt::format("* ENABLED{}\r\n{} {}", enabled,
	           argv[0], resource_get_imap_code(1731, 1));
	imap_parser_safe_write(&ctx, buf.c_str(), buf.size());
	return DISPATCH_CONTINUE;
} catch (const std::bad_alloc &) {
	return 1918;
}

/**
 * NAMESPACE (RFC 2342, part of the IMAP4rev2 capability set). Gromox exposes a
 * single personal namespace rooted at "" with a "/" hierarchy delimiter and no
 * "other users" or shared namespaces. Valid once authenticated.
 */
int icp_namespace(std::span<std::string> argv, imap_context &ctx) try
{
	if (!ctx.is_authed())
		return 1804;
	auto buf = fmt::format("* NAMESPACE ((\"\" \"/\")) NIL NIL\r\n{} {}",
	           argv[0], resource_get_imap_code(1732, 1));
	imap_parser_safe_write(&ctx, buf.c_str(), buf.size());
	return DISPATCH_CONTINUE;
} catch (const std::bad_alloc &) {
	return 1918;
}

int icp_id(std::span<std::string> argv, imap_context &ctx) try
{
	auto pcontext = &ctx;
	if (pcontext->proto_stat == iproto_stat::select)
		imap_parser_echo_modify(pcontext, NULL);
	std::string buf;
	if (parse_bool(g_config_file->get_value("enable_rfc2971_commands")))
		/* IMAP_CODE_2170029: OK ID completed */
		buf = fmt::format("* ID (\"name\" \"gromox-imap\" "
		      "version \"{}\")\r\n{} {}", PACKAGE_VERSION,
		      argv[0], resource_get_imap_code(1729, 1));
	else
		buf = argv[0] + " "s + resource_get_imap_code(1800, 1);
	imap_parser_safe_write(pcontext, buf.c_str(), buf.size());
	return DISPATCH_CONTINUE;
} catch (const std::bad_alloc &) {
	return 1918;
}

int icp_noop(std::span<std::string> argv, imap_context &ctx)
{
	auto pcontext = &ctx;
	if (pcontext->proto_stat == iproto_stat::select)
		imap_parser_echo_modify(pcontext, NULL);
	return 1702;
}

int icp_logout(std::span<std::string> argv, imap_context &ctx) try
{
	/* IMAP_CODE_2160001: BYE logging out */
	/* IMAP_CODE_2170003: OK LOGOUT completed */
	auto buf = "* "s + resource_get_imap_code(1601, 1) +
	           argv[0] + " " + resource_get_imap_code(1703, 1);
	imap_parser_safe_write(&ctx, buf.c_str(), buf.size());
	return DISPATCH_SHOULD_CLOSE;
} catch (const std::bad_alloc &) {
	return 1918;
}

int icp_starttls(std::span<std::string> argv, imap_context &ctx)
{
	auto pcontext = &ctx;
	if (pcontext->connection.ssl != nullptr)
		return 1800;
	if (!g_support_tls)
		return 1800;
	if (pcontext->proto_stat > iproto_stat::noauth)
		return 1801;
	pcontext->sched_stat = isched_stat::stls;	
	return 1704;
}

int icp_authenticate(std::span<std::string> argv, imap_context &ctx)
{
	auto pcontext = &ctx;
	if (g_support_tls && g_force_tls && pcontext->connection.ssl == nullptr)
		return 1802;
	if (argv.size() != 3 || strcasecmp(argv[2].c_str(), "LOGIN") != 0)
		return 1800;
	if (pcontext->is_authed())
		return 1803;
	gx_strlcpy(pcontext->tag_string, argv[0].c_str(), std::size(pcontext->tag_string));
	pcontext->proto_stat = iproto_stat::username;
	static constexpr char prompt[] = "+ VXNlciBOYW1lAA==\r\n";
	imap_parser_safe_write(pcontext, prompt, strlen(prompt));
    return DISPATCH_CONTINUE;
}

static int icp_username2(const char *cmdbuf, imap_context &ctx)
{
	auto pcontext = &ctx;
	size_t temp_len;
	
	if (base64nl_decode_sized(cmdbuf,
	    pcontext->username, std::size(pcontext->username),
	    &temp_len) != 0) {
		pcontext->proto_stat = iproto_stat::noauth;
		return 1819 | DISPATCH_TAG;
	}
	pcontext->proto_stat = iproto_stat::password;
	static constexpr char prompt[] = "+ UGFzc3dvcmQA\r\n";
	imap_parser_safe_write(pcontext, prompt, strlen(prompt));
    return DISPATCH_CONTINUE;
}

int icp_username(const char *cmdbuf, imap_context &ctx)
{
	return icp_dval(cmdbuf, ctx, icp_username2(cmdbuf, ctx));
}

static inline const char *tag_or_bug(const char *s)
{
	return *s != '\0' ? s : "BUG";
}

static bool store_owner_over(const char *actor, const char *mbox, const char *mboxdir)
{
	if (mbox == nullptr)
		return true; /* No impersonation of another store */
	if (strcmp(actor, mbox) == 0)
		return true; /* Silly way of logging in to your own mailbox but ok */
	uint32_t perms = 0;
	imrpc_build_env();
	auto ok = exmdb_client->get_mbox_perm(mboxdir, actor, &perms) &&
	          perms & frightsGromoxStoreOwner;
	imrpc_free_env();
	return ok;
}

static int icp_password2(const char *cmdbuf, imap_context &ctx) try
{
	auto pcontext = &ctx;
	size_t temp_len;
	char temp_password[256];
	
	pcontext->proto_stat = iproto_stat::noauth;
	if (base64nl_decode_sized(cmdbuf,
	    temp_password, std::size(temp_password), &temp_len) != 0)
		return 1820 | DISPATCH_TAG;

	auto target_mbox = strchr(pcontext->username, '!');
	if (target_mbox != nullptr)
		*target_mbox++ = '\0';
	HX_strltrim(pcontext->username);
	if (!system_services_judge_user(pcontext->username)) {
		imap_parser_log_info(pcontext, LV_WARN, "LOGIN phase2 rejected: denied by user filter");
		return 1901 | DISPATCH_TAG | DISPATCH_SHOULD_CLOSE;
    }
	sql_meta_result mres_auth, mres /* target */;
	if (!system_services_auth_login(pcontext->username, temp_password,
	    USER_PRIVILEGE_IMAP, mres_auth)) {
		safe_memset(temp_password, 0, std::size(temp_password));
		imap_parser_log_info(pcontext, LV_WARN, "LOGIN phase2 rejected: %s",
			mres_auth.errstr.c_str());
		pcontext->auth_times ++;
		if (pcontext->auth_times < g_max_auth_times)
			return 1904 | DISPATCH_CONTINUE | DISPATCH_TAG;
		system_services_ban_user(pcontext->username, g_block_auth_fail);
		return 1903 | DISPATCH_TAG | DISPATCH_SHOULD_CLOSE;
	}
	safe_memset(temp_password, 0, std::size(temp_password));
	if (target_mbox == nullptr) {
		mres = std::move(mres_auth);
	} else {
		if (mysql_adaptor_meta(target_mbox, WANTPRIV_METAONLY, mres) != 0)
			return 1902 | DISPATCH_CONTINUE | DISPATCH_TAG;
		if (!store_owner_over(mres_auth.username.c_str(), mres.username.c_str(),
		    mres.maildir.c_str())) {
			imap_parser_log_info(pcontext, LV_WARN, "LOGIN phase2 rejected: %s", mres.errstr.c_str());
			++pcontext->auth_times;
			if (pcontext->auth_times < g_max_auth_times)
				return 1904 | DISPATCH_CONTINUE | DISPATCH_TAG;
			system_services_ban_user(pcontext->username, g_block_auth_fail);
			return 1903 | DISPATCH_TAG | DISPATCH_SHOULD_CLOSE;
		}
	}
	gx_strlcpy(pcontext->username, mres.username.c_str(), std::size(pcontext->username));
	gx_strlcpy(pcontext->maildir, mres.maildir.c_str(), std::size(pcontext->maildir));
	if (*pcontext->maildir == '\0')
		return 1902 | DISPATCH_TAG;
	if (mres.lang.empty())
		mres.lang = znul(g_config_file->get_value("default_lang"));
	gx_strlcpy(pcontext->defcharset, resource_get_default_charset(mres.lang.c_str()),
		std::size(pcontext->defcharset));
	pcontext->proto_stat = iproto_stat::auth;
	imap_parser_log_info(pcontext, LV_DEBUG, "LOGIN ok");
	char caps[256];
	capability_list(caps, std::size(caps), pcontext);
	auto buf = fmt::format("{} OK [CAPABILITY {}] Logged in\r\n",
		   tag_or_bug(pcontext->tag_string), caps);
	imap_parser_safe_write(pcontext, buf.c_str(), buf.size());
	return DISPATCH_CONTINUE;
} catch (const std::bad_alloc &) {
	return 1918;
}

int icp_password(const char *cmdbuf, imap_context &ctx)
{
	return icp_dval(cmdbuf, ctx, icp_password2(cmdbuf, ctx));
}

int icp_login(std::span<std::string> argv, imap_context &ctx)
{
	auto pcontext = &ctx;
	char temp_password[256];
    
	if (g_support_tls && g_force_tls && pcontext->connection.ssl == nullptr)
		return 1802;
	if (argv.size() != 4 || argv[2].size() >= std::size(pcontext->username) ||
	    argv[3].size() > 255)
		return 1800;
	if (pcontext->is_authed())
		return 1803;
	auto target_mbox = strchr(argv[2].data(), '!');
	if (target_mbox != nullptr)
		*target_mbox++ = '\0';
	gx_strlcpy(pcontext->username, argv[2].c_str(), std::size(pcontext->username));
	HX_strltrim(pcontext->username);
	if (!system_services_judge_user(pcontext->username)) {
		imap_parser_log_info(pcontext, LV_WARN, "LOGIN phase0 rejecting \"%s\": denied by user filter",
			pcontext->username);
		return 1901 | DISPATCH_SHOULD_CLOSE;
    }
	gx_strlcpy(temp_password, argv[3].c_str(), std::size(temp_password));
	HX_strltrim(temp_password);

	sql_meta_result mres_auth, mres /* target */;
	if (!system_services_auth_login(pcontext->username, temp_password,
	    USER_PRIVILEGE_IMAP, mres_auth)) {
		imap_parser_log_info(pcontext, LV_WARN, "LOGIN phase1 rejecting \"%s\": %s",
			pcontext->username, mres.errstr.c_str());
		pcontext->auth_times++;
		if (pcontext->auth_times < g_max_auth_times) {
			gx_strlcpy(pcontext->tag_string, argv[0].c_str(), std::size(pcontext->tag_string));
			return 1904 | DISPATCH_CONTINUE | DISPATCH_TAG;
		}
		system_services_ban_user(pcontext->username, g_block_auth_fail);
		return 1903 | DISPATCH_SHOULD_CLOSE;
	}
	safe_memset(temp_password, 0, std::size(temp_password));
	if (target_mbox == nullptr) {
		mres = std::move(mres_auth);
	} else {
		if (mysql_adaptor_meta(target_mbox, WANTPRIV_METAONLY, mres) != 0)
			return 1902 | DISPATCH_CONTINUE | DISPATCH_TAG;
		if (!store_owner_over(mres_auth.username.c_str(), mres.username.c_str(),
		    mres.maildir.c_str())) {
			imap_parser_log_info(pcontext, LV_WARN, "LOGIN phase1 rejected: %s", mres.errstr.c_str());
			++pcontext->auth_times;
			if (pcontext->auth_times < g_max_auth_times)
				return 1904 | DISPATCH_CONTINUE | DISPATCH_TAG;
			system_services_ban_user(pcontext->username, g_block_auth_fail);
			return 1903 | DISPATCH_SHOULD_CLOSE;
		}
	}
	gx_strlcpy(pcontext->username, mres.username.c_str(), std::size(pcontext->username));
	gx_strlcpy(pcontext->maildir, mres.maildir.c_str(), std::size(pcontext->maildir));
	if (*pcontext->maildir == '\0')
		return 1902;
	if (mres.lang.empty())
		mres.lang = znul(g_config_file->get_value("default_lang"));
	gx_strlcpy(pcontext->defcharset, resource_get_default_charset(mres.lang.c_str()),
		std::size(pcontext->defcharset));
	pcontext->proto_stat = iproto_stat::auth;
	imap_parser_log_info(pcontext, LV_DEBUG, "LOGIN ok");
	return 1705;
}

int icp_idle(std::span<std::string> argv, imap_context &ctx)
{
	auto pcontext = &ctx;
	if (!pcontext->is_authed())
		return 1804;
	if (argv.size() != 2)
		return 1800;
	gx_strlcpy(pcontext->tag_string, argv[0].c_str(), std::size(pcontext->tag_string));
	pcontext->sched_stat = isched_stat::idling;
	size_t len = 0;
	auto reply = resource_get_imap_code(1602, 1, &len);
	pcontext->connection.write(reply, len);
	return 0;
}

static int m2icode(int r, int e)
{
	switch (r) {
	case MIDB_RESULT_OK:
		return 0;
	case MIDB_NO_SERVER:
		return 1905;
	case MIDB_RDWR_ERROR:
		return 1906;
	case MIDB_RESULT_ERROR:
		return DISPATCH_MIDB | static_cast<uint16_t>(e);
	case MIDB_LOCAL_ENOMEM:
		return 1920;
	case MIDB_TOO_MANY_RESULTS:
		return 1921;
	default:
		return 1919;
	}
}

/**
 * Get a listing of all mails in the folder to build the uid<->seqid mapping.
 */
int content_array::refresh(imap_context &ctx, const std::string &folder,
    bool fresh_numbers)
{
	XARRAY xa;
	int errnum = 0;
	imap_seq_list all_seq;
	all_seq.insert(1, SEQ_STAR);
	auto ssr = midb_agent::fetch_simple_uid(ctx.maildir, folder,
	           all_seq, &xa, &errnum);
	auto ret = m2icode(ssr, errnum);
	if (ret != 0)
		return ret;

	if (fresh_numbers) {
		for (size_t i = 0; i < xa.m_vec.size(); ++i)
			xa.m_vec[i].id = i + 1;
		*this = std::move(xa);
	} else {
		auto start = m_vec.size();
		for (auto &newmail : xa.m_vec) {
			if (get_itemx(newmail.uid) != nullptr)
				continue; /* already known */
			auto uid = newmail.uid;
			append(std::move(newmail), uid);
			m_vec[start].id = start + 1;
			++start;
		}
	}
	n_recent = std::count_if(m_vec.cbegin(), m_vec.cend(),
	           [](const MITEM &m) { return m.flag_bits & FLAG_RECENT; });
	auto iter = std::find_if(m_vec.cbegin(), m_vec.cend(),
	            [](const MITEM &m) { return !(m.flag_bits & FLAG_SEEN); });
	firstunseen = iter == m_vec.end() ? 0 : iter - m_vec.cbegin() + 1;
	return 0;
}

static int icp_selex(std::span<std::string> argv, imap_context &ctx, bool readonly) try
{
	auto pcontext = &ctx;
	int errnum;
	std::string sys_name;
    
	if (!pcontext->is_authed())
		return 1804;
	if (argv.size() < 3 || argv[2].size() == 0 || argv[2].size() >= 1024)
		return 1800;
	if (!icp_imapfolder_to_sysfolder(argv[2].c_str(), sys_name,
	    ctx.enabled_rev2))
		/* Undecodable (e.g. bad modified-UTF-7) name: no such mailbox. */
		return 1925;
	/*
	 * RFC 9051 §6.3.2: Switching mailboxes closes the prior one and the
	 * client gets a `* OK [CLOSED]` before the new mailbox's data. Capture
	 * the state before the deselect below clears proto_stat.
	 */
	bool was_selected = ctx.proto_stat == iproto_stat::select;
	if (iproto_stat::select == pcontext->proto_stat) {
		imap_parser_remove_select(pcontext);
		pcontext->proto_stat = iproto_stat::auth;
		pcontext->selected_folder.clear();
	}
	
	uint32_t uidvalid = 0, uidnext = 0;
	auto ssr = midb_agent::summary_folder(pcontext->maildir, sys_name,
	           nullptr, nullptr, nullptr, &uidvalid, &uidnext, &errnum);
	auto ret = m2icode(ssr, errnum);
	if (ret != 0)
		return ret;
	ret = pcontext->contents.refresh(*pcontext, sys_name, true);
	if (ret != 0)
		return ret;
	pcontext->selected_folder = std::move(sys_name);
	pcontext->proto_stat = iproto_stat::select;
	pcontext->b_readonly = readonly;
	imap_parser_add_select(pcontext);

	/*
	 * Seed this session's announced keyword list from the folder's
	 * distinct custom keywords. RFC 3501 §7.2.6: Only plain keyword atoms
	 * are allowed here. "\*" is PERMANENTFLAGS-only and emitting it could
	 * make strict clients disconnect. A failure to query keywords degrades
	 * gracefully and will just emit the usual built-in keywords.
	 */
	std::vector<std::string> kw;
	int kwerr = 0;
	midb_agent::get_folder_keywords(pcontext->maildir,
		pcontext->selected_folder, kw, &kwerr);
	pcontext->announced_keywords = kw;
	ctx.saved_uids.clear();
	std::string kw_join;
	for (const auto &k : kw)
		kw_join += " " + k;

	std::string recent_line;
	if (!ctx.enabled_rev2)
		recent_line = fmt::format("* {} RECENT\r\n", ctx.contents.n_recent);
	/* RFC 9051 §6.3.2: announce the prior mailbox is closed when switching. */
	auto closed_line = ctx.enabled_rev2 && was_selected ?
	                   "* OK [CLOSED] previous mailbox closed\r\n" : "";
	auto buf = fmt::format(
		"{}"
		"* {} EXISTS\r\n"
		"{}"
		"* FLAGS ({}{})\r\n"
		"* OK {}\r\n",
		closed_line, pcontext->contents.n_exists(),
		recent_line, IMAP_FLAGS_SYSTEM, kw_join, readonly ?
		"[PERMANENTFLAGS ()] no permanent flags permitted" :
		"[PERMANENTFLAGS (\\Answered \\Flagged \\Deleted \\Seen \\Draft $Forwarded \\*)] limited");
	/* RFC 9051 §6.3.2 removed the [UNSEEN] response code from SELECT/EXAMINE. */
	if (!ctx.enabled_rev2 && ctx.contents.firstunseen != 0)
		buf += fmt::format("* OK [UNSEEN {}] message {} is first unseen\r\n",
			pcontext->contents.firstunseen,
			pcontext->contents.firstunseen);
	auto s_readonly = readonly ? "READ-ONLY" : "READ-WRITE";
	auto s_command  = readonly ? "EXAMINE" : "SELECT";
	buf += fmt::format("* OK [UIDVALIDITY {}] UIDs valid\r\n"
	       "* OK [UIDNEXT {}] predicted next UID\r\n", uidvalid, uidnext);
	if (ctx.enabled_rev2)
		buf += fmt::format("* LIST () \"/\" {}\r\n", quote_encode(argv[2]));
	buf += fmt::format("{} OK [{}] {} completed\r\n",
		argv[0], s_readonly, s_command);
	imap_parser_safe_write(pcontext, buf.c_str(), buf.size());
	return DISPATCH_CONTINUE;
} catch (const std::bad_alloc &) {
	return 1915;
}

int icp_select(std::span<std::string> argv, imap_context &ctx)
{
	return icp_selex(argv, ctx, false);
}

int icp_examine(std::span<std::string> argv, imap_context &ctx)
{
	return icp_selex(argv, ctx, true);
}

int icp_create(std::span<std::string> argv, imap_context &ctx)
{
	auto pcontext = &ctx;
	int errnum;

	if (!pcontext->is_authed())
		return 1804;
	if (argv.size() < 3 || argv[2].size() == 0)
		return 1800;
	std::vector<enum_folder_t> folder_list;
	auto ssr = midb_agent::enum_folders(pcontext->maildir, folder_list, &errnum);
	auto ret = m2icode(ssr, errnum);
	if (ret != 0)
		return ret;
	icp_convert_folderlist(folder_list, ctx.enabled_rev2);
	std::string sys_name = argv[2]; // Go back to non-encoded string
	if (sys_name.size() > 0 && sys_name.back() == '/')
		sys_name.pop_back();
	if (std::any_of(folder_list.cbegin(), folder_list.cend(),
	    [&](const enum_folder_t &e) { return strcasecmp(e.second.c_str(), sys_name.c_str()) == 0; }))
		return 1926;
	auto len = sys_name.size();
	for (size_t i = 0; i <= len; ++i) {
		if (sys_name[i] != '/' && sys_name[i] != '\0')
			continue;
		sys_name[i] = '\0';
		if (std::any_of(folder_list.cbegin(), folder_list.cend(),
		    [&](const enum_folder_t &e) { return strcasecmp(e.second.c_str(), sys_name.c_str()) == 0; })) {
			sys_name[i] = '/';
			continue;
		}
		std::string converted_name;
		if (!icp_imapfolder_to_sysfolder(sys_name.c_str(),
		    converted_name, ctx.enabled_rev2))
			return 1800;
		ssr = midb_agent::make_folder(pcontext->maildir,
		      converted_name, &errnum);
		ret = m2icode(ssr, errnum);
		if (ret != 0)
			return ret;
		sys_name[i] = '/';
	}
	if (pcontext->proto_stat == iproto_stat::select)
		imap_parser_echo_modify(pcontext, NULL);
	return 1706;
}

int icp_delete(std::span<std::string> argv, imap_context &ctx)
{
	auto pcontext = &ctx;
	int errnum;
	std::string encoded_name;

	if (!pcontext->is_authed())
		return 1804;
	if (argv.size() < 3 || argv[2].size() == 0 || argv[2].size() >= 1024 ||
	    !icp_imapfolder_to_sysfolder(argv[2].c_str(), encoded_name,
	    ctx.enabled_rev2))
		return 1800;

	{
		std::vector<enum_folder_t> folder_list;
		auto ssr = midb_agent::enum_folders(pcontext->maildir,
			   folder_list, &errnum);
		auto ret = m2icode(ssr, errnum);
		if (ret != 0)
			return ret;
		icp_convert_folderlist(folder_list, ctx.enabled_rev2);
		dir_tree folder_tree;
		folder_tree.load_from_memfile(std::move(folder_list));
		auto dh = folder_tree.match(argv[2].c_str());
		if (dh == nullptr)
			return 1925;
		/*
		 * Disallow deleting names with inferior hierarchical names
		 * (RFC 3501 §6.3.4).
		 */
		if (dh->has_children())
			return 1924;
	}

	/* This call is a recursive hard-delete */
	auto ssr = midb_agent::remove_folder(pcontext->maildir,
	           encoded_name, &errnum);
	auto ret = m2icode(ssr, errnum);
	if (ret != 0)
		return ret;
	if (pcontext->proto_stat == iproto_stat::select)
		imap_parser_echo_modify(pcontext, NULL);
	return 1707;
}

int icp_rename(std::span<std::string> argv, imap_context &ctx)
{
	auto pcontext = &ctx;
	int errnum;
	std::string encoded_name, encoded_name1;

	if (!pcontext->is_authed())
		return 1804;
	if (argv.size() < 4 || argv[2].size() == 0 || argv[2].size() >= 1024 ||
	    argv[3].size() == 0 || argv[3].size() >= 1024)
		return 1800;
	if (!icp_imapfolder_to_sysfolder(argv[2].c_str(), encoded_name, ctx.enabled_rev2) ||
	    !icp_imapfolder_to_sysfolder(argv[3].c_str(), encoded_name1, ctx.enabled_rev2))
		return 1800;
	if (strpbrk(argv[3].c_str(), "%*?") != nullptr)
		return 1910;
	auto ssr = midb_agent::rename_folder(pcontext->maildir,
	           encoded_name, encoded_name1, &errnum);
	auto ret = m2icode(ssr, errnum);
	if (ret != 0)
		return ret;
	if (pcontext->proto_stat == iproto_stat::select)
		imap_parser_echo_modify(pcontext, NULL);
	return 1708;
}

int icp_subscribe(std::span<std::string> argv, imap_context &ctx)
{
	auto pcontext = &ctx;
	int errnum;
	std::string sys_name;

	if (!pcontext->is_authed())
		return 1804;
	if (argv.size() < 3 || argv[2].size() == 0 || argv[2].size() >= 1024 ||
	    !icp_imapfolder_to_sysfolder(argv[2].c_str(), sys_name, ctx.enabled_rev2))
		return 1800;
	auto ssr = midb_agent::subscribe_folder(pcontext->maildir,
	           sys_name, &errnum);
	auto ret = m2icode(ssr, errnum);
	if (ret != 0)
		return ret;
	if (pcontext->proto_stat == iproto_stat::select)
		imap_parser_echo_modify(pcontext, NULL);
	return 1709;
}

int icp_unsubscribe(std::span<std::string> argv, imap_context &ctx)
{
	auto pcontext = &ctx;
	int errnum;
	std::string sys_name;

	if (!pcontext->is_authed())
		return 1804;
	if (argv.size() < 3 || argv[2].size() == 0 || argv[2].size() >= 1024 ||
	    !icp_imapfolder_to_sysfolder(argv[2].c_str(), sys_name, ctx.enabled_rev2))
		return 1800;
	auto ssr = midb_agent::unsubscribe_folder(pcontext->maildir,
	           sys_name, &errnum);
	auto ret = m2icode(ssr, errnum);
	if (ret != 0)
		return ret;
	if (pcontext->proto_stat == iproto_stat::select)
		imap_parser_echo_modify(pcontext, NULL);
	return 1710;
}

/**
 * Build a ``* STATUS <display> (<items>)\r\n`` line for one folder. The output
 * is shared by the STATUS command and LIST-STATUS (RFC 9051 §6.3.10).
 * @sys_name is the base64 midb folder name; @display is the already-quoted
 * name for the wire. SIZE and DELETED (RFC 8438) are fetched lazily; RECENT is
 * rejected once rev2 is on. Returns an IMAP code (0 = ok).
 */
static int icp_status_line(imap_context &ctx, const std::string &sys_name,
    const std::string &display, std::span<std::string> items,
    std::string &out) try
{
	int errnum;
	size_t exists = 0, recent = 0, unseen = 0;
	uint32_t uidvalid = 0, uidnext = 0;
	auto ssr = midb_agent::summary_folder(ctx.maildir, sys_name,
	           &exists, &recent, &unseen, &uidvalid, &uidnext, &errnum);
	auto ret = m2icode(ssr, errnum);
	if (ret != 0)
		return ret;
	size_t fsize = 0, fdeleted = 0;
	bool have_sizes = false;
	auto obtain_sizes = [&]() -> int {
		if (have_sizes)
			return 0;
		auto sr = midb_agent::folder_sizes(ctx.maildir, sys_name,
		          &fsize, &fdeleted, &errnum);
		auto ic = m2icode(sr, errnum);
		if (ic == 0)
			have_sizes = true;
		return ic;
	};
	out = fmt::format("* STATUS {} (", display);
	bool b_first = true;
	for (const auto &arg_s : items) {
		auto keyword = arg_s.c_str();
		if (!b_first)
			out += ' ';
		else
			b_first = false;
		if (strcasecmp(keyword, "MESSAGES") == 0) {
			out += fmt::format("MESSAGES {}", exists);
		} else if (strcasecmp(keyword, "RECENT") == 0) {
			if (ctx.enabled_rev2)
				return 1800; /* RFC 9051 removed RECENT */
			out += fmt::format("RECENT {}", recent);
		} else if (strcasecmp(keyword, "UIDNEXT") == 0) {
			out += fmt::format("UIDNEXT {}", uidnext);
		} else if (strcasecmp(keyword, "UIDVALIDITY") == 0) {
			out += fmt::format("UIDVALIDITY {}", uidvalid);
		} else if (strcasecmp(keyword, "UNSEEN") == 0) {
			out += fmt::format("UNSEEN {}", unseen);
		} else if (strcasecmp(keyword, "SIZE") == 0) {
			auto ic = obtain_sizes();
			if (ic != 0)
				return ic;
			out += fmt::format("SIZE {}", fsize);
		} else if (strcasecmp(keyword, "DELETED") == 0) {
			auto ic = obtain_sizes();
			if (ic != 0)
				return ic;
			out += fmt::format("DELETED {}", fdeleted);
		} else {
			return 1800;
		}
	}
	out += ")\r\n";
	return 0;
} catch (const std::bad_alloc &) {
	return 1915;
}

/* LIST selection options (RFC 9051 §6.3.9 / RFC 5258 §3.1) */
enum { LSEL_SUBSCRIBED = 1, LSEL_REMOTE = 2, LSEL_RECURSIVE = 4, LSEL_SPECIALUSE = 8 };

/* LIST return options (RFC 9051 §6.3.9 / RFC 5258 §3.2, RFC 5819 LIST-STATUS) */
enum { LRET_SUBSCRIBED = 1, LRET_CHILDREN = 2, LRET_SPECIALUSE = 4, LRET_STATUS = 8 };

/**
 * Parse a "(...)" LIST option list into flags. is_return selects the return
 * option vocabulary (SUBSCRIBED CHILDREN SPECIAL-USE STATUS(...)) vs. the
 * selection vocabulary (SUBSCRIBED REMOTE RECURSIVEMATCH SPECIAL-USE).
 * STATUS's own "(...)" item list is captured into @status_items. Unknown
 * options will led to false being returned.
 */
static bool icp_parse_list_opts(const std::string &tok, bool is_return,
    unsigned int &flags, std::vector<std::string> &status_items)
{
	if (tok.size() < 2 || tok.front() != '(' || tok.back() != ')')
		return false;
	auto in = tok.substr(1, tok.size() - 2);
	size_t i = 0;
	while (i < in.size()) {
		while (i < in.size() && in[i] == ' ')
			++i;
		if (i >= in.size())
			break;
		size_t s = i;
		while (i < in.size() && in[i] != ' ' && in[i] != '(')
			++i;
		auto w = in.substr(s, i - s);
		if (w.empty())
			continue;
		if (is_return && strcasecmp(w.c_str(), "STATUS") == 0) {
			flags |= LRET_STATUS;
			while (i < in.size() && in[i] == ' ')
				++i;
			if (i >= in.size() || in[i] != '(')
				return false;
			int depth = 0;
			size_t st = i;
			for (; i < in.size(); ++i) {
				if (in[i] == '(')
					++depth;
				else if (in[i] == ')' && --depth == 0) {
					++i;
					break;
				}
			}
			if (depth != 0)
				return false;
			auto sp = in.substr(st + 1, i - st - 2);
			size_t k = 0;
			while (k < sp.size()) {
				while (k < sp.size() && sp[k] == ' ')
					++k;
				size_t ks = k;
				while (k < sp.size() && sp[k] != ' ')
					++k;
				if (k > ks)
					status_items.push_back(sp.substr(ks, k - ks));
			}
			continue;
		}
		if (strcasecmp(w.c_str(), "SUBSCRIBED") == 0) {
			if (is_return)
				flags |= LRET_SUBSCRIBED;
			else
				flags |= LSEL_SUBSCRIBED;
		} else if (strcasecmp(w.c_str(), "SPECIAL-USE") == 0) {
			if (is_return)
				flags |= LRET_SPECIALUSE;
			else
				flags |= LSEL_SPECIALUSE;
		} else if (is_return && strcasecmp(w.c_str(), "CHILDREN") == 0) {
			flags |= LRET_CHILDREN;
		} else if (!is_return && strcasecmp(w.c_str(), "REMOTE") == 0) {
			flags |= LSEL_REMOTE;
		} else if (!is_return && strcasecmp(w.c_str(), "RECURSIVEMATCH") == 0) {
			flags |= LSEL_RECURSIVE;
		} else {
			return false;
		}
	}
	return true;
}

int icp_list(std::span<std::string> argv, imap_context &ctx) try
{
	auto pcontext = &ctx;
	int errnum;
	
	if (!pcontext->is_authed())
		return 1804;
	/*
	 * LIST-EXTENDED (RFC 9051 §6.3.9 / RFC 5258, RFC 5819, RFC 6154):
	 * 	LIST (SUBSCRIBED RECURSIVEMATCH SPECIAL-USE) "" "*"
	 * 	LIST "" ("INBOX" "Work/%") RETURN (SUBSCRIBED CHILDREN
	 * 	         SPECIAL-USE STATUS (MESSAGES UNSEEN))
	 * The classic forms (LIST "" "*", LIST "" % RETURN (SPECIAL-USE),
	 * LIST (SPECIAL-USE) "" %) keep their exact rev1 wire output.
	 */
	if (argv.size() < 3)
		return 1800;
	size_t apos = 2;
	unsigned int sel = 0, ret_opt = 0;
	std::vector<std::string> status_items, dummy;
	if (!argv[2].empty() && argv[2].front() == '(') {
		if (!icp_parse_list_opts(argv[2], false, sel, dummy))
			return 1800;
		++apos;
	}
	if (argv.size() < apos + 2)
		return 1800;
	const auto &reference = argv[apos++];
	/* Mailbox pattern: a single string, or "(pat1 pat2 ...)". */
	std::vector<std::string> patterns;
	const auto &mp = argv[apos++];
	if (mp.size() >= 2 && mp.front() == '(' && mp.back() == ')') {
		std::string in = mp.substr(1, mp.size() - 2);
		size_t k = 0;
		while (k < in.size()) {
			while (k < in.size() && in[k] == ' ')
				++k;
			size_t ks = k;
			while (k < in.size() && in[k] != ' ')
				++k;
			if (k > ks)
				patterns.emplace_back(in.substr(ks, k - ks));
		}
	} else {
		patterns.emplace_back(mp);
	}
	if (argv.size() >= apos + 2 && strcasecmp(argv[apos].c_str(), "RETURN") == 0) {
		if (!icp_parse_list_opts(argv[apos + 1], true, ret_opt, status_items))
			return 1800;
		apos += 2;
	}
	bool want_special  = (sel & LSEL_SPECIALUSE) || (ret_opt & LRET_SPECIALUSE);
	bool only_special  = sel & LSEL_SPECIALUSE;
	bool want_sub_attr = (sel & LSEL_SUBSCRIBED) || (ret_opt & LRET_SUBSCRIBED);
	bool only_sub      = sel & LSEL_SUBSCRIBED;
	bool recursive     = sel & LSEL_RECURSIVE;
	/*
	 * RFC 5258 §3 / RFC 9051 §6.3.9.1: RECURSIVEMATCH is not a selection
	 * option in its own right.
	 */
	if (recursive && (sel & ~(unsigned(LSEL_RECURSIVE) | unsigned(LSEL_REMOTE))) == 0)
		return 1800;
	if (reference.size() >= 1024)
		return 1800;
	for (const auto &p : patterns)
		if (reference.size() + p.size() >= 1024)
			return 1800;

	/* Lone empty pattern: hierarchy delimiter probe (RFC 3501 §6.3.9). */
	if (patterns.size() == 1 && patterns[0].empty()) {
		if (pcontext->proto_stat == iproto_stat::select)
			imap_parser_echo_modify(pcontext, NULL);
		/* IMAP_CODE_2170011: OK LIST completed */
		auto buf = fmt::format("* LIST (\\Noselect) \"/\" \"\"\r\n{} {}",
		           argv[0], resource_get_imap_code(1711, 1));
		imap_parser_safe_write(pcontext, buf.c_str(), buf.size());
		return DISPATCH_CONTINUE;
	}

	std::vector<enum_folder_t> folder_list;
	auto ssr = midb_agent::enum_folders(pcontext->maildir,
	           folder_list, &errnum);
	auto ret = m2icode(ssr, errnum);
	if (ret != 0)
		return ret;
	/*
	 * Capture the midb (base64) names before icp_convert_folderlist
	 * rewrites folder_list to the mutf7 display form, so LIST-STATUS can
	 * query midb.
	 */
	std::map<uint64_t, std::string> midb_name;
	if (ret_opt & LRET_STATUS)
		for (const auto &e : folder_list)
			midb_name.emplace(e.first, e.second);
	icp_convert_folderlist(folder_list, ctx.enabled_rev2);
	dir_tree folder_tree;
	folder_tree.load_from_memfile(folder_list);

	std::set<std::string, dir_tree::cmp> subscribed;
	if (want_sub_attr || only_sub || recursive) {
		std::vector<enum_folder_t> sub_list;
		if (midb_agent::enum_subscriptions(ctx.maildir,
		    sub_list, &errnum) == MIDB_RESULT_OK) {
			icp_convert_folderlist(sub_list, ctx.enabled_rev2);
			for (const auto &e : sub_list)
				subscribed.insert(e.second);
		}
	}

	auto match_any = [&](const std::string &name) {
		for (const auto &p : patterns)
			if (!p.empty() && icp_wildcard_match(name.c_str(),
			    (std::string(reference) + p).c_str()))
				return true;
		return false;
	};
	std::set<std::string, dir_tree::cmp> emitted;
	auto write_entry = [&](const std::string &sys_name, uint64_t fid, bool subbed,
	    const builtin_folder *sp, const char *childinfo) -> int {
		auto dir = folder_tree.match(sys_name.c_str());
		auto have_cld = dir != nullptr && dir->has_children();
		std::string attrs = have_cld ? "\\HasChildren" : "\\HasNoChildren";
		if (want_sub_attr && subbed)
			attrs += " \\Subscribed";
		if (want_special && sp != nullptr) {
			attrs += ' ';
			attrs += sp->use_flags;
		}
		auto line = fmt::format("* LIST ({}) \"/\" {}{}\r\n", attrs,
		            quote_encode(sys_name), childinfo != nullptr ? childinfo : "");
		if (ctx.stream.write(line.c_str(), line.size()) != STREAM_WRITE_OK)
			return 1922;
		if (ret_opt & LRET_STATUS) {
			/*
			 * LIST-STATUS: a per-mailbox STATUS reply follows the
			 * LIST line, queried against the midb (base64) name. A
			 * STATUS failure for one mailbox is not fatal.
			 */
			auto it = midb_name.find(fid);
			std::string sline;
			if (it != midb_name.end() &&
			    icp_status_line(ctx, it->second, quote_encode(sys_name),
			    status_items, sline) == 0 &&
			    ctx.stream.write(sline.c_str(), sline.size()) != STREAM_WRITE_OK)
				return 1922;
		}
		return 0;
	};

	/*
	 * RECURSIVEMATCH (RFC 5258 §3.5): a matching mailbox that does not
	 * itself satisfy the SUBSCRIBED selection is still reported -- without
	 * the \Subscribed attribute but tagged with CHILDINFO -- when it has a
	 * subscribed descendant that is NOT itself returned by this command.
	 * "Descendant" = any subscribed name strictly below "<name>/". A
	 * subscribed descendant that also matches the pattern is returned in
	 * its own right, so it must not additionally induce a CHILDINFO on the
	 * parent.
	 */
	auto has_sub_descendant = [&](const std::string &name) {
		auto prefix = name + "/";
		for (const auto &s : subscribed)
			if (s.size() > prefix.size() &&
			    s.compare(0, prefix.size(), prefix) == 0 &&
			    !match_any(s))
				return true;
		return false;
	};

	ctx.stream.clear();
	for (const auto &enf : folder_list) {
		const auto &sys_name = enf.second;
		auto sp = special_folder(enf.first);
		bool subbed = subscribed.find(sys_name) != subscribed.cend();
		if (!match_any(sys_name))
			continue;
		if (only_special && sp == nullptr)
			continue;
		const char *childinfo = nullptr;
		if (only_sub && !subbed) {
			/*
			 * Not subscribed: only include it under RECURSIVEMATCH, and
			 * only if a subscribed descendant exists -- in which case it
			 * is reported via CHILDINFO rather than as \Subscribed.
			 */
			if (!recursive || !has_sub_descendant(sys_name))
				continue;
			childinfo = " (CHILDINFO (\"SUBSCRIBED\"))";
		}
		if (!emitted.insert(sys_name).second)
			continue;
		/* subbed only contributes \Subscribed when it is a real match. */
		auto rc = write_entry(sys_name, enf.first, childinfo == nullptr && subbed,
		          sp, childinfo);
		if (rc != 0)
			return rc;
	}

	folder_list.clear();
	if (pcontext->proto_stat == iproto_stat::select)
		imap_parser_echo_modify(pcontext, &pcontext->stream);
	/* IMAP_CODE_2170011: OK LIST completed */
	auto buf = fmt::format("{} {}", argv[0], resource_get_imap_code(1711, 1));
	if (pcontext->stream.write(buf.c_str(), buf.size()) != STREAM_WRITE_OK)
		return 1922;
	pcontext->write_offset = 0;
	pcontext->sched_stat = isched_stat::wrlst;
	return DISPATCH_BREAK;
} catch (const std::bad_alloc &) {
	return 1915;
}

int icp_xlist(std::span<std::string> argv, imap_context &ctx) try
{
	auto pcontext = &ctx;
	int errnum;
	
	if (!pcontext->is_authed())
		return 1804;
	if (argv.size() < 4)
		return 1800;
	if (argv[2].size() + argv[3].size() >= 1024)
		return 1800;
	std::string search_pattern = argv[2];
	if (argv[3].empty())
		search_pattern += "*";
	else
		search_pattern += argv[3];
	std::vector<enum_folder_t> folder_list;
	auto ssr = midb_agent::enum_folders(pcontext->maildir,
	           folder_list, &errnum);
	auto ret = m2icode(ssr, errnum);
	if (ret != 0)
		return ret;
	icp_convert_folderlist(folder_list, ctx.enabled_rev2);
	dir_tree folder_tree;
	folder_tree.load_from_memfile(folder_list);
	pcontext->stream.clear();

	for (const auto &fentry : folder_list) {
		const auto &sys_name = fentry.second;
		if (!icp_wildcard_match(sys_name.c_str(), search_pattern.c_str()))
			continue;
		auto special = special_folder(fentry.first);
		auto pdir = folder_tree.match(sys_name.c_str());
		auto have = pdir != nullptr && pdir->has_children();
		auto buf  = fmt::format("* XLIST (\\Has{}Children{}{}) \"/\" {}\r\n",
		            have ? "" : "No",
		            special != nullptr ? " " : "",
		            special != nullptr ? special->use_flags : "",
		            quote_encode(sys_name));
		if (pcontext->stream.write(buf.c_str(), buf.size()) != 0)
			return 1922;
	}
	folder_list.clear();
	if (pcontext->proto_stat == iproto_stat::select)
		imap_parser_echo_modify(pcontext, &pcontext->stream);
	/* IMAP_CODE_2170012: OK XLIST completed */
	auto buf = fmt::format("{} {}", argv[0], resource_get_imap_code(1712, 1));
	if (pcontext->stream.write(buf.c_str(), buf.size()) != STREAM_WRITE_OK)
		return 1922;
	pcontext->write_offset = 0;
	pcontext->sched_stat = isched_stat::wrlst;
	return DISPATCH_BREAK;
} catch (const std::bad_alloc &) {
	return 1915;
}

int icp_lsub(std::span<std::string> argv, imap_context &ctx) try
{
	auto pcontext = &ctx;
	int errnum;
	
	if (!pcontext->is_authed())
		return 1804;
	if (argv.size() < 4)
		return 1800;
	if (argv[2].size() + argv[3].size() >= 1024)
		return 1800;
	if ('\0' == argv[3][0]) {
		if (pcontext->proto_stat == iproto_stat::select)
			imap_parser_echo_modify(pcontext, NULL);
		/* IMAP_CODE_2170011: OK LIST completed */
		auto buf = fmt::format("* LSUB (\\Noselect) \"/\" \"\"\r\n{} {}",
		           argv[0], resource_get_imap_code(1711, 1));
		imap_parser_safe_write(pcontext, buf.c_str(), buf.size());
		return DISPATCH_CONTINUE;
	}
	auto search_pattern = std::string(argv[2]) + argv[3];
	std::vector<enum_folder_t> sub_list;
	auto ssr = midb_agent::enum_subscriptions(pcontext->maildir,
	           sub_list, &errnum);
	auto ret = m2icode(ssr, errnum);
	if (ret != 0)
		return ret;
	icp_convert_folderlist(sub_list, ctx.enabled_rev2);

	/*
	 * Membership set of exactly-subscribed names, folded the same way as
	 * dir_tree so the \Noselect test matches the tree's own comparisons.
	 */
	std::set<std::string, dir_tree::cmp> subscribed;
	for (const auto &fentry : sub_list)
		subscribed.insert(fentry.second);
	/*
	 * The tree is built from subscriptions, so a non-subscribed
	 * intermediate ancestor of a subscribed child materializes as a node
	 * and can be reported with \Noselect (RFC 3501 §6.3.9).
	 */
	dir_tree sub_tree;
	sub_tree.load_from_memfile(sub_list);
	sub_list.clear();
	std::vector<std::pair<std::string, bool>> nodes; /* full_name, has_children */
	dir_tree_collect(sub_tree, "", nodes);

	ctx.stream.clear();
	for (const auto &[sys_name, have] : nodes) {
		if (!icp_wildcard_match(sys_name.c_str(), search_pattern.c_str()))
			continue;
		std::string flags;
		if (subscribed.find(sys_name) == subscribed.cend())
			flags = "\\Noselect"; /* synthesized ancestor, not itself subscribed */
		else
			flags = have ? "\\HasChildren" : "\\HasNoChildren";
		auto buf = fmt::format("* LSUB ({}) \"/\" {}\r\n",
		           flags, quote_encode(sys_name));
		if (pcontext->stream.write(buf.c_str(), buf.size()) != STREAM_WRITE_OK)
			return 1922;
	}
	if (pcontext->proto_stat == iproto_stat::select)
		imap_parser_echo_modify(pcontext, &pcontext->stream);
	/* IMAP_CODE_2170013: OK LSUB completed */
	auto buf = fmt::format("{} {}", argv[0], resource_get_imap_code(1713, 1));
	if (pcontext->stream.write(buf.c_str(), buf.size()) != STREAM_WRITE_OK)
		return 1922;
	pcontext->write_offset = 0;
	pcontext->sched_stat = isched_stat::wrlst;
	return DISPATCH_BREAK;
} catch (const std::bad_alloc &) {
	return 1915;
}

int icp_status(std::span<std::string> argv, imap_context &ctx) try
{
	auto pcontext = &ctx;
	std::vector<std::string> temp_argv;
	std::string sys_name;
    
	if (!pcontext->is_authed())
		return 1804;
	if (argv.size() < 4 || argv[2].size() == 0 || argv[2].size() >= 1024 ||
	    !icp_imapfolder_to_sysfolder(argv[2].c_str(), sys_name, ctx.enabled_rev2) ||
	    argv[3][0] != '(' || argv[3].back() != ')')
		return 1800;
	if (parse_imap_args(&argv[3][1], argv[3].size() - 2, temp_argv) < 0)
		return 1800;

	std::string buf;
	auto rc = icp_status_line(ctx, sys_name, quote_encode(argv[2]), temp_argv, buf);
	if (rc != 0)
		return rc;
	/* IMAP_CODE_2170014: OK STATUS completed */
	if (pcontext->stream.write(buf.c_str(), buf.size()) != STREAM_WRITE_OK)
		return 1922;
	if (pcontext->proto_stat == iproto_stat::select)
		imap_parser_echo_modify(pcontext, &pcontext->stream);
	buf = fmt::format("{} {}", argv[0], resource_get_imap_code(1714, 1));
	if (pcontext->stream.write(buf.c_str(), buf.size()) != STREAM_WRITE_OK)
		return 1922;
	pcontext->write_offset = 0;
	pcontext->sched_stat = isched_stat::wrlst;
	return DISPATCH_BREAK;
} catch (const std::bad_alloc &) {
	return 1915;
}

int icp_append(std::span<std::string> argv, imap_context &ctx) try
{
	if (!ctx.is_authed())
		return 1804;
	unsigned int uid;
	int errnum, i;
	std::string *str_received = nullptr, *flags_string = nullptr;
	std::string sys_name;
	
	if (argv.size() < 4 || argv.size() > 6 ||
	    argv[2].size() == 0 || argv[2].size() >= 1024 ||
	    !icp_imapfolder_to_sysfolder(argv[2].c_str(), sys_name, ctx.enabled_rev2))
		return 1800;
	if (argv.size() == 6) {
		flags_string = &argv[3];
		str_received = &argv[4];
	} else if (argv.size() == 5) {
		if (argv[3][0] == '(')
			flags_string = &argv[3];
		else
			str_received = &argv[3];
	} 
	std::string flag_buff = "()";
	if (NULL != flags_string) {
		if (flags_string->front() != '(' || flags_string->back() != ')')
			return 1800;
		auto &fstr = *flags_string;
		std::vector<std::string> temp_argv;
		if (parse_imap_args(&fstr[1], fstr.size() - 2, temp_argv) < 0)
			return 1800;
		auto bg = temp_argv.cbegin();
		auto ed = temp_argv.cend();
		flag_buff = flagbits_to_s(
		            std::any_of(bg, ed, [](const std::string &s) { return strcasecmp(s.c_str(), "\\Seen") == 0; }),
		            std::any_of(bg, ed, [](const std::string &s) { return strcasecmp(s.c_str(), "\\Answered") == 0; }),
		            std::any_of(bg, ed, [](const std::string &s) { return strcasecmp(s.c_str(), "\\Flagged") == 0; }),
		            std::any_of(bg, ed, [](const std::string &s) { return strcasecmp(s.c_str(), "\\Draft") == 0; }),
		            std::any_of(bg, ed, [](const std::string &s) { return strcasecmp(s.c_str(), "\\Deleted") == 0; }),
		            std::any_of(bg, ed, [](const std::string &s) { return strcasecmp(s.c_str(), "$Forwarded") == 0; }));
	}
	std::string mid_string;
	time_t tmp_time = time(nullptr);
	if (str_received != nullptr &&
	    icp_convert_imaptime(str_received->c_str(), &tmp_time))
		/* good */;
	char txt[GUIDSTR_SIZE]{};
	GUID::random_new().to_str(txt, std::size(txt), 32);
	mid_string = fmt::format("R-{}/{}", &txt[30], txt);

	auto pcontext = &ctx;
	imrpc_build_env();
	auto cl_0 = HX::make_scope_exit(imrpc_free_env);
	if (!exmdb_client->imapfile_write(ctx.maildir, "eml",
	    mid_string, argv.back())) {
		mlog(LV_ERR, "E-1763: write %s/eml/%s failed", ctx.maildir, mid_string.c_str());
		return 1909;
	}

	auto ssr = midb_agent::insert_mail(pcontext->maildir, sys_name,
	           mid_string.c_str(), flag_buff.c_str(), tmp_time, &errnum);
	auto ret = m2icode(ssr, errnum);
	if (ret != 0)
		return ret;
	imap_parser_log_info(pcontext, LV_DEBUG, "message %s is appended OK", mid_string.c_str());
	imap_parser_bcast_touch(nullptr, pcontext->username, pcontext->selected_folder);
	if (pcontext->proto_stat == iproto_stat::select)
		imap_parser_echo_modify(pcontext, NULL);
	/* IMAP_CODE_2170015: OK <APPENDUID> APPEND completed */
	auto imap_reply_str = resource_get_imap_code(1715, 1);
	auto imap_reply_str1 = resource_get_imap_code(1715, 2);
	std::string buf;
	for (i = 0; i < 10; ++i) {
		// wait for midb's actions showing up... woah terrible
		uint32_t uidvalid = 0;
		if (midb_agent::summary_folder(pcontext->maildir,
		    sys_name, nullptr, nullptr, nullptr, &uidvalid, nullptr,
		    &errnum) == MIDB_RESULT_OK &&
		    midb_agent::get_uid(pcontext->maildir, sys_name,
		    mid_string.c_str(), &uid) == MIDB_RESULT_OK) {
			buf = fmt::format("{} {} [APPENDUID {} {}] {}",
			      argv[0], imap_reply_str, uidvalid,
			      uid, imap_reply_str1);
			break;
		}
		usleep(50000);
	}
	if (i == 10)
		buf = fmt::format("{} {} {}", argv[0], imap_reply_str,
		      imap_reply_str1);
	else if (pcontext->proto_stat == iproto_stat::select &&
	    pcontext->selected_folder == sys_name)
		/*
		 * This connection "sees" the very message it added,
		 * so it is not recent to anyone else.
		 */
		midb_agent::unset_flags(pcontext->maildir, sys_name,
			mid_string.c_str(), FLAG_RECENT, nullptr, &errnum);
	imap_parser_safe_write(pcontext, buf.c_str(), buf.size());
	return DISPATCH_CONTINUE;
} catch (const std::bad_alloc &) {
	mlog(LV_ERR, "E-1456: ENOMEM");
	return 1918;
}

static inline bool append_allowed_flag_name(const char *flag)
{
	static constexpr const char *names[] = {"\\Answered", "\\Flagged", "\\Seen", "\\Draft", "\\Deleted", "$Forwarded"};
	for (auto s : names)
		if (strcasecmp(flag, s) == 0)
			return true;
	return false;
}

static int icp_long_append_begin2(std::span<std::string> argv, imap_context &ctx) try
{
	if (!ctx.is_authed())
		return 1804 | DISPATCH_BREAK;
	std::string *str_received = nullptr, *flags_string = nullptr;
	char str_flags[128];
	std::string sys_name;
	
	if (argv.size() < 3 || argv.size() > 5 ||
	    argv[2].size() == 0 || argv[2].size() >= 1024 ||
	    !icp_imapfolder_to_sysfolder(argv[2].c_str(), sys_name, ctx.enabled_rev2))
		return 1800 | DISPATCH_BREAK;
	if (argv.size() == 5) {
		flags_string = &argv[3];
		str_received = &argv[4];
	} else if (argv.size() == 4) {
		if (argv[3][0] == '(')
			flags_string = &argv[3];
		else
			str_received = &argv[3];
	}
	if (NULL != flags_string) {
		auto &fstr = *flags_string;
		gx_strlcpy(str_flags, fstr.c_str(), std::size(str_flags));
		if (fstr.front() != '(' || fstr.back() != ')')
			return 1800 | DISPATCH_BREAK;
		std::vector<std::string> flaglist;
		if (parse_imap_args(&fstr[1], fstr.size() - 2, flaglist) < 0)
			return 1800 | DISPATCH_BREAK;
		for (const auto &flag : flaglist)
			if (!append_allowed_flag_name(flag.c_str()))
				return 1800 | DISPATCH_BREAK;
	}
	auto pcontext = &ctx;
	char guidtxt[GUIDSTR_SIZE]{};
	GUID::random_new().to_str(guidtxt, std::size(guidtxt), 32);
	pcontext->mid = fmt::format("R-{}/{}", &guidtxt[30], guidtxt);
	ctx.append_stream.clear();
	ctx.append_folder = std::move(sys_name);
	ctx.append_flags  = flagbits_to_s(
	                    strcasestr(str_flags, "\\Seen") != nullptr,
	                    strcasestr(str_flags, "\\Answered") != nullptr,
	                    strcasestr(str_flags, "\\Flagged") != nullptr,
	                    strcasestr(str_flags, "\\Draft") != nullptr,
	                    strcasestr(str_flags, "\\Deleted") != nullptr,
	                    strcasestr(str_flags, "$Forwarded") != nullptr);
	if (str_received == nullptr || str_received->empty() ||
	    !icp_convert_imaptime(str_received->c_str(), &ctx.append_time))
		ctx.append_time = time(nullptr);
	gx_strlcpy(pcontext->tag_string, argv[0].c_str(), std::size(pcontext->tag_string));
	pcontext->stream.clear();
	return DISPATCH_CONTINUE;
} catch (const std::bad_alloc &) {
	return 1918 | DISPATCH_BREAK;
}

int icp_long_append_begin(std::span<std::string> argv, imap_context &ctx)
{
	return icp_dval(argv.size() > 0 ? argv[0].c_str() : nullptr,
	       ctx, icp_long_append_begin2(argv, ctx));
}

static int icp_long_append_end2(imap_context &ctx) try
{
	auto pcontext = &ctx;
	std::string content;
	void *strb;
	unsigned int strb_size = STREAM_BLOCK_SIZE;
	ctx.append_stream.reset_reading();
	while ((strb = ctx.append_stream.get_read_buf(&strb_size)) != nullptr) {
		content.append(static_cast<char *>(strb), strb_size);
		strb_size = STREAM_BLOCK_SIZE;
	}
	ctx.append_stream.clear();
	imrpc_build_env();
	auto cl_0 = HX::make_scope_exit(imrpc_free_env);
	if (!exmdb_client->imapfile_write(ctx.maildir, "eml",
	    ctx.mid, content)) {
		mlog(LV_ERR, "E-1764: write to %s/eml/%s failed",
			pcontext->maildir, pcontext->mid.c_str());
		return 1909 | DISPATCH_TAG;
	}

	int errnum;
	auto sys_name = ctx.append_folder.c_str();
	auto ssr = midb_agent::insert_mail(pcontext->maildir, sys_name,
	           pcontext->mid.c_str(), ctx.append_flags.c_str(),
	           ctx.append_time, &errnum);
	auto cmid = std::move(pcontext->mid);
	pcontext->mid.clear();
	auto ret = m2icode(ssr, errnum);
	if (ret != 0)
		return ret | DISPATCH_TAG;
	imap_parser_log_info(pcontext, LV_DEBUG, "message %s is appended OK", cmid.c_str());
	imap_parser_bcast_touch(nullptr, pcontext->username, pcontext->selected_folder);
	if (pcontext->proto_stat == iproto_stat::select)
		imap_parser_echo_modify(pcontext, NULL);
	/* IMAP_CODE_2170015: OK <APPENDUID> APPEND completed */
	auto imap_reply_str = resource_get_imap_code(1715, 1);
	auto imap_reply_str1 = resource_get_imap_code(1715, 2);
	std::string buf;
	unsigned int i;
	for (i=0; i<10; i++) {
		uint32_t uidvalid = 0;
		unsigned int uid = 0;
		if (midb_agent::summary_folder(pcontext->maildir,
		    sys_name, nullptr, nullptr, nullptr, &uidvalid,
		    nullptr, &errnum) == MIDB_RESULT_OK &&
		    midb_agent::get_uid(pcontext->maildir, sys_name,
		    cmid.c_str(), &uid) == MIDB_RESULT_OK) {
			buf = fmt::format("{} {} [APPENDUID {} {}] {}",
			      pcontext->tag_string, imap_reply_str, uidvalid,
			      uid, imap_reply_str1);
			break;
		}
		usleep(50000);
	}
	if (i == 10)
		buf = fmt::format("{} {} {}", pcontext->tag_string,
		      imap_reply_str, imap_reply_str1);
	imap_parser_safe_write(pcontext, buf.c_str(), buf.size());
	return DISPATCH_CONTINUE;
} catch (const std::bad_alloc &) {
	mlog(LV_ERR, "E-1460: ENOMEM");
	return 1918;
}

int icp_long_append_end(std::span<std::string> argv, imap_context &ctx)
{
	return icp_dval(argv.size() > 0 ? argv[0].c_str() : nullptr,
	       ctx, icp_long_append_end2(ctx));
}

int icp_check(std::span<std::string> argv, imap_context &ctx)
{
	auto pcontext = &ctx;
	if (pcontext->proto_stat != iproto_stat::select)
		return 1805;
	imap_parser_echo_modify(pcontext, NULL);
	return 1716;
}

int icp_close(std::span<std::string> argv, imap_context &ctx)
{
	if (ctx.proto_stat != iproto_stat::select)
		return 1805;
	icp_clsfld(ctx);
	return 1717;
}

static bool zero_uid_bit(const MITEM &i)
{
	return i.uid == 0 || !(i.flag_bits & FLAG_DELETED);
}

int icp_expunge(std::span<std::string> argv, imap_context &ctx) try
{
	auto pcontext = &ctx;
	if (pcontext->proto_stat != iproto_stat::select)
		return 1805;
	if (pcontext->b_readonly)
		return 1806;
	XARRAY xarray;
	int errnum;
	auto ssr = midb_agent::list_deleted(pcontext->maildir,
	           pcontext->selected_folder, &xarray, &errnum);
	auto ret = m2icode(ssr, errnum);
	if (ret != 0)
		return ret;
	auto num = xarray.get_capacity();
	if (num == 0) {
		imap_parser_echo_modify(pcontext, nullptr);
		return 1726;
	}
	std::vector<MITEM *> exp_list;
	imrpc_build_env();
	auto cl_0 = HX::make_scope_exit(imrpc_free_env);
	for (size_t i = 0; i < num; ++i) {
		auto pitem = xarray.get_item(i);
		if (zero_uid_bit(*pitem))
			continue;
		auto ct_item = pcontext->contents.get_itemx(pitem->uid);
		if (ct_item == nullptr)
			continue;
		exp_list.push_back(pitem);
	}
	ssr = midb_agent::remove_mail(pcontext->maildir,
	      pcontext->selected_folder, exp_list, &errnum);
	ret = m2icode(ssr, errnum);
	if (ret != 0)
		return ret;

	pcontext->stream.clear();
	for (size_t i = 0; i < xarray.get_capacity(); ++i) {
		auto pitem = xarray.get_item(i);
		if (zero_uid_bit(*pitem))
			continue;
		auto ct_item = pcontext->contents.get_itemx(pitem->uid);
		if (ct_item == nullptr)
			continue;
		if (!exmdb_client->imapfile_delete(ctx.maildir, "eml", pitem->mid))
			mlog(LV_WARN, "W-2030: remove %s/eml/%s failed",
				ctx.maildir, pitem->mid.c_str());
		else
			imap_parser_log_info(pcontext, LV_DEBUG, "message %s has been deleted",
				pitem->mid.c_str());
	}
	if (!exp_list.empty())
		imap_parser_bcast_expunge(*pcontext, exp_list, pcontext->selected_folder);
	imap_parser_echo_modify(pcontext, &pcontext->stream);
	/* IMAP_CODE_2170026: OK EXPUNGE completed */
	auto buf = fmt::format("{} {}", argv[0], resource_get_imap_code(1726, 1));
	if (pcontext->stream.write(buf.c_str(), buf.size()) != STREAM_WRITE_OK)
		return 1922;
	pcontext->write_offset = 0;
	pcontext->sched_stat = isched_stat::wrlst;
	return DISPATCH_BREAK;
} catch (const std::bad_alloc &) {
	mlog(LV_ERR, "E-1246: ENOMEM");
	return 1918;
}

int icp_unselect(std::span<std::string> argv, imap_context &ctx)
{
	auto pcontext = &ctx;
	if (pcontext->proto_stat != iproto_stat::select)
		return 1805;
	imap_parser_remove_select(pcontext);
	pcontext->proto_stat = iproto_stat::auth;
	pcontext->selected_folder.clear();
	pcontext->announced_keywords.clear();
	ctx.saved_uids.clear();
	return 1718;
}

/* SEARCH RETURN options (RFC 4731 / RFC 9051 §6.4.4) */
enum {
	ESR_MIN = 0x1U,
	ESR_MAX = 0x2U,
	ESR_COUNT = 0x4U,
	ESR_ALL = 0x8U,
	ESR_SAVE = 0x10U,
};

/**
 * Parse a "(MIN MAX COUNT ALL SAVE)" RETURN token into a flag set.
 */
static bool icp_parse_search_return(const std::string &tok, unsigned int &flags)
{
	if (tok.size() < 2 || tok.front() != '(' || tok.back() != ')')
		return false;
	flags = 0;
	std::string inner = tok.substr(1, tok.size() - 2);
	size_t i = 0;
	while (i < inner.size()) {
		while (i < inner.size() && inner[i] == ' ')
			++i;
		size_t s = i;
		while (i < inner.size() && inner[i] != ' ')
			++i;
		if (i == s)
			break;
		auto w = inner.substr(s, i - s);
		if (strcasecmp(w.c_str(), "MIN") == 0)
			flags |= ESR_MIN;
		else if (strcasecmp(w.c_str(), "MAX") == 0)
			flags |= ESR_MAX;
		else if (strcasecmp(w.c_str(), "COUNT") == 0)
			flags |= ESR_COUNT;
		else if (strcasecmp(w.c_str(), "ALL") == 0)
			flags |= ESR_ALL;
		else if (strcasecmp(w.c_str(), "SAVE") == 0)
			flags |= ESR_SAVE; /* SEARCHRES deferred: parsed, not stored */
		else
			return false; /* unknown return option */
	}
	return true;
}

/**
 * Collapse an ascending id list into RFC sequence-set form, e.g. "1:3,5,7:9".
 */
static std::string icp_seqset(const std::vector<uint32_t> &ids)
{
	std::string out;
	for (size_t i = 0; i < ids.size(); ) {
		size_t j = i;
		while (j + 1 < ids.size() && ids[j+1] == ids[j] + 1)
			++j;
		if (!out.empty())
			out += ',';
		if (j == i)
			out += std::to_string(ids[i]);
		else
			out += fmt::format("{}:{}", ids[i], ids[j]);
		i = j + 1;
	}
	return out;
}

/**
 * Build the untagged SEARCH response. rev2 (or any RETURN clause) gets an
 * ESEARCH line: `* ESEARCH (TAG "<tag>") [UID] [MIN n] [MAX n] [COUNT n] [ALL
 * seq-set]`. MIN/MAX/ALL are omitted on an empty result. COUNT reports 0.
 * @id_list is midb's space-separated match list.
 */
static std::string icp_esearch_line(const char *tag, bool uid_mode,
    const std::string &id_list, unsigned int flags)
{
	std::vector<uint32_t> ids; /* XXX: this should be a range_set<> */
	const char *p = id_list.c_str();
	while (*p != '\0') {
		while (*p == ' ')
			++p;
		if (*p == '\0')
			break;
		char *end = nullptr;
		auto v = strtoul(p, &end, 10);
		if (end == p)
			break;
		ids.push_back(v);
		p = end;
	}
	std::sort(ids.begin(), ids.end());
	ids.erase(std::unique(ids.begin(), ids.end()), ids.end());
	std::string out = fmt::format("* ESEARCH (TAG \"{}\")", tag);
	if (uid_mode)
		out += " UID";
	if (!ids.empty()) {
		if (flags & ESR_MIN)
			out += fmt::format(" MIN {}", ids.front());
		if (flags & ESR_MAX)
			out += fmt::format(" MAX {}", ids.back());
		if (flags & ESR_COUNT)
			out += fmt::format(" COUNT {}", ids.size());
		if (flags & ESR_ALL) {
			out += " ALL ";
			out += icp_seqset(ids);
		}
	} else if (flags & ESR_COUNT) {
		out += " COUNT 0";
	}
	return out;
}

/**
 * Parse midb's space-separated id list into a vector.
 * XXX: This should produce a range_set<> instead.
 */
static std::vector<uint32_t> icp_parse_idlist(const std::string &s)
{
	std::vector<uint32_t> v;
	const char *p = s.c_str();
	while (*p != '\0') {
		while (*p == ' ')
			++p;
		if (*p == '\0')
			break;
		char *end = nullptr;
		auto n = strtoul(p, &end, 10);
		if (end == p)
			break;
		v.emplace_back(n);
		p = end;
	}
	return v;
}

/**
 * Render the saved SEARCH result ($, RFC 5182) as a sequence set string, i.e.
 * UIDs when in @uid_mode, otherwise the current sequence numbers, with absent
 * messages getting skipped.
 */
static std::string icp_searchres_set(imap_context &ctx, bool uid_mode)
{
	std::string out;
	for (auto uid : ctx.saved_uids) {
		unsigned int v;
		if (uid_mode) {
			v = uid;
		} else {
			auto it = ctx.contents.get_itemx(uid);
			if (it == nullptr)
				continue;
			v = it->id;
		}
		if (!out.empty())
			out += ',';
		out += std::to_string(v);
	}
	return out;
}

/**
 * Expand any `$` element of a sequence set @set in place with the saved SEARCH
 * result. This is a no-op when `$` is absent, so ordinary sets are untouched.
 * An empty saved result drops the `$` element, yielding an empty set, i.e. no
 * matches.
 */
static void icp_subst_searchres(imap_context &ctx, std::string &set, bool uid_mode)
{
	if (set.find('$') == std::string::npos)
		return;
	auto repl = icp_searchres_set(ctx, uid_mode);
	std::string out;
	size_t i = 0;
	while (i <= set.size()) {
		auto c = set.find(',', i);
		auto end = c == std::string::npos ? set.size() : c;
		auto tok = set.substr(i, end - i);
		const std::string &add = tok == "$" ? repl : tok;
		if (!add.empty()) {
			if (!out.empty())
				out += ',';
			out += add;
		}
		if (c == std::string::npos)
			break;
		i = c + 1;
	}
	set = std::move(out);
}

/**
 * SEARCHRES helper function for UID commands. Expands "$" in @set (UID mode)
 * in place, so that later uses like COPYUID see real UIDs.
 */
static const char *icp_uidseq(imap_context &ctx, std::string &set)
{
	icp_subst_searchres(ctx, set, true);
	return set.c_str();
}

int icp_search(std::span<std::string> argv, imap_context &ctx)
{
	auto pcontext = &ctx;
	int errnum;
	
	if (pcontext->proto_stat != iproto_stat::select)
		return 1805;
	if (argv.size() < 3 || argv.size() > 1024)
		return 1800;
	unsigned int ret_flags = 0;
	bool has_return = false;
	size_t crit_off = 2;
	if (argv.size() >= 4 && strcasecmp(argv[2].c_str(), "RETURN") == 0) {
		if (!icp_parse_search_return(argv[3], ret_flags))
			return 1800;
		has_return = true;
		crit_off = 4;
	}
	if ((ret_flags & (ESR_MIN | ESR_MAX | ESR_COUNT | ESR_ALL)) == 0 &&
	    !(ret_flags & ESR_SAVE))
		/* default / RETURN () -> ALL; SAVE-only -> none */
		ret_flags |= ESR_ALL;

	auto esearch = ctx.enabled_rev2 || has_return;
	std::string buff;
	auto ssr = midb_agent::search(pcontext->maildir,
	           pcontext->selected_folder, pcontext->defcharset,
	           argv.subspan(crit_off), buff, &errnum);
	auto result = m2icode(ssr, errnum);
	if (result != 0)
		return result;
	if (ret_flags & ESR_SAVE) {
		/* SEARCHRES: save the matched UIDs (mapped from seq numbers). */
		ctx.saved_uids.clear();
		for (auto seq : icp_parse_idlist(buff)) {
			auto it = ctx.contents.get_item(seq - 1);
			if (it != nullptr)
				ctx.saved_uids.push_back(it->uid);
		}
	}
	std::string resp = esearch ?
		icp_esearch_line(argv[0].c_str(), false, buff, ret_flags) :
		"* SEARCH " + buff;
	resp.append("\r\n");
	pcontext->stream.clear();
	if (ctx.stream.write(resp.c_str(), resp.size()) != STREAM_WRITE_OK)
		return 1922;
	if (pcontext->proto_stat == iproto_stat::select)
		imap_parser_echo_modify(pcontext, &pcontext->stream, echomod::suppress_expunge);
	/* IMAP_CODE_2170019: OK SEARCH completed */
	buff = fmt::format("{} {}", argv[0], resource_get_imap_code(1719, 1));
	if (pcontext->stream.write(buff.c_str(), buff.size()) != STREAM_WRITE_OK)
		return 1922;
	pcontext->write_offset = 0;
	pcontext->sched_stat = isched_stat::wrlst;
	return DISPATCH_BREAK;
}

/**
 * Convert sequence numbers to a UID list, resolving "*" along the way.
 *
 * @range_string:	sequence numbers, e.g. "1,2:3,4:*,*:5,*:*,*"
 * @uid_list:		split-up range
 */
static errno_t parse_imap_seqx(imap_context &ctx, const char *range_string,
    imap_seq_list &uid_list) try
{
	if (range_string == nullptr)
		return EINVAL;

	/* SEARCHRES: expand "$" against the saved result (sequence numbers). */
	std::string subst;
	if (strchr(range_string, '$') != nullptr) {
		subst = range_string;
		icp_subst_searchres(ctx, subst, false);
		range_string = subst.c_str();
	}
	imap_seq_list seq_list;
	auto err = parse_imap_seq(seq_list, range_string);
	if (err != 0)
		return err;
	for (auto &seq : seq_list) {
		if (seq.lo == SEQ_STAR && seq.hi == SEQ_STAR) {
			/* MAX:MAX */
			seq.lo = seq.hi = ctx.contents.m_vec.size();
		} else if (seq.lo == SEQ_STAR) {
			/* MAX:99 = (99:MAX) */
			seq.lo = seq.hi;
			seq.hi = ctx.contents.m_vec.size();
		} else if (seq.hi == SEQ_STAR) {
			/* 99:MAX */
			seq.hi = ctx.contents.m_vec.size();
		}
		if (seq.lo < 1)
			seq.lo = 1;
		if (seq.hi > ctx.contents.m_vec.size())
			seq.hi = ctx.contents.m_vec.size();
		for (size_t i = seq.lo; i <= seq.hi; ++i) {
			auto uid = ctx.contents.m_vec[i-1].uid;
			uid_list.insert(uid);
		}
	}
	return 0;
} catch (const std::bad_alloc &) {
	return ENOMEM;
}

static int fetch_trivial_uid(imap_context &ctx, const imap_seq_list &range_list,
    XARRAY &xa) try
{
	for (auto &range : range_list)
		for (unsigned int uid = range.lo; uid <= range.hi; ++uid) {
			auto mitem = ctx.contents.get_itemx(uid);
			if (mitem != nullptr)
				xa.append(MITEM{*mitem}, mitem->uid);
		}
	return 0;
} catch (const std::bad_alloc &) {
	return MIDB_LOCAL_ENOMEM;
}

int icp_fetch(std::span<std::string> argv, imap_context &ctx)
{
	auto pcontext = &ctx;
	int i, num, errnum = 0;
	std::vector<std::string> tmp_argv;
	imap_seq_list list_uid;
	mdi_list list_data;
	
	if (pcontext->proto_stat != iproto_stat::select)
		return 1805;
	if (argv.size() < 4 || parse_imap_seqx(*pcontext, argv[2].c_str(), list_uid) != 0)
		return 1800;
	bool b_detail = false, b_simple = false, b_data = false;
	if (!icp_parse_fetch_args(list_data, &b_detail, &b_simple, &b_data,
	    argv[3].data(), tmp_argv))
		return 1800;
	XARRAY xarray;
	auto ssr = b_detail ?
	           midb_agent::fetch_detail_uid(pcontext->maildir,
	           pcontext->selected_folder, list_uid, &xarray, &errnum) :
	           b_simple ?
	           midb_agent::fetch_simple_uid(pcontext->maildir,
	           pcontext->selected_folder, list_uid, &xarray, &errnum) :
	           fetch_trivial_uid(*pcontext, list_uid, xarray);
	auto result = m2icode(ssr, errnum);
	if (result != 0)
		return result;
	pcontext->stream.clear();
	num = xarray.get_capacity();
	imrpc_build_env();
	auto cl_0 = HX::make_scope_exit(imrpc_free_env);
	for (i=0; i<num; i++) {
		auto pitem = xarray.get_item(i);
		/*
		 * fetch_detail_uid might have yielded new mails, so filter
		 * with respect to current sequence assignment.
		 */
		auto ct_item = pcontext->contents.get_itemx(pitem->uid);
		if (ct_item == nullptr)
			continue;
		result = icp_process_fetch_item(ctx, b_data,
		         pitem, xarray.get_digest(*pitem),
		         ct_item->id, list_data);
		if (result != 0)
			return result;
	}
	xarray.clear();
	imap_parser_echo_modify(pcontext, &pcontext->stream, echomod::suppress_expunge);
	/* IMAP_CODE_2170020: OK FETCH completed */
	auto buf = fmt::format("{} {}", argv[0], resource_get_imap_code(1720, 1));
	if (pcontext->stream.write(buf.c_str(), buf.size()) != STREAM_WRITE_OK)
		return 1922;
	pcontext->write_length = 0;
	pcontext->write_offset = 0;
	if (b_data) {
		pcontext->write_buff = pcontext->command_buffer;
		pcontext->sched_stat = isched_stat::wrdat;
	} else {
		pcontext->sched_stat = isched_stat::wrlst;
	}
	return DISPATCH_BREAK;
}

static bool store_flagkeyword(const char *str)
{
	static constexpr const char *names[] =
		{"FLAGS", "FLAGS.SILENT", "+FLAGS", "+FLAGS.SILENT",
		"-FLAGS", "-FLAGS.SILENT"};
	for (auto elem : names)
		if (strcasecmp(str, elem) == 0)
			return true;
	return false;
}

static void icp_expunge_flagged(imap_context &ctx, XARRAY &xarray) try
{
	std::vector<MITEM *> exp_list;
	auto num = xarray.get_capacity();
	for (size_t i = 0; i < num; ++i) {
		auto pitem = xarray.get_item(i);
		if (pitem->uid == 0 ||
		    ctx.contents.get_itemx(pitem->uid) == nullptr)
			continue;
		exp_list.push_back(pitem);
	}
	if (exp_list.empty())
		return;
	int errnum;
	if (midb_agent::remove_mail(ctx.maildir, ctx.selected_folder,
	    exp_list, &errnum) != MIDB_RESULT_OK)
		return;

	imrpc_build_env();
	auto cl_0 = HX::make_scope_exit(imrpc_free_env);
	for (auto pitem : exp_list) {
		if (!exmdb_client->imapfile_delete(ctx.maildir, "eml", pitem->mid))
			mlog(LV_WARN, "W-2459: remove %s/eml/%s failed",
				ctx.maildir, pitem->mid.c_str());
		else
			imap_parser_log_info(&ctx, LV_DEBUG, "message %s has been deleted",
				pitem->mid.c_str());
	}
	imap_parser_bcast_expunge(ctx, exp_list, ctx.selected_folder);
} catch (const std::bad_alloc &) {
	mlog(LV_ERR, "%s: ENOMEM", __func__);
}

int icp_store(std::span<std::string> argv, imap_context &ctx)
{
	auto pcontext = &ctx;
	int errnum, i;
	int flag_bits;
	std::vector<std::string> temp_argv;
	imap_seq_list list_uid;

	if (pcontext->proto_stat != iproto_stat::select)
		return 1805;
	if (argv.size() < 5 || parse_imap_seqx(*pcontext, argv[2].c_str(), list_uid) != 0 ||
	    !store_flagkeyword(argv[3].c_str()))
		return 1800;
	if (argv[4].front() == '(' && argv[4].back() == ')') {
		auto temp_argc = parse_imap_args(&argv[4][1], argv[4].size() - 2, temp_argv, true);
		if (temp_argc == -1)
			return 1800;
	} else {
		temp_argv.emplace_back(argv[4]);
	}
	if (pcontext->b_readonly)
		return 1806;
	flag_bits = 0;
	std::vector<std::string> kw_list;
	if (!icp_classify_store_flags(temp_argv, flag_bits, kw_list))
		return 1807;
	XARRAY xarray;
	auto ssr = midb_agent::fetch_simple_uid(pcontext->maildir,
	           pcontext->selected_folder, list_uid, &xarray, &errnum);
	auto result = m2icode(ssr, errnum);
	if (result != 0)
		return result;
	int num = xarray.get_capacity();
	for (i=0; i<num; i++) {
		auto pitem = xarray.get_item(i);
		auto ct_item = pcontext->contents.get_itemx(pitem->uid);
		if (ct_item == nullptr)
			continue;
		icp_store_flags(argv[3].c_str(), pitem->mid,
			ct_item->id, 0, flag_bits, kw_list, ctx);
		imap_parser_bcast_flags(*pcontext, pitem->uid);
	}
	if (g_expunge_on_delete && flag_bits & FLAG_DELETED &&
	    argv[3].front() != '-')
		icp_expunge_flagged(ctx, xarray);
	imap_parser_echo_modify(pcontext, nullptr, echomod::suppress_expunge);
	return 1721;
}

int icp_copy(std::span<std::string> argv, imap_context &ctx) try
{
	auto pcontext = &ctx;
	unsigned int uid;
	int errnum;
	int i, j;
	std::string sys_name;
	imap_seq_list list_uid;
    
	if (pcontext->proto_stat != iproto_stat::select)
		return 1805;
	if (argv.size() < 4 || parse_imap_seqx(*pcontext, argv[2].c_str(), list_uid) != 0 ||
	    argv[3].size() == 0 || argv[3].size() >= 1024 ||
	    !icp_imapfolder_to_sysfolder(argv[3].c_str(), sys_name, ctx.enabled_rev2))
		return 1800;
	XARRAY xarray;
	auto ssr = midb_agent::fetch_simple_uid(pcontext->maildir,
	           pcontext->selected_folder, list_uid, &xarray, &errnum);
	auto result = m2icode(ssr, errnum);
	if (result != 0)
		return result;
	uint32_t uidvalidity = 0;
	if (midb_agent::summary_folder(pcontext->maildir,
	    sys_name, nullptr, nullptr, nullptr, &uidvalidity, nullptr,
	    &errnum) != MIDB_RESULT_OK)
		uidvalidity = 0;

	bool b_copied = true, b_first = false;
	int num = xarray.get_capacity();
	std::string uid_string, uid_string1;
	for (i=0; i<num; i++) {
		auto pitem = xarray.get_item(i);
		pitem = pcontext->contents.get_itemx(pitem->uid);
		if (pitem == nullptr)
			continue;
		if (midb_agent::copy_mail(pcontext->maildir,
		    pcontext->selected_folder, pitem->mid, sys_name,
		    pitem->mid, &errnum) != MIDB_RESULT_OK) {
			b_copied = FALSE;
			break;
		}
		if (uidvalidity == 0)
			continue;
		for (j = 0; j < 10; j++) {
			if (midb_agent::get_uid(pcontext->maildir,
			    sys_name, pitem->mid, &uid) != MIDB_RESULT_OK) {
				usleep(500000);
				continue;
			}
			if (b_first) {
				uid_string += ',';
				uid_string1 += ',';
			} else {
				b_first =  TRUE;
			}
			uid_string += std::to_string(pitem->uid);
			uid_string1 += std::to_string(uid);
			break;
		}
		if (j == 10)
			uidvalidity = 0;
	}
	if (!b_copied) {
		std::vector<MITEM *> exp_list;
		for (;i>0; i--) {
			auto pitem = xarray.get_item(i - 1);
			if (pitem->uid == 0)
				continue;
			exp_list.push_back(pitem);
		}
		midb_agent::remove_mail(pcontext->maildir,
			sys_name, exp_list, &errnum);
	}
	pcontext->stream.clear();
	std::string buf;
	if (b_copied) {
		imap_parser_echo_modify(pcontext, &pcontext->stream);
		/* IMAP_CODE_2170022: OK <COPYUID> COPY completed */
		auto imap_reply_str = resource_get_imap_code(1722, 1);
		auto imap_reply_str1 = resource_get_imap_code(1722, 2);
		if (uidvalidity != 0)
			buf = fmt::format("{} {} [COPYUID {} {} {}] {}",
			      argv[0], imap_reply_str, uidvalidity,
			      uid_string, uid_string1, imap_reply_str1);
		else
			buf = fmt::format("{} {} {}", argv[0],
			      imap_reply_str, imap_reply_str1);
	} else {
		/* IMAP_CODE_2190016: NO COPY failed */
		buf = fmt::format("{} {}", argv[0], resource_get_imap_code(1916, 1));
	}
	if (pcontext->stream.write(buf.c_str(), buf.size()) != STREAM_WRITE_OK)
		return 1922;
	pcontext->write_offset = 0;
	pcontext->sched_stat = isched_stat::wrlst;
	return DISPATCH_BREAK;
} catch (const std::bad_alloc &) {
	mlog(LV_ERR, "E-1245: ENOMEM");
	return 1918;
}

int icp_uid_search(std::span<std::string> argv, imap_context &ctx) try
{
	auto pcontext = &ctx;
	int errnum;
	
	if (pcontext->proto_stat != iproto_stat::select)
		return 1805;
	if (argv.size() < 3 || argv.size() > 1024)
		return 1800;
	unsigned int ret_flags = 0;
	bool has_return = false;
	size_t crit_off = 3;
	if (argv.size() >= 5 && strcasecmp(argv[3].c_str(), "RETURN") == 0) {
		if (!icp_parse_search_return(argv[4], ret_flags))
			return 1800;
		has_return = true;
		crit_off = 5;
	}
	if ((ret_flags & (ESR_MIN | ESR_MAX | ESR_COUNT | ESR_ALL)) == 0 &&
	    !(ret_flags & ESR_SAVE))
		ret_flags |= ESR_ALL;
	auto esearch = ctx.enabled_rev2 || has_return;
	std::string buff;
	auto ssr = midb_agent::search_uid(pcontext->maildir,
	           pcontext->selected_folder, pcontext->defcharset,
	           argv.subspan(crit_off), buff, &errnum);
	auto ret = m2icode(ssr, errnum);
	if (ret != 0)
		return ret;
	if (ret_flags & ESR_SAVE)
		/* SEARCHRES: UID SEARCH already yields UIDs. */
		ctx.saved_uids = icp_parse_idlist(buff);
	std::string resp = esearch ?
		icp_esearch_line(argv[0].c_str(), true, buff, ret_flags) :
		"* SEARCH " + buff;
	buff = std::move(resp);
	buff.append("\r\n");
	pcontext->stream.clear();
	if (ctx.stream.write(buff.c_str(), buff.size()) != STREAM_WRITE_OK)
		return 1922;
	imap_parser_echo_modify(pcontext, &pcontext->stream, echomod::suppress_expunge);
	/* IMAP_CODE_2170023: OK UID SEARCH completed */
	buff = fmt::format("{} {}", argv[0], resource_get_imap_code(1723, 1));
	if (pcontext->stream.write(buff.c_str(), buff.size()) != STREAM_WRITE_OK)
		return 1922;
	pcontext->write_offset = 0;
	pcontext->sched_stat = isched_stat::wrlst;
	return DISPATCH_BREAK;
} catch (const std::bad_alloc &) {
	mlog(LV_ERR, "E-2396: ENOMEM");
	return 1918;
}

int icp_uid_fetch(std::span<std::string> argv, imap_context &ctx) try
{
	auto pcontext = &ctx;
	int num;
	int errnum;
	int i;
	imap_seq_list list_seq;
	mdi_list list_data;
	
	if (pcontext->proto_stat != iproto_stat::select)
		return 1805;
	if (argv.size() < 5 || parse_imap_seq(list_seq, icp_uidseq(ctx, argv[3])) != 0)
		return 1800;
	std::vector<std::string> temp_argv;
	bool b_detail = false, b_simple = false, b_data = false;
	if (!icp_parse_fetch_args(list_data, &b_detail, &b_simple,
	    &b_data, argv[4].data(), temp_argv))
		return 1800;
	if (std::none_of(list_data.cbegin(), list_data.cend(),
	    [](const std::string &e) { return strcasecmp(e.c_str(), "UID") == 0; }))
		list_data.emplace_back("UID");
	XARRAY xarray;
	auto ssr = b_detail ?
	           midb_agent::fetch_detail_uid(pcontext->maildir,
	           pcontext->selected_folder, list_seq, &xarray, &errnum) :
	           midb_agent::fetch_simple_uid(pcontext->maildir,
	           pcontext->selected_folder, list_seq, &xarray, &errnum);
	auto ret = m2icode(ssr, errnum);
	if (ret != 0)
		return ret;
	pcontext->stream.clear();
	num = xarray.get_capacity();
	imrpc_build_env();
	auto cl_0 = HX::make_scope_exit(imrpc_free_env);
	for (i=0; i<num; i++) {
		auto pitem = xarray.get_item(i);
		auto ct_item = pcontext->contents.get_itemx(pitem->uid);
		if (ct_item == nullptr)
			continue;
		ret = icp_process_fetch_item(ctx, b_data,
		      pitem, xarray.get_digest(*pitem),
		      ct_item->id, list_data);
		if (ret != 0)
			return ret;
	}
	xarray.clear();
	imap_parser_echo_modify(pcontext, &pcontext->stream, echomod::suppress_expunge);
	/* IMAP_CODE_2170028: OK UID FETCH completed */
	auto buf = fmt::format("{} {}", argv[0], resource_get_imap_code(1728, 1));
	if (pcontext->stream.write(buf.c_str(), buf.size()) != STREAM_WRITE_OK)
		return 1922;
	pcontext->write_length = 0;
	pcontext->write_offset = 0;
	if (b_data) {
		pcontext->write_buff = pcontext->command_buffer;
		pcontext->sched_stat = isched_stat::wrdat;
	} else {
		pcontext->sched_stat = isched_stat::wrlst;
	}
	return DISPATCH_BREAK;
} catch (const std::bad_alloc &) {
	mlog(LV_ERR, "E-2397: ENOMEM");
	return 1918;
}

int icp_uid_store(std::span<std::string> argv, imap_context &ctx)
{
	auto pcontext = &ctx;
	int errnum, i, flag_bits;
	imap_seq_list list_seq;

	if (pcontext->proto_stat != iproto_stat::select)
		return 1805;
	if (argv.size() < 6 || parse_imap_seq(list_seq, icp_uidseq(ctx, argv[3])) != 0 ||
	    !store_flagkeyword(argv[4].c_str()))
		return 1800;
	std::vector<std::string> temp_argv;
	if (argv[5].front() == '(' && argv[5].back() == ')') {
		auto temp_argc = parse_imap_args(&argv[5][1], argv[5].size() - 2, temp_argv, true);
		if (temp_argc == -1)
			return 1800;
	} else {
		temp_argv.emplace_back(argv[5]);
	}
	if (pcontext->b_readonly)
		return 1806;
	flag_bits = 0;
	std::vector<std::string> kw_list;
	if (!icp_classify_store_flags(temp_argv, flag_bits, kw_list))
		return 1807;
	XARRAY xarray;
	auto ssr = midb_agent::fetch_simple_uid(pcontext->maildir,
	           pcontext->selected_folder, list_seq, &xarray, &errnum);
	auto ret = m2icode(ssr, errnum);
	if (ret != 0)
		return ret;
	int num = xarray.get_capacity();
	for (i=0; i<num; i++) {
		auto pitem = xarray.get_item(i);
		auto ct_item = pcontext->contents.get_itemx(pitem->uid);
		if (ct_item == nullptr)
			continue;
		icp_store_flags(argv[4].c_str(), pitem->mid,
			ct_item->id, pitem->uid, flag_bits, kw_list, ctx);
		imap_parser_bcast_flags(*pcontext, pitem->uid);
	}
	if (g_expunge_on_delete && flag_bits & FLAG_DELETED &&
	    argv[4].front() != '-')
		icp_expunge_flagged(ctx, xarray);
	imap_parser_echo_modify(pcontext, nullptr, echomod::suppress_expunge);
	return 1724;
}

int icp_uid_copy(std::span<std::string> argv, imap_context &ctx) try
{
	auto pcontext = &ctx;
	unsigned int uid;
	int errnum;
	int i, j;
	std::string sys_name;
	imap_seq_list list_seq;
	
	if (pcontext->proto_stat != iproto_stat::select)
		return 1805;
	if (argv.size() < 5 || parse_imap_seq(list_seq, icp_uidseq(ctx, argv[3])) != 0 ||
	    argv[4].size() == 0 || argv[4].size() >= 1024 ||
	    !icp_imapfolder_to_sysfolder(argv[4].c_str(), sys_name, ctx.enabled_rev2))
		return 1800;
	XARRAY xarray;
	auto ssr = midb_agent::fetch_simple_uid(pcontext->maildir,
	           pcontext->selected_folder, list_seq, &xarray, &errnum);
	auto ret = m2icode(ssr, errnum);
	if (ret != 0)
		return ret;
	uint32_t uidvalidity = 0;
	if (midb_agent::summary_folder(pcontext->maildir,
	    sys_name, nullptr, nullptr, nullptr, &uidvalidity,
	    nullptr, &errnum) != MIDB_RESULT_OK)
		uidvalidity = 0;

	bool b_copied = true, b_first = false;
	int num = xarray.get_capacity();
	std::string uid_string;
	for (i=0; i<num; i++) {
		auto pitem = xarray.get_item(i);
		if (midb_agent::copy_mail(pcontext->maildir,
		    pcontext->selected_folder, pitem->mid, sys_name,
		    pitem->mid, &errnum) != MIDB_RESULT_OK) {
			b_copied = FALSE;
			break;
		}
		if (uidvalidity == 0)
			continue;
		for (j = 0; j < 10; j++) {
			if (midb_agent::get_uid(pcontext->maildir,
			    sys_name, pitem->mid, &uid) != MIDB_RESULT_OK) {
				usleep(500000);
				continue;
			}
			if (b_first)
				uid_string += ',';
			else
				b_first =  TRUE;
			uid_string += std::to_string(uid);
			break;
		}
		if (j == 10)
			uidvalidity = 0;
	}
	if (!b_copied) {
		std::vector<MITEM *> exp_list;
		for (;i>0; i--) {
			auto pitem = xarray.get_item(i - 1);
			if (pitem->uid == 0)
				continue;
			exp_list.push_back(pitem);
		}
		midb_agent::remove_mail(pcontext->maildir,
			sys_name, exp_list, &errnum);
	}
	pcontext->stream.clear();
	std::string buf;
	if (b_copied) {
		imap_parser_echo_modify(pcontext, &pcontext->stream);
		/* IMAP_CODE_2170025: OK <COPYUID> UID COPY completed */
		auto imap_reply_str = resource_get_imap_code(1725, 1);
		auto imap_reply_str1 = resource_get_imap_code(1725, 2);
		if (uidvalidity != 0)
			buf = fmt::format("{} {} [COPYUID {} {} {}] {}", argv[0],
				imap_reply_str, uidvalidity, argv[3],
				uid_string, imap_reply_str1);
		else
			buf = fmt::format("{} {} {}", argv[0], imap_reply_str,
			      imap_reply_str1);
	} else {
		/* IMAP_CODE_2190017: NO UID COPY failed */
		buf = fmt::format("{} {}", argv[0], resource_get_imap_code(1917, 1));
	}
	if (pcontext->stream.write(buf.c_str(), buf.size()) != STREAM_WRITE_OK)
		return 1922;
	pcontext->write_offset = 0;
	pcontext->sched_stat = isched_stat::wrlst;
	return DISPATCH_BREAK;
} catch (const std::bad_alloc &) {
	mlog(LV_ERR, "E-1244: ENOMEM");
	return 1918;
}

/**
 * MOVE support (RFC 6851 / RFC 9051 §6.4.8). Copy the messages to the target,
 * then expunge them from the (currently selected) source. Per RFC 6851 the
 * COPYUID is sent in an untagged "* OK [COPYUID ...]" before the EXPUNGE
 * responses, and the tagged reply carries no COPYUID. On a copy failure the
 * partial target copies are rolled back and the source is left untouched.
 */
int icp_move(std::span<std::string> argv, imap_context &ctx) try
{
	unsigned int uid;
	int errnum;
	std::string sys_name;
	imap_seq_list list_uid;

	if (ctx.proto_stat != iproto_stat::select)
		return 1805;
	if (ctx.b_readonly)
		return 1806;
	if (argv.size() < 4 || parse_imap_seqx(ctx, argv[2].c_str(), list_uid) != 0 ||
	    argv[3].size() == 0 || argv[3].size() >= 1024 ||
	    !icp_imapfolder_to_sysfolder(argv[3].c_str(), sys_name, ctx.enabled_rev2))
		return 1800;
	XARRAY xarray;
	auto ssr = midb_agent::fetch_simple_uid(ctx.maildir,
	           ctx.selected_folder, list_uid, &xarray, &errnum);
	auto result = m2icode(ssr, errnum);
	if (result != 0)
		return result;
	uint32_t uidvalidity = 0;
	if (midb_agent::summary_folder(ctx.maildir,
	    sys_name, nullptr, nullptr, nullptr, &uidvalidity, nullptr,
	    &errnum) != MIDB_RESULT_OK)
		uidvalidity = 0;

	bool b_copied = true, b_first = false;
	auto num = xarray.get_capacity();
	std::string uid_string, uid_string1;
	std::vector<MITEM *> moved;
	size_t i;
	for (i = 0; i < num; ++i) {
		auto xi = xarray.get_item(i);
		auto pitem = ctx.contents.get_itemx(xi->uid);
		if (pitem == nullptr)
			continue;
		std::string dst_mid = pitem->mid;
		if (midb_agent::copy_mail(ctx.maildir,
		    ctx.selected_folder, pitem->mid, sys_name,
		    dst_mid, &errnum) != MIDB_RESULT_OK) {
			b_copied = FALSE;
			break;
		}
		moved.push_back(xi);
		if (uidvalidity == 0)
			continue;
		unsigned int j;
		for (j = 0; j < 10; j++) {
			if (midb_agent::get_uid(ctx.maildir,
			    sys_name, dst_mid, &uid) != MIDB_RESULT_OK) {
				usleep(500000);
				continue;
			}
			if (b_first) {
				uid_string += ',';
				uid_string1 += ',';
			} else {
				b_first = TRUE;
			}
			uid_string += std::to_string(xi->uid);
			uid_string1 += std::to_string(uid);
			break;
		}
		if (j == 10)
			uidvalidity = 0;
	}
	if (!b_copied) {
		/* roll back the copies already made to the target */
		std::vector<MITEM *> exp_list;
		while (i > 0) {
			auto pitem = xarray.get_item(--i);
			if (pitem->uid != 0)
				exp_list.push_back(pitem);
		}
		midb_agent::remove_mail(ctx.maildir, sys_name, exp_list, &errnum);
		ctx.stream.clear();
		/* IMAP_CODE_2190027: NO MOVE failed */
		auto buf = fmt::format("{} {}", argv[0], resource_get_imap_code(1927, 1));
		if (ctx.stream.write(buf.c_str(), buf.size()) != STREAM_WRITE_OK)
			return 1922;
		ctx.write_offset = 0;
		ctx.sched_stat = isched_stat::wrlst;
		return DISPATCH_BREAK;
	}
	imrpc_build_env();
	auto cl_0 = HX::make_scope_exit(imrpc_free_env);
	midb_agent::remove_mail(ctx.maildir, ctx.selected_folder,
		moved, &errnum);
	for (auto pitem : moved)
		if (!exmdb_client->imapfile_delete(ctx.maildir, "eml", pitem->mid))
			mlog(LV_WARN, "W-2031: remove %s/eml/%s failed",
				ctx.maildir, pitem->mid.c_str());
	if (!moved.empty())
		imap_parser_bcast_expunge(ctx, moved, ctx.selected_folder);
	ctx.stream.clear();
	std::string buf;
	if (uidvalidity != 0) {
		buf = fmt::format("* OK [COPYUID {} {} {}] moved\r\n",
		      uidvalidity, uid_string, uid_string1);
		if (ctx.stream.write(buf.c_str(), buf.size()) != STREAM_WRITE_OK)
			return 1922;
	}
	imap_parser_echo_modify(&ctx, &ctx.stream);
	/* IMAP_CODE_2170033: OK MOVE completed */
	buf = fmt::format("{} {}", argv[0], resource_get_imap_code(1733, 1));
	if (ctx.stream.write(buf.c_str(), buf.size()) != STREAM_WRITE_OK)
		return 1922;
	ctx.write_offset = 0;
	ctx.sched_stat = isched_stat::wrlst;
	return DISPATCH_BREAK;
} catch (const std::bad_alloc &) {
	mlog(LV_ERR, "E-2701: ENOMEM");
	return 1918;
}

int icp_uid_move(std::span<std::string> argv, imap_context &ctx) try
{
	auto pcontext = &ctx;
	unsigned int uid;
	int errnum;
	int i, j;
	std::string sys_name;
	imap_seq_list list_seq;

	if (pcontext->proto_stat != iproto_stat::select)
		return 1805;
	if (pcontext->b_readonly)
		return 1806;
	if (argv.size() < 5 || parse_imap_seq(list_seq, icp_uidseq(ctx, argv[3])) != 0 ||
	    argv[4].size() == 0 || argv[4].size() >= 1024 ||
	    !icp_imapfolder_to_sysfolder(argv[4].c_str(), sys_name, ctx.enabled_rev2))
		return 1800;
	XARRAY xarray;
	auto ssr = midb_agent::fetch_simple_uid(pcontext->maildir,
	           pcontext->selected_folder, list_seq, &xarray, &errnum);
	auto ret = m2icode(ssr, errnum);
	if (ret != 0)
		return ret;
	uint32_t uidvalidity = 0;
	if (midb_agent::summary_folder(pcontext->maildir,
	    sys_name, nullptr, nullptr, nullptr, &uidvalidity,
	    nullptr, &errnum) != MIDB_RESULT_OK)
		uidvalidity = 0;

	bool b_copied = true, b_first = false;
	int num = xarray.get_capacity();
	std::string uid_string;
	std::vector<MITEM *> moved;
	for (i = 0; i < num; i++) {
		auto pitem = xarray.get_item(i);
		std::string dst_mid = pitem->mid;
		if (midb_agent::copy_mail(pcontext->maildir,
		    pcontext->selected_folder, pitem->mid, sys_name,
		    dst_mid, &errnum) != MIDB_RESULT_OK) {
			b_copied = FALSE;
			break;
		}
		moved.push_back(pitem);
		if (uidvalidity == 0)
			continue;
		for (j = 0; j < 10; j++) {
			if (midb_agent::get_uid(pcontext->maildir,
			    sys_name, dst_mid, &uid) != MIDB_RESULT_OK) {
				usleep(500000);
				continue;
			}
			if (b_first)
				uid_string += ',';
			else
				b_first = TRUE;
			uid_string += std::to_string(uid);
			break;
		}
		if (j == 10)
			uidvalidity = 0;
	}
	if (!b_copied) {
		std::vector<MITEM *> exp_list;
		for (; i > 0; i--) {
			auto pitem = xarray.get_item(i - 1);
			if (pitem->uid == 0)
				continue;
			exp_list.push_back(pitem);
		}
		midb_agent::remove_mail(pcontext->maildir, sys_name, exp_list, &errnum);
		pcontext->stream.clear();
		/* IMAP_CODE_2190028: NO UID MOVE failed */
		auto buf = fmt::format("{} {}", argv[0], resource_get_imap_code(1928, 1));
		if (pcontext->stream.write(buf.c_str(), buf.size()) != STREAM_WRITE_OK)
			return 1922;
		pcontext->write_offset = 0;
		pcontext->sched_stat = isched_stat::wrlst;
		return DISPATCH_BREAK;
	}
	imrpc_build_env();
	auto cl_0 = HX::make_scope_exit(imrpc_free_env);
	midb_agent::remove_mail(pcontext->maildir, pcontext->selected_folder,
		moved, &errnum);
	for (auto pitem : moved)
		if (!exmdb_client->imapfile_delete(ctx.maildir, "eml", pitem->mid))
			mlog(LV_WARN, "W-2032: remove %s/eml/%s failed",
				ctx.maildir, pitem->mid.c_str());
	if (!moved.empty())
		imap_parser_bcast_expunge(*pcontext, moved, pcontext->selected_folder);
	pcontext->stream.clear();
	std::string buf;
	if (uidvalidity != 0) {
		buf = fmt::format("* OK [COPYUID {} {} {}] moved\r\n",
		      uidvalidity, argv[3], uid_string);
		if (pcontext->stream.write(buf.c_str(), buf.size()) != STREAM_WRITE_OK)
			return 1922;
	}
	imap_parser_echo_modify(pcontext, &pcontext->stream);
	/* IMAP_CODE_2170034: OK UID MOVE completed */
	buf = fmt::format("{} {}", argv[0], resource_get_imap_code(1734, 1));
	if (pcontext->stream.write(buf.c_str(), buf.size()) != STREAM_WRITE_OK)
		return 1922;
	pcontext->write_offset = 0;
	pcontext->sched_stat = isched_stat::wrlst;
	return DISPATCH_BREAK;
} catch (const std::bad_alloc &) {
	mlog(LV_ERR, "E-2328: ENOMEM");
	return 1918;
}

int icp_uid_expunge(std::span<std::string> argv, imap_context &ctx) try
{
	auto pcontext = &ctx;
	int errnum;
	int max_uid;
	imap_seq_list list_seq;
	
	if (pcontext->proto_stat != iproto_stat::select)
		return 1805;
	if (pcontext->b_readonly)
		return 1806;
	if (argv.size() < 4 || parse_imap_seq(list_seq, icp_uidseq(ctx, argv[3])) != 0)
		return 1800;
	XARRAY xarray;
	auto ssr = midb_agent::list_deleted(pcontext->maildir,
	           pcontext->selected_folder, &xarray, &errnum);
	auto ret = m2icode(ssr, errnum);
	if (ret != 0)
		return ret;
	auto num = xarray.get_capacity();
	if (0 == num) {
		imap_parser_echo_modify(pcontext, nullptr);
		return 1730;
	}
	auto pitem = xarray.get_item(num - 1);
	max_uid = pitem->uid;
	std::vector<MITEM *> exp_list;
	imrpc_build_env();
	auto cl_0 = HX::make_scope_exit(imrpc_free_env);
	for (size_t i = 0; i < num; ++i) {
		pitem = xarray.get_item(i);
		if (zero_uid_bit(*pitem) ||
		    !iseq_contains(list_seq, pitem->uid, max_uid))
			continue;
		exp_list.push_back(pitem);
	}
	ssr = midb_agent::remove_mail(pcontext->maildir,
	      pcontext->selected_folder, exp_list, &errnum);
	ret = m2icode(ssr, errnum);
	if (ret != 0)
		return ret;

	pcontext->stream.clear();
	for (size_t i = 0; i < xarray.get_capacity(); ++i) {
		pitem = xarray.get_item(i);
		if (zero_uid_bit(*pitem) ||
		    !iseq_contains(list_seq, pitem->uid, max_uid))
			continue;
		auto ct_item = pcontext->contents.get_itemx(pitem->uid);
		if (ct_item == nullptr)
			continue;
		if (!exmdb_client->imapfile_delete(ctx.maildir, "eml", pitem->mid))
			mlog(LV_WARN, "W-2086: remove %s/eml/%s failed",
				ctx.maildir, pitem->mid.c_str());
		else
			imap_parser_log_info(pcontext, LV_DEBUG, "message %s has been deleted",
				pitem->mid.c_str());
	}
	if (!exp_list.empty())
		imap_parser_bcast_expunge(*pcontext, exp_list, pcontext->selected_folder);
	imap_parser_echo_modify(pcontext, &pcontext->stream);
	/* IMAP_CODE_2170026: OK UID EXPUNGE completed */
	auto buf = fmt::format("{} {}", argv[0], resource_get_imap_code(1726, 1));
	if (pcontext->stream.write(buf.c_str(), buf.size()) != STREAM_WRITE_OK)
		return 1922;
	pcontext->write_offset = 0;
	pcontext->sched_stat = isched_stat::wrlst;
	return DISPATCH_BREAK;
} catch (const std::bad_alloc &) {
	mlog(LV_ERR, "E-1243: ENOMEM");
	return 1918;
}

void icp_clsfld(imap_context &ctx) try
{
	auto pcontext = &ctx;
	int errnum, result, i;
	std::string prev_selected;
	
	if (pcontext->selected_folder.empty())
		return;
	imap_parser_remove_select(pcontext);
	pcontext->proto_stat = iproto_stat::auth;
	prev_selected = std::move(pcontext->selected_folder);
	pcontext->selected_folder.clear();
	pcontext->announced_keywords.clear();
	ctx.saved_uids.clear();
	if (pcontext->b_readonly)
		return;
	/*
	 * RFC3501 §7.4.2: a read-write SELECT consumes \Recent. Clear it for the
	 * folder's messages as the session leaves it so a later STATUS/SELECT
	 * reports RECENT 0. (read-only EXAMINE returned above without clearing.)
	 */
	for (size_t ri = 0; ri < pcontext->contents.get_capacity(); ++ri) {
		auto pitem = pcontext->contents.get_item(ri);
		if (pitem != nullptr && pitem->flag_bits & FLAG_RECENT)
			midb_agent::unset_flags(pcontext->maildir, prev_selected,
				pitem->mid, FLAG_RECENT, nullptr, &errnum);
	}
	XARRAY xarray;
	result = midb_agent::list_deleted(pcontext->maildir,
	         prev_selected, &xarray, &errnum);
	std::string buf;
	switch(result) {
	case MIDB_RESULT_OK:
		break;
	case MIDB_NO_SERVER:
		/* IMAP_CODE_2190005: NO server internal
			error, missing MIDB connection */
		buf = fmt::format("* {}", resource_get_imap_code(1905, 1));
		break;
	case MIDB_RDWR_ERROR:
		/* IMAP_CODE_2190006: NO server internal
		error, fail to communicate with MIDB */
		buf = fmt::format("* {}", resource_get_imap_code(1906, 1));
		break;
	case MIDB_LOCAL_ENOMEM:
		buf = fmt::format("* {}", resource_get_imap_code(1920, 1));
		break;
	default:
		/* IMAP_CODE_2190007: NO server internal error, */
		buf = fmt::format("* {}{}", resource_get_imap_code(1907, 1),
		      resource_get_error_string(errnum));
		break;
	}
	if (result != MIDB_RESULT_OK) {
		imap_parser_safe_write(pcontext, buf.c_str(), buf.size());
		return;
	}

	bool b_deleted = false;
	int num = xarray.get_capacity();
	std::vector<MITEM *> exp_list;
	for (i=0; i<num; i++) {
		auto pitem = xarray.get_item(i);
		if (zero_uid_bit(*pitem))
			continue;
		exp_list.push_back(pitem);
	}
	result = midb_agent::remove_mail(pcontext->maildir,
	         prev_selected, exp_list, &errnum);
	switch(result) {
	case MIDB_RESULT_OK: {
		imrpc_build_env();
		auto cl_0 = HX::make_scope_exit(imrpc_free_env);
		for (i = 0; i < num; ++i) {
			auto pitem = xarray.get_item(i);
			if (zero_uid_bit(*pitem))
				continue;
			if (!exmdb_client->imapfile_delete(ctx.maildir, "eml", pitem->mid))
				mlog(LV_WARN, "W-2087: remove %s/eml/%s failed",
				        ctx.maildir, pitem->mid.c_str());
			else
				imap_parser_log_info(pcontext, LV_DEBUG,
					"message %s has been deleted", pitem->mid.c_str());
			b_deleted = TRUE;
		}
		break;
	}
	case MIDB_NO_SERVER:
		/* IMAP_CODE_2190005: NO server internal
			error, missing MIDB connection */
		buf = fmt::format("* {}", resource_get_imap_code(1905, 1));
		break;
	case MIDB_RDWR_ERROR:
		/* IMAP_CODE_2190006: NO server internal
		error, fail to communicate with MIDB */
		buf = fmt::format("* {}", resource_get_imap_code(1906, 1));
		break;
	case MIDB_LOCAL_ENOMEM:
		buf = fmt::format("* {}", resource_get_imap_code(1920, 1));
		break;
	default:
		/* IMAP_CODE_2190007: NO server internal error, */
		buf = fmt::format("* {}{}", resource_get_imap_code(1907, 1),
		      resource_get_error_string(errnum));
		break;
	}
	if (result != MIDB_RESULT_OK) {
		imap_parser_safe_write(pcontext, buf.c_str(), buf.size());
		return;
	}
	if (b_deleted)
		imap_parser_bcast_expunge(*pcontext, exp_list, prev_selected);
} catch (const std::bad_alloc &) {
	mlog(LV_ERR, "E-1242: ENOMEM");
}

/**
 * Helper function. Takes a multi-purpose dispatch return code
 * (imap_cmd_parser.h), "unpacks" it, possibly sends a response line to the
 * client before yielding the unpacked dispatch action.
 */
int icp_dval(const char *tag, imap_context &ctx, unsigned int ret)
{
	auto code = ret & DISPATCH_VALMASK;
	if (code == 0)
		return ret & DISPATCH_ACTMASK;
	bool trycreate = code == MIDB_E_NO_FOLDER_TRYCREATE;
	auto estr = (ret & DISPATCH_MIDB) ? resource_get_error_string(code) : nullptr;
	if (ret & DISPATCH_MIDB)
		code = 1907;
	auto str = resource_get_imap_code(code, 1);
	char buff[1024];
	tag = tag_or_bug((ret & DISPATCH_TAG) ? ctx.tag_string :
	                 tag != nullptr ? tag : "*");
	if (trycreate && strncmp(str, "NO ", 3) == 0)
		str += 2; /* avoid double NO */
	auto len = gx_snprintf(buff, std::size(buff), "%s%s %s%s", tag,
	      trycreate ? " NO [TRYCREATE]" : "", str, znul(estr));
	imap_parser_safe_write(&ctx, buff, len);
	return ret & DISPATCH_ACTMASK;
}
