/* Configuring Message Options and Message Groups (but not sub-boards) */

/****************************************************************************
 * @format.tab-size 4		(Plain Text/Source Code File Header)			*
 * @format.use-tabs true	(see http://www.synchro.net/ptsc_hdr.html)		*
 *																			*
 * Copyright Rob Swindell - http://www.synchro.net/copyright.html			*
 *																			*
 * This program is free software; you can redistribute it and/or			*
 * modify it under the terms of the GNU General Public License				*
 * as published by the Free Software Foundation; either version 2			*
 * of the License, or (at your option) any later version.					*
 * See the GNU General Public License for more details: gpl.txt or			*
 * http://www.fsf.org/copyleft/gpl.html										*
 *																			*
 * For Synchronet coding style and modification guidelines, see				*
 * http://www.synchro.net/source.html										*
 *																			*
 * Note: If this box doesn't appear square, then you need to fix your tabs.	*
 ****************************************************************************/

#include "scfg.h"
#include "strwrap.h"    /* itoa() */
#include "str_util.h"
#include "ciolib.h" // CIO_KEY_*
#include <stdbool.h>

#define CUT_GROUPNUM    USHRT_MAX

static const char* strDuplicateGrpName = "Group with that name already exists!";

char *utos(char *str)
{
	static char out[128];
	int         i;

	for (i = 0; str[i] && i < sizeof(out) - 1; i++)
		if (str[i] == '_')
			out[i] = ' ';
		else
			out[i] = str[i];
	out[i] = 0;
	return out;
}

static bool code_prefix_exists(const char* prefix)
{
	int i;

	for (i = 0; i < cfg.total_grps; i++)
		if (cfg.grp[i]->code_prefix[0] && stricmp(cfg.grp[i]->code_prefix, prefix) == 0)
			return true;
	return false;
}

static bool new_grp(int new_grpnum)
{
	grp_t* new_group = malloc(sizeof(grp_t));
	if (new_group == NULL) {
		errormsg(WHERE, ERR_ALLOC, "group", sizeof(grp_t));
		return false;
	}
	memset(new_group, 0, sizeof(*new_group));

	grp_t** new_grp_list;
	if ((new_grp_list = (grp_t **)realloc(cfg.grp, sizeof(grp_t *) * (cfg.total_grps + 1))) == NULL) {
		free(new_group);
		errormsg(WHERE, ERR_ALLOC, "group list", cfg.total_grps + 1);
		return false;
	}
	cfg.grp = new_grp_list;

	for (int u = cfg.total_grps; u > new_grpnum; u--)
		cfg.grp[u] = cfg.grp[u - 1];

	if (new_grpnum != cfg.total_grps) { /* Inserting group? Renumber (higher) existing groups */
		for (int j = 0; j < cfg.total_subs; j++) {
			if (cfg.sub[j]->grp >= new_grpnum && cfg.sub[j]->grp != CUT_GROUPNUM)
				cfg.sub[j]->grp++;
		}
	}
	cfg.grp[new_grpnum] = new_group;
	cfg.total_grps++;
	return true;
}

// Return number of imported (including over-written) subs or negative on error
long import_msg_areas(enum import_list_type type, FILE* stream, int grpnum
                      , int min_confnum, int max_confnum, qhub_t* qhub, const char* pkt_orig, faddr_t* faddr, uint32_t misc
                      , long* added)
{
	char       str[256];
	sub_t      tmpsub;
	char       tmp_code[128];
	long       ported = 0;
	int        total_qwk_confs = 0;
	int        read_qwk_confs = 0;
	int        qwk_confnum = -1;
	size_t     grpname_len = strlen(cfg.grp[grpnum]->sname);
	char       duplicate_code[LEN_CODE + 1] = "";
	uint       duplicate_codes = 0;     // consecutive duplicate codes
	long       new_sub_misc = 0;
	str_list_t ini = NULL;
	str_list_t list = NULL;
	uint       i = 0;

	if (added != NULL)
		*added = 0;
	if (grpnum >= cfg.total_grps)
		return -1;

	// Set the new_sub_misc and perform any necessary preprocessing of the input file
	switch (type) {
		case IMPORT_LIST_TYPE_SUBS_TXT:
			new_sub_misc = 0;
			break;
		case IMPORT_LIST_TYPE_NEWSGROUPS:
			new_sub_misc = SUB_INET;
			break;
		case IMPORT_LIST_TYPE_QWK_CONTROL_DAT:
			new_sub_misc = SUB_QNET;

			/* Skip/ignore the first 10 lines */
			for (int i = 0; i < 10; i++) {
				if (!fgets(str, sizeof(str), stream))
					break;
			}
			str[0] = 0;
			if (fgets(str, sizeof(str), stream) == NULL)
				total_qwk_confs = 0;
			else
				total_qwk_confs = atoi(str) + 1;
			break;
		case IMPORT_LIST_TYPE_ECHOSTATS:
			new_sub_misc = SUB_FIDO;
			/* intentional fall-through */
		case IMPORT_LIST_TYPE_SUBS_INI:
			ini = iniReadFile(stream);
			if (ini == NULL)
				return 0;
			list = iniGetSectionList(ini, /* prefix: */ NULL);
			if (list == NULL) {
				strListFree(&ini);
				return 0;
			}
			break;
		default: // EchoLists (e.g. BACKBONE.NA, badareas.lst) and AREAS.BBS
			new_sub_misc = SUB_FIDO;
			break;
	}
	new_sub_misc |= misc;

	while (1) {
		memset(&tmpsub, 0, sizeof(tmpsub));
//		tmpsub.misc = (SUB_FIDO|SUB_NAME|SUB_TOUSER|SUB_QUOTE|SUB_HYPER);
		tmpsub.grp = grpnum;
		if (type == IMPORT_LIST_TYPE_ECHOSTATS) {
			char value[INI_MAX_VALUE_LEN];
			char areatag[FIDO_AREATAG_LEN + 1];
			if (list[i] == NULL)
				break;
			SAFECOPY(areatag, list[i]);
			i++;
			if (iniGetBool(ini, areatag, "Known", TRUE))
				continue;
			if (pkt_orig != NULL && *pkt_orig
			    && strcmp(pkt_orig, iniGetString(ini, areatag, "LastReceived.pkt_orig", "", value)) != 0)
				continue;
			SAFECOPY(tmp_code, areatag);            // Copy tag to internal code suffix
			SAFECOPY(tmpsub.sname, utos(areatag));  // ... to short name, converting underscores to spaces
			if (strlen(areatag) > sizeof(tmpsub.area_tag) - 1)
				SAFECOPY(tmpsub.newsgroup, areatag);
			else
				SAFECOPY(tmpsub.area_tag, areatag);
			SAFECOPY(tmpsub.lname, iniGetString(ini, areatag, "Title", "", value));
		} else if (type == IMPORT_LIST_TYPE_SUBS_INI) {
			if (list[i] == NULL)
				break;
			SAFECOPY(tmp_code, list[i]);
			read_sub_ini_section(&cfg, ini, list[i], &tmpsub, tmp_code);
			i++;
		} else {
			if (feof(stream))
				break;
			if (!fgets(str, sizeof(str), stream))
				break;
			truncsp(str);
			if (!str[0])
				continue;

			if (type == IMPORT_LIST_TYPE_SUBS_TXT) {
				sprintf(tmpsub.lname, "%.*s", LEN_SLNAME, str);
				if (!fgets(str, 128, stream))
					break;
				truncsp(str);
				sprintf(tmpsub.sname, "%.*s", LEN_SSNAME, str);
				if (!fgets(str, 128, stream))
					break;
				truncsp(str);
				sprintf(tmpsub.qwkname, "%.*s", 10, str);
				if (!fgets(str, 128, stream))
					break;
				truncsp(str);
				SAFECOPY(tmp_code, str);
				if (!fgets(str, 128, stream))
					break;
				truncsp(str);
				sprintf(tmpsub.data_dir, "%.*s", LEN_DIR, str);
				if (!fgets(str, 128, stream))
					break;
				truncsp(str);
				sprintf(tmpsub.arstr, "%.*s", LEN_ARSTR, str);
				if (!fgets(str, 128, stream))
					break;
				truncsp(str);
				sprintf(tmpsub.read_arstr, "%.*s", LEN_ARSTR, str);
				if (!fgets(str, 128, stream))
					break;
				truncsp(str);
				sprintf(tmpsub.post_arstr, "%.*s", LEN_ARSTR, str);
				if (!fgets(str, 128, stream))
					break;
				truncsp(str);
				sprintf(tmpsub.op_arstr, "%.*s", LEN_ARSTR, str);
				if (!fgets(str, 128, stream))
					break;
				truncsp(str);
				tmpsub.misc = ahtoul(str);
				if (!fgets(str, 128, stream))
					break;
				truncsp(str);
				sprintf(tmpsub.tagline, "%.*s", 80, str);
				if (!fgets(str, 128, stream))
					break;
				truncsp(str);
				sprintf(tmpsub.origline, "%.*s", 50, str);
				if (!fgets(str, 128, stream))
					break;
				truncsp(str);
				sprintf(tmpsub.post_sem, "%.*s", LEN_DIR, str);
				if (!fgets(str, 128, stream))
					break;
				truncsp(str);
				SAFECOPY(tmpsub.newsgroup, str);
				if (!fgets(str, 128, stream))
					break;
				truncsp(str);
				tmpsub.faddr = smb_atofaddr(NULL, str);
				if (!fgets(str, 128, stream))
					break;
				truncsp(str);
				tmpsub.maxmsgs = atol(str);
				if (!fgets(str, 128, stream))
					break;
				truncsp(str);
				tmpsub.maxcrcs = atol(str);
				if (!fgets(str, 128, stream))
					break;
				truncsp(str);
				tmpsub.maxage = atoi(str);
				if (!fgets(str, 128, stream))
					break;
				truncsp(str);
				tmpsub.ptridx = atoi(str);
				if (!fgets(str, 128, stream))
					break;
				truncsp(str);
				sprintf(tmpsub.mod_arstr, "%.*s", LEN_ARSTR, str);

				while (!feof(stream)
				       && strcmp(str, "***END-OF-SUB***")) {
					if (!fgets(str, 128, stream))
						break;
					truncsp(str);
				}
			}
			else if (type == IMPORT_LIST_TYPE_QWK_CONTROL_DAT) {
				if (read_qwk_confs >= total_qwk_confs)
					break;
				read_qwk_confs++;
				qwk_confnum = atoi(str);
				if (!fgets(str, 128, stream))
					break;
				if (qwk_confnum < min_confnum || qwk_confnum > max_confnum)
					continue;
				truncsp(str);
				char* p = str;
				SKIP_WHITESPACE(p);
				if (*p == 0)
					continue;
				if (strlen(p) > grpname_len && strnicmp(p, cfg.grp[grpnum]->sname, grpname_len) == 0)
					p += grpname_len;
				SKIP_WHITESPACE(p);
				SAFECOPY(tmp_code, p);
				SAFECOPY(tmpsub.lname, p);
				SAFECOPY(tmpsub.sname, p);
				SAFECOPY(tmpsub.qwkname, p);
			}
			else if (type == IMPORT_LIST_TYPE_NEWSGROUPS) {
				char* p = str;
				SKIP_WHITESPACE(p);
				if (*p == 0)
					continue;
				char* tp = p + 1;
				FIND_WHITESPACE(tp);
				*tp = 0;
				tp++;
				SKIP_WHITESPACE(tp);
				SAFECOPY(tmpsub.newsgroup, p);
				if (strlen(p) > grpname_len && strnicmp(p, cfg.grp[grpnum]->sname, grpname_len) == 0)
					p += grpname_len;
				SKIP_CHAR(p, '.');
				SAFECOPY(tmp_code, p);
				SAFECOPY(tmpsub.lname, tp);
				SAFECOPY(tmpsub.sname, p);
				SAFECOPY(tmpsub.qwkname, p);
			}
			else {
				char* p = str;
				SKIP_WHITESPACE(p);
				if (!*p || !isalnum(*p))
					continue;
				if (type == IMPORT_LIST_TYPE_GENERIC_AREAS_BBS) {        /* AREAS.BBS Generic/.MSG */
					p = str;
					SKIP_WHITESPACE(p);         /* Find path	*/
					FIND_WHITESPACE(p);         /* Skip path	*/
					SKIP_WHITESPACE(p);         /* Find tag		*/
					truncstr(p, " \t");          /* Truncate tag */
					SAFECOPY(tmp_code, p);       /* Copy tag to internal code */
					SAFECOPY(tmpsub.lname, utos(p));
					SAFECOPY(tmpsub.sname, tmpsub.lname);
					if (strlen(p) > sizeof(tmpsub.area_tag) - 1)
						SAFECOPY(tmpsub.newsgroup, p);
					else
						SAFECOPY(tmpsub.area_tag, p);
				}
				else if (type == IMPORT_LIST_TYPE_SBBSECHO_AREAS_BBS) { /* AREAS.BBS SBBSecho */
					p = str;
					SKIP_WHITESPACE(p);         /* Find internal code */
					char* tp = p;
					FIND_WHITESPACE(tp);
					*tp = 0;                      /* Truncate internal code */
					SAFECOPY(tmp_code, p);       /* Copy internal code suffix */
					p = tp + 1;
					SKIP_WHITESPACE(p);         /* Find echo tag */
					truncstr(p, " \t");          /* Truncate tag */
					SAFECOPY(tmpsub.lname, utos(p));
					SAFECOPY(tmpsub.sname, tmpsub.lname);
					if (strlen(p) > sizeof(tmpsub.area_tag) - 1)
						SAFECOPY(tmpsub.newsgroup, p);
					else
						SAFECOPY(tmpsub.area_tag, p);
				}
				else if (type == IMPORT_LIST_TYPE_BACKBONE_NA) { /* BACKBONE.NA */
					p = str;
					SKIP_WHITESPACE(p);         /* Find echo tag */
					char* tp = p;
					FIND_WHITESPACE(tp);        /* Find end of tag */
					*tp = 0;                      /* Truncate echo tag */
					SAFECOPY(tmp_code, p);       /* Copy tag to internal code suffix */
					SAFECOPY(tmpsub.sname, utos(p)); /* ... to short name, converting underscores to spaces */
					if (strlen(p) > sizeof(tmpsub.area_tag) - 1)
						SAFECOPY(tmpsub.newsgroup, p);
					else
						SAFECOPY(tmpsub.area_tag, p);
					p = tp + 1;
					SKIP_WHITESPACE(p);         /* Find description */
					SAFECOPY(tmpsub.lname, p);   /* Copy description to long name */
				}
			}
		}
		SAFECOPY(tmpsub.code_suffix, prep_code(tmp_code, cfg.grp[grpnum]->code_prefix));
		truncsp(tmpsub.sname);
		truncsp(tmpsub.lname);
		truncsp(tmpsub.qwkname);
		if (tmpsub.qwkname[0] == 0) {
			SAFECOPY(tmpsub.qwkname, tmpsub.sname);
		}

		if (tmpsub.code_suffix[0] == 0 || tmpsub.sname[0] == 0)
			continue;

		if (tmpsub.lname[0] == 0) {
			if (tmpsub.newsgroup[0])
				SAFECOPY(tmpsub.lname, tmpsub.newsgroup);
			else if (tmpsub.area_tag[0])
				SAFECOPY(tmpsub.lname, tmpsub.area_tag);
			else
				SAFECOPY(tmpsub.lname, tmpsub.sname);
		}

		int j;
		int attempts = 0;   // attempts to generate a unique internal code
		if (stricmp(tmpsub.code_suffix, duplicate_code) == 0)
			attempts = ++duplicate_codes;
		else
			duplicate_codes = 0;
		for (j = 0; j < cfg.total_subs && attempts < (36 * 36 * 36); j++) {
			/* same group and duplicate name/echotag or QWK confname? overwrite the sub entry */
			if (cfg.sub[j]->grp == grpnum) {
				if (cfg.sub[j]->newsgroup[0] || tmpsub.newsgroup[0]) {
					if (stricmp(cfg.sub[j]->newsgroup, tmpsub.newsgroup) == 0)
						break;
				} else if (cfg.sub[j]->area_tag[0] || tmpsub.area_tag[0]) {
					if (stricmp(cfg.sub[j]->area_tag, tmpsub.area_tag) == 0)
						break;
				} else {
					if (stricmp(cfg.sub[j]->sname, tmpsub.sname) == 0)
						break;
				}
			} else {
				if ((cfg.grp[grpnum]->code_prefix[0] || cfg.grp[cfg.sub[j]->grp]->code_prefix[0]))
					continue;
			}
			if (stricmp(cfg.sub[j]->code_suffix, tmpsub.code_suffix) == 0) {
				if (type == IMPORT_LIST_TYPE_SUBS_TXT)   /* subs.txt import (don't modify internal code) */
					break;
				if (attempts == 0)
					SAFECOPY(duplicate_code, tmpsub.code_suffix);
				int code_len = strlen(tmpsub.code_suffix);
				if (code_len < 1)
					break;
				tmpsub.code_suffix[code_len - 1] = random_code_char();
				if (attempts > 10 && code_len > 1)
					tmpsub.code_suffix[code_len - 2] = random_code_char();
				if (attempts > 100 && code_len > 2)
					tmpsub.code_suffix[code_len - 3] = random_code_char();
				j = 0;
				attempts++;
			}
		}
		if (attempts >= (36 * 36 * 36)) {
			ported = -2;
			break;
		}

		if (j == cfg.total_subs) {
			if (!new_sub(j, grpnum, /* pasted_sub: */ NULL, new_sub_misc)) {
				ported = -3;
				break;
			}
			if (added != NULL)
				(*added)++;
		}
		if (type == IMPORT_LIST_TYPE_SUBS_TXT || type == IMPORT_LIST_TYPE_SUBS_INI) {
			/* Complete-detail formats: apply the fields the format carries,     */
			/* leaving any others intact.  ptridx is deliberately not copied so  */
			/* the local pointer-file linkage is preserved (the exported ptridx  */
			/* is meaningless on the importing system).  The legacy subs.txt     */
			/* format can't express qwkconf/print-modes/area_tag, so a re-import */
			/* of that format leaves those existing settings untouched.          */
			sub_t* sub = cfg.sub[j];
			sub->grp = grpnum;
			SAFECOPY(sub->code_suffix, tmpsub.code_suffix);
			SAFECOPY(sub->lname, tmpsub.lname);
			SAFECOPY(sub->sname, tmpsub.sname);
			SAFECOPY(sub->qwkname, tmpsub.qwkname);
			SAFECOPY(sub->data_dir, tmpsub.data_dir);
			SAFECOPY(sub->arstr, tmpsub.arstr);
			SAFECOPY(sub->read_arstr, tmpsub.read_arstr);
			SAFECOPY(sub->post_arstr, tmpsub.post_arstr);
			SAFECOPY(sub->op_arstr, tmpsub.op_arstr);
			SAFECOPY(sub->mod_arstr, tmpsub.mod_arstr);
			sub->misc = tmpsub.misc;
			SAFECOPY(sub->tagline, tmpsub.tagline);
			SAFECOPY(sub->origline, tmpsub.origline);
			SAFECOPY(sub->post_sem, tmpsub.post_sem);
			SAFECOPY(sub->newsgroup, tmpsub.newsgroup);
			sub->faddr = tmpsub.faddr;
			sub->maxmsgs = tmpsub.maxmsgs;
			sub->maxcrcs = tmpsub.maxcrcs;
			sub->maxage = tmpsub.maxage;
			if (type == IMPORT_LIST_TYPE_SUBS_INI) {
				/* Fields the .ini format carries that subs.txt does not */
				SAFECOPY(sub->area_tag, tmpsub.area_tag);
				sub->qwkconf = tmpsub.qwkconf;
				sub->pmode = tmpsub.pmode;
				sub->n_pmode = tmpsub.n_pmode;
			}
		} else {
			cfg.sub[j]->grp = grpnum;
			SAFECOPY(cfg.sub[j]->code_suffix, tmpsub.code_suffix);
			SAFECOPY(cfg.sub[j]->sname, tmpsub.sname);
			SAFECOPY(cfg.sub[j]->lname, tmpsub.lname);
			SAFECOPY(cfg.sub[j]->newsgroup, tmpsub.newsgroup);
			SAFECOPY(cfg.sub[j]->qwkname, tmpsub.qwkname);
			SAFECOPY(cfg.sub[j]->area_tag, tmpsub.area_tag);
			if (tmpsub.data_dir[0])
				SAFECOPY(cfg.sub[j]->data_dir, tmpsub.data_dir);
			if (strcasestr(tmpsub.lname, "sysop") != NULL && strcasestr(tmpsub.lname, "only") != NULL) {
				if (cfg.sub[j]->arstr[0]) {
					SAFECAT(cfg.sub[j]->arstr, " ");
				}
				SAFECAT(cfg.sub[j]->arstr, "SYSOP");
			}
		}
		if (faddr != NULL && faddr->zone)
			cfg.sub[j]->faddr = *faddr;
		if (qhub != NULL)
			new_qhub_sub(qhub, qhub->subs, cfg.sub[j], qwk_confnum);
		ported++;
	}
	if (ported > 0 && cfg.grp[grpnum]->sort)
		sort_subs(grpnum);

	strListFree(&ini);
	strListFree(&list);

	return ported;
}

int subs_in_group(int grpnum)
{
	int total = 0;

	for (int u = 0; u < cfg.total_subs; u++)
		if (cfg.sub[u]->grp == grpnum)
			total++;

	return total;
}

void msgs_cfg()
{
	static int   dflt, msgs_dflt, bar;
	static int   import_list_type;
	static int   export_list_type;
	char         str[256], str2[256], done = 0;
	char         tmp[128];
	int          j, k;
	int          i, file;
	long         ported;
	static grp_t savgrp;
	FILE*        stream;

	char*        grp_short_name_help =
		"`Group Short Name:`\n"
		"\n"
		"This is a short description of the message group which is used for the\n"
		"main menu and reading message prompts.\n"
	;
	char* grp_long_name_help =
		"`Group Long Name:`\n"
		"\n"
		"This is a description of the message group which is displayed when a\n"
		"user of the system uses the `/*` command from the main menu.\n"
	;
	char* grp_code_prefix_help =
		"`Internal Code Prefix:`\n"
		"\n"
		"This is an `optional`, but highly recommended `code prefix` used to help\n"
		"generate `unique` internal codes for the sub-boards in this message group.\n"
		"When code prefixes are used, sub-board `internal codes` will be\n"
		"constructed from a combination of the prefix and the specified code \n"
		"suffix for each sub-board.\n"
		"\n"
		"Code prefixes may contain up to 16 legal filename characters.\n"
		"\n"
		"Code prefixes should be unique among the message groups on the system.\n"
		"\n"
		"Changing a group's code prefix implicitly changes all the internal codes\n"
		"of the sub-boards within the group, so change this value with caution.\n"
	;

	while (1) {
		for (i = 0; i < cfg.total_grps && i < MAX_OPTS; i++)
			snprintf(opt[i], MAX_OPLN, "%-*s %5u", LEN_GLNAME, cfg.grp[i]->lname, subs_in_group(i));
		opt[i][0] = 0;
		int mode = WIN_ORG | WIN_ACT | WIN_CHE;
		if (cfg.total_grps)
			mode |= WIN_DEL | WIN_DELACT | WIN_COPY | WIN_CUT;
		if (cfg.total_grps < MAX_OPTS)
			mode |= WIN_INS | WIN_INSACT | WIN_XTR;
		if (savgrp.sname[0])
			mode |= WIN_PASTE | WIN_PASTEXTR;
		uifc.helpbuf =
			"`Message Groups:`\n"
			"\n"
			"This is a listing of message groups for your BBS. Message groups are\n"
			"used to logically separate your message `sub-boards` into groups. Every\n"
			"sub-board belongs to a message group. You must have at least one message\n"
			"group and one sub-board configured.\n"
			"\n"
			"One popular use for message groups is to separate local sub-boards and\n"
			"networked sub-boards. One might have a `Local` message group that contains\n"
			"non-networked sub-boards of various topics and also have a `FidoNet`\n"
			"message group that contains sub-boards that are echoed across FidoNet.\n"
			"\n"
			"Some sysops separate sub-boards into more specific areas such as `Main`,\n"
			"`Technical`, or `Adult`. If you have many sub-boards that have a common\n"
			"subject denominator, you may want to have a separate message group for\n"
			"those sub-boards for a more organized message structure.\n"
		;
		i = uifc.list(mode, 0, 0, 0, &msgs_dflt, &bar, "Message Groups                                          Sub-boards", opt);
		if (i == -1) {
			j = save_changes(WIN_MID);
			if (j == -1)
				continue;
			if (!j) {
				save_msgs_cfg(&cfg);
				refresh_cfg(&cfg);
			}
			return;
		}
		int grpnum = i & MSK_OFF;
		int msk = i & MSK_ON;
		if (msk == MSK_INS) {
			char long_name[LEN_GLNAME + 1];
			uifc.helpbuf = grp_long_name_help;
			if (uifc.input(WIN_MID | WIN_SAV, 0, 0, "Group Long Name", long_name, sizeof(long_name) - 1, K_FIND) < 1)
				continue;

			char short_name[LEN_GSNAME + 1];
			uifc.helpbuf = grp_short_name_help;
			SAFECOPY(short_name, long_name);
			if (uifc.input(WIN_MID | WIN_SAV, 0, 0, "Group Short Name", short_name, sizeof(short_name) - 1, K_EDIT | K_FIND) < 1)
				continue;
			if (grpnum_is_valid(&cfg, getgrpnum_from_name(&cfg, short_name))) {
				uifc.msg(strDuplicateGrpName);
				continue;
			}
			char code_prefix[LEN_EXTCODE + 1];    /* purposely extra-long */
			SAFECOPY(code_prefix, short_name);
			prep_code(code_prefix, NULL);
			if (strlen(code_prefix) < LEN_CODE)
				strcat(code_prefix, "_");
			uifc.helpbuf = grp_code_prefix_help;
			if (uifc.input(WIN_MID | WIN_SAV, 0, 0, "Internal Code Prefix", code_prefix, LEN_CODE, K_EDIT | K_UPPER | K_NOSPACE | K_FIND) < 0)
				continue;
			if (code_prefix_exists(code_prefix)) {
				uifc.msg(strDuplicateCodePrefix);
				continue;
			}
			if (code_prefix[0] != 0 && !code_ok(code_prefix)) {
				uifc.helpbuf = invalid_code;
				uifc.msg(strInvalidCodePrefix);
				continue;
			}
			if (!new_grp(grpnum))
				continue;
			SAFECOPY(cfg.grp[grpnum]->lname, long_name);
			SAFECOPY(cfg.grp[grpnum]->sname, short_name);
			SAFECOPY(cfg.grp[grpnum]->code_prefix, code_prefix);
			uifc.changes = TRUE;
			continue;
		}
		if (msk == MSK_DEL || msk == MSK_CUT) {
			if (msk == MSK_DEL) {
				uifc.helpbuf =
					"`Delete All Data in Group:`\n"
					"\n"
					"If you wish to delete the messages in all the sub-boards in this group,\n"
					"select `Yes`.\n"
				;
				j = 1;
				j = uifc.list(WIN_MID | WIN_SAV, 0, 0, 0, &j, 0, "Delete All Data in Group", uifcYesNoOpts);
				if (j == -1)
					continue;
				uifc.pop("Deleting Data Files...");
				if (j == 0) {
					for (j = 0; j < cfg.total_subs; j++) {
						if (cfg.sub[j]->grp == grpnum) {
							SAFEPRINTF2(str, "%s%s.*"
							            , cfg.grp[cfg.sub[j]->grp]->code_prefix
							            , cfg.sub[j]->code_suffix);
							strlwr(str);
							if (!cfg.sub[j]->data_dir[0])
								SAFEPRINTF(tmp, "%ssubs/", cfg.data_dir);
							else
								SAFECOPY(tmp, cfg.sub[j]->data_dir);
							delfiles(tmp, str, /* keep: */ 0);
						}
					}
				}
				uifc.pop(NULL);
			}
			if (msk == MSK_CUT)
				savgrp = *cfg.grp[grpnum];
			free(cfg.grp[grpnum]);
			if (msk == MSK_DEL) {
				for (j = 0; j < cfg.total_subs;) {
					if (cfg.sub[j]->grp == grpnum) {    /* delete subs of this group */
						remove_sub(&cfg, j, /* cut: */ false);
					}
					else
						j++;
				}
				for (j = 0; j < cfg.total_subs; j++)    /* move sub group numbers down */
					if (cfg.sub[j]->grp > grpnum)
						cfg.sub[j]->grp--;
			}
			else { /* CUT */
				for (j = 0; j < cfg.total_subs; j++) {
					if (cfg.sub[j]->grp > grpnum)
						cfg.sub[j]->grp--;
					else if (cfg.sub[j]->grp == grpnum)
						cfg.sub[j]->grp = CUT_GROUPNUM;
				}
			}
			cfg.total_grps--;
			for (i = grpnum; i < cfg.total_grps; i++)
				cfg.grp[i] = cfg.grp[i + 1];
			uifc.changes = TRUE;
			continue;
		}
		if (msk == MSK_COPY) {
			savgrp = *cfg.grp[grpnum];
			continue;
		}
		if (msk == MSK_PASTE) {
			if (!new_grp(grpnum))
				continue;
			/* Restore previously cut sub-boards to newly-pasted group */
			for (i = 0; i < cfg.total_subs; i++)
				if (cfg.sub[i]->grp == CUT_GROUPNUM)
					cfg.sub[i]->grp = grpnum;
			*cfg.grp[grpnum] = savgrp;
			uifc.changes = TRUE;
			continue;
		}
		if (msk != 0)
			continue;
		done = 0;
		while (!done) {
			j = 0;
			snprintf(opt[j++], MAX_OPLN, "%-27.27s%s", "Long Name", cfg.grp[grpnum]->lname);
			snprintf(opt[j++], MAX_OPLN, "%-27.27s%s", "Short Name", cfg.grp[grpnum]->sname);
			snprintf(opt[j++], MAX_OPLN, "%-27.27s%s", "Internal Code Prefix", cfg.grp[grpnum]->code_prefix);
			snprintf(opt[j++], MAX_OPLN, "%-27.27s%s", "Access Requirements"
			         , cfg.grp[grpnum]->arstr);
			snprintf(opt[j++], MAX_OPLN, "%-27.27s%s", "Sort Group by Sub-board", area_sort_desc[cfg.grp[grpnum]->sort]);
			strcpy(opt[j++], "Clone Options");
			strcpy(opt[j++], "Export Areas...");
			strcpy(opt[j++], "Import Areas...");
			strcpy(opt[j++], "Message Sub-boards...");
			opt[j][0] = 0;
			sprintf(str, "%s Group", cfg.grp[grpnum]->sname);
			uifc.helpbuf =
				"`Message Group Configuration:`\n"
				"\n"
				"This menu allows you to configure the security requirements for access\n"
				"to this message group. You can also add, delete, and configure the\n"
				"sub-boards of this group by selecting the `Messages Sub-boards...` option.\n"
				"\n"
				"The left and right arrow keys may be used to cycle through message\n"
				"groups.\n"
			;
			uifc_winmode_t wmode = WIN_ACT | WIN_T2B | WIN_EXTKEYS;
			if (grpnum > 0)
				wmode |= WIN_LEFTKEY;
			if (grpnum + 1 < cfg.total_grps)
				wmode |= WIN_RIGHTKEY;
			switch (uifc.list(wmode, 6, 0, 60, &dflt, 0, str, opt)) {
				case -1:
					done = 1;
					break;
				case -CIO_KEY_LEFT - 2:
					if (grpnum > 0)
						grpnum--;
					break;
				case -CIO_KEY_RIGHT - 2:
					if (grpnum + 1 < cfg.total_grps)
						grpnum++;
					break;
				case __COUNTER__:
					uifc.helpbuf = grp_long_name_help;
					SAFECOPY(str, cfg.grp[grpnum]->lname);
					if (uifc.input(WIN_MID | WIN_SAV, 0, 17, "Name to use for Listings"
					               , str, LEN_GLNAME, K_EDIT) > 0)
						SAFECOPY(cfg.grp[grpnum]->lname, str);
					break;
				case __COUNTER__:
					uifc.helpbuf = grp_short_name_help;
					SAFECOPY(str, cfg.grp[grpnum]->sname);
					if (uifc.input(WIN_MID | WIN_SAV, 0, 17, "Name to use for Prompts"
					               , str, LEN_GSNAME, K_EDIT | K_CHANGED | K_FIND) > 0) {
						if (grpnum_is_valid(&cfg, getgrpnum_from_name(&cfg, str)))
							uifc.msg(strDuplicateGrpName);
						else {
							SAFECOPY(cfg.grp[grpnum]->sname, str);
							uifc.changes = TRUE;
						}
					}
					break;
				case __COUNTER__:
				{
					char code_prefix[LEN_CODE + 1];
					SAFECOPY(code_prefix, cfg.grp[grpnum]->code_prefix);
					uifc.helpbuf = grp_code_prefix_help;
					if (uifc.input(WIN_MID | WIN_SAV, 0, 17, "Internal Code Prefix"
					               , code_prefix, LEN_CODE, K_EDIT | K_UPPER | K_NOSPACE) < 0)
						continue;
					if (stricmp(code_prefix, cfg.grp[grpnum]->code_prefix) == 0)
						break;
					if (code_prefix_exists(code_prefix))
						uifc.msg(strDuplicateCodePrefix);
					else if (code_prefix[0] == 0 || code_ok(code_prefix)) {
						SAFECOPY(cfg.grp[grpnum]->code_prefix, code_prefix);
						for (j = 0; j < cfg.total_subs; j++) {
							if (cfg.sub[j]->grp == grpnum)
								cfg.sub[j]->cfg_modified = true;
						}
					} else {
						uifc.helpbuf = invalid_code;
						uifc.msg(strInvalidCodePrefix);
					}
					break;
				}
				case __COUNTER__:
					SAFEPRINTF(str, "%s Group", cfg.grp[grpnum]->sname);
					getar(str, cfg.grp[grpnum]->arstr, /* helpbuf: */ NULL);
					break;
				case __COUNTER__:
					uifc.helpbuf = "`Sort Group By Sub-board:`\n"
					               "\n"
					               "Normally, the sub-boards within a message group are listed in the order\n"
					               "that the sysop created them or a logical order determined by the sysop.\n"
					               "\n"
					               "Optionally, you can have the group sorted by sub-board `Long Name`,\n"
					               "`Short Name`, or `Internal Code`.\n"
					               "\n"
					               "The group will be automatically re-sorted when new sub-boards\n"
					               "are added via `SCFG` or when existing sub-boards are modified.\n"
					;
					j = cfg.grp[grpnum]->sort;
					j = uifc.list(WIN_MID | WIN_SAV, 0, 0, 0, &j, 0, "Sort Group by Sub-board", area_sort_desc);
					if (j >= 0) {
						cfg.grp[grpnum]->sort = j;
						sort_subs(grpnum);
						uifc.changes = TRUE;
					}
					break;
				case __COUNTER__:   /* Clone Options */
					j = 0;
					uifc.helpbuf =
						"`Clone Sub-board Options:`\n"
						"\n"
						"If you want to clone the options of the template sub-board of this group\n"
						"into all sub-boards of this group, select `Yes`.\n"
						"\n"
						"If no template sub-board is found, then the first sub-board in the group\n"
						"is used as the default template.  A sub-board is marked as the template\n"
						"for the group in its `Toggle Options` menu.\n"
						"\n"
						"The options cloned are: posting requirements, reading requirements,\n"
						"operator requirements, moderated user requirements, toggle options,\n"
						"network options (including EchoMail origin line, EchoMail address,\n"
						"and QWK Network tagline), maximum number of messages, maximum number\n"
						"of CRCs, maximum age of messages, storage method, and data directory.\n"
					;
					j = uifc.list(WIN_MID | WIN_SAV, 0, 0, 0, &j, 0
					              , "Clone Options of Template Sub-board into All of Group", uifcYesNoOpts);
					if (j == 0) {
						sub_t* template = NULL;
						for (j = 0; j < cfg.total_subs; j++) {
							if (cfg.sub[j]->grp == grpnum && cfg.sub[j]->misc & SUB_TEMPLATE) {
								template = cfg.sub[j];
								break;
							}
						}
						for (j = 0; j < cfg.total_subs; j++)
							if (cfg.sub[j]->grp == grpnum) {
								if (template == NULL)
									template = cfg.sub[j];
								else if (cfg.sub[j] != template) {
									uifc.changes = TRUE;
									cfg.sub[j]->misc = template->misc;
									cfg.sub[j]->misc &= ~SUB_TEMPLATE;
									SAFECOPY(cfg.sub[j]->post_arstr, template->post_arstr);
									SAFECOPY(cfg.sub[j]->read_arstr, template->read_arstr);
									SAFECOPY(cfg.sub[j]->op_arstr, template->op_arstr);
									SAFECOPY(cfg.sub[j]->mod_arstr, template->mod_arstr);
									SAFECOPY(cfg.sub[j]->origline, template->origline);
									SAFECOPY(cfg.sub[j]->tagline, template->tagline);
									SAFECOPY(cfg.sub[j]->data_dir, template->data_dir);
									SAFECOPY(cfg.sub[j]->post_sem, template->post_sem);
									cfg.sub[j]->maxmsgs = template->maxmsgs;
									cfg.sub[j]->maxcrcs = template->maxcrcs;
									cfg.sub[j]->maxage  = template->maxage;
									cfg.sub[j]->pmode   = template->pmode;
									cfg.sub[j]->n_pmode = template->n_pmode;
									cfg.sub[j]->faddr   = template->faddr;
									cfg.sub[j]->cfg_modified = true;
								}
							}
					}
					break;
				case __COUNTER__:
					k = 0;
					ported = 0;
					strcpy(opt[k++], "areas.bbs      SBBSecho Area File");
					strcpy(opt[k++], "areas.ini      SBBSecho Area File");
					strcpy(opt[k++], "*subs.ini      Synchronet Shared Config");
					strcpy(opt[k++], "subs.txt       Synchronet Sub-boards");
					strcpy(opt[k++], "backbone.na    FidoNet EchoList");
					strcpy(opt[k++], "newsgroup.lst  USENET Newsgroup List");
					opt[k][0] = 0;
					uifc.helpbuf =
						"`Export Area File Format:`\n"
						"\n"
						"This menu allows you to choose the format of the area file you wish to\n"
						"export the current message group into.\n"
						"\n"
						"The supported message area list file formats to be exported are:\n"
						"\n"
						"`areas.bbs`\n"
						"  Area File as used by the Synchronet Fido EchoMail program, `SBBSecho`.\n"
						"\n"
						"`areas.ini`\n"
						"  Area File as used by the Synchronet Fido EchoMail program, `SBBSecho`\n"
						"  (optionally, as of SBBSecho v3.23).\n"
						"\n"
						"`*subs.ini`\n"
						"  File format supported by Synchronet v3.22 and later for sharing\n"
						"  sub-board configuration data between Synchronet BBSes.\n"
						"\n"
						"`subs.txt`\n"
						"  Complete details of a group of `Synchronet sub-boards` in the legacy\n"
						"  line-oriented text format (superseded by `*subs.ini`).\n"
						"\n"
						"`backbone.na` (also `fidonet.na` and `badareas.lst`)\n"
						"  FidoNet standard EchoList containing standardized echo `Area Tags`\n"
						"  and (optional) descriptions.\n"
						"\n"
						"`newsgroup.lst`\n"
						"  Standard (RFC3977) NNTP `LIST NEWSGROUPS` output format:\n"
						"  Newsgroup names and (optional) descriptions, one line per newsgroup."
					;
					k = uifc.list(WIN_MID | WIN_SAV, 0, 0, 0, &export_list_type, 0
					              , "Export Area File Format", opt);
					if (k == -1)
						break;
					if (k == 0)
						sprintf(str, "%sareas.bbs", cfg.data_dir);
					else if (k == 1)
						sprintf(str, "%sareas.ini", cfg.data_dir);
					else if (k == 2) {
						if (cfg.grp[grpnum]->code_prefix[0] == '\0') {
							snprintf(str, sizeof str, "%s_subs.ini", cfg.grp[grpnum]->sname);
							replace_chars(str, ' ', '_');
						} else
							snprintf(str, sizeof str, "%ssubs.ini", cfg.grp[grpnum]->code_prefix);
					}
					else if (k == 3)
						sprintf(str, "%ssubs.txt", cfg.ctrl_dir);
					else if (k == 4)
						sprintf(str, "backbone.na");
					else if (k == 5)
						sprintf(str, "newsgroup.lst");
					if (k == 0 || k == 1) {
						uifc.helpbuf =
							"`Links:`\n"
							"\n"
							"Enter a space-separated list of FidoNet node addresses that the exported\n"
							"areas should be linked with.\n"
							"\n"
							"At the least, your uplink (hub) address should be included in the list."
						;
						if (uifc.input(WIN_MID | WIN_SAV, 0, 0, "Links"
						               , str2, sizeof(str2) - 1, K_FIND) <= 0) {
							break;
						}
					}
					uifc.helpbuf =
						"`Filename:`\n"
						"\n"
						"Enter the path/filename of the file you wish to export to.\n"
					;
					if (uifc.input(WIN_MID | WIN_SAV, 0, 0, "Filename"
					               , str, sizeof(str) - 1, K_EDIT | K_FIND) <= 0) {
						break;
					}
					if (getdircase(str)) {
						uifc.msgf("Directory exists: %s", str);
						break;
					}
					if (fexistcase(str)) {
						snprintf(tmp, sizeof tmp, "File exists: %s", str);
						strcpy(opt[0], "Overwrite");
						strcpy(opt[1], "Append");
						opt[2][0] = 0;
						j = 0;
						j = uifc.list(WIN_MID | WIN_SAV, 0, 0, 0, &j, 0, tmp, opt);
						if (j == -1)
							break;
						if (j == 0)
							j = O_WRONLY | O_TRUNC;
						else
							j = O_WRONLY | O_APPEND;
					}
					else
						j = O_WRONLY | O_CREAT;
					if ((stream = fnopen(&file, str, j | O_TEXT)) == NULL) {
						uifc.msgf("Error %d opening %s", errno, str);
						break;
					}
					uifc.pop("Exporting Areas...");
					for (j = 0; j < cfg.total_subs; j++) {
						if (cfg.sub[j]->grp != grpnum)
							continue;
						ported++;
						if (k == 0) {      /* AREAS.BBS SBBSecho */
							char extcode[LEN_EXTCODE + 1];
							SAFEPRINTF2(extcode, "%s%s"
							            , cfg.grp[cfg.sub[j]->grp]->code_prefix
							            , cfg.sub[j]->code_suffix);

							fprintf(stream, "%-*s %-*s %s\n"
							        , LEN_EXTCODE, extcode
							        , FIDO_AREATAG_LEN
							        , sub_area_tag(&cfg, cfg.sub[j], str, sizeof(str))
							        , str2);
							continue;
						}
						if (k == 1) {      /* areas.ini */
							fprintf(stream, "[%s]\n"
							        , sub_area_tag(&cfg, cfg.sub[j], str, sizeof(str)));
							fprintf(stream, "sub = %s%s\n"
							        , cfg.grp[cfg.sub[j]->grp]->code_prefix
							        , cfg.sub[j]->code_suffix);
							fprintf(stream, "links = %s\n", str2);
							fprintf(stream, "\n");
							continue;
						}
						if (k == 2) {      /* subs.ini */
							str_list_t section = sub_ini_section(&cfg, cfg.sub[j], cfg.sub[j]->code_suffix);
							strListWriteFile(stream, section, "\n");
							free(section);
							continue;
						}
						if (k == 3) {      /* subs.txt */
							fprintf(stream, "%s\n%s\n%s\n%s\n%s\n%s\n"
							        "%s\n%s\n%s\n"
							        , cfg.sub[j]->lname
							        , cfg.sub[j]->sname
							        , cfg.sub[j]->qwkname
							        , cfg.sub[j]->code_suffix
							        , cfg.sub[j]->data_dir
							        , cfg.sub[j]->arstr
							        , cfg.sub[j]->read_arstr
							        , cfg.sub[j]->post_arstr
							        , cfg.sub[j]->op_arstr
							        );
							fprintf(stream, "%" PRIX32 "\n%s\n%s\n%s\n%s\n%s\n"
							        , cfg.sub[j]->misc
							        , cfg.sub[j]->tagline
							        , cfg.sub[j]->origline
							        , cfg.sub[j]->post_sem
							        , cfg.sub[j]->newsgroup
							        , smb_faddrtoa(&cfg.sub[j]->faddr, tmp)
							        );
							fprintf(stream, "%" PRIu32 "\n%" PRIu32 "\n%u\n%u\n%s\n"
							        , cfg.sub[j]->maxmsgs
							        , cfg.sub[j]->maxcrcs
							        , cfg.sub[j]->maxage
							        , cfg.sub[j]->ptridx
							        , cfg.sub[j]->mod_arstr
							        );
							fprintf(stream, "***END-OF-SUB***\n\n");
							continue;
						}
						if (k == 4) {      /* BACKBONE.NA */
							fprintf(stream, "%-*s %s\n"
							        , FIDO_AREATAG_LEN
							        , sub_area_tag(&cfg, cfg.sub[j], str, sizeof(str))
							        , cfg.sub[j]->lname);
							continue;
						}
						if (k == 5) {      /* newsgroup.lst */
							fprintf(stream, "%s %s\n"
							        , sub_newsgroup_name(&cfg, cfg.sub[j], str, sizeof(str))
							        , cfg.sub[j]->lname);
							continue;
						}
					}
					fclose(stream);
					uifc.pop(0);
					sprintf(str, "%lu Message Areas Exported Successfully", ported);
					uifc.msg(str);
					break;
				case __COUNTER__:
					ported = 0;
					k = 0;
					strcpy(opt[k++], "control.dat     QWK Conference List");
					strcpy(opt[k++], "areas.bbs       Generic Area File");
					strcpy(opt[k++], "areas.bbs       SBBSecho Area File");
					strcpy(opt[k++], "*subs.ini       Synchronet Shared Config");
					strcpy(opt[k++], "subs.txt        Synchronet Sub-boards");
					strcpy(opt[k++], "backbone.na     FidoNet EchoList");
					strcpy(opt[k++], "badareas.lst    SBBSecho Bad Area List");
					strcpy(opt[k++], "echostats.ini   SBBSecho EchoMail Statistics");
					strcpy(opt[k++], "newsgroup.lst   USENET Newsgroup List");
					opt[k][0] = 0;
					uifc.helpbuf =
						"`Import Area File Format:`\n"
						"\n"
						"This menu allows you to choose the format of the area file you wish to\n"
						"import into the current message group.\n"
						"\n"
						"The supported message area list file formats to be imported are:\n"
						"\n"
						"`control.dat`\n"
						"  Standard file contained within QWK packets (typically ZIP archives).\n"
						"\n"
						"`areas.bbs`\n"
						"  FidoNet EchoMail Area File, in either `Generic` or `SBBSecho` flavors,\n"
						"  as used by most FidoNet EchoMail Programs or SBBSecho.\n"
						"\n"
						"`*subs.ini`\n"
						"  Complete details of a group of sub-boards as exported from `SCFG`.\n"
						"\n"
						"`subs.txt`\n"
						"  Complete details of a group of sub-boards as exported from `SCFG`, in\n"
						"  the legacy line-oriented text format (superseded by `*subs.ini`).\n"
						"\n"
						"`backbone.na` (also `fidonet.na` and `badareas.lst`)\n"
						"  FidoNet standard EchoList containing standardized echo `Area Tags`\n"
						"  and optional `Titles` (descriptions).\n"
						"\n"
						"`echostats.ini`\n"
						"  SBBSecho cumulative EchoMail statistics (imports `Unknown Areas` only).\n"
						"\n"
						"`newsgroup.lst`\n"
						"  Standard (RFC3977) NNTP `LIST NEWSGROUPS` output format:\n"
						"  Newsgroup names and (optional) descriptions, one line per newsgroup."
					;
					k = uifc.list(WIN_MID | WIN_SAV, 0, 0, 0, &import_list_type, 0
					              , "Import Area File Format", opt);
					if (k < 0)
						break;
					char filename[MAX_PATH + 1];
					switch (k) {
						case IMPORT_LIST_TYPE_QWK_CONTROL_DAT:
							snprintf(filename, sizeof filename, "control.dat");
							break;
						case IMPORT_LIST_TYPE_GENERIC_AREAS_BBS:
							snprintf(filename, sizeof filename, "areas.bbs");
							break;
						case IMPORT_LIST_TYPE_SBBSECHO_AREAS_BBS:
							snprintf(filename, sizeof filename, "%sareas.bbs", cfg.data_dir);
							break;
						case IMPORT_LIST_TYPE_SUBS_INI:
							if (cfg.grp[grpnum]->code_prefix[0] == '\0') {
								snprintf(filename, sizeof filename, "%s_subs.ini", cfg.grp[grpnum]->sname);
								replace_chars(filename, ' ', '_');
							} else
								snprintf(filename, sizeof filename, "%ssubs.ini", cfg.grp[grpnum]->code_prefix);
							break;
						case IMPORT_LIST_TYPE_SUBS_TXT:
							snprintf(filename, sizeof filename, "%ssubs.txt", cfg.ctrl_dir);
							break;
						case IMPORT_LIST_TYPE_BACKBONE_NA:
							snprintf(filename, sizeof filename, "backbone.na");
							break;
						case IMPORT_LIST_TYPE_NEWSGROUPS:
							snprintf(filename, sizeof filename, "newsgroup.lst");
							break;
						case IMPORT_LIST_TYPE_ECHOSTATS:
							snprintf(filename, sizeof filename, "%sechostats.ini", cfg.data_dir);
							break;
						default:
							snprintf(filename, sizeof filename, "%sbadareas.lst", cfg.data_dir);
							k = IMPORT_LIST_TYPE_BACKBONE_NA;
							break;
					}

					/* QWK Conference number range */
					int min_confnum = 1, max_confnum = 99999;
					if (k == IMPORT_LIST_TYPE_QWK_CONTROL_DAT) {
						strcpy(str, "1000");
						uifc.helpbuf = "`Minimum / Maximum QWK Conference Number:`\n"
						               "\n"
						               "This setting allows you to control the range of QWK conference numbers\n"
						               "that will be imported from the `control.dat` file.";
						if (uifc.input(WIN_MID | WIN_SAV, 0, 0, "Minimum QWK Conference Number"
						               , str, 5, K_EDIT | K_NUMBER | K_FIND) < 1)
							break;
						min_confnum = atoi(str);
						sprintf(str, "%u", min_confnum + 999);
						if (uifc.input(WIN_MID | WIN_SAV, 0, 0, "Maximum QWK Conference Number"
						               , str, 5, K_EDIT | K_NUMBER | K_FIND) < 1)
							break;
						max_confnum = atoi(str);
					}
					char pkt_orig[25] = "";
					if (k == IMPORT_LIST_TYPE_ECHOSTATS) {
						uifc.helpbuf = "`Filter Areas by Originating Packet Address:`\n"
						               "\n"
						               "Enter the relevant uplink's FidoNet address or ENTER for All origins."
						;
						if (uifc.input(WIN_MID | WIN_SAV, 0, 0, "Filter Areas by Packet Address"
						               , pkt_orig, sizeof(pkt_orig) - 1, K_EDIT | K_FIND) < 0)
							break;
					}
					uifc.helpbuf = "Enter the path to the Area List file to import";
					if (uifc.input(WIN_MID | WIN_SAV, 0, 0, "Filename"
					               , filename, sizeof(filename) - 1, K_EDIT | K_FIND) <= 0)
						break;
					if (!fexistcase(filename)) {
						uifc.msgf("File does not exist: %s", filename);
						break;
					}
					if ((stream = fnopen(&file, filename, O_RDONLY)) == NULL) {
						uifc.msgf("Error %d opening %s", errno, filename);
						break;
					}
					uifc.pop("Importing Areas...");
					long added = 0;
					ported = import_msg_areas(k, stream, i, min_confnum, max_confnum, /* qhub: */ NULL
					                          , pkt_orig, /* faddr: */ NULL, /* misc: */ 0, &added);
					fclose(stream);
					uifc.pop(0);
					if (ported < 0)
						sprintf(str, "!ERROR %ld imported message areas", ported);
					else {
						sprintf(str, "%ld Message Areas Imported Successfully (%ld added)", ported, added);
						if (ported > 0)
							uifc.changes = TRUE;
					}
					uifc.msg(str);
					break;

				case __COUNTER__:
					sub_cfg(grpnum);
					break;
			}
		}
	}
}

/* Configuring Message Options */
void msg_opts()
{
	char       str[128];
	static int msg_dflt;
	static int msg_bar;
	int        i, n;

	while (1) {
		i = 0;
		snprintf(opt[i++], MAX_OPLN, "%-33.33s%s"
		         , "BBS ID for QWK Packets", cfg.sys_id);
		snprintf(opt[i++], MAX_OPLN, "%-33.33s%u seconds"
		         , "Maximum Retry Time", cfg.smb_retry_time);
		if (cfg.max_qwkmsgs)
			sprintf(str, "%" PRIu32, cfg.max_qwkmsgs);
		else
			sprintf(str, "Unlimited");
		snprintf(opt[i++], MAX_OPLN, "%-33.33s%s"
		         , "Maximum QWK Messages", str);
		if (cfg.max_qwkmsgage)
			sprintf(str, "%u days", cfg.max_qwkmsgage);
		else
			sprintf(str, "Unlimited");
		snprintf(opt[i++], MAX_OPLN, "%-33.33s%s"
		         , "Maximum QWK Message Age", str);
		if (cfg.mail_maxage)
			SAFEPRINTF(str, "Enabled (%u days old)", cfg.mail_maxage);
		else
			SAFECOPY(str, "Disabled");
		snprintf(opt[i++], MAX_OPLN, "%-33.33s%s", "Purge E-mail by Age", str);
		if (cfg.max_spamage)
			SAFEPRINTF(str, "Enabled (%u days old)", cfg.max_spamage);
		else
			SAFECOPY(str, "Disabled");
		snprintf(opt[i++], MAX_OPLN, "%-33.33s%s", "Purge SPAM by Age", str);
		if (cfg.sys_misc & SM_DELEMAIL)
			SAFECOPY(str, "Immediately");
		else
			SAFECOPY(str, "Daily");
		snprintf(opt[i++], MAX_OPLN, "%-33.33s%s", "Purge Deleted E-mail", str);
		if (cfg.mail_maxcrcs)
			SAFEPRINTF(str, "Enabled (%" PRIu32 " mail CRCs)", cfg.mail_maxcrcs);
		else
			SAFECOPY(str, "Disabled");
		snprintf(opt[i++], MAX_OPLN, "%-33.33s%s", "Duplicate E-mail Checking", str);
		snprintf(opt[i++], MAX_OPLN, "%-33.33s%s", "Allow Anonymous E-mail"
		         , cfg.sys_misc & SM_ANON_EM ? "Yes" : "No");
		snprintf(opt[i++], MAX_OPLN, "%-33.33s%s", "Allow Quoting in E-mail"
		         , cfg.sys_misc & SM_QUOTE_EM ? "Yes" : "No");
		snprintf(opt[i++], MAX_OPLN, "%-33.33s%s", "Allow Uploads in E-mail"
		         , cfg.sys_misc & SM_FILE_EM ? "Yes" : "No");
		snprintf(opt[i++], MAX_OPLN, "%-33.33s%s", "Allow Forwarding to NetMail"
		         , cfg.sys_misc & SM_FWDTONET ? "Yes" : "No");
		snprintf(opt[i++], MAX_OPLN, "%-33.33s%s", "Kill Read E-mail"
		         , cfg.sys_misc & SM_DELREADM ? "Yes" : "No");
		snprintf(opt[i++], MAX_OPLN, "%-33.33s%s", "Receive E-mail by Real Name"
		         , cfg.msg_misc & MM_REALNAME ? "Yes" : "No");
		snprintf(opt[i++], MAX_OPLN, "%-33.33s%s", "Include Signatures in E-mail"
		         , cfg.msg_misc & MM_EMAILSIG ? "Yes" : "No");
		snprintf(opt[i++], MAX_OPLN, "%-33.33s%s", "Users Can View Deleted Messages"
		         , cfg.sys_misc & SM_USRVDELM ? "Yes" : cfg.sys_misc & SM_SYSVDELM
		        ? "Sysops Only":"No");
		snprintf(opt[i++], MAX_OPLN, "%-33.33s%s", "MailBase Storage Method"
		         , cfg.sys_misc & SM_FASTMAIL ? "Fast Allocation" : "Self-packing");
		snprintf(opt[i++], MAX_OPLN, "%-33.33s%hu", "Days of New Messages for Guest", cfg.guest_msgscan_init);
		opt[i][0] = 0;
		uifc.helpbuf =
			"`Message Options:`\n"
			"\n"
			"System-wide message-base policy applying to both private e-mail and\n"
			"public sub-board posts.  Per-sub-board overrides (where they exist)\n"
			"live under each sub's `Toggle Options`.\n"
			"\n"
			"`BBS ID for QWK Packets`: short ID (max 8 chars) that identifies this\n"
			"system in outgoing QWK packets and QWKnet routing.  Must be unique\n"
			"within any QWKnet you participate in.\n"
			"\n"
			"`Maximum Retry Time`: how many seconds to keep trying to open or lock\n"
			"a message base before giving up.  `10`-`45` is the sweet spot; raise\n"
			"only if message bases are seeing genuine lock contention.\n"
			"\n"
			"`Maximum QWK Messages`: cap on messages (excluding e-mail) any user\n"
			"may pull in a single QWK packet.  `0` = unlimited.  Does not affect\n"
			"QWKnet nodes (those with the `Q` restriction).\n"
			"\n"
			"`Maximum QWK Message Age`: cap on the age (in days) of messages\n"
			"included in a QWK packet.  `0` = no age limit.\n"
			"\n"
			"`Purge E-mail by Age`: automatically delete e-mail older than the\n"
			"configured threshold during daily maintenance.\n"
			"\n"
			"`Purge SPAM by Age`: automatically delete SPAM-tagged e-mail older\n"
			"than the configured threshold.\n"
			"\n"
			"`Purge Deleted E-mail`: `Immediately` removes deleted messages on the\n"
			"spot; `Daily` defers physical removal to nightly maintenance (faster\n"
			"delete UX, briefly recoverable in the meantime).\n"
			"\n"
			"`Duplicate E-mail Checking`: keep a rolling CRC list of N recent\n"
			"e-mails so duplicates can be detected and rejected.  `Disabled` if\n"
			"the count is `0`.\n"
			"\n"
			"`Allow Anonymous E-mail`: permit users with the `A` exemption to send\n"
			"e-mail anonymously.\n"
			"\n"
			"`Allow Quoting in E-mail`: allow users to quote prior messages when\n"
			"composing e-mail replies.\n"
			"\n"
			"`Allow Uploads in E-mail`: allow users to attach files to outgoing\n"
			"e-mail.\n"
			"\n"
			"`Allow Forwarding to NetMail`: allow forwarding of incoming e-mail to\n"
			"NetMail addresses (e.g. FidoNet) via a user's auto-forward setting.\n"
			"\n"
			"`Kill Read E-mail`: automatically mark read e-mail for deletion (per\n"
			"the `Purge Deleted E-mail` policy above).\n"
			"\n"
			"`Receive E-mail by Real Name`: in addition to alias, accept the\n"
			"user's real name (with spaces or '.' as separators) as a valid\n"
			"recipient on incoming SMTP mail.\n"
			"\n"
			"`Include Signatures in E-mail`: append the sender's configured\n"
			"signature to outgoing e-mail (otherwise signatures apply only to\n"
			"sub-board posts).\n"
			"\n"
			"`Users Can View Deleted Messages`: who may see messages marked for\n"
			"deletion -- `Yes` = all users, `Sysops Only`, or `No`.\n"
			"\n"
			"`MailBase Storage Method`: `Self-packing` reclaims space as messages\n"
			"are deleted (more I/O, smaller files); `Fast Allocation` keeps slots\n"
			"and packs on a schedule (less I/O, larger files).\n"
			"\n"
			"`Days of New Messages for Guest`: how many days of message history\n"
			"a guest (G-restricted) account sees on first scan.  `0` = none.\n"
		;

		switch (uifc.list(WIN_ORG | WIN_ACT | WIN_MID | WIN_CHE, 0, 0, 72, &msg_dflt, &msg_bar
		                  , "Message Options", opt)) {
			case -1:
				i = save_changes(WIN_MID);
				if (i == -1)
					continue;
				if (!i) {
					cfg.new_install = new_install;
					save_msgs_cfg(&cfg);
					save_main_cfg(&cfg);
					refresh_cfg(&cfg);
				}
				return;
			case 0:
				edit_sys_id(false, false);
				break;
			case 1:
				uifc.helpbuf =
					"`Maximum Message Base Retry Time:`\n"
					"\n"
					"This is the maximum number of seconds to allow while attempting to open\n"
					"or lock a message base (a value in the range of 10 to 45 seconds should\n"
					"be fine).\n"
				;
				ultoa(cfg.smb_retry_time, str, 10);
				uifc.input(WIN_MID | WIN_SAV, 0, 0
				           , "Maximum Message Base Retry Time (in seconds)"
				           , str, 3, K_NUMBER | K_EDIT);
				cfg.smb_retry_time = atoi(str);
				break;
			case 2:
				uifc.helpbuf =
					"`Maximum Messages Per QWK Packet:`\n"
					"\n"
					"This is the maximum number of messages (excluding E-mail), that a user\n"
					"can have in one QWK packet for download. This limit does not effect\n"
					"QWK network nodes (`Q` restriction). If set to `0`, no limit is imposed.\n"
				;

				ultoa(cfg.max_qwkmsgs, str, 10);
				uifc.input(WIN_MID | WIN_SAV, 0, 0
				           , "Maximum Messages Per QWK Packet (0=No Limit)"
				           , str, 6, K_NUMBER | K_EDIT);
				cfg.max_qwkmsgs = atol(str);
				break;
			case 3:
				uifc.helpbuf =
					"`Maximum Age of Messages Imported From QWK Packets:`\n"
					"\n"
					"This is the maximum age of messages (in days), allowed for messages in\n"
					"QWK packets. Messages with an age older than this value will not be\n"
					"imported. If set to `0`, no age limit is imposed.\n"
				;

				itoa(cfg.max_qwkmsgage, str, 10);
				uifc.input(WIN_MID | WIN_SAV, 0, 0
				           , "Maximum Age (in days) of QWK-imported Messages (0=No Limit)"
				           , str, 4, K_NUMBER | K_EDIT);
				cfg.max_qwkmsgage = atoi(str);
				break;
			case 4:
				sprintf(str, "%u", cfg.mail_maxage);
				uifc.helpbuf =
					"`Maximum Age of E-mail:`\n"
					"\n"
					"This value is the maximum number of days that E-mail will be kept (read\n"
					"or not). Use `0` for `unlimited`.\n"
				;
				uifc.input(WIN_MID | WIN_SAV, 0, 17, "Maximum Age of E-mail "
				           "(in days)", str, 5, K_EDIT | K_NUMBER);
				cfg.mail_maxage = atoi(str);
				break;
			case 5:
				sprintf(str, "%u", cfg.max_spamage);
				uifc.helpbuf =
					"`Maximum Age of SPAM:`\n"
					"\n"
					"This value is the maximum number of days that SPAM-tagged E-mail will be\n"
					"kept (read or not). Use `0` for `unlimited`.\n"
				;
				uifc.input(WIN_MID | WIN_SAV, 0, 17, "Maximum Age of SPAM-tagged E-mail "
				           "(in days)", str, 5, K_EDIT | K_NUMBER);
				cfg.max_spamage = atoi(str);
				break;
			case 6:
				strcpy(opt[0], "Daily");
				strcpy(opt[1], "Immediately");
				opt[2][0] = 0;
				i = cfg.sys_misc & SM_DELEMAIL ? 1:0;
				uifc.helpbuf =
					"`Purge Deleted E-mail:`\n"
					"\n"
					"If you wish to have deleted e-mail physically (and permanently) removed\n"
					"from your e-mail database immediately upon a user exiting the reading\n"
					"e-mail prompt, set this option to `Immediately`.\n"
					"\n"
					"For the best system performance and to avoid delays when deleting e-mail\n"
					"from a large e-mail database, set this option to `Daily` (the default).\n"
					"Your system maintenance will automatically purge deleted e-mail once per\n"
					"day.\n"
				;

				i = uifc.list(WIN_MID | WIN_SAV, 0, 0, 0, &i, 0
				              , "Purge Deleted E-mail", opt);
				if (!i && cfg.sys_misc & SM_DELEMAIL) {
					cfg.sys_misc &= ~SM_DELEMAIL;
					uifc.changes = TRUE;
				}
				else if (i == 1 && !(cfg.sys_misc & SM_DELEMAIL)) {
					cfg.sys_misc |= SM_DELEMAIL;
					uifc.changes = TRUE;
				}
				break;
			case 7:
				sprintf(str, "%" PRIu32, cfg.mail_maxcrcs);
				uifc.helpbuf =
					"`Maximum Number of E-mail CRCs:`\n"
					"\n"
					"This value is the maximum number of CRCs that will be kept for duplicate\n"
					"mail checking. Once this maximum number of CRCs is reached, the oldest\n"
					"CRCs will be automatically purged.\n"
				;
				uifc.input(WIN_MID | WIN_SAV, 0, 17, "Maximum Number of E-mail "
				           "CRCs", str, 5, K_EDIT | K_NUMBER);
				cfg.mail_maxcrcs = atol(str);
				break;
			case 8:
				i = cfg.sys_misc & SM_ANON_EM ? 0:1;
				uifc.helpbuf =
					"`Allow Anonymous E-mail:`\n"
					"\n"
					"If you want users with the `A` exemption to be able to send E-mail\n"
					"anonymously, set this option to `Yes`.\n"
				;

				i = uifc.list(WIN_MID | WIN_SAV, 0, 0, 0, &i, 0
				              , "Allow Anonymous E-mail", uifcYesNoOpts);
				if (!i && !(cfg.sys_misc & SM_ANON_EM)) {
					cfg.sys_misc |= SM_ANON_EM;
					uifc.changes = TRUE;
				}
				else if (i == 1 && cfg.sys_misc & SM_ANON_EM) {
					cfg.sys_misc &= ~SM_ANON_EM;
					uifc.changes = TRUE;
				}
				break;
			case 9:
				i = cfg.sys_misc & SM_QUOTE_EM ? 0:1;
				uifc.helpbuf =
					"`Allow Quoting in E-mail:`\n"
					"\n"
					"If you want your users to be allowed to use message quoting when\n"
					"responding in E-mail, set this option to `Yes`.\n"
				;

				i = uifc.list(WIN_MID | WIN_SAV, 0, 0, 0, &i, 0
				              , "Allow Quoting in E-mail", uifcYesNoOpts);
				if (!i && !(cfg.sys_misc & SM_QUOTE_EM)) {
					cfg.sys_misc |= SM_QUOTE_EM;
					uifc.changes = TRUE;
				}
				else if (i == 1 && cfg.sys_misc & SM_QUOTE_EM) {
					cfg.sys_misc &= ~SM_QUOTE_EM;
					uifc.changes = TRUE;
				}
				break;
			case 10:
				i = cfg.sys_misc & SM_FILE_EM ? 0:1;
				uifc.helpbuf =
					"`Allow File Attachment Uploads in E-mail:`\n"
					"\n"
					"If you want your users to be allowed to attach an uploaded file to\n"
					"an E-mail message, set this option to `Yes`.\n"
				;

				i = uifc.list(WIN_MID | WIN_SAV, 0, 0, 0, &i, 0
				              , "Allow File Attachment Uploads in E-mail", uifcYesNoOpts);
				if (!i && !(cfg.sys_misc & SM_FILE_EM)) {
					cfg.sys_misc |= SM_FILE_EM;
					uifc.changes = TRUE;
				}
				else if (i == 1 && cfg.sys_misc & SM_FILE_EM) {
					cfg.sys_misc &= ~SM_FILE_EM;
					uifc.changes = TRUE;
				}
				break;
			case 11:
				i = cfg.sys_misc & SM_FWDTONET ? 0:1;
				uifc.helpbuf =
					"`Allow Users to Have Their E-mail Forwarded to NetMail:`\n"
					"\n"
					"If you want your users to be able to have any e-mail sent to them\n"
					"optionally (at the sender's discretion) forwarded to a NetMail address,\n"
					"set this option to `Yes`.\n"
				;

				i = uifc.list(WIN_MID | WIN_SAV, 0, 0, 0, &i, 0
				              , "Allow Forwarding of E-mail to NetMail", uifcYesNoOpts);
				if (!i && !(cfg.sys_misc & SM_FWDTONET)) {
					cfg.sys_misc |= SM_FWDTONET;
					uifc.changes = TRUE;
				}
				else if (i == 1 && cfg.sys_misc & SM_FWDTONET) {
					cfg.sys_misc &= ~SM_FWDTONET;
					uifc.changes = TRUE;
				}
				break;
			case 12:
				i = cfg.sys_misc & SM_DELREADM ? 0:1;
				uifc.helpbuf =
					"`Kill Read E-mail Automatically:`\n"
					"\n"
					"If this option is set to `Yes`, e-mail that has been read will be\n"
					"automatically deleted when message base maintenance is run.\n"
				;

				i = uifc.list(WIN_MID | WIN_SAV, 0, 0, 0, &i, 0
				              , "Kill Read E-mail Automatically", uifcYesNoOpts);
				if (!i && !(cfg.sys_misc & SM_DELREADM)) {
					cfg.sys_misc |= SM_DELREADM;
					uifc.changes = TRUE;
				}
				else if (i == 1 && cfg.sys_misc & SM_DELREADM) {
					cfg.sys_misc &= ~SM_DELREADM;
					uifc.changes = TRUE;
				}
				break;
			case 13:
				i = cfg.msg_misc & MM_REALNAME ? 0:1;
				uifc.helpbuf =
					"`Receive E-mail by Real Name:`\n"
					"\n"
					"If this option is set to ~Yes~, e-mail messages may be received when\n"
					"addressed to a user's real name (rather than their alias).\n"
				;

				i = uifc.list(WIN_MID | WIN_SAV, 0, 0, 0, &i, 0
				              , "Receive E-mail by Real Name", uifcYesNoOpts);
				if (!i && !(cfg.msg_misc & MM_REALNAME)) {
					cfg.msg_misc |= MM_REALNAME;
					uifc.changes = TRUE;
				}
				else if (i == 1 && cfg.msg_misc & MM_REALNAME) {
					cfg.msg_misc &= ~MM_REALNAME;
					uifc.changes = TRUE;
				}
				break;
			case 14:
				n = (cfg.msg_misc & MM_EMAILSIG) ? 0:1;
				uifc.helpbuf =
					"`Include User Signatures in E-mail:`\n"
					"\n"
					"If you wish to have user signatures automatically appended to e-mail\n"
					"messages, set this option to ~Yes~.\n"
				;
				n = uifc.list(WIN_SAV | WIN_MID, 0, 0, 0, &n, 0
				              , "Include User Signatures in E-mail", uifcYesNoOpts);
				if (n == -1)
					break;
				if (!n && !(cfg.msg_misc & MM_EMAILSIG)) {
					uifc.changes = TRUE;
					cfg.msg_misc |= MM_EMAILSIG;
					break;
				}
				if (n == 1 && cfg.msg_misc & MM_EMAILSIG) {
					uifc.changes = TRUE;
					cfg.msg_misc &= ~MM_EMAILSIG;
				}
				break;
			case 15:
				edit_sys_delmsg_policy(false, false);
				break;
			case 16:
				n = (cfg.sys_misc & SM_FASTMAIL) ? 1:0;
				strcpy(opt[0], "Self-packing");
				strcpy(opt[1], "Fast Allocation");
				opt[2][0] = 0;
				uifc.helpbuf =
					"`MailBase Storage Method:`\n"
					"\n"
					"To have all new mail message headers and data `appended` to the existing\n"
					"data/mail.shd and .sdt files (for performance and reliability reasons)\n"
					"set this option to `Fast Allocation`, but know that you will need to\n"
					"periodically pack the MailBase (using `'smbutil p'`) to keep its files\n"
					"from infinitely growing in size.\n"
					"\n"
					"The default and normally-recommended setting is `Self-packing`.\n"
				;
				n = uifc.list(WIN_SAV | WIN_MID, 0, 0, 0, &n, 0
				              , "Mailbase Storage Method", opt);
				if (n < 0)
					break;
				if (n == 0 && (cfg.sys_misc & SM_FASTMAIL)) {
					uifc.changes = TRUE;
					cfg.sys_misc &= ~SM_FASTMAIL;
				}
				else if (n == 1 && !(cfg.sys_misc & SM_FASTMAIL)) {
					uifc.changes = TRUE;
					cfg.sys_misc |= SM_FASTMAIL;
				}
				break;
			case 17:
				uifc.helpbuf =
					"`Days of New Messages for Guest:`\n"
					"\n"
					"This option allows you to set the number of days worth of messages\n"
					"which will be included in a Guest login's `new message scan`.\n"
					"\n"
					"The value `0` means there will be `no` new messages for the Guest account.\n"
				;
				sprintf(str, "%hu", cfg.guest_msgscan_init);
				uifc.input(WIN_SAV | WIN_MID, 0, 0
				           , "Days of New Messages for Guest's new message scan"
				           , str, 4, K_EDIT | K_NUMBER);
				cfg.guest_msgscan_init = atoi(str);
				break;
		}
	}
}

void toggle_xattr_support(const char* title, uint32_t* val, int shift)
{
	uifc.helpbuf =
		"`Extra Attribute Codes...`\n"
		"\n"
		"Synchronet can support the native text attribute (e.g. color) codes of\n"
		"other BBS programs in messages and display files (e.g. menus). To enable\n"
		"extra attribute codes for another BBS program, set the corresponding\n"
		"option to `Yes`.\n"
		"\n"
		"- `WWIV` color codes are preceded by a Ctrl-C (ASCII 3) character.\n"
		"- `PCBoard` color codes are of the form `@Xxx` where `xx` are hex digits.\n"
		"- `Wildcat` color codes are of the form `@xx@` where `xx` are hex digits.\n"
		"- `Celerity` color codes are of the form `|x` where `x` is an alpha character.\n"
		"- `Renegade` color codes are of the form `|xx` where `xx` are decimal digits.\n"
		"  Renegade codes are the most popular/common among non-Synchronet BBSes.\n"
		"\n"
		"See `http://wiki.synchro.net/custom:colors` for details.\n"
	;

	int j = 0;
	while (1) {
		int i = 0;
		snprintf(opt[i++], MAX_OPLN, "%-30s %-3.3s", "WWIV (Heart) Codes"
					, (*val) & (SM_WWIV << shift) ? "Yes":"No");
		snprintf(opt[i++], MAX_OPLN, "%-30s %-3.3s", "PCBoard (@Xxx) Codes"
					, (*val) & (SM_PCBOARD << shift) ? "Yes":"No");
		snprintf(opt[i++], MAX_OPLN, "%-30s %-3.3s", "Wildcat (@xx@) Codes"
					, (*val) & (SM_WILDCAT << shift) ? "Yes":"No");
		snprintf(opt[i++], MAX_OPLN, "%-30s %-3.3s", "Celerity (|x) Codes"
					, (*val) & (SM_CELERITY << shift) ? "Yes":"No");
		snprintf(opt[i++], MAX_OPLN, "%-30s %-3.3s", "Renegade (|xx) Codes"
					, (*val) & (SM_RENEGADE << shift) ? "Yes":"No");
		opt[i][0] = 0;
		j = uifc.list(WIN_BOT | WIN_RHT | WIN_SAV, 2, 2, 0, &j, 0
					    , title, opt);
		if (j == -1)
			break;

		uifc.changes = TRUE;
		switch (j) {
			case 0:
				(*val) ^= (SM_WWIV << shift);
				break;
			case 1:
				(*val) ^= (SM_PCBOARD << shift);
				break;
			case 2:
				(*val) ^= (SM_WILDCAT << shift);
				break;
			case 3:
				(*val) ^= (SM_CELERITY << shift);
				break;
			case 4:
				(*val) ^= (SM_RENEGADE << shift);
				break;
		}
	}
}
