diff --git a/src/sbbs3/GNUmakefile b/src/sbbs3/GNUmakefile
index 1eeba71b343ec5ff316e12134d896fcbc3ed13a2..6a69f59b5aa33e610e861d5b50ac61405dab557a 100644
--- a/src/sbbs3/GNUmakefile
+++ b/src/sbbs3/GNUmakefile
@@ -322,6 +322,11 @@ $(FMSGDUMP): $(FMSGDUMP_OBJS) | $(EXEODIR) $(OBJODIR)
 	@echo Linking $@
 	$(QUIET)$(CC) $(CONSOLE_LDFLAGS) -o $@ $(FMSGDUMP_OBJS)
 
+# TRASHMAN
+$(TRASHMAN): $(TRASHMAN_OBJS) | $(EXEODIR) $(OBJODIR)
+	@echo Linking $@
+	$(QUIET)$(CC) $(CONSOLE_LDFLAGS) -o $@ $^ -lm
+
 $(UPGRADE_TO_V319): $(UPGRADE_TO_V319_OBJS) $(OBJODIR) | $(EXEODIR)
 	@echo Linking $@
 	$(QUIET)$(CC) $(CONSOLE_LDFLAGS) -o $@ $(UPGRADE_TO_V319_OBJS) $(SMBLIB_LIBS) $(XPDEV_LIBS) $(ENCODE_LIBS) $(HASH_LIBS) $(FILE_LIBS)
diff --git a/src/sbbs3/objects.mk b/src/sbbs3/objects.mk
index ff20d2664ebb496a80733880dd671fedc40b4280..4261783d089a46ab189cad975c1678a018cf9c31 100644
--- a/src/sbbs3/objects.mk
+++ b/src/sbbs3/objects.mk
@@ -296,4 +296,7 @@ TEXTGEN_OBJS = 		$(OBJODIR)/textgen$(OFILE) \
 			$(OBJODIR)/getctrl$(OFILE) \
 			$(OBJODIR)/str_util$(OFILE)
 
-
+TRASHMAN_OBJS = 	$(OBJODIR)/trashman$(OFILE) \
+			$(OBJODIR)/trash$(OFILE) \
+ 		  	$(OBJODIR)/nopen$(OFILE) \
+			$(OBJODIR)/findstr$(OFILE)
diff --git a/src/sbbs3/scfglib.h b/src/sbbs3/scfglib.h
index 85f07c30be110d0dcba1494cbfd93c3b31bb4ce3..51f02132f89c49fb1e353c645719170eb227a851 100644
--- a/src/sbbs3/scfglib.h
+++ b/src/sbbs3/scfglib.h
@@ -78,9 +78,6 @@ DLLEXPORT bool	is_valid_grpnum(scfg_t*, int);
 DLLEXPORT bool	is_valid_xtrnnum(scfg_t*, int);
 DLLEXPORT bool	is_valid_xtrnsec(scfg_t*, int);
 
-DLLEXPORT char *	trashcan_fname(scfg_t* cfg, const char *name, char* fname, size_t);
-DLLEXPORT char *	twitlist_fname(scfg_t* cfg, char* fname, size_t);
-
 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);
diff --git a/src/sbbs3/scfglib1.c b/src/sbbs3/scfglib1.c
index 56ca69705cde99529e73ae5c1288cc4f5152e794..3ba078f6f355bddbf1fbd1595c4f98b1d57fcc42 100644
--- a/src/sbbs3/scfglib1.c
+++ b/src/sbbs3/scfglib1.c
@@ -931,20 +931,6 @@ faddr_t* nearest_sysfaddr(scfg_t* cfg, faddr_t* addr)
 	return &cfg->faddr[i];
 }
 
-/****************************************************************************/
-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;
-}
-
-/****************************************************************************/
-char* twitlist_fname(scfg_t* cfg, char* fname, size_t maxlen)
-{
-	safe_snprintf(fname, maxlen, "%stwitlist.cfg", cfg->ctrl_dir);
-	return fname;
-}
-
 char* sub_newsgroup_name(scfg_t* cfg, sub_t* sub, char* str, size_t size)
 {
 	memset(str, 0, size);
diff --git a/src/sbbs3/targets.mk b/src/sbbs3/targets.mk
index f23a519c8213126a85e1bacd0be0a537e726afa2..117d6154752e99ab29222e25edeed021062f2d67 100644
--- a/src/sbbs3/targets.mk
+++ b/src/sbbs3/targets.mk
@@ -34,6 +34,7 @@ DUPEFIND	= $(EXEODIR)/dupefind$(EXEFILE)
 READSAUCE	= $(EXEODIR)/readsauce$(EXEFILE)
 PKTDUMP		= $(EXEODIR)/pktdump$(EXEFILE)
 FMSGDUMP	= $(EXEODIR)/fmsgdump$(EXEFILE)
+TRASHMAN	= $(EXEODIR)/trashman$(EXEFILE)
 UPGRADE_TO_V319 = $(EXEODIR)/upgrade_to_v319$(EXEFILE)
 UPGRADE_TO_V320 = $(EXEODIR)/upgrade_to_v320$(EXEFILE)
 
@@ -45,7 +46,9 @@ UTILS		= $(FIXSMB) $(CHKSMB) \
 			  $(QWKNODES) $(SLOG) \
 			  $(DELFILES) $(DUPEFIND) \
 			  $(SEXYZ) $(READSAUCE) \
-			  $(PKTDUMP) $(FMSGDUMP) $(UPGRADE_TO_V319) \
+			  $(PKTDUMP) $(FMSGDUMP) \
+			  $(TRASHMAN) \
+			  $(UPGRADE_TO_V319) \
 			  $(UPGRADE_TO_V320)
 
 GIT_INFO	= git_hash.h git_branch.h
@@ -74,7 +77,7 @@ standalone-utils: $(FIXSMB) $(CHKSMB) \
 			  $(QWKNODES) \
 			  $(DELFILES) $(DUPEFIND) \
 			  $(SEXYZ) $(READSAUCE) \
-			  $(PKTDUMP) $(FMSGDUMP)
+			  $(PKTDUMP) $(FMSGDUMP) $(TRASHMAN)
 
 .PHONY: libdeps
 libdeps: $(JS_DEPS) gitinfo smblib xpdev-mt $(MTOBJODIR) $(LIBODIR)
@@ -200,6 +203,7 @@ $(SLOG): $(XPDEV_LIB)
 $(DELFILES): $(XPDEV_LIB) $(SMBLIB)
 $(DUPEFIND): $(XPDEV_LIB) $(SMBLIB)
 $(READSAUCE): $(XPDEV_LIB)
+$(TRASHMAN): $(XPDEV_LIB)
 $(UPGRADE_TO_V319): $(XPDEV_LIB) $(SMBLIB)
 $(UPGRADE_TO_V320): $(XPDEV_LIB)
 
diff --git a/src/sbbs3/trash.c b/src/sbbs3/trash.c
index 90207e0998ff45cdd290e32e46f179080f117d28..56f818228027f57ed69f9fa9c62cd97831eb9661 100644
--- a/src/sbbs3/trash.c
+++ b/src/sbbs3/trash.c
@@ -27,6 +27,20 @@
 #include "findstr.h"
 #include "nopen.h"
 
+/****************************************************************************/
+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;
+}
+
+/****************************************************************************/
+char* twitlist_fname(scfg_t* cfg, char* fname, size_t maxlen)
+{
+	safe_snprintf(fname, maxlen, "%stwitlist.cfg", cfg->ctrl_dir);
+	return fname;
+}
+
 /****************************************************************************/
 /* Searches the file <name>.can in the TEXT directory for matches			*/
 /* Returns true if found in list, false if not.								*/
@@ -38,23 +52,32 @@ bool trashcan(scfg_t* cfg, const char* insearchof, const char* name)
 }
 
 /****************************************************************************/
-static void parse_trash_details(char* p, struct trash* trash)
+bool trash_parse_details(const char* p, struct trash* trash, char* item, size_t size)
 {
 	memset(trash, 0, sizeof(*trash));
 
-	str_list_t list = strListSplit(NULL, p, "\t");
+	str_list_t list = strListSplitCopy(NULL, p, "\t");
 	if(list == NULL)
-		return;
-
-	trash->added = iniGetDateTime(list, ROOT_SECTION, "t", 0);
-	trash->expires = iniGetDateTime(list, ROOT_SECTION, "e", 0);
-	if((p = iniGetValue(list, ROOT_SECTION, "p", NULL, NULL)) != NULL)
-		SAFECOPY(trash->prot, p);
-	if((p = iniGetValue(list, ROOT_SECTION, "u", NULL, NULL)) != NULL)
-		SAFECOPY(trash->user, p);
-	if((p = iniGetValue(list, ROOT_SECTION, "r", NULL, NULL)) != NULL)
-		SAFECOPY(trash->reason, p);
+		return false;
+
+	if(item != NULL && size > 0) {
+		if(list[0] == NULL)
+			*item = '\0';
+		else
+			strlcpy(item, list[0], size);
+	}
+	if(strListFastDelete(list, /* index: */0, /* count: */1)) {
+		trash->added = iniGetDateTime(list, ROOT_SECTION, "t", 0);
+		trash->expires = iniGetDateTime(list, ROOT_SECTION, "e", 0);
+		if((p = iniGetValue(list, ROOT_SECTION, "p", NULL, NULL)) != NULL)
+			SAFECOPY(trash->prot, p);
+		if((p = iniGetValue(list, ROOT_SECTION, "u", NULL, NULL)) != NULL)
+			SAFECOPY(trash->user, p);
+		if((p = iniGetValue(list, ROOT_SECTION, "r", NULL, NULL)) != NULL)
+			SAFECOPY(trash->reason, p);
+	}
 	strListFree(&list);
+	return true;
 }
 
 /****************************************************************************/
@@ -87,8 +110,8 @@ bool trashcan2(scfg_t* cfg, const char* str1, const char* str2, const char* name
 	if(!find2strs(str1, str2, trashcan_fname(cfg,name,fname,sizeof(fname)), details))
 		return false;
 	if(trash != NULL) {
-		parse_trash_details(details, trash);
-		if(trash->expires && trash->expires <= time(NULL))
+		if(trash_parse_details(details, trash, NULL, 0)
+			&& trash->expires && trash->expires <= time(NULL))
 			return false;
 	}
 	return true;
@@ -102,8 +125,8 @@ bool trash_in_list(const char* str1, const char* str2, str_list_t list, struct t
 	if(!find2strs_in_list(str1, str2, list, details))
 		return false;
 	if(trash != NULL) {
-		parse_trash_details(details, trash);
-		if(trash->expires && trash->expires <= time(NULL))
+		if(trash_parse_details(details, trash, NULL, 0)
+			&& trash->expires && trash->expires <= time(NULL))
 			return false;
 	}
 	return true;
diff --git a/src/sbbs3/trash.h b/src/sbbs3/trash.h
index f37630e85a6e5ae8d16b38a0d52c7b017b1e73d9..a94f638300ebfd5c00b692e6b900c2231b4ad78c 100644
--- a/src/sbbs3/trash.h
+++ b/src/sbbs3/trash.h
@@ -41,9 +41,12 @@ struct trash {
 extern "C" {
 #endif
 
+DLLEXPORT char*		trashcan_fname(scfg_t* cfg, const char *name, char* fname, size_t);
+DLLEXPORT char*		twitlist_fname(scfg_t* cfg, char* fname, size_t);
 DLLEXPORT bool		trashcan(scfg_t* cfg, const char *insearch, const char *name);
 DLLEXPORT bool		trashcan2(scfg_t* cfg, const char* str1, const char* str2, const char *name, struct trash*);
 DLLEXPORT bool		trash_in_list(const char* str1, const char* str2, str_list_t list, struct trash*);
+DLLEXPORT bool		trash_parse_details(const char* p, struct trash* trash, char* item, size_t);
 DLLEXPORT char *	trash_details(const struct trash*, char* str, size_t);
 DLLEXPORT str_list_t trashcan_list(scfg_t* cfg, const char* name);
 DLLEXPORT bool		is_host_exempt(scfg_t*, const char* ip_addr, const char* host_name);
diff --git a/src/sbbs3/trashman.c b/src/sbbs3/trashman.c
new file mode 100644
index 0000000000000000000000000000000000000000..e2b7b5e7817f93854814494105795bb569d1962f
--- /dev/null
+++ b/src/sbbs3/trashman.c
@@ -0,0 +1,107 @@
+/* Synchronet client/content-filtering (trashcan/twit) maintenance */
+
+/****************************************************************************
+ * @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 "trash.h"
+//#include "datewrap.h"
+//#include "xpdatetime.h"
+//#include "ini_file.h"
+//#include "scfglib.h"
+#include "findstr.h"
+#include "nopen.h"
+
+int verbosity = 0;
+
+int maint(const char* fname)
+{
+	int removed = 0;
+	str_list_t list = findstr_list(fname);
+	if(list == NULL) {
+		perror(fname);
+		return -1;
+	}
+	FILE* fp = fnopen(NULL, fname, O_WRONLY|O_TRUNC);
+	if(fp == NULL) {
+		perror(fname);
+		return -2;
+	}
+	time_t now = time(NULL);
+	for(int i = 0; list[i] != NULL; ++i) {
+		struct trash trash;
+		char item[256];
+		if(!trash_parse_details(list[i], &trash, item, sizeof item)) {
+			fputs(list[i], fp);
+			continue;
+		}
+		if(verbosity > 1) {
+			char details[256];
+			printf("%s %s\n", item, trash_details(&trash, details, sizeof details));
+		}
+		if(trash.expires && trash.expires < now) {
+			if(verbosity > 0)
+				printf("%s expired %s", item, ctime(&trash.expires));
+			++removed;
+			continue;
+		}
+		fputs(list[i], fp);
+	}
+	fclose(fp);
+	strListFree(&list);
+	return removed;
+}
+
+int usage(const char* prog)
+{
+	printf("usage: %s [-v][...] /path/to/file1.can [/path/to/file2.can][...]\n"
+		,getfname(prog));
+	return EXIT_SUCCESS;
+}
+
+int main(int argc, const char** argv)
+{
+	printf("\nSynchronet trash/filter file manager v1.0\n");
+
+	if(argc < 2)
+		return usage(argv[0]);
+	for(int i = 1; i < argc; ++i) {
+		const char* arg = argv[i];
+		if(*arg != '-')
+			continue;
+		switch(arg[1]) {
+			case 'v':
+				++verbosity;
+				break;
+			default:
+				return usage(argv[0]);
+		}
+	}
+	int total = 0;
+	for(int i = 1; i < argc; ++i) {
+		const char* arg = argv[i];
+		if(*arg == '-')
+			continue;
+		int  removed = maint(arg);
+		if(removed < 0)
+			return EXIT_FAILURE;
+		total += removed;
+	}
+	printf("%d total items removed\n", total);
+	return EXIT_SUCCESS;
+}