diff --git a/src/sbbs3/echocfg.vcxproj b/src/sbbs3/echocfg.vcxproj
index d8f6fbb481da19a780cec9783549a11b389f1f16..155aa4f52ba72bf471cf45adddf907ce55bbf201 100644
--- a/src/sbbs3/echocfg.vcxproj
+++ b/src/sbbs3/echocfg.vcxproj
@@ -153,6 +153,7 @@
       <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
       <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
     </ClCompile>
+    <ClCompile Include="getctrl.c" />
     <ClCompile Include="nopen.c">
       <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
       <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
diff --git a/src/sbbs3/findstr.c b/src/sbbs3/findstr.c
new file mode 100644
index 0000000000000000000000000000000000000000..1cfd0d9dff08b591fca74d8699c81e40aaa71b52
--- /dev/null
+++ b/src/sbbs3/findstr.c
@@ -0,0 +1,215 @@
+/* Synchronet find string routines */
+
+/****************************************************************************
+ * @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 "genwrap.h"
+#include "findstr.h"
+
+/****************************************************************************/
+/* Pattern matching string search of 'insearchof' in 'pattern'.				*/
+/* pattern matching is case-insensitive										*/
+/* patterns beginning with ';' are comments (never match)					*/
+/* patterns beginning with '!' are reverse-matched (returns FALSE if match)	*/
+/* patterns ending in '~' will match string anywhere (sub-string search)	*/
+/* patterns ending in '^' will match left string fragment only				*/
+/* patterns including '*' must match both left and right string fragments	*/
+/* all other patterns are exact-match checking								*/
+/****************************************************************************/
+BOOL findstr_in_string(const char* search, const char* pattern)
+{
+	char	buf[256];
+	char*	p;
+	char*	last;
+	const char*	splat;
+	size_t	len;
+	BOOL	found = FALSE;
+
+	if(pattern == NULL || search == NULL)
+		return FALSE;
+
+	SAFECOPY(buf, pattern);
+	p = buf;
+
+	if(*p == ';')		/* comment */
+		return FALSE;
+
+	if(*p == '!')	{	/* reverse-match */
+		found = TRUE;
+		p++;
+	}
+
+	truncsp(p);
+	len = strlen(p);
+	if(len > 0) {
+		last = p + len - 1;
+		if(*last == '~') {
+			*last = '\0';
+			if(strcasestr(search, p) != NULL)
+				found = !found; 
+		}
+
+		else if(*last == '^') {
+			if(strnicmp(p, search, len - 1) == 0)
+				found = !found; 
+		}
+
+		else if((splat = strchr(p, '*')) != NULL) {
+			int left = splat - p;
+			int right = len - (left + 1);
+			int slen = strlen(search);
+			if(slen < left + right)
+				return found;
+			if(strnicmp(search, p, left) == 0
+				&& strnicmp(p + left + 1, search + (slen - right), right) == 0)
+				found = !found;
+		}
+
+		else if(stricmp(p, search) == 0)
+			found = !found; 
+	} 
+	return found;
+}
+
+static uint32_t encode_ipv4_address(unsigned int byte[])
+{
+	if(byte[0] > 0xff || byte[1] > 0xff || byte[2] > 0xff || byte[3] > 0xff)
+		return 0;
+	return (byte[0]<<24) | (byte[1]<<16) | (byte[2]<<8) | byte[3];
+}
+
+static uint32_t parse_ipv4_address(const char* str)
+{
+	unsigned int byte[4];
+
+	if(sscanf(str, "%u.%u.%u.%u", &byte[0], &byte[1], &byte[2], &byte[3]) != 4)
+		return 0;
+	return encode_ipv4_address(byte);
+}
+
+static uint32_t parse_cidr(const char* p, unsigned* subnet)
+{
+	unsigned int byte[4];
+
+	if(*p == '!')
+		p++;
+
+	*subnet = 0;
+	if(sscanf(p, "%u.%u.%u.%u/%u", &byte[0], &byte[1], &byte[2], &byte[3], subnet) != 5 || *subnet > 32)
+		return 0;
+	return encode_ipv4_address(byte);
+}
+
+static BOOL is_cidr_match(const char *p, uint32_t ip_addr, uint32_t cidr, unsigned subnet)
+{
+	BOOL	match = FALSE;
+
+	if(*p == '!')
+		match = TRUE;
+
+	if(((ip_addr ^ cidr) >> (32-subnet)) == 0)
+		match = !match;
+
+	return match;
+}
+
+/****************************************************************************/
+/* Pattern matching string search of 'insearchof' in 'list'.				*/
+/****************************************************************************/
+BOOL findstr_in_list(const char* insearchof, str_list_t list)
+{
+	size_t	index;
+	BOOL	found=FALSE;
+	char*	p;
+	uint32_t ip_addr, cidr;
+	unsigned subnet;
+
+	if(list==NULL || insearchof==NULL)
+		return FALSE;
+	ip_addr = parse_ipv4_address(insearchof);
+	for(index=0; list[index]!=NULL; index++) {
+		p=list[index];
+//		SKIP_WHITESPACE(p);
+		if(ip_addr != 0 && (cidr = parse_cidr(p, &subnet)) != 0)
+			found = is_cidr_match(p, ip_addr, cidr, subnet);
+		else
+			found = findstr_in_string(insearchof,p);
+		if(found != (*p=='!'))
+			break;
+	}
+	return found;
+}
+
+/****************************************************************************/
+/* Pattern matching string search of 'insearchof' in 'fname'.				*/
+/****************************************************************************/
+BOOL findstr(const char* insearchof, const char* fname)
+{
+	char		str[256];
+	BOOL		found=FALSE;
+	FILE*		fp;
+	uint32_t	ip_addr, cidr;
+	unsigned	subnet;
+
+	if(insearchof==NULL || fname==NULL || *fname == '\0')
+		return FALSE;
+
+	if((fp=fopen(fname,"r"))==NULL)
+		return FALSE; 
+
+	ip_addr = parse_ipv4_address(insearchof);
+	while(!feof(fp) && !ferror(fp) && !found) {
+		if(!fgets(str,sizeof(str),fp))
+			break;
+		char* p = str;
+		SKIP_WHITESPACE(p);
+		c_unescape_str(p);
+		if(ip_addr !=0 && (cidr = parse_cidr(p, &subnet)) != 0)
+			found = is_cidr_match(p, ip_addr, cidr, subnet);
+		else
+			found = findstr_in_string(insearchof, p);
+	}
+
+	fclose(fp);
+	return found;
+}
+
+static char* process_findstr_item(size_t index, char *str, void* cbdata)
+{
+	SKIP_WHITESPACE(str);
+	return c_unescape_str(str);
+}
+
+/****************************************************************************/
+str_list_t findstr_list(const char* fname)
+{
+	FILE*	fp;
+	str_list_t	list;
+
+	if((fp=fopen(fname,"r"))==NULL)
+		return NULL;
+
+	list=strListReadFile(fp, NULL, /* Max line length: */255);
+	strListModifyEach(list, process_findstr_item, /* cbdata: */NULL);
+
+	fclose(fp);
+
+	return list;
+}
+
diff --git a/src/sbbs3/findstr.h b/src/sbbs3/findstr.h
new file mode 100644
index 0000000000000000000000000000000000000000..0a5c2814bd4c2b0938925b2c39f77dcf2ae24fab
--- /dev/null
+++ b/src/sbbs3/findstr.h
@@ -0,0 +1,40 @@
+/* Synchronet find string functions */
+
+/****************************************************************************
+ * @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.	*
+ ****************************************************************************/
+
+#ifndef _FINDSTR_H_
+#define _FINDSTR_H_
+
+#include "str_list.h"
+#include "dllexport.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+DLLEXPORT BOOL		findstr(const char *insearch, const char *fname);
+DLLEXPORT BOOL		findstr_in_string(const char* insearchof, const char* pattern);
+DLLEXPORT BOOL		findstr_in_list(const char* insearchof, str_list_t list);
+DLLEXPORT str_list_t findstr_list(const char* fname);
+
+#ifdef __cplusplus
+}
+#endif
+#endif /* Don't add anything after this line */
diff --git a/src/sbbs3/getctrl.c b/src/sbbs3/getctrl.c
index 3f6e1f809134122de24ddb19fda34c58c9f4df2b..c3ffbcfbed49e8e03bc173b6b73b72d265357926 100644
--- a/src/sbbs3/getctrl.c
+++ b/src/sbbs3/getctrl.c
@@ -1,37 +1,35 @@
-#include <gen_defs.h>
-#include <dirwrap.h>
+/* Synchronet get "control" directory function */
 
-char *get_ctrl_dir(char *path, size_t pathsz)
-{
-#ifdef PREFIX
-	char	ini_file[MAX_PATH];
-#endif
-	char *p;
-
-	p=getenv("SBBSCTRL");
-	if(p!=NULL) {
-		strncpy(path, p, pathsz);
-		if(pathsz > 0)
-			path[pathsz-1]=0;
-		return path;
-	}
+/****************************************************************************
+ * @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.	*
+ ****************************************************************************/
 
-#ifdef PREFIX
-	strncpy(path, PREFIX"/etc", pathsz);
-	if(pathsz > 0)
-		path[pathsz-1]=0;
-	iniFileName(ini_file, sizeof(ini_file)-1, PREFIX"/etc", "sbbs.ini");
-	if(fexistcase(ini_file)) {
-		FILE*	fini;
-		char*	str;
+#include <stdio.h>
+#include "getctrl.h"
+#include "sbbsdefs.h"
 
-		fini=iniOpenFile(ini_file, FALSE);
-		if(fini==NULL)
-			return NULL;
-		str = iniReadExistingString(fini, "Global", "CtrlDirectory", NULL, ini_file);
-		iniCloseFile(fini);
-		return str;
+const char* get_ctrl_dir(BOOL warn)
+{
+	char* p = getenv("SBBSCTRL");
+	if(p == NULL || *p == '\0') {
+		if(warn)
+			fprintf(stderr, "!SBBSCTRL environment variable not set, using default value: " SBBSCTRL_DEFAULT "\n\n");
+		p = SBBSCTRL_DEFAULT;
 	}
-#endif
-	return NULL;
+	return p;
 }
diff --git a/src/sbbs3/getctrl.h b/src/sbbs3/getctrl.h
index 20713e9221becf013d5b504e3a3a3f3fe467a7d0..5a26f64665001038d25f7e2fff038bcec559ea84 100644
--- a/src/sbbs3/getctrl.h
+++ b/src/sbbs3/getctrl.h
@@ -1,9 +1,32 @@
-#ifndef GETCTRL_H
-#define GETCTRL_H
+/* Synchronet get "control" directory function */
+
+/****************************************************************************
+ * @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.	*
+ ****************************************************************************/
+
+#ifndef GETCTRL_H_
+#define GETCTRL_H_
+
+#include "gen_defs.h"
 
 #if defined(__cplusplus)
-	extern "C"
+extern "C"
 #endif
-char *get_ctrl_dir(char *path, size_t pathsz)
+const char* get_ctrl_dir(BOOL warn);
 
 #endif
diff --git a/src/sbbs3/jsexec.vcxproj b/src/sbbs3/jsexec.vcxproj
index 0bd5f22da99aefbfff95f91154129aaacc690e9d..fc16d40e6fb80e7d0f9dada63685535e719a9d01 100644
--- a/src/sbbs3/jsexec.vcxproj
+++ b/src/sbbs3/jsexec.vcxproj
@@ -157,6 +157,7 @@
     </ProjectReference>
   </ItemDefinitionGroup>
   <ItemGroup>
+    <ClCompile Include="getctrl.c" />
     <ClCompile Include="jsdebug.c" />
     <ClCompile Include="js_conio.c">
       <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
diff --git a/src/sbbs3/load_cfg.vcxproj b/src/sbbs3/load_cfg.vcxproj
index 6a164bb296fc83665a2716e1b86bfc2ac0ef67b5..3464a21fbde7bc0327ca557b026d29a1cae84f3f 100644
--- a/src/sbbs3/load_cfg.vcxproj
+++ b/src/sbbs3/load_cfg.vcxproj
@@ -101,6 +101,8 @@
   <ItemGroup>
     <ClCompile Include="..\encode\utf8.c" />
     <ClCompile Include="ars.c" />
+    <ClCompile Include="findstr.c" />
+    <ClCompile Include="getctrl.c" />
     <ClCompile Include="load_cfg.c" />
     <ClCompile Include="nopen.c" />
     <ClCompile Include="readtext.c" />
diff --git a/src/sbbs3/ntsvcs.vcxproj b/src/sbbs3/ntsvcs.vcxproj
index 20557eebb0504aa1800f428eb44bf72d447c0691..dd7180f6e7ab044ae8fe5c74dc588141aa82af52 100644
--- a/src/sbbs3/ntsvcs.vcxproj
+++ b/src/sbbs3/ntsvcs.vcxproj
@@ -148,6 +148,7 @@
     </Bscmake>
   </ItemDefinitionGroup>
   <ItemGroup>
+    <ClCompile Include="getctrl.c" />
     <ClCompile Include="ntsvcs.c">
       <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
       <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
diff --git a/src/sbbs3/sbbs.h b/src/sbbs3/sbbs.h
index ef00b376105744de4c578d04c30f40b74337b68e..61cc7b49bebe8e649ab5bcc66e401f19f5caa595 100644
--- a/src/sbbs3/sbbs.h
+++ b/src/sbbs3/sbbs.h
@@ -289,6 +289,7 @@ extern int	thread_suid_broken;			/* NPTL is no longer broken */
 #include "nopen.h"
 #include "text.h"
 #include "str_util.h"
+#include "findstr.h"
 #include "date_str.h"
 #include "load_cfg.h"
 #include "getstats.h"
diff --git a/src/sbbs3/sbbs.vcxproj b/src/sbbs3/sbbs.vcxproj
index 7d16015d0f5054032e140c842e77fa2d468cfff5..640e73a482c99b8a844e1253e24c0224cee90523 100644
--- a/src/sbbs3/sbbs.vcxproj
+++ b/src/sbbs3/sbbs.vcxproj
@@ -328,6 +328,7 @@
       <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
       <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
     </ClCompile>
+    <ClCompile Include="findstr.c" />
     <ClCompile Include="getkey.cpp">
       <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
       <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
diff --git a/src/sbbs3/sbbscon.vcxproj b/src/sbbs3/sbbscon.vcxproj
index 62e836144e096536cc18f31cdfae8e45574b6dce..8e96a5491b1dc180674ca689584a1634edceff3c 100644
--- a/src/sbbs3/sbbscon.vcxproj
+++ b/src/sbbs3/sbbscon.vcxproj
@@ -147,6 +147,7 @@
     </Bscmake>
   </ItemDefinitionGroup>
   <ItemGroup>
+    <ClCompile Include="getctrl.c" />
     <ClCompile Include="sbbs_ini.c">
       <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
       <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
diff --git a/src/sbbs3/sbbsecho.c b/src/sbbs3/sbbsecho.c
index 45e693fd71553ea0ab2bda277c70023b2559d1fd..9020bb1e2f44b92a7e79e7a250a1b34e9d775f50 100644
--- a/src/sbbs3/sbbsecho.c
+++ b/src/sbbs3/sbbsecho.c
@@ -46,6 +46,7 @@
 #include "link_list.h"
 #include "str_list.h"
 #include "str_util.h"
+#include "findstr.h"
 #include "datewrap.h"
 #include "nopen.h"
 #include "crc32.h"
diff --git a/src/sbbs3/scfglib.h b/src/sbbs3/scfglib.h
index 9aae1d3781f540e1f611b5dc95abf806d0d367df..1c70181c1dc46395a764f189095fb527c5c3ea7c 100644
--- a/src/sbbs3/scfglib.h
+++ b/src/sbbs3/scfglib.h
@@ -72,6 +72,14 @@ DLLEXPORT BOOL	is_valid_libnum(scfg_t*, int);
 DLLEXPORT BOOL	is_valid_subnum(scfg_t*, int);
 DLLEXPORT BOOL	is_valid_grpnum(scfg_t*, int);
 
+DLLEXPORT BOOL		trashcan(scfg_t* cfg, const char *insearch, const char *name);
+DLLEXPORT char *	trashcan_fname(scfg_t* cfg, const char *name, char* fname, size_t);
+DLLEXPORT str_list_t trashcan_list(scfg_t* cfg, const char* name);
+
+DLLEXPORT char *	sub_newsgroup_name(scfg_t*, sub_t*, char*, size_t);
+DLLEXPORT char *	sub_area_tag(scfg_t*, sub_t*, char*, size_t);
+DLLEXPORT char *	dir_area_tag(scfg_t*, dir_t*, char*, size_t);
+
 faddr_t* nearest_sysfaddr(scfg_t*, faddr_t*);
 
 #ifdef __cplusplus
diff --git a/src/sbbs3/scfglib1.c b/src/sbbs3/scfglib1.c
index 0809b7e1631a920285b26831d84947b11994a767..1a7a8c4fc1d077724e9551a832e3802937ce4e88 100644
--- a/src/sbbs3/scfglib1.c
+++ b/src/sbbs3/scfglib1.c
@@ -23,6 +23,7 @@
 #include "load_cfg.h"
 #include "nopen.h"
 #include "ars_defs.h"
+#include "findstr.h"
 
 BOOL allocerr(FILE* fp, char* error, size_t maxerrlen, long offset, const char *fname, size_t size)
 {
@@ -874,3 +875,96 @@ faddr_t* nearest_sysfaddr(scfg_t* cfg, faddr_t* addr)
 			return &cfg->faddr[i];
 	return &cfg->faddr[0];
 }
+
+/****************************************************************************/
+/* Searches the file <name>.can in the TEXT directory for matches			*/
+/* Returns TRUE if found in list, FALSE if not.								*/
+/****************************************************************************/
+BOOL trashcan(scfg_t* cfg, const char* insearchof, const char* name)
+{
+	char fname[MAX_PATH+1];
+
+	return(findstr(insearchof,trashcan_fname(cfg,name,fname,sizeof(fname))));
+}
+
+/****************************************************************************/
+char* trashcan_fname(scfg_t* cfg, const char* name, char* fname, size_t maxlen)
+{
+	safe_snprintf(fname,maxlen,"%s%s.can",cfg->text_dir,name);
+	return fname;
+}
+
+/****************************************************************************/
+str_list_t trashcan_list(scfg_t* cfg, const char* name)
+{
+	char	fname[MAX_PATH+1];
+
+	return findstr_list(trashcan_fname(cfg, name, fname, sizeof(fname)));
+}
+
+char* sub_newsgroup_name(scfg_t* cfg, sub_t* sub, char* str, size_t size)
+{
+	memset(str, 0, size);
+	if(sub->newsgroup[0])
+		strncpy(str, sub->newsgroup, size - 1);
+	else {
+		snprintf(str, size - 1, "%s.%s", cfg->grp[sub->grp]->sname, sub->sname);
+		/*
+		 * From RFC5536:
+		 * newsgroup-name  =  component *( "." component )
+		 * component       =  1*component-char
+		 * component-char  =  ALPHA / DIGIT / "+" / "-" / "_"
+		 */
+		if (str[0] == '.')
+			str[0] = '_';
+		size_t c;
+		for(c = 0; str[c] != 0; c++) {
+			/* Legal characters */
+			if ((str[c] >= 'A' && str[c] <= 'Z')
+					|| (str[c] >= 'a' && str[c] <= 'z')
+					|| (str[c] >= '0' && str[c] <= '9')
+					|| str[c] == '+'
+					|| str[c] == '-'
+					|| str[c] == '_'
+					|| str[c] == '.')
+				continue;
+			str[c] = '_';
+		}
+		c--;
+		if (str[c] == '.')
+			str[c] = '_';
+	}
+	return str;
+}
+
+char* sub_area_tag(scfg_t* cfg, sub_t* sub, char* str, size_t size)
+{
+	char* p;
+
+	memset(str, 0, size);
+	if(sub->area_tag[0])
+		strncpy(str, sub->area_tag, size - 1);
+	else if(sub->newsgroup[0])
+		strncpy(str, sub->newsgroup, size - 1);
+	else {
+		strncpy(str, sub->sname, size - 1);
+		REPLACE_CHARS(str, ' ', '_', p);
+	}
+	strupr(str);
+	return str;
+}
+
+char* dir_area_tag(scfg_t* cfg, dir_t* dir, char* str, size_t size)
+{
+	char* p;
+
+	memset(str, 0, size);
+	if(dir->area_tag[0])
+		strncpy(str, dir->area_tag, size - 1);
+	else {
+		strncpy(str, dir->sname, size - 1);
+		REPLACE_CHARS(str, ' ', '_', p);
+	}
+	strupr(str);
+	return str;
+}
diff --git a/src/sbbs3/str_util.c b/src/sbbs3/str_util.c
index 9991892fbd22fe2b413d993b62f8f600feb526e3..ae91f70c47ec3b3df24f7d5271be64d4209ecc2f 100644
--- a/src/sbbs3/str_util.c
+++ b/src/sbbs3/str_util.c
@@ -19,6 +19,8 @@
  * Note: If this box doesn't appear square, then you need to fix your tabs.	*
  ****************************************************************************/
 
+#include "genwrap.h"
+#include "dirwrap.h"
 #include "str_util.h"
 #include "utf8.h"
 #include "unicode.h"
@@ -257,223 +259,6 @@ char* strip_char(const char* str, char* dest, char ch)
 	return retval;
 }
 
-/****************************************************************************/
-/* Pattern matching string search of 'insearchof' in 'pattern'.				*/
-/* pattern matching is case-insensitive										*/
-/* patterns beginning with ';' are comments (never match)					*/
-/* patterns beginning with '!' are reverse-matched (returns FALSE if match)	*/
-/* patterns ending in '~' will match string anywhere (sub-string search)	*/
-/* patterns ending in '^' will match left string fragment only				*/
-/* patterns including '*' must match both left and right string fragments	*/
-/* all other patterns are exact-match checking								*/
-/****************************************************************************/
-BOOL findstr_in_string(const char* search, const char* pattern)
-{
-	char	buf[256];
-	char*	p;
-	char*	last;
-	const char*	splat;
-	size_t	len;
-	BOOL	found = FALSE;
-
-	if(pattern == NULL || search == NULL)
-		return FALSE;
-
-	SAFECOPY(buf, pattern);
-	p = buf;
-
-	if(*p == ';')		/* comment */
-		return FALSE;
-
-	if(*p == '!')	{	/* reverse-match */
-		found = TRUE;
-		p++;
-	}
-
-	truncsp(p);
-	len = strlen(p);
-	if(len > 0) {
-		last = p + len - 1;
-		if(*last == '~') {
-			*last = '\0';
-			if(strcasestr(search, p) != NULL)
-				found = !found; 
-		}
-
-		else if(*last == '^') {
-			if(strnicmp(p, search, len - 1) == 0)
-				found = !found; 
-		}
-
-		else if((splat = strchr(p, '*')) != NULL) {
-			int left = splat - p;
-			int right = len - (left + 1);
-			int slen = strlen(search);
-			if(slen < left + right)
-				return found;
-			if(strnicmp(search, p, left) == 0
-				&& strnicmp(p + left + 1, search + (slen - right), right) == 0)
-				found = !found;
-		}
-
-		else if(stricmp(p, search) == 0)
-			found = !found; 
-	} 
-	return found;
-}
-
-static uint32_t encode_ipv4_address(unsigned int byte[])
-{
-	if(byte[0] > 0xff || byte[1] > 0xff || byte[2] > 0xff || byte[3] > 0xff)
-		return 0;
-	return (byte[0]<<24) | (byte[1]<<16) | (byte[2]<<8) | byte[3];
-}
-
-static uint32_t parse_ipv4_address(const char* str)
-{
-	unsigned int byte[4];
-
-	if(sscanf(str, "%u.%u.%u.%u", &byte[0], &byte[1], &byte[2], &byte[3]) != 4)
-		return 0;
-	return encode_ipv4_address(byte);
-}
-
-static uint32_t parse_cidr(const char* p, unsigned* subnet)
-{
-	unsigned int byte[4];
-
-	if(*p == '!')
-		p++;
-
-	*subnet = 0;
-	if(sscanf(p, "%u.%u.%u.%u/%u", &byte[0], &byte[1], &byte[2], &byte[3], subnet) != 5 || *subnet > 32)
-		return 0;
-	return encode_ipv4_address(byte);
-}
-
-static BOOL is_cidr_match(const char *p, uint32_t ip_addr, uint32_t cidr, unsigned subnet)
-{
-	BOOL	match = FALSE;
-
-	if(*p == '!')
-		match = TRUE;
-
-	if(((ip_addr ^ cidr) >> (32-subnet)) == 0)
-		match = !match;
-
-	return match;
-}
-
-/****************************************************************************/
-/* Pattern matching string search of 'insearchof' in 'list'.				*/
-/****************************************************************************/
-BOOL findstr_in_list(const char* insearchof, str_list_t list)
-{
-	size_t	index;
-	BOOL	found=FALSE;
-	char*	p;
-	uint32_t ip_addr, cidr;
-	unsigned subnet;
-
-	if(list==NULL || insearchof==NULL)
-		return FALSE;
-	ip_addr = parse_ipv4_address(insearchof);
-	for(index=0; list[index]!=NULL; index++) {
-		p=list[index];
-//		SKIP_WHITESPACE(p);
-		if(ip_addr != 0 && (cidr = parse_cidr(p, &subnet)) != 0)
-			found = is_cidr_match(p, ip_addr, cidr, subnet);
-		else
-			found = findstr_in_string(insearchof,p);
-		if(found != (*p=='!'))
-			break;
-	}
-	return found;
-}
-
-/****************************************************************************/
-/* Pattern matching string search of 'insearchof' in 'fname'.				*/
-/****************************************************************************/
-BOOL findstr(const char* insearchof, const char* fname)
-{
-	char		str[256];
-	BOOL		found=FALSE;
-	FILE*		fp;
-	uint32_t	ip_addr, cidr;
-	unsigned	subnet;
-
-	if(insearchof==NULL || fname==NULL)
-		return FALSE;
-
-	if((fp=fopen(fname,"r"))==NULL)
-		return FALSE; 
-
-	ip_addr = parse_ipv4_address(insearchof);
-	while(!feof(fp) && !ferror(fp) && !found) {
-		if(!fgets(str,sizeof(str),fp))
-			break;
-		char* p = str;
-		SKIP_WHITESPACE(p);
-		c_unescape_str(p);
-		if(ip_addr !=0 && (cidr = parse_cidr(p, &subnet)) != 0)
-			found = is_cidr_match(p, ip_addr, cidr, subnet);
-		else
-			found = findstr_in_string(insearchof, p);
-	}
-
-	fclose(fp);
-	return found;
-}
-
-/****************************************************************************/
-/* Searches the file <name>.can in the TEXT directory for matches			*/
-/* Returns TRUE if found in list, FALSE if not.								*/
-/****************************************************************************/
-BOOL trashcan(scfg_t* cfg, const char* insearchof, const char* name)
-{
-	char fname[MAX_PATH+1];
-
-	return(findstr(insearchof,trashcan_fname(cfg,name,fname,sizeof(fname))));
-}
-
-/****************************************************************************/
-char* trashcan_fname(scfg_t* cfg, const char* name, char* fname, size_t maxlen)
-{
-	safe_snprintf(fname,maxlen,"%s%s.can",cfg->text_dir,name);
-	return fname;
-}
-
-static char* process_findstr_item(size_t index, char *str, void* cbdata)
-{
-	SKIP_WHITESPACE(str);
-	return c_unescape_str(str);
-}
-
-/****************************************************************************/
-str_list_t findstr_list(const char* fname)
-{
-	FILE*	fp;
-	str_list_t	list;
-
-	if((fp=fopen(fname,"r"))==NULL)
-		return NULL;
-
-	list=strListReadFile(fp, NULL, /* Max line length: */255);
-	strListModifyEach(list, process_findstr_item, /* cbdata: */NULL);
-
-	fclose(fp);
-
-	return list;
-}
-
-/****************************************************************************/
-str_list_t trashcan_list(scfg_t* cfg, const char* name)
-{
-	char	fname[MAX_PATH+1];
-
-	return findstr_list(trashcan_fname(cfg, name, fname, sizeof(fname)));
-}
-
 /****************************************************************************/
 /* Returns in 'string' a character representation of the number in l with   */
 /* thousands separators (e.g. commas).										*/
@@ -910,81 +695,3 @@ char* utf8_to_cp437_inplace(char* str)
 		,/* decode error char: */CP437_INVERTED_EXCLAMATION_MARK);
 }
 
-char* sub_newsgroup_name(scfg_t* cfg, sub_t* sub, char* str, size_t size)
-{
-	memset(str, 0, size);
-	if(sub->newsgroup[0])
-		strncpy(str, sub->newsgroup, size - 1);
-	else {
-		snprintf(str, size - 1, "%s.%s", cfg->grp[sub->grp]->sname, sub->sname);
-		/*
-		 * From RFC5536:
-		 * newsgroup-name  =  component *( "." component )
-		 * component       =  1*component-char
-		 * component-char  =  ALPHA / DIGIT / "+" / "-" / "_"
-		 */
-		if (str[0] == '.')
-			str[0] = '_';
-		size_t c;
-		for(c = 0; str[c] != 0; c++) {
-			/* Legal characters */
-			if ((str[c] >= 'A' && str[c] <= 'Z')
-					|| (str[c] >= 'a' && str[c] <= 'z')
-					|| (str[c] >= '0' && str[c] <= '9')
-					|| str[c] == '+'
-					|| str[c] == '-'
-					|| str[c] == '_'
-					|| str[c] == '.')
-				continue;
-			str[c] = '_';
-		}
-		c--;
-		if (str[c] == '.')
-			str[c] = '_';
-	}
-	return str;
-}
-
-char* sub_area_tag(scfg_t* cfg, sub_t* sub, char* str, size_t size)
-{
-	char* p;
-
-	memset(str, 0, size);
-	if(sub->area_tag[0])
-		strncpy(str, sub->area_tag, size - 1);
-	else if(sub->newsgroup[0])
-		strncpy(str, sub->newsgroup, size - 1);
-	else {
-		strncpy(str, sub->sname, size - 1);
-		REPLACE_CHARS(str, ' ', '_', p);
-	}
-	strupr(str);
-	return str;
-}
-
-char* dir_area_tag(scfg_t* cfg, dir_t* dir, char* str, size_t size)
-{
-	char* p;
-
-	memset(str, 0, size);
-	if(dir->area_tag[0])
-		strncpy(str, dir->area_tag, size - 1);
-	else {
-		strncpy(str, dir->sname, size - 1);
-		REPLACE_CHARS(str, ' ', '_', p);
-	}
-	strupr(str);
-	return str;
-}
-
-char* get_ctrl_dir(BOOL warn)
-{
-	char* p = getenv("SBBSCTRL");
-	if(p == NULL || *p == '\0') {
-		if(warn)
-			fprintf(stderr, "!SBBSCTRL environment variable not set, using default value: " SBBSCTRL_DEFAULT "\n\n");
-		p = SBBSCTRL_DEFAULT;
-	}
-	return p;
-}
-
diff --git a/src/sbbs3/str_util.h b/src/sbbs3/str_util.h
index 5b9e4b44a4325213c2e707ebf23f910c2d83ae58..8acc11918f5beb7cd095a7465adcfd209dda8ddd 100644
--- a/src/sbbs3/str_util.h
+++ b/src/sbbs3/str_util.h
@@ -22,7 +22,7 @@
 #ifndef _STR_UTIL_H_
 #define _STR_UTIL_H_
 
-#include "scfgdefs.h"	// scfg_t
+#include "gen_defs.h"
 #include "dllexport.h"
 
 #ifdef __cplusplus
@@ -45,13 +45,6 @@ DLLEXPORT char *    replace_named_values(const char* src ,char* buf, size_t bufl
                        named_int_t* int_list, BOOL case_sensitive);
 DLLEXPORT char *	condense_whitespace(char* str);
 DLLEXPORT char		exascii_to_ascii_char(uchar ch);
-DLLEXPORT BOOL		findstr(const char *insearch, const char *fname);
-DLLEXPORT BOOL		findstr_in_string(const char* insearchof, const char* pattern);
-DLLEXPORT BOOL		findstr_in_list(const char* insearchof, str_list_t list);
-DLLEXPORT str_list_t findstr_list(const char* fname);
-DLLEXPORT BOOL		trashcan(scfg_t* cfg, const char *insearch, const char *name);
-DLLEXPORT char *	trashcan_fname(scfg_t* cfg, const char *name, char* fname, size_t);
-DLLEXPORT str_list_t trashcan_list(scfg_t* cfg, const char* name);
 DLLEXPORT char *	convert_ansi(const char* src, char* dest, size_t, int width, BOOL ice_color);
 DLLEXPORT char *	strip_ansi(char* str);
 DLLEXPORT char *	strip_exascii(const char *str, char* dest);
@@ -59,7 +52,6 @@ DLLEXPORT char *	strip_cp437_graphics(const char *str, char* dest);
 DLLEXPORT char *	strip_space(const char *str, char* dest);
 DLLEXPORT char *	strip_ctrl(const char *str, char* dest);
 DLLEXPORT char *	strip_char(const char* str, char* dest, char);
-DLLEXPORT char *	net_addr(net_t* net);
 DLLEXPORT BOOL		valid_ctrl_a_attr(char a);
 DLLEXPORT BOOL		valid_ctrl_a_code(char a);
 DLLEXPORT size_t	strip_invalid_attr(char *str);
@@ -70,10 +62,6 @@ DLLEXPORT uint32_t	str_to_bits(uint32_t currval, const char *str);
 DLLEXPORT BOOL		str_has_ctrl(const char*);
 DLLEXPORT BOOL		str_is_ascii(const char*);
 DLLEXPORT char *	utf8_to_cp437_inplace(char* str);
-DLLEXPORT char *	sub_newsgroup_name(scfg_t*, sub_t*, char*, size_t);
-DLLEXPORT char *	sub_area_tag(scfg_t*, sub_t*, char*, size_t);
-DLLEXPORT char *	dir_area_tag(scfg_t*, dir_t*, char*, size_t);
-DLLEXPORT char * 	get_ctrl_dir(BOOL warn);
 
 #ifdef __cplusplus
 }
diff --git a/src/sbbs3/textgen.vcxproj b/src/sbbs3/textgen.vcxproj
index ca934051992391d8e7534f6afb9e8eb9cb40bb41..29a6f3c99867cd91e710eb9e7cfb2ba43fbc61fe 100644
--- a/src/sbbs3/textgen.vcxproj
+++ b/src/sbbs3/textgen.vcxproj
@@ -156,6 +156,7 @@
   </ItemDefinitionGroup>
   <ItemGroup>
     <ClCompile Include="..\encode\utf8.c" />
+    <ClCompile Include="getctrl.c" />
     <ClCompile Include="str_util.c" />
     <ClCompile Include="textgen.c">
       <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
diff --git a/src/sbbs3/userdat.c b/src/sbbs3/userdat.c
index 88d62e8cfb0b01eae5e3b9a2c4655a3ada1ffe95..b800fe26c3b096242fcbf8fc100bdfa5de27b0fa 100644
--- a/src/sbbs3/userdat.c
+++ b/src/sbbs3/userdat.c
@@ -20,6 +20,7 @@
  ****************************************************************************/
 
 #include "str_util.h"
+#include "findstr.h"
 #include "cmdshell.h"
 #include "userdat.h"
 #include "filedat.h"