// SPDX-License-Identifier: GPL-2.0-only WITH linking exception
// SPDX-FileCopyrightText: 2021–2026 grommunio GmbH
// This file is part of Gromox.
#include <algorithm>
#include <atomic>
#include <cassert>
#include <cerrno>
#include <csignal>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <list>
#include <mutex>
#include <poll.h>
#include <pthread.h>
#include <span>
#include <string>
#include <string_view>
#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/socket.h>
#include <libHX/string.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <gromox/algorithm.hpp>
#include <gromox/atomic.hpp>
#include <gromox/config_file.hpp>
#include <gromox/defs.h>
#include <gromox/fileio.h>
#include <gromox/json.hpp>
#include <gromox/list_file.hpp>
#include <gromox/midb.hpp>
#include <gromox/midb_agent.hpp>
#include <gromox/process.hpp>
#include <gromox/range_set.hpp>
#include <gromox/svc_common.h>
#include <gromox/util.hpp>
#include <gromox/xarray2.hpp>

using namespace std::string_literals;
using namespace gromox;
using AGENT_MITEM = MITEM;
DECLARE_SVC_API(,);

namespace {

struct BACK_SVR;
struct BACK_CONN {
	int sockd = -1;
	time_t last_time = 0;
	BACK_SVR *psvr = nullptr;
};

struct BACK_CONN_floating {
	BACK_CONN_floating() = default;
	BACK_CONN_floating(BACK_CONN_floating &&);
	~BACK_CONN_floating() { reset(true); }
	void operator=(BACK_CONN_floating &&) = delete;
	BACK_CONN *operator->() { return tmplist.size() != 0 ? &tmplist.front() : nullptr; }
	bool operator==(std::nullptr_t) const { return tmplist.size() == 0; }
	void reset(bool lost = false);

	std::list<BACK_CONN> tmplist;
};

struct BACK_SVR {
	std::string prefix;
	char ip_addr[40]{};
	uint16_t port = 0;
	std::list<BACK_CONN> conn_list;
};

}

static void *midbag_scanwork(void *);
static ssize_t read_line(int sockd, char *buff, size_t length);
static ssize_t read_line_dyn(int sockd, std::string &out);
static int connect_midb(const char *host, uint16_t port);

std::atomic<size_t> g_midb_command_buffer_size{256 * 1024};
/*
 * Timeout for reading a command response from midb. This must be strictly
 * greater than midb's own giant_lock wait (mail_engine.cpp:DB_LOCK_TIMEOUT,
 * typically 60s), because when a command queues behind an in-progress resync,
 * the server only answers (with an E-2403 FALSE) after its lock wait expires.
 * If the client's read timeout were equal, it would deterministically give up
 * first – it starts the clock at write time, before the server thread even
 * begins waiting – and tear down the connection instead of receiving the
 * orderly error. The 30s margin keeps the client waiting just past the server.
 */
static constexpr int MIDB_CMD_TIMEOUT_MS = 90000;
static int g_conn_num;
static gromox::atomic_bool g_midbagent_stop;
static pthread_t g_scan_id;
static std::list<BACK_CONN> g_lost_list;
static std::list<BACK_SVR> g_server_list;
static std::mutex g_server_lock;
static int g_file_ratio;

static constexpr cfg_directive midb_agent_cfg_defaults[] = {
	{"connection_num", "5", CFG_SIZE, "2", "100"},
	{"context_average_mem", "1024", CFG_SIZE},
	{"midb_agent_command_buffer_size", "256K", CFG_SIZE},
	CFG_TABLE_END,
};

static bool list_file_read_midb(const char *filename) try
{
	struct MIDB_ITEM {
		char prefix[256], ip_addr[40];
		int port;
	};
	auto plist = list_file_initd(filename, get_config_path(),
	             /* MIDB_ITEM */ "%s:256%s:40%d");
	if (plist == nullptr) {
		printf("[midb_agent]: list_file_initd %s: %s\n",
		       filename, strerror(errno));
		return false;
	}
	auto list_num = plist->get_size();
	auto pitem = static_cast<MIDB_ITEM *>(plist->get_list());
	if (list_num == 0) {
		auto &svr = g_server_list.emplace_back();
		auto pserver = &svr;
		svr.prefix = "/";
		strcpy(pserver->ip_addr, "::1");
		pserver->port = 5555;
		for (decltype(g_conn_num) j = 0; j < g_conn_num; ++j)
			g_lost_list.push_back(BACK_CONN{-1, 0, pserver});
		return true;
	}
	for (decltype(list_num) i = 0; i < list_num; ++i) {
		auto &svr = g_server_list.emplace_back();
		auto pserver = &svr;
		svr.prefix = pitem[i].prefix;
		gx_strlcpy(pserver->ip_addr, pitem[i].ip_addr, std::size(pserver->ip_addr));
		pserver->port = pitem[i].port;
		for (decltype(g_conn_num) j = 0; j < g_conn_num; ++j)
			g_lost_list.emplace_back(BACK_CONN{-1, 0, pserver});
	}
	return true;
} catch (const std::bad_alloc &) {
	mlog(LV_ERR, "E-1656: ENOMEM");
	return false;
}

static bool midb_agent_reload(std::shared_ptr<config_file> &&cfg)
{
	if (cfg == nullptr)
		cfg = config_file_initd("midb_agent.cfg", get_config_path(), midb_agent_cfg_defaults);
	if (cfg == nullptr) {
		fprintf(stderr, "[midb_agent]: config_file_initd midb_agent.cfg: %s\n", strerror(errno));
		return false;
	}
	g_conn_num = cfg->get_ll("connection_num");
	g_file_ratio = cfg->get_ll("context_average_mem");
	if (g_file_ratio == 0)
		fprintf(stderr, "[midb_agent]: memory pool is switched off through config\n");
	g_midb_command_buffer_size = cfg->get_ll("midb_agent_command_buffer_size");
	return true;
}

bool SVC_midb_agent(enum plugin_op reason, const struct dlfuncs &ppdata)
{
	switch(reason) {
	case PLUGIN_RELOAD:
		midb_agent_reload(nullptr);
		return TRUE;
	case PLUGIN_INIT: {
		LINK_SVC_API(ppdata);
		g_midbagent_stop = true;
		auto pconfig = config_file_initd("midb_agent.cfg",
		               get_config_path(), midb_agent_cfg_defaults);
		if (NULL == pconfig) {
			mlog(LV_ERR, "midb_agent: config_file_initd midb_agent.cfg: %s",
				strerror(errno));
			return FALSE;
		}
		if (!midb_agent_reload(std::move(pconfig)))
			return false;
		if (!list_file_read_midb("midb_list.txt"))
			return false;

		g_midbagent_stop = false;
		auto ret = pthread_create4(&g_scan_id, nullptr, midbag_scanwork, nullptr);
		if (ret != 0) {
			printf("[midb_agent]: failed to create scan thread: %s\n", strerror(ret));
			return FALSE;
		}
		pthread_setname_np(g_scan_id, "midb_agent");
		return TRUE;
	}
	case PLUGIN_FREE:
		if (!g_midbagent_stop) {
			g_midbagent_stop = true;
			if (!pthread_equal(g_scan_id, {})) {
				pthread_kill(g_scan_id, SIGALRM);
				pthread_join(g_scan_id, NULL);
			}
		}
		g_lost_list.clear();
		for (auto &srv : g_server_list) {
			for (auto &c : srv.conn_list) {
				auto pback = &c;
				if (HXio_fullwrite(pback->sockd, "QUIT\r\n", 6) != 6)
					/* ignore */;
				close(pback->sockd);
			}
		}
		g_server_list.clear();
		return TRUE;
	default:
		return TRUE;
	}
}

static void *midbag_scanwork(void *param)
{
	char temp_buff[1024];
	struct pollfd pfd_read;
	std::list<BACK_CONN> temp_list;

	while (!g_midbagent_stop) {
		{
		std::unique_lock sv_hold(g_server_lock);
		auto now_time = time(nullptr);
		for (auto &srv : g_server_list)
			temp_list.splice(temp_list.end(),
				gromox::splice_if(srv.conn_list, [&](const BACK_CONN &conn) {
					return now_time - conn.last_time >= SOCKET_TIMEOUT - 3;
				}));
		}

		while (temp_list.size() > 0) {
			auto pback = &temp_list.front();
			pfd_read.fd = pback->sockd;
			pfd_read.events = POLLIN|POLLPRI;
			if (HXio_fullwrite(pback->sockd, "PING\r\n", 6) != 6 ||
			    poll(&pfd_read, 1, SOCKET_TIMEOUT_MS) != 1 ||
			    read(pback->sockd, temp_buff, 1024) <= 0) {
				close(pback->sockd);
				pback->sockd = -1;
				std::unique_lock sv_hold(g_server_lock);
				g_lost_list.splice(g_lost_list.end(), temp_list, temp_list.begin());
			} else {
				pback->last_time = time(nullptr);
				std::unique_lock sv_hold(g_server_lock);
				pback->psvr->conn_list.splice(pback->psvr->conn_list.end(), temp_list, temp_list.begin());
			}
		}

		{
		std::unique_lock sv_hold(g_server_lock);
		temp_list = std::move(g_lost_list);
		g_lost_list.clear();
		}

		while (temp_list.size() > 0) {
			auto pback = &temp_list.front();
			pback->sockd = connect_midb(pback->psvr->ip_addr,
							pback->psvr->port);
			if (-1 != pback->sockd) {
				pback->last_time = time(nullptr);
				std::unique_lock sv_hold(g_server_lock);
				pback->psvr->conn_list.splice(pback->psvr->conn_list.end(), temp_list, temp_list.begin());
			} else {
				std::unique_lock sv_hold(g_server_lock);
				g_lost_list.splice(g_lost_list.end(), temp_list, temp_list.begin());
			}
		}
		sleep(1);
	}
	return NULL;
}

static BACK_CONN_floating get_connection(const char *prefix)
{
	BACK_CONN_floating fc;
	auto i = std::find_if(g_server_list.begin(), g_server_list.end(),
	         [&](const BACK_SVR &s) { return strncmp(prefix, s.prefix.c_str(), s.prefix.size()) == 0; });
	if (i == g_server_list.end())
		return fc;

	{
	std::unique_lock sv_hold(g_server_lock);
	if (i->conn_list.size() > 0) {
		fc.tmplist.splice(fc.tmplist.end(), i->conn_list, i->conn_list.begin());
		return fc;
	}
	}

	for (size_t j = 0; j < SOCKET_TIMEOUT && !g_midbagent_stop; ++j) {
		sleep(1);
		std::unique_lock sv_hold(g_server_lock);
		if (i->conn_list.size() > 0) {
			fc.tmplist.splice(fc.tmplist.end(),
				i->conn_list, i->conn_list.begin());
			return fc;
		}
	}
	return fc;
}

void BACK_CONN_floating::reset(bool lost)
{
	if (tmplist.size() == 0)
		return;
	auto pconn = &tmplist.front();
	if (!lost) {
		std::unique_lock sv_hold(g_server_lock);
		pconn->psvr->conn_list.splice(pconn->psvr->conn_list.end(), tmplist, tmplist.begin());
	} else {
		close(pconn->sockd);
		pconn->sockd = -1;
		std::unique_lock sv_hold(g_server_lock);
		g_lost_list.splice(g_lost_list.end(), tmplist, tmplist.begin());
	}
	tmplist.clear();
}

BACK_CONN_floating::BACK_CONN_floating(BACK_CONN_floating &&o)
{
	reset(true);
	tmplist = std::move(o.tmplist);
}

/*
 * Older midb versions stored MAPI category keywords with atom-special
 * characters in them; coerce all tokens (banned set mirrors midb's
 * atom_special) so that emitted FLAGS lists stay parseable.
 */
static bool kw_atom_special(unsigned char c)
{
	return c == '(' || c == ')' || c == '{' || c == '}' || c == ' ' ||
	       c <= 0x1F || c == 0x7F || c == '%' || c == '*' || c == '"' ||
	       c == '\\' || c == '[' || c == ']';
}

static std::string kw_sanitize(std::string_view in)
{
	std::string out;
	for (size_t pos = 0; pos < in.size(); ) {
		auto sp = in.find(' ', pos);
		if (sp == in.npos)
			sp = in.size();
		if (sp > pos) {
			std::string tok(in.substr(pos, sp - pos));
			std::replace_if(tok.begin(), tok.end(), kw_atom_special, '_');
			if (!out.empty())
				out += ' ';
			out += std::move(tok);
		}
		pos = sp + 1;
	}
	return out;
}

namespace midb_agent {

int list_mail(const char *path, const std::string &folder,
    std::vector<MSG_UNIT> &parray, int *pnum, uint64_t *psize) try
{
	static constexpr size_t temp_line_size = 64U * 1024;
	auto temp_line = std::make_unique<char[]>(temp_line_size);
	struct pollfd pfd_read;

	auto pback = get_connection(path);
	if (pback == nullptr)
		return MIDB_NO_SERVER;
	auto EH = HX::make_scope_exit([&]() { parray.clear(); });
	auto buff = fmt::format("P-SIMU {} {} 1 -1\r\n", path, folder);
	auto wrret = write(pback->sockd, buff.c_str(), buff.size());
	if (wrret < 0 || static_cast<size_t>(wrret) != buff.size())
		return MIDB_RDWR_ERROR;

	buff.resize(256 * 1024);
	*psize = 0;
	int count = 0, lines = -1;
	size_t offset = 0, last_pos = 0, line_pos = 0;
	bool b_fail = false;
	while (true) {
		pfd_read.fd = pback->sockd;
		pfd_read.events = POLLIN|POLLPRI;
		if (poll(&pfd_read, 1, MIDB_CMD_TIMEOUT_MS) != 1)
			return MIDB_RDWR_ERROR;
		auto read_len = read(pback->sockd, &buff[offset], buff.size() - offset);
		if (read_len <= 0)
			return MIDB_RDWR_ERROR;
		offset += read_len;
		buff[offset] = '\0';
		
		if (-1 == lines) {
			for (size_t i = 0; i < offset - 1 && i < 36; ++i) {
				if (buff[i] != '\r' || buff[i+1] != '\n')
					continue;
				if (strncmp(buff.c_str(), "TRUE ", 5) == 0) {
					lines = strtol(&buff[5], nullptr, 0);
					if (lines < 0)
						return MIDB_RDWR_ERROR;
					*pnum = lines;
					last_pos = i + 2;
					line_pos = 0;
					break;
				} else if (strncmp(buff.c_str(), "FALSE ", 6) == 0) {
					pback.reset();
					EH.release(); // ?
					return MIDB_RESULT_ERROR;
				}
			}
			if (-1 == lines) {
				if (offset > 1024)
					return MIDB_RDWR_ERROR;
				continue;
			}
		}

		for (size_t i = last_pos; i < offset; ++i) {
			if ('\r' == buff[i] && i < offset - 1 && '\n' == buff[i + 1]) {
				count ++;
				continue;
			} else if ('\n' == buff[i] && '\r' == buff[i - 1]) {
				temp_line[line_pos] = '\0';
				try {
					auto parts = gx_split(temp_line.get(), ' ');
					if (parts.size() != 6)
						throw 0;
					MSG_UNIT msg{std::move(parts[1])};
					msg.size = strtoul(parts[4].c_str(), nullptr, 0);
					auto msg_size = msg.size;
					parray.push_back(std::move(msg));
					*psize += msg_size;
				} catch (...) {
					b_fail = TRUE;
				}
				line_pos = 0;
			} else if (buff[i] != '\r' || i != offset - 1) {
				temp_line[line_pos++] = buff[i];
				if (line_pos >= temp_line_size - 1)
					return MIDB_RDWR_ERROR;
			}
		}

		if (count >= lines) {
			pback.reset();
			if (b_fail)
				return MIDB_RESULT_ERROR;
			EH.release();
			return MIDB_RESULT_OK;
		}
		last_pos = buff[offset-1] == '\r' ? offset - 1 : offset;
		if (offset >= buff.size()) {
			if ('\r' != buff[offset - 1]) {
				offset = 0;
			} else {
				buff[0] = '\r';
				offset = 1;
			}
			last_pos = 0;
		}
	}
} catch (const std::bad_alloc &) {
	mlog(LV_ERR, "E-1780: ENOMEM");
	return MIDB_E_NO_MEMORY;
}

static int rw_command(int fd, char *buff, size_t olen, size_t ilen)
{
	auto ret = write(fd, buff, olen);
	if (ret < 0 || static_cast<size_t>(ret) != olen)
		return MIDB_RDWR_ERROR;
	ret = read_line(fd, buff, ilen);
	if (ret == -ENOBUFS) {
		auto ilog10 = [](size_t x) -> size_t { size_t i = 0; for (; x >= 10; ++i) x /= 10; return i; };
		auto b = g_midb_command_buffer_size.load();
		size_t ap = b / (2 + ilog10(b));
		mlog(LV_ERR, "E-2154: midb response is longer than expected (%zu), rejecting IMAP command. "
		        "Consider raising midb_agent.cfg:midb_agent_command_buffer_size "
		        "or having fewer mails in the folder. (Approx. limit %zu messages.)",
		        ilen, ap);
		return MIDB_TOO_MANY_RESULTS;
	} else if (ret < 0) {
		return MIDB_RDWR_ERROR;
	}
	return 0;
}

int delete_mail(const char *path, const std::string &folder,
    const std::vector<MSG_UNIT *> &plist)
{
	char buff[128*1025];

	if (plist.size() == 0)
		return MIDB_RESULT_OK;
	auto pback = get_connection(path);
	if (pback == nullptr)
		return MIDB_NO_SERVER;
	auto length = gx_snprintf(buff, std::size(buff), "M-DELE %s %s",
	              path, folder.c_str());
	int cmd_len = length;
	
	for (auto pmsg : plist) {
		buff[length++] = ' ';
		auto temp_len = pmsg->file_name.size();
		memcpy(buff + length, pmsg->file_name.c_str(), temp_len);
		length += temp_len;
		if (length <= 128 * 1024)
			continue;
		buff[length++] = '\r';
		buff[length++] = '\n';
		auto ret = rw_command(pback->sockd, buff, length, std::size(buff));
		if (ret != 0)
			return ret;
		if (0 == strncmp(buff, "TRUE", 4)) {
			length = gx_snprintf(buff, std::size(buff), "M-DELE %s %s",
			         path, folder.c_str());
			continue;
		} else if (0 == strncmp(buff, "FALSE ", 6)) {
			pback.reset();
			return MIDB_RESULT_ERROR;
		}
		return MIDB_RDWR_ERROR;
	}

	if (length > cmd_len) {
		buff[length++] = '\r';
		buff[length++] = '\n';
		auto ret = rw_command(pback->sockd, buff, length, std::size(buff));
		if (ret != 0)
			return ret;
		if (0 == strncmp(buff, "TRUE", 4)) {
			pback.reset();
			return MIDB_RESULT_OK;
		} else if (0 == strncmp(buff, "FALSE ", 6)) {
			pback.reset();
			return MIDB_RESULT_ERROR;
		} else {
			return MIDB_RDWR_ERROR;
		}
	}
	return MIDB_RDWR_ERROR;
}

/**
 * Shared implementation of the SEARCH (P-SRHL) and UID SEARCH (P-SRHU)
 * commands. The two differ only in the command verb. The matching id set is
 * read back with read_line_dyn() so that large folders (the midb server emits
 * the whole set as one line) are not rejected for exceeding a fixed buffer.
 */
static int search_common(const char *cmd, const char *path,
    const std::string &folder, const char *charset,
    std::span<std::string> argv, std::string &ret_buff, int *perrno) try
{
	auto pback = get_connection(path);
	if (pback == nullptr)
		return MIDB_NO_SERVER;

	/* Pack the criteria as a NUL-separated, NUL-terminated list. */
	std::string crit;
	for (const auto &elem : argv) {
		crit.append(elem);
		crit.push_back('\0');
	}
	crit.push_back('\0');

	auto req = fmt::format("{} {} {} {} {}\r\n", cmd, path, folder,
	           charset, base64_encode(crit));
	auto wrret = write(pback->sockd, req.c_str(), req.size());
	if (wrret < 0 || static_cast<size_t>(wrret) != req.size())
		return MIDB_RDWR_ERROR;

	std::string resp;
	auto ret = read_line_dyn(pback->sockd, resp);
	if (ret == -ENOMEM)
		return MIDB_LOCAL_ENOMEM;
	if (ret <= 0)
		return MIDB_RDWR_ERROR;
	if (strncmp(resp.c_str(), "TRUE", 4) == 0) {
		pback.reset();
		if (resp.size() <= 5) {
			/* "TRUE" or "TRUE " with no ids */
			ret_buff.clear();
			return MIDB_RESULT_OK;
		}
		/* skip "TRUE " (verb plus the one trailing space) */
		ret_buff.assign(resp, 5, std::string::npos);
		return MIDB_RESULT_OK;
	} else if (strncmp(resp.c_str(), "FALSE ", 6) == 0) {
		pback.reset();
		*perrno = strtol(resp.c_str() + 6, nullptr, 0);
		return MIDB_RESULT_ERROR;
	}
	return MIDB_RDWR_ERROR;
} catch (const std::bad_alloc &) {
	return MIDB_LOCAL_ENOMEM;
}

int search(const char *path, const std::string &folder,
    const char *charset, std::span<std::string> argv, std::string &ret_buff,
    int *perrno)
{
	return search_common("P-SRHL", path, folder, charset, argv,
	       ret_buff, perrno);
}

int search_uid(const char *path, const std::string &folder,
   const char *charset, std::span<std::string> argv, std::string &ret_buff,
   int *perrno)
{
	return search_common("P-SRHU", path, folder, charset, argv,
	       ret_buff, perrno);
}

int get_uid(const char *path, const std::string &folder,
    const std::string &mid_string, unsigned int *puid)
{
	char buff[1024];

	auto pback = get_connection(path);
	if (pback == nullptr)
		return MIDB_NO_SERVER;
	auto length = gx_snprintf(buff, std::size(buff), "P-UNID %s %s %s\r\n",
	              path, folder.c_str(), mid_string.c_str());
	auto ret = rw_command(pback->sockd, buff, length, std::size(buff));
	if (ret != 0)
		return ret;
	if (0 == strncmp(buff, "TRUE", 4)) {
		*puid = strtol(buff + 5, nullptr, 0);
		pback.reset();
		return MIDB_RESULT_OK;
	} else if (0 == strncmp(buff, "FALSE ", 6)) {
		pback.reset();
		return MIDB_RESULT_ERROR;
	}
	return MIDB_RDWR_ERROR;
}

int summary_folder(const char *path, const std::string &folder, size_t *pexists,
    size_t *precent, size_t *punseen, uint32_t *puidvalid, uint32_t *puidnext,
    int *perrno)
{
	char buff[1024];
	size_t exists, recent, unseen;
	unsigned long uidvalid, uidnext;

	auto pback = get_connection(path);
	if (pback == nullptr)
		return MIDB_NO_SERVER;
	auto length = gx_snprintf(buff, std::size(buff), "P-FDDT %s %s\r\n",
	              path, folder.c_str());
	auto ret = rw_command(pback->sockd, buff, length, std::size(buff));
	if (ret != 0)
		return ret;
	if (strncmp(buff, "FALSE ", 6) == 0) {
		pback.reset();
		*perrno = strtol(buff + 6, nullptr, 0);
		return MIDB_RESULT_ERROR;
	} else if (strncmp(buff, "TRUE", 4) != 0) {
		return MIDB_RDWR_ERROR;
	}

	if (sscanf(buff, "TRUE %zu %zu %zu %lu %lu", &exists,
	    &recent, &unseen, &uidvalid, &uidnext) != 5) {
		*perrno = -1;
		pback.reset();
		return MIDB_RESULT_ERROR;
	}
	if (pexists != nullptr)
		*pexists = exists;
	if (precent != nullptr)
		*precent = recent;
	if (punseen != nullptr)
		*punseen = unseen;
	if (puidvalid != nullptr)
		*puidvalid = uidvalid;
	if (puidnext != nullptr)
		*puidnext = uidnext;
	pback.reset();
	return MIDB_RESULT_OK;
}
	
int folder_sizes(const char *path, const std::string &folder, size_t *psize,
    size_t *pdeleted, int *perrno)
{
	char buff[1024];
	size_t size, deleted;

	auto back = get_connection(path);
	if (back == nullptr)
		return MIDB_NO_SERVER;
	auto length = gx_snprintf(buff, std::size(buff), "P-FDSZ %s %s\r\n",
	              path, folder.c_str());
	auto ret = rw_command(back->sockd, buff, length, std::size(buff));
	if (ret != 0)
		return ret;
	if (strncmp(buff, "FALSE ", 6) == 0) {
		back.reset();
		*perrno = strtol(buff + 6, nullptr, 0);
		return MIDB_RESULT_ERROR;
	} else if (strncmp(buff, "TRUE", 4) != 0) {
		return MIDB_RDWR_ERROR;
	}
	if (sscanf(buff, "TRUE %zu %zu", &size, &deleted) != 2) {
		*perrno = -1;
		back.reset();
		return MIDB_RESULT_ERROR;
	}
	if (psize != nullptr)
		*psize = size;
	if (pdeleted != nullptr)
		*pdeleted = deleted;
	back.reset();
	return MIDB_RESULT_OK;
}

int make_folder(const char *path, const std::string &folder, int *perrno)
{
	char buff[1024];

	auto pback = get_connection(path);
	if (pback == nullptr)
		return MIDB_NO_SERVER;
	auto length = gx_snprintf(buff, std::size(buff), "M-MAKF %s %s\r\n",
	              path, folder.c_str());
	auto ret = rw_command(pback->sockd, buff, length, std::size(buff));
	if (ret != 0)
		return ret;
	if (0 == strncmp(buff, "TRUE", 4)) {
		pback.reset();
		return MIDB_RESULT_OK;
	} else if (0 == strncmp(buff, "FALSE ", 6)) {
		pback.reset();
		*perrno = strtol(buff + 6, nullptr, 0);
		return MIDB_RESULT_ERROR;
	}
	return MIDB_RDWR_ERROR;
}

int remove_folder(const char *path, const std::string &folder, int *perrno)
{
	char buff[1024];
	
	auto pback = get_connection(path);
	if (pback == nullptr)
		return MIDB_NO_SERVER;
	auto length = gx_snprintf(buff, std::size(buff), "M-REMF %s %s\r\n",
	              path, folder.c_str());
	auto ret = rw_command(pback->sockd, buff, length, std::size(buff));
	if (ret != 0)
		return ret;
	if (0 == strncmp(buff, "TRUE", 4)) {
		pback.reset();
		return MIDB_RESULT_OK;
	} else if (0 == strncmp(buff, "FALSE ", 6)) {
		pback.reset();
		*perrno = strtol(buff + 6, nullptr, 0);
		return MIDB_RESULT_ERROR;
	}
	return MIDB_RDWR_ERROR;
}

int ping_mailbox(const char *path, int *perrno)
{
	char buff[1024];
	
	auto pback = get_connection(path);
	if (pback == nullptr)
		return MIDB_NO_SERVER;
	auto length = gx_snprintf(buff, std::size(buff), "M-PING %s\r\n", path);
	auto ret = rw_command(pback->sockd, buff, length, std::size(buff));
	if (ret != 0)
		return ret;
	if (0 == strncmp(buff, "TRUE", 4)) {
		pback.reset();
		return MIDB_RESULT_OK;
	} else if (0 == strncmp(buff, "FALSE ", 6)) {
		pback.reset();
		*perrno = strtol(buff + 6, nullptr, 0);
		return MIDB_RESULT_ERROR;
	}
	return MIDB_RDWR_ERROR;
}

int sync_mailbox(const char *path, uint64_t folder_id, int *err)
{
	auto conn = get_connection(path);
	if (conn == nullptr)
		return MIDB_NO_SERVER;
	char buf[1024];
	int len;
	if (folder_id == 0)
		len = gx_snprintf(buf, std::size(buf), "X-RSYM %s\r\n", path);
	else
		len = gx_snprintf(buf, std::size(buf), "X-RSYF %s %llu\r\n",
		      path, static_cast<unsigned long long>(folder_id));
	auto ret = rw_command(conn->sockd, buf, len, std::size(buf));
	if (ret != 0)
		return ret;
	if (strncmp(buf, "TRUE", 4) == 0) {
		conn.reset();
		return MIDB_RESULT_OK;
	} else if (strncmp(buf, "FALSE ", 6) == 0) {
		conn.reset();
		if (err != nullptr)
			*err = strtol(&buf[6], nullptr, 0);
		return MIDB_RESULT_ERROR;
	}
	return MIDB_RDWR_ERROR;
}

int rename_folder(const char *path, const std::string &src_name,
    const std::string &dst_name, int *perrno)
{
	char buff[1024];

	auto pback = get_connection(path);
	if (pback == nullptr)
		return MIDB_NO_SERVER;
	auto length = gx_snprintf(buff, std::size(buff), "M-RENF %s %s %s\r\n",
	              path, src_name.c_str(), dst_name.c_str());
	auto ret = rw_command(pback->sockd, buff, length, std::size(buff));
	if (ret != 0)
		return ret;
	if (0 == strncmp(buff, "TRUE", 4)) {
		pback.reset();
		return MIDB_RESULT_OK;
	} else if (0 == strncmp(buff, "FALSE ", 6)) {
		pback.reset();
		*perrno = strtol(buff + 6, nullptr, 0);
		return MIDB_RESULT_ERROR;
	}
	return MIDB_RDWR_ERROR;
}

int subscribe_folder(const char *path, const std::string &folder, int *perrno)
{
	char buff[1024];

	auto pback = get_connection(path);
	if (pback == nullptr)
		return MIDB_NO_SERVER;
	auto length = gx_snprintf(buff, std::size(buff), "P-SUBF %s %s\r\n",
	              path, folder.c_str());
	auto ret = rw_command(pback->sockd, buff, length, std::size(buff));
	if (ret != 0)
		return ret;
	if (0 == strncmp(buff, "TRUE", 4)) {
		pback.reset();
		return MIDB_RESULT_OK;
	} else if (0 == strncmp(buff, "FALSE ", 6)) {
		pback.reset();
		*perrno = strtol(buff + 6, nullptr, 0);
		return MIDB_RESULT_ERROR;
	}
	return MIDB_RDWR_ERROR;
}

int unsubscribe_folder(const char *path, const std::string &folder, int *perrno)
{
	char buff[1024];

	auto pback = get_connection(path);
	if (pback == nullptr)
		return MIDB_NO_SERVER;
	auto length = gx_snprintf(buff, std::size(buff), "P-UNSF %s %s\r\n",
	              path, folder.c_str());
	auto ret = rw_command(pback->sockd, buff, length, std::size(buff));
	if (ret != 0)
		return ret;
	if (0 == strncmp(buff, "TRUE", 4)) {
		pback.reset();
		return MIDB_RESULT_OK;
	} else if (0 == strncmp(buff, "FALSE ", 6)) {
		pback.reset();
		*perrno = strtol(buff + 6, nullptr, 0);
		return MIDB_RESULT_ERROR;
	}
	return MIDB_RDWR_ERROR;
}

int enum_folders(const char *path, std::vector<enum_folder_t> &pfile,
    int *perrno) try
{
	char temp_line[512];
	struct pollfd pfd_read;

	auto pback = get_connection(path);
	if (pback == nullptr)
		return MIDB_NO_SERVER;
	auto buff = "M-ENUM "s + path + "\r\n";
	auto wrret = write(pback->sockd, buff.c_str(), buff.size());
	if (wrret < 0 || static_cast<size_t>(wrret) != buff.size())
		return MIDB_RDWR_ERROR;
	
	buff.resize(256 * 1024);
	int count = 0, lines = -1;
	size_t offset = 0, last_pos = 0, line_pos = 0;
	while (true) {
		pfd_read.fd = pback->sockd;
		pfd_read.events = POLLIN|POLLPRI;
		if (poll(&pfd_read, 1, MIDB_CMD_TIMEOUT_MS) != 1)
			return MIDB_RDWR_ERROR;
		auto read_len = read(pback->sockd, &buff[offset], buff.size() - offset);
		if (read_len <= 0)
			return MIDB_RDWR_ERROR;
		offset += read_len;
		buff[offset] = '\0';
		
		if (-1 == lines) {
			for (size_t i = 0; i < offset - 1 && i < 36; ++i) {
				if (buff[i] != '\r' || buff[i+1] != '\n')
					continue;
				if (strncmp(buff.c_str(), "TRUE ", 5) == 0) {
					lines = strtol(&buff[5], nullptr, 0);
					if (lines < 0)
						return MIDB_RDWR_ERROR;
					last_pos = i + 2;
					line_pos = 0;
					break;
				} else if (strncmp(buff.c_str(), "FALSE ", 6) == 0) {
					pback.reset();
					*perrno = strtol(&buff[6], nullptr, 0);
					return MIDB_RESULT_ERROR;
				}
				return MIDB_RDWR_ERROR;
			}
			if (-1 == lines) {
				if (offset > 1024)
					return MIDB_RDWR_ERROR;
				continue;
			}
		}

		for (size_t i = last_pos; i < offset; ++i) {
			if ('\r' == buff[i] && i < offset - 1 && '\n' == buff[i + 1]) {
				count ++;
			} else if ('\n' == buff[i] && '\r' == buff[i - 1]) {
				temp_line[line_pos] = '\0';
				char *end = nullptr;
				uint64_t fid = strtoul(temp_line, &end, 0);
				while (HX_isspace(*end))
					++end;
				pfile.emplace_back(fid, end);
				line_pos = 0;
			} else if (buff[i] != '\r' || i != offset - 1) {
				temp_line[line_pos++] = buff[i];
				if (line_pos >= 512)
					return MIDB_RDWR_ERROR;
			}
		}

		if (count >= lines) {
			pback.reset();
			return MIDB_RESULT_OK;
		}
		last_pos = buff[offset-1] == '\r' ? offset - 1 : offset;
		if (offset >= buff.size()) {
			if ('\r' != buff[offset - 1]) {
				offset = 0;
			} else {
				buff[0] = '\r';
				offset = 1;
			}
			last_pos = 0;
		}
	}
	return MIDB_RDWR_ERROR;
} catch (const std::bad_alloc &) {
	mlog(LV_ERR, "E-1812: ENOMEM");
	return MIDB_LOCAL_ENOMEM;
}

int enum_subscriptions(const char *path, std::vector<enum_folder_t> &pfile,
    int *perrno) try
{
	char temp_line[512];
	struct pollfd pfd_read;
	
	auto pback = get_connection(path);
	if (pback == nullptr)
		return MIDB_NO_SERVER;
	auto buff = "P-SUBL "s + path + "\r\n";
	auto wrret = write(pback->sockd, buff.c_str(), buff.size());
	if (wrret < 0 || static_cast<size_t>(wrret) != buff.size())
		return MIDB_RDWR_ERROR;

	buff.resize(256 * 1024);
	int count = 0, lines = -1;
	size_t offset = 0, last_pos = 0, line_pos = 0;
	while (true) {
		pfd_read.fd = pback->sockd;
		pfd_read.events = POLLIN|POLLPRI;
		if (poll(&pfd_read, 1, MIDB_CMD_TIMEOUT_MS) != 1)
			return MIDB_RDWR_ERROR;
		auto read_len = read(pback->sockd, &buff[offset], buff.size() - offset);
		if (read_len <= 0)
			return MIDB_RDWR_ERROR;
		offset += read_len;
		buff[offset] = '\0';
		
		if (-1 == lines) {
			for (size_t i = 0; i < offset - 1 && i < 36; ++i) {
				if (buff[i] != '\r' || buff[i+1] != '\n')
					continue;
				if (strncmp(buff.c_str(), "TRUE ", 5) == 0) {
					lines = strtol(&buff[5], nullptr, 0);
					if (lines < 0)
						return MIDB_RDWR_ERROR;
					last_pos = i + 2;
					line_pos = 0;
					break;
				} else if (strncmp(buff.c_str(), "FALSE ", 6) == 0) {
					pback.reset();
					*perrno = strtol(&buff[6], nullptr, 0);
					return MIDB_RESULT_ERROR;
				}
				return MIDB_RDWR_ERROR;
			}
			if (-1 == lines) {
				if (offset > 1024)
					return MIDB_RDWR_ERROR;
				continue;
			}
		}

		for (size_t i = last_pos; i < offset; ++i) {
			if ('\r' == buff[i] && i < offset - 1 && '\n' == buff[i + 1]) {
				count ++;
			} else if ('\n' == buff[i] && '\r' == buff[i - 1]) {
				temp_line[line_pos] = '\0';
				char *end = nullptr;
				uint64_t fid = strtoul(temp_line, &end, 0);
				while (HX_isspace(*end))
					++end;
				pfile.emplace_back(fid, end);
				line_pos = 0;
			} else if (buff[i] != '\r' || i != offset - 1) {
				temp_line[line_pos++] = buff[i];
				if (line_pos > 150)
					return MIDB_RDWR_ERROR;
			}
		}

		if (count >= lines) {
			pback.reset();
			return MIDB_RESULT_OK;
		}
		last_pos = buff[offset-1] == '\r' ? offset - 1 : offset;
		if (offset >= buff.size()) {
			if ('\r' != buff[offset - 1]) {
				offset = 0;
			} else {
				buff[0] = '\r';
				offset = 1;
			}
			last_pos = 0;
		}
	}
	return MIDB_RDWR_ERROR;
} catch (const std::bad_alloc &) {
	mlog(LV_ERR, "E-1815: ENOMEM");
	return MIDB_LOCAL_ENOMEM;
}

int insert_mail(const char *path, const std::string &folder,
    const char *file_name, const char *flags_string, long time_stamp,
    int *perrno)
{
	char buff[1024];

	auto pback = get_connection(path);
	if (pback == nullptr)
		return MIDB_NO_SERVER;
	auto length = gx_snprintf(buff, std::size(buff), "M-INST %s %s %s %s %ld\r\n",
	              path, folder.c_str(), file_name, flags_string, time_stamp);
	auto ret = rw_command(pback->sockd, buff, length, std::size(buff));
	if (ret != 0)
		return ret;
	if (0 == strncmp(buff, "TRUE", 4)) {
		pback.reset();
		return MIDB_RESULT_OK;
	} else if (0 == strncmp(buff, "FALSE ", 6)) {
		pback.reset();
		*perrno = strtol(buff + 6, nullptr, 0);
		return MIDB_RESULT_ERROR;
	}
	return MIDB_RDWR_ERROR;
}

int remove_mail(const char *path, const std::string &folder,
    const std::vector<MITEM *> &plist, int *perrno)
{
	char buff[128*1025];

	if (plist.empty())
		return MIDB_RESULT_OK;
	auto pback = get_connection(path);
	if (pback == nullptr)
		return MIDB_NO_SERVER;
	auto length = gx_snprintf(buff, std::size(buff), "M-DELE %s %s",
	              path, folder.c_str());
	int cmd_len = length;
	
	for (auto pitem : plist) {
		buff[length++] = ' ';
		auto temp_len = pitem->mid.size();
		memcpy(&buff[length], pitem->mid.c_str(), temp_len);
		length += temp_len;
		if (length <= 128*1024)
			continue;
		buff[length++] = '\r';
		buff[length++] = '\n';
		auto ret = rw_command(pback->sockd, buff, length, std::size(buff));
		if (ret != 0)
			return ret;
		if (0 == strncmp(buff, "TRUE", 4)) {
			length = gx_snprintf(buff, std::size(buff), "M-DELE %s %s",
			         path, folder.c_str());
			continue;
		} else if (0 == strncmp(buff, "FALSE ", 6)) {
			pback.reset();
			*perrno = strtol(buff + 6, nullptr, 0);
			return MIDB_RESULT_ERROR;
		}
		return MIDB_RDWR_ERROR;
	}

	if (length > cmd_len) {
		buff[length++] = '\r';
		buff[length++] = '\n';
		auto ret = rw_command(pback->sockd, buff, length, std::size(buff));
		if (ret != 0)
			return ret;
		if (0 == strncmp(buff, "TRUE", 4)) {
			pback.reset();
			return MIDB_RESULT_OK;
		} else if (0 == strncmp(buff, "FALSE ", 6)) {
			pback.reset();
			*perrno = strtol(buff + 6, nullptr, 0);
			return MIDB_RESULT_ERROR;
		}
		return MIDB_RDWR_ERROR;
	}
	return MIDB_RDWR_ERROR;
}

static unsigned int s_to_flagbits(std::string_view s)
{
	unsigned int fl = 0;
	if (s.find(midb_flag::answered) != s.npos) fl |= FLAG_ANSWERED;
	if (s.find(midb_flag::unsent) != s.npos)   fl |= FLAG_DRAFT;
	if (s.find(midb_flag::flagged) != s.npos)  fl |= FLAG_FLAGGED;
	if (s.find(midb_flag::deleted) != s.npos)  fl |= FLAG_DELETED;
	if (s.find(midb_flag::seen) != s.npos)     fl |= FLAG_SEEN;
	if (s.find(midb_flag::recent) != s.npos)   fl |= FLAG_RECENT;
	if (s.find(midb_flag::forwarded) != s.npos)fl |= FLAG_FORWARDED;
	return fl;
}

static std::string flagbits_to_s(unsigned int v)
{
	std::string s;
	if (v & FLAG_ANSWERED) s += midb_flag::answered;
	if (v & FLAG_DRAFT)    s += midb_flag::unsent;
	if (v & FLAG_FLAGGED)  s += midb_flag::flagged;
	if (v & FLAG_DELETED)  s += midb_flag::deleted;
	if (v & FLAG_SEEN)     s += midb_flag::seen;
	if (v & FLAG_RECENT)   s += midb_flag::recent;
	if (v & FLAG_FORWARDED)s += midb_flag::forwarded;
	return s;
}

static bool get_digest_integer(const Json::Value &jv, const char *tag, int &i)
{
	if (jv.type() != Json::ValueType::objectValue || !jv.isMember(tag))
		return false;
	i = jv[tag].asUInt();
	return true;
}

static unsigned int di_to_flagbits(const Json::Value &jv)
{
	unsigned int fl = 0;
	if (jv.type() != Json::ValueType::objectValue)
		return fl;
	if (jv.isMember("replied") && jv["replied"].asUInt() != 0)
		fl |= FLAG_ANSWERED;
	if (jv.isMember("unsent") && jv["unsent"].asUInt() != 0)
		fl |= FLAG_DRAFT;
	if (jv.isMember("flag") && jv["flag"].asUInt() != 0)
		fl |= FLAG_FLAGGED;
	if (jv.isMember("deleted") && jv["deleted"].asUInt() != 0)
		fl |= FLAG_DELETED;
	if (jv.isMember("read") && jv["read"].asUInt() != 0)
		fl |= FLAG_SEEN;
	if (jv.isMember("recent") && jv["recent"].asUInt() != 0)
		fl |= FLAG_RECENT;
	if (jv.isMember("forwarded") && jv["forwarded"].asUInt() != 0)
		fl |= FLAG_FORWARDED;
	return fl;
}

int list_deleted(const char *path, const std::string &folder, XARRAY *pxarray,
    int *perrno) try
{
	char *pspace;
	char *pspace1;
	char temp_line[512];
	struct pollfd pfd_read;

	auto pback = get_connection(path);
	if (pback == nullptr)
		return MIDB_NO_SERVER;
	auto EH = HX::make_scope_exit([=]() { pxarray->clear(); });
	auto buff = fmt::format("P-DELL {} {}\r\n", path, folder);
	auto wrret = write(pback->sockd, buff.c_str(), buff.size());
	if (wrret < 0 || static_cast<size_t>(wrret) != buff.size())
		return MIDB_RDWR_ERROR;
	
	buff.resize(256 * 1024);
	int count = 0, lines = -1;
	size_t offset = 0, last_pos = 0, line_pos = 0;
	bool b_format_error = false;
	while (true) {
		pfd_read.fd = pback->sockd;
		pfd_read.events = POLLIN|POLLPRI;
		if (poll(&pfd_read, 1, MIDB_CMD_TIMEOUT_MS) != 1)
			return MIDB_RDWR_ERROR;
		auto read_len = read(pback->sockd, &buff[offset], buff.size() - offset);
		if (read_len <= 0)
			return MIDB_RDWR_ERROR;
		offset += read_len;
		buff[offset] = '\0';
		
		if (-1 == lines) {
			for (size_t i = 0; i < offset - 1 && i < 36; ++i) {
				if (buff[i] != '\r' || buff[i+1] != '\n')
					continue;
				if (strncmp(buff.c_str(), "TRUE ", 5) == 0) {
					lines = strtol(&buff[5], nullptr, 0);
					if (lines < 0)
						return MIDB_RDWR_ERROR;
					last_pos = i + 2;
					line_pos = 0;
					break;
				} else if (strncmp(buff.c_str(), "FALSE ", 6) == 0) {
					pback.reset();
					EH.release(); // ?
					*perrno = strtol(&buff[6], nullptr, 0);
					return MIDB_RESULT_ERROR;
				}
			}
			if (-1 == lines) {
				if (offset > 1024)
					return MIDB_RDWR_ERROR;
				continue;
			}
		}

		for (size_t i = last_pos; i < offset; ++i) {
			if ('\r' == buff[i] && i < offset - 1 && '\n' == buff[i + 1]) {
				count ++;
			} else if ('\n' == buff[i] && '\r' == buff[i - 1]) {
				temp_line[line_pos] = '\0';
				pspace = strchr(temp_line, ' ');
				if (NULL != pspace) {
					pspace1 = strchr(pspace + 1, ' ');
					if (NULL != pspace1) {
						*pspace++ = '\0';
						*pspace1++ = '\0';
						MITEM mitem;
						try {
							mitem.mid = pspace;
						} catch (...) {
							b_format_error = TRUE;
						}
						mitem.uid = strtol(pspace1, nullptr, 0);
						mitem.flag_bits = FLAG_DELETED;
						auto mitem_uid = mitem.uid;
						pxarray->append(std::move(mitem), mitem_uid);
					} else {
						b_format_error = TRUE;
					}
				} else {
					b_format_error = TRUE;
				}
				line_pos = 0;
			} else if (buff[i] != '\r' || i != offset - 1) {
				temp_line[line_pos++] = buff[i];
				if (line_pos >= 128)
					return MIDB_RDWR_ERROR;
			}
		}

		if (count >= lines) {
			pback.reset();
			if (b_format_error) {
				*perrno = -1;
				return MIDB_RESULT_ERROR;
			}
			EH.release();
			return MIDB_RESULT_OK;
		}
		last_pos = buff[offset-1] == '\r' ? offset - 1 : offset;
		if (offset >= buff.size()) {
			if ('\r' != buff[offset - 1]) {
				offset = 0;
			} else {
				buff[0] = '\r';
				offset = 1;
			}
			last_pos = 0;
		}
	}
} catch (const std::bad_alloc &) {
	mlog(LV_ERR, "E-1779: ENOMEM");
	return MIDB_E_NO_MEMORY;
}

int fetch_simple_uid(const char *path, const std::string &folder,
    const imap_seq_list &list, XARRAY *pxarray, int *perrno) try
{
	char *pspace;
	char *pspace1;
	char *pspace2;
	static constexpr size_t temp_line_size = 64U * 1024;
	auto temp_line = std::make_unique<char[]>(temp_line_size);
	struct pollfd pfd_read;
	std::string buff;

	buff.resize(64 * 1024);
	auto pback = get_connection(path);
	if (pback == nullptr)
		return MIDB_NO_SERVER;
	
	for (const auto &seq : list) {
		auto pseq = &seq;
		auto cbuf = fmt::format("P-SIMU {} {} {} {}\r\n",
		            path, folder, pseq->lo, pseq->hi);
		auto wrret = write(pback->sockd, cbuf.c_str(), cbuf.size());
		if (wrret < 0 || static_cast<size_t>(wrret) != cbuf.size())
			return MIDB_RDWR_ERROR;
		
		int count = 0, lines = -1;
		size_t offset = 0, last_pos = 0, line_pos = 0;
		bool b_format_error = false;
		while (true) {
			pfd_read.fd = pback->sockd;
			pfd_read.events = POLLIN|POLLPRI;
			if (poll(&pfd_read, 1, MIDB_CMD_TIMEOUT_MS) != 1)
				return MIDB_RDWR_ERROR;
			auto read_len = read(pback->sockd, &buff[offset], buff.size() - offset);
			if (read_len <= 0)
				return MIDB_RDWR_ERROR;
			offset += read_len;
			buff[offset] = '\0';

			if (-1 == lines) {
				for (size_t i = 0; i < offset - 1 && i < 36; ++i) {
					if (buff[i] != '\r' || buff[i+1] != '\n')
						continue;
					if (strncmp(buff.c_str(), "TRUE ", 5) == 0) {
						lines = strtol(&buff[5], nullptr, 0);
						if (lines < 0)
							return MIDB_RDWR_ERROR;
						last_pos = i + 2;
						line_pos = 0;
						break;
					} else if (strncmp(buff.c_str(), "FALSE ", 6) == 0) {
						pback.reset();
						*perrno = strtol(&buff[6], nullptr, 0);
						return MIDB_RESULT_ERROR;
					}
				}
				if (-1 == lines) {
					if (offset > 1024)
						return MIDB_RDWR_ERROR;
					continue;
				}
			}

			for (size_t i = last_pos; i < offset; ++i) {
				if ('\r' == buff[i] && i < offset - 1 && '\n' == buff[i + 1]) {
					count ++;
				} else if ('\n' == buff[i] && '\r' == buff[i - 1]) {
					temp_line[line_pos] = '\0';
					pspace = strchr(temp_line.get(), ' ');
					if (NULL != pspace) {
						pspace1 = strchr(pspace + 1, ' ');
						if (NULL != pspace1) {
							pspace2 = strchr(pspace1 + 1, ' ');
							if (NULL != pspace2) {
								*pspace++ = '\0';
								*pspace1++ = '\0';
								*pspace2++ = '\0';
								int uid = strtol(pspace1, nullptr, 0);
								/*
								 * pspace2 now points at "<flags> <size>
								 * <base64(keywords)>". Bound the flag scan
								 * at ')' because the base64 keyword field
								 * may contain flag letters (A/D/F/R/S/U/W).
								 * The keyword field is the last space-
								 * separated token (possibly empty).
								 */
								auto flag_end = strchr(pspace2, ')');
								unsigned int flag_bits = flag_end != nullptr ?
									s_to_flagbits(std::string_view(pspace2, &flag_end[1] - pspace2)) :
									s_to_flagbits(pspace2);
								auto kw_beg = strrchr(pspace2, ' ');
								std::string kw = kw_beg != nullptr ?
									kw_sanitize(base64_decode(&kw_beg[1])) : std::string();
								if (pxarray->append(MITEM{}, uid) >= 0) {
									auto num = pxarray->get_capacity();
									assert(num > 0);
									auto pitem = pxarray->get_item(num - 1);
									pitem->uid = uid;
									try {
										pitem->mid = pspace;
										pitem->keywords = std::move(kw);
									} catch (const std::bad_alloc &) {
										b_format_error = TRUE;
									}
									pitem->flag_bits = flag_bits;
								}
							} else {
								b_format_error = TRUE;
							}
						} else {
							b_format_error = TRUE;
						}
					} else {
						b_format_error = TRUE;
					}
					line_pos = 0;
				} else if (buff[i] != '\r' || i != offset - 1) {
					temp_line[line_pos++] = buff[i];
					if (line_pos >= temp_line_size - 1)
						return MIDB_RDWR_ERROR;
				}
			}

			if (count >= lines) {
				if (!b_format_error)
					break;
				pback.reset();
				*perrno = -1;
				return MIDB_RESULT_ERROR;
			}
			last_pos = buff[offset-1] == '\r' ? offset - 1 : offset;
			if (offset >= buff.size()) {
				if ('\r' != buff[offset - 1]) {
					offset = 0;
				} else {
					buff[0] = '\r';
					offset = 1;
				}
				last_pos = 0;
			}
		}
	}

	pback.reset();
	return MIDB_RESULT_OK;
} catch (const std::bad_alloc &) {
	mlog(LV_ERR, "E-1781: ENOMEM");
	return MIDB_E_NO_MEMORY;
}

int fetch_detail_uid(const char *path, const std::string &folder,
    const imap_seq_list &list, XARRAY *pxarray, int *perrno) try
{
	char *pspace;
	char temp_line[257*1024];
	struct pollfd pfd_read;
	std::string buff;

	buff.resize(64 * 1024);
	auto pback = get_connection(path);
	if (pback == nullptr)
		return MIDB_NO_SERVER;
	auto EH = HX::make_scope_exit([=]() {
		pxarray->clear();
	});
	
	for (const auto &seq : list) {
		auto pseq = &seq;
		auto cbuf = fmt::format("P-DTLU {} {} {} {}\r\n", path, folder, pseq->lo, pseq->hi);
		auto wrret = write(pback->sockd, cbuf.c_str(), cbuf.size());
		if (wrret < 0 || static_cast<size_t>(wrret) != cbuf.size())
			return MIDB_RDWR_ERROR;
		
		int count = 0, lines = -1;
		size_t offset = 0, last_pos = 0, line_pos = 0;
		bool b_format_error = false;
		while (true) {
			pfd_read.fd = pback->sockd;
			pfd_read.events = POLLIN|POLLPRI;
			if (poll(&pfd_read, 1, MIDB_CMD_TIMEOUT_MS) != 1)
				return MIDB_RDWR_ERROR;
			auto read_len = read(pback->sockd, &buff[offset], buff.size() - offset);
			if (read_len <= 0)
				return MIDB_RDWR_ERROR;
			offset += read_len;
			buff[offset] = '\0';

			if (-1 == lines) {
				for (size_t i = 0; i < offset - 1 && i < 36; ++i) {
					if (buff[i] != '\r' || buff[i+1] != '\n')
						continue;
					if (strncmp(buff.c_str(), "TRUE ", 5) == 0) {
						lines = strtol(&buff[5], nullptr, 0);
						if (lines < 0)
							return MIDB_RDWR_ERROR;
						last_pos = i + 2;
						line_pos = 0;

						auto new_vec_size = pxarray->m_vec.size() + lines;
						auto new_hash_size = pxarray->m_hash.size() + lines;
						auto new_dpool_size = pxarray->m_dpool.size() + static_cast<size_t>(lines) * 1024;
						/* Armortized O(1) requires exponential growth (replicate emplace_back's implicit logic) */
						pxarray->m_vec.reserve(std::max(new_vec_size, pxarray->m_vec.size() * 2));
						pxarray->m_hash.reserve(std::max(new_hash_size, pxarray->m_hash.size() * 2));
						pxarray->m_dpool.reserve(std::max(new_dpool_size, pxarray->m_dpool.size() * 2));
						break;
					} else if (strncmp(buff.c_str(), "FALSE ", 6) == 0) {
						pback.reset();
						*perrno = strtol(&buff[6], nullptr, 0);
						return MIDB_RESULT_ERROR;
					}
				}
				if (-1 == lines) {
					if (offset > 1024)
						return MIDB_RDWR_ERROR;
					continue;
				}
			}

			for (size_t i = last_pos; i < offset; ++i) {
				if ('\r' == buff[i] && i < offset - 1 && '\n' == buff[i + 1]) {
					count ++;
				} else if ('\n' == buff[i] && '\r' == buff[i - 1]) {
					temp_line[line_pos] = '\0';
					pspace = strchr(temp_line, ' ');
					if (pspace == nullptr) {
						b_format_error = TRUE;
						line_pos = 0;
						continue;
					}
					std::string_view digest_sv(&pspace[1], line_pos - (pspace + 1 - temp_line));
					Json::Value digest;
					if (!str_to_json(digest_sv, digest)) {
						b_format_error = TRUE;
					} else {
						MITEM mitem;
						if (get_digest(digest, "file", mitem.mid) &&
						    get_digest_integer(digest, "uid", mitem.uid)) {
							mitem.keywords = digest.isMember("keywords") ?
								kw_sanitize(digest["keywords"].asString()) : "";
							mitem.digest_off = pxarray->m_dpool.size();
							mitem.digest_len = digest_sv.size();
							pxarray->m_dpool.append(digest_sv);
							auto mitem_uid = mitem.uid;
							if (pxarray->append(std::move(mitem), mitem_uid) >= 0) {
								auto num = pxarray->get_capacity();
								assert(num > 0);
								auto pitem = pxarray->get_item(num - 1);
								pitem->flag_bits = FLAG_LOADED | di_to_flagbits(digest);
							}
						}
					}
					line_pos = 0;
				} else if (buff[i] != '\r' || i != offset - 1) {
					temp_line[line_pos++] = buff[i];
					if (line_pos >= sizeof(temp_line) - 1)
						return MIDB_RDWR_ERROR;
				}
			}

			if (count >= lines) {
				if (!b_format_error)
					break;
				pback.reset();
				*perrno = -1;
				EH.release();
				// no pxarray->clear?
				return MIDB_RESULT_ERROR;
			}
			last_pos = buff[offset-1] == '\r' ? offset - 1 : offset;
			if (offset >= buff.size()) {
				if ('\r' != buff[offset - 1]) {
					offset = 0;
				} else {
					buff[0] = '\r';
					offset = 1;
				}
				last_pos = 0;
			}
		}
	}
	
	pback.reset();
	EH.release();
	return MIDB_RESULT_OK;
} catch (const std::bad_alloc &) {
	return MIDB_LOCAL_ENOMEM;
}

int set_flags(const char *path, const std::string &folder,
    const std::string &mid_string, unsigned int flag_bits,
    unsigned int *new_bits, int *perrno)
{
	char buff[1024];
	auto pback = get_connection(path);
	if (pback == nullptr)
		return MIDB_NO_SERVER;
	auto flags_string = flagbits_to_s(flag_bits);
	auto length = gx_snprintf(buff, std::size(buff), "P-SFLG %s %s %s (%s)\r\n",
	              path, folder.c_str(), mid_string.c_str(), flags_string.c_str());
	auto ret = rw_command(pback->sockd, buff, length, std::size(buff));
	if (ret != 0)
		return ret;
	if (0 == strncmp(buff, "TRUE", 4)) {
		pback.reset();
		if (new_bits != nullptr)
			*new_bits = ~0U;
		if (buff[5] == '\r' || buff[5] == '\n')
			return MIDB_RESULT_OK;
		auto ptr = &buff[5];
		if (!HX_isspace(*ptr))
			return MIDB_RDWR_ERROR;
		while (HX_isspace(*ptr))
			++ptr;
		if (*ptr == '-')
			return MIDB_RESULT_OK;
		if (*ptr != '(')
			return MIDB_RDWR_ERROR;
		auto bg = ptr + 1;
		ptr = strchr(bg, ')');
		if (ptr == nullptr)
			return MIDB_RDWR_ERROR;
		if (new_bits != nullptr)
			*new_bits = s_to_flagbits(std::string_view(bg, ptr - bg));
		return MIDB_RESULT_OK;
	} else if (0 == strncmp(buff, "FALSE ", 6)) {
		pback.reset();
		*perrno = strtol(buff + 6, nullptr, 0);
		return MIDB_RESULT_ERROR;
	}
	return MIDB_RDWR_ERROR;
}
	
int unset_flags(const char *path, const std::string &folder,
    const std::string &mid_string, unsigned int flag_bits,
    unsigned int *new_bits, int *perrno)
{
	char buff[1024];
	auto pback = get_connection(path);
	if (pback == nullptr)
		return MIDB_NO_SERVER;
	auto flags_string = flagbits_to_s(flag_bits);
	auto length = gx_snprintf(buff, std::size(buff), "P-RFLG %s %s %s (%s)\r\n",
	              path, folder.c_str(), mid_string.c_str(), flags_string.c_str());
	auto ret = rw_command(pback->sockd, buff, length, std::size(buff));
	if (ret != 0)
		return ret;
	if (0 == strncmp(buff, "TRUE", 4)) {
		pback.reset();
		if (new_bits != nullptr)
			*new_bits = -1;
		if (buff[5] == '\r' || buff[5] == '\n')
			return MIDB_RESULT_OK;
		auto ptr = &buff[5];
		if (!HX_isspace(*ptr))
			return MIDB_RDWR_ERROR;
		while (HX_isspace(*ptr))
			++ptr;
		if (*ptr == '-')
			return MIDB_RESULT_OK;
		if (*ptr != '(')
			return MIDB_RDWR_ERROR;
		auto bg = ptr + 1;
		ptr = strchr(bg, ')');
		if (ptr == nullptr)
			return MIDB_RDWR_ERROR;
		if (new_bits != nullptr)
			*new_bits = s_to_flagbits(std::string_view(bg, ptr - bg));
		return MIDB_RESULT_OK;
	} else if (0 == strncmp(buff, "FALSE ", 6)) {
		pback.reset();
		*perrno = strtol(buff + 6, nullptr, 0);
		return MIDB_RESULT_ERROR;
	}
	return MIDB_RDWR_ERROR;
}
	
int get_flags(const char *path, const std::string &folder,
    const std::string &mid_string, unsigned int *pflag_bits, int *perrno,
    std::string *keywords)
{
	char buff[1024];

	auto pback = get_connection(path);
	if (pback == nullptr)
		return MIDB_NO_SERVER;
	auto length = gx_snprintf(buff, std::size(buff), "P-GFLG %s %s %s\r\n",
	              path, folder.c_str(), mid_string.c_str());
	auto ret = rw_command(pback->sockd, buff, length, std::size(buff));
	if (ret != 0)
		return ret;
	if (0 == strncmp(buff, "TRUE", 4)) {
		pback.reset();
		*pflag_bits = 0;
		/*
		 * Response: TRUE (<flags>)[ <base64-keywords>]. Bound the flag
		 * scan at ')' because the base64 keyword blob may contain flag
		 * letters, then decode any trailing keyword field.
		 */
		auto close = buff[4] == ' ' ? strchr(buff + 5, ')') : nullptr;
		if (close != nullptr) {
			*pflag_bits = s_to_flagbits(std::string_view(buff + 5, close - (buff + 5)));
			if (keywords != nullptr && close[1] == ' ') {
				auto kw_beg = close + 2;
				auto kw_end = kw_beg;
				while (*kw_end != '\0' && *kw_end != '\r' && *kw_end != '\n')
					++kw_end;
				*keywords = kw_sanitize(base64_decode(std::string_view(kw_beg, kw_end - kw_beg)));
			}
		} else if (buff[4] == ' ') {
			*pflag_bits = s_to_flagbits(buff + 5);
		}
		return MIDB_RESULT_OK;
	} else if (0 == strncmp(buff, "FALSE ", 6)) {
		pback.reset();
		*perrno = strtol(buff + 6, nullptr, 0);
		return MIDB_RESULT_ERROR;
	}
	return MIDB_RDWR_ERROR;
}
	
int set_keywords(const char *path, const std::string &folder,
    const std::string &mid_string, const std::string &keywords, int *perrno) try
{
	auto pback = get_connection(path);
	if (pback == nullptr)
		return MIDB_NO_SERVER;
	auto cbufsize = g_midb_command_buffer_size.load();
	auto buff = std::make_unique<char[]>(cbufsize);
	/* keywords are arbitrary atoms, so transmit them base64-encoded */
	auto length = gx_snprintf(buff.get(), cbufsize, "M-SKWD %s %s %s %s\r\n",
	              path, folder.c_str(), mid_string.c_str(),
	              base64_encode(keywords).c_str());
	auto ret = rw_command(pback->sockd, buff.get(), length, cbufsize);
	if (ret != 0)
		return ret;
	if (strncmp(buff.get(), "TRUE", 4) == 0) {
		pback.reset();
		return MIDB_RESULT_OK;
	} else if (strncmp(buff.get(), "FALSE ", 6) == 0) {
		pback.reset();
		*perrno = strtol(&buff[6], nullptr, 0);
		return MIDB_RESULT_ERROR;
	}
	return MIDB_RDWR_ERROR;
} catch (const std::bad_alloc &) {
	mlog(LV_ERR, "E-2339: ENOMEM");
	return MIDB_RDWR_ERROR;
}

int get_folder_keywords(const char *path, const std::string &folder,
    std::vector<std::string> &out, int *perrno) try
{
	auto pback = get_connection(path);
	if (pback == nullptr)
		return MIDB_NO_SERVER;
	auto cbufsize = g_midb_command_buffer_size.load();
	auto buff = std::make_unique<char[]>(cbufsize);
	/* folder is already base64 (selected_folder); pass raw like get_flags() */
	auto length = gx_snprintf(buff.get(), cbufsize, "P-KWLS %s %s\r\n",
	              path, folder.c_str());
	auto ret = rw_command(pback->sockd, buff.get(), length, cbufsize);
	if (ret != 0)
		return ret;
	if (strncmp(buff.get(), "TRUE", 4) == 0) {
		pback.reset();
		out.clear();
		/* TRUE[ <base64>]...  split trailing tokens, base64-decode each */
		char *p = buff.get() + 4;
		while (*p == ' ') {
			++p;
			char *e = p;
			while (*e != '\0' && *e != ' ' && *e != '\r' && *e != '\n')
				++e;
			if (e != p) {
				auto kw = kw_sanitize(base64_decode(std::string_view(p, e - p)));
				if (!kw.empty())
					out.emplace_back(std::move(kw));
			}
			p = e;
		}
		return MIDB_RESULT_OK;
	} else if (strncmp(buff.get(), "FALSE ", 6) == 0) {
		pback.reset();
		*perrno = strtol(&buff[6], nullptr, 0);
		return MIDB_RESULT_ERROR;
	}
	return MIDB_RDWR_ERROR;
} catch (const std::bad_alloc &) {
	mlog(LV_ERR, "E-2336: ENOMEM");
	return MIDB_LOCAL_ENOMEM;
}

int copy_mail(const char *path, const std::string &src_folder,
    const std::string &mid_string, const std::string &dst_folder,
    std::string &dst_mid, int *perrno) try
{
	char buff[1024];

	auto pback = get_connection(path);
	if (pback == nullptr)
		return MIDB_NO_SERVER;
	auto length = gx_snprintf(buff, std::size(buff), "M-COPY %s %s %s %s\r\n",
	              path, src_folder.c_str(), mid_string.c_str(), dst_folder.c_str());
	auto ret = rw_command(pback->sockd, buff, length, std::size(buff));
	if (ret != 0)
		return ret;
	if (0 == strncmp(buff, "TRUE", 4)) {
		pback.reset();
		dst_mid = &buff[5];
		return MIDB_RESULT_OK;
	} else if (0 == strncmp(buff, "FALSE ", 6)) {
		pback.reset();
		*perrno = strtol(buff + 6, nullptr, 0);
		return MIDB_RESULT_ERROR;
	}
	return MIDB_RDWR_ERROR;
} catch (const std::bad_alloc &) {
	return MIDB_LOCAL_ENOMEM;
}

}

static ssize_t read_line(int sockd, char *buff, size_t length)
{
	if (length == 0)
		return 0;
	size_t offset = 0;
	--length;
	struct pollfd pfd_read;

	while (true) {
		pfd_read.fd = sockd;
		pfd_read.events = POLLIN|POLLPRI;
		if (poll(&pfd_read, 1, MIDB_CMD_TIMEOUT_MS) != 1)
			return -ETIMEDOUT;
		auto read_len = read(sockd, buff + offset,  length - offset);
		if (read_len < 0)
			return read_len;
		buff[offset+read_len] = '\0';
		if (read_len == 0)
			return 0;
		offset += read_len;
		if (offset >= 2 && '\r' == buff[offset - 2] &&
			'\n' == buff[offset - 1]) {
			offset -= 2;
			buff[offset] = '\0';
			return 1;
		}
		if (length == offset)
			return -ENOBUFS;
	}
}

/**
 * Like read_line(), but reads one CRLF-terminated logical line of *unbounded*
 * length into a growable std::string. Used by the SEARCH path, where the midb
 * server emits the entire match set (potentially several MB for folders with
 * hundreds of thousands of messages) as a single line. The fixed-buffer
 * read_line() rejected such responses with -ENOBUFS, which surfaced to IMAP
 * clients as error 1921 ("Too many messages in folder ...") and made large
 * folders unsearchable (and thus unusable in clients like Thunderbird that
 * SEARCH on folder open). Memory is bounded to one reusable staging buffer
 * plus the result string the caller needs anyway.
 *
 * Returns 1 on a complete line (CRLF stripped, stored in @out), 0 on orderly
 * peer close before a line terminator, or a negative errno on poll/read error.
 */
static ssize_t read_line_dyn(int sockd, std::string &out) try
{
	static constexpr size_t stage_size = 64 * 1024;
	auto stage = std::make_unique<char[]>(stage_size);
	struct pollfd pfd_read;

	out.clear();
	while (true) {
		pfd_read.fd = sockd;
		pfd_read.events = POLLIN | POLLPRI;
		if (poll(&pfd_read, 1, MIDB_CMD_TIMEOUT_MS) != 1)
			return -ETIMEDOUT;
		auto read_len = read(sockd, stage.get(), stage_size);
		if (read_len < 0)
			return read_len;
		else if (read_len == 0)
			return 0;
		out.append(stage.get(), read_len);
		if (out.size() >= 2 && out[out.size()-2] == '\r' &&
		    out[out.size()-1] == '\n') {
			out.resize(out.size() - 2);
			return 1;
		}
	}
} catch (const std::bad_alloc &) {
	return -ENOMEM;
}

static int connect_midb(const char *ip_addr, uint16_t port)
{
    char temp_buff[1024];
	struct pollfd pfd_read;

	auto sockd = HX_inet_connect(ip_addr, port, 0);
	if (sockd < 0) {
		static std::atomic<time_t> g_lastwarn_time;
		auto prev = g_lastwarn_time.load();
		auto next = prev + 60;
		auto now = time(nullptr);
		if (next <= now && g_lastwarn_time.compare_exchange_strong(prev, now))
			fprintf(stderr, "HX_inet_connect midb_agent@[%s]:%hu: %s\n",
			        ip_addr, port, strerror(-sockd));
		return -1;
	}
	pfd_read.fd = sockd;
	pfd_read.events = POLLIN|POLLPRI;
	if (poll(&pfd_read, 1, SOCKET_TIMEOUT_MS) != 1) {
		close(sockd);
		return -1;
	}
	auto read_len = read(sockd, temp_buff, std::size(temp_buff) - 1);
	if (read_len <= 0) {
        close(sockd);
        return -1;
	}
	temp_buff[read_len] = '\0';
	if (0 != strcasecmp(temp_buff, "OK\r\n")) {
		close(sockd);
		return -1;
	}
	return sockd;
}
