diff --git a/ctrl/text.dat b/ctrl/text.dat
index 6c14523244d973dca5ed13a31a59773bddd82154..5699f8a9aea973aeaee6a35b24426ebf12610fda 100644
--- a/ctrl/text.dat
+++ b/ctrl/text.dat
@@ -50,9 +50,9 @@
 "\1[\1n\1mPosted on \1h%s\1n\1m %s.\r\n"                      037 Posted
 "\7\1_\1w\1hNode %2d: \1g%s\1n\1g sent you E-mail.\r\n"       038 EmailNodeMsg
 "\1n\r\nYou can't forward mail.\r\n"                     039 R_Forward
-"\1n\1m\r\nForwarded by \1h%s\1n\1m on "\                    040 ForwardedFrom
+"\1n\1mForwarded by \1h%s\1n\1m on "\                    040 ForwardedFrom
 	"\1h%s\1n\r\n"
-"\1n\1m\r\nMail forwarded to \1h%s \1n\1m#%d.\1n\r\n"         041 Forwarded
+"\1n\1m\r\nMail forwarded to \1h%s\1n\r\n"                    041 Forwarded
 "\1b\1hAuto message by: \1c%s\1b on %s\1n\r\n\r\n"            042 AutoMsgBy
 "\r\nAuto Message - ~Read, ~Write, or ~Quit: "          043 AutoMsg
 "\1n\r\nYou can't write to the auto-message.\r\n"        044 R_AutoMsg
diff --git a/exec/emailval.js b/exec/emailval.js
index d04927377864fcabc8ef6623b282ac04d2023e73..b9f368bf6b629bd1188e75ddca86839cf0406ea5 100644
--- a/exec/emailval.js
+++ b/exec/emailval.js
@@ -1,4 +1,4 @@
-// $Id: emailval.js,v 1.7 2019/07/15 04:41:35 rswindell Exp $
+
 /*******************************************************************************
 Originally based on:
 FILE: emailval.js v0.2
@@ -21,7 +21,7 @@ STEP 2:
 Edit ctrl/modopts.ini in a text editor and edit the following values in the
 [emailval] section (create if it necessary) to match your pre-validation and
 post-validation security levels.
-        level_before_validation (default: 50)
+		level_before_validation (default: 50)
 		level_after_validation (default: 60)
 		flags1_after_validation (default: no change)
 		flags2_after_validation (default: no change)
@@ -31,6 +31,8 @@ post-validation security levels.
 		restrictions_after_validation (default: no change)
 		expiration_after_validation (default: false)
 		expiration_days_after_validation (default: no change)
+		valid_chars=ACDEFHJKLMNPQRTUVWXY23456789!@#$%&*
+		code_length=16
 
 Note: the flags, exemptions, and restrictions .ini values support 'A' through
       'Z' with the optional '+' (add) and '-' (remove) modifiers.
@@ -65,9 +67,10 @@ if(options.level_after_validation === undefined)
 	options.level_after_validation = 60;
 
 //other constants, shouldn't need changing.
-var cValChars='ACDEFHJKLMNPQRTUVWXY23456789!@#$%&*';
+var cValChars = options.valid_chars !== undefined ? options.valid_chars :
+    'ACDEFHJKLMNPQRTUVWXY23456789!@#$%&*';
 var cPrevalText = "telvalcode";
-var cValCodeLen = 16;
+var cValCodeLen = options.code_length !== undefined ? options.code_length : 16;
 
 //include SBBS Definition constants
 require("sbbsdefs.js", 'NET_NONE'); 
@@ -214,4 +217,4 @@ function CheckValidation() {
 		}
 	}
 }
-CheckValidation();
\ No newline at end of file
+CheckValidation();
diff --git a/exec/init-fidonet.ini b/exec/init-fidonet.ini
index 0591376899a7cad042950b37ba3d27a2aaaca894..7c48a208e9915cb6c959f9491de9df5cb2c9b716 100644
--- a/exec/init-fidonet.ini
+++ b/exec/init-fidonet.ini
@@ -98,6 +98,16 @@ fido  = 1:19/37
 host  = hub.cybernetbbs.net
 areatag_prefix = CN_
 
+[zone:42]
+name = SFNet
+desc = Science Fiction
+pack = http://furmenservices.net/sfnet.zip
+coord = Dallas Vinson
+addr = 42:1/1
+host = furmenservices.net
+echolist = sfnet.na
+areatag_prefix = SF_
+
 [zone:44]
 name  = DoRENET
 desc  = BBS modifications, coding, ansi/asci etc
diff --git a/exec/install-xtrn.js b/exec/install-xtrn.js
index ded2392bca380079860efa02adae69c673d2c8a6..7b5deb824466c7689cc2ed475944197be6903da2 100644
--- a/exec/install-xtrn.js
+++ b/exec/install-xtrn.js
@@ -25,6 +25,12 @@
 //
 // The .ini sections and keys supported (zero or more of each may be included):
 //
+// [pre-exec:<file>.js [args]] ; execute file.js before installing programs
+//		startup_dir		= directory to make current before execution
+//
+// [pre-eval:<js-expression>] ; evaluate js-expression before installing progs
+//      cmd             = evaluate this string rather than the js-expression
+//
 // [prog:<code>]
 // 		name 			= program name or description (40 chars max)
 //      cats            = additional target installation categories (sections)
@@ -61,10 +67,10 @@
 // [service:<protocol>]
 //      see ctrl/services.ini 
 //
-// [exec:<file>.js [args]]  ; execute file.js with these arguments
+// [exec:<file>.js [args]] ; execute file.js with these arguments - after
 //		startup_dir		= directory to make current before execution
 //
-// [eval:<js-expression>]
+// [eval:<js-expression>] ; evaluate js-expression after installing programs
 //      cmd             = evaluate this string rather than the js-expression
 //
 // [ini:<filename.ini>[:section]]
@@ -75,8 +81,9 @@
 // Additionally, each section can have the following optional keys that are
 // only used by this script (i.e. not written to any configuration files):
 //		note			= note to sysop displayed before installation
+//		fail			= note to sysop displayed upon failure
 //      prompt          = confirmation prompt (or false if no prompting)
-//		required		= if true, this item must be installed to continue
+//		required		= if true, this item must be successful to continue
 //      last            = if true, this item will be the last of its type
 //      done            = if true, no more installer items will be processed
 //
@@ -91,7 +98,7 @@
 
 "use strict";
 
-const REVISION = "3.18b";
+const REVISION = "3.18c";
 const ini_fname = "install-xtrn.ini";
 
 load("sbbsdefs.js");
@@ -210,6 +217,76 @@ function install_xtrn_item(cnf, type, name, desc, item, cats)
 	return true;
 }
 
+function install_exec_cmd(ini_file, section, startup_dir)
+{
+	var list = ini_file.iniGetAllObjects("cmd", section);
+	for (var i = 0; i < list.length; i++) {
+		var item = list[i];
+		var js_args = item.cmd.split(/\s+/);
+		var js_file = js_args.shift();
+
+		if (file_getext(js_file).toLowerCase() != ".js")
+			return "Only '.js' files may be executed: " + js_file;
+
+		if (item.note)
+			print(item.note);
+
+		var prompt = "Execute: " + item.cmd;
+		if (item.prompt !== undefined)
+			prompt = item.prompt;
+
+		if (prompt && !confirm(prompt)) {
+			if (item.required == true)
+				return prompt + " is required to continue";
+			continue;
+		}
+
+		if (item.startup_dir === undefined)
+			item.startup_dir = startup_dir;
+		if (!item.args)
+			item.args = "";
+		var result = js.exec.apply(null
+			,[js_file, item.startup_dir, {}].concat(js_args));
+		if (result !== 0 && item.required)
+			return item.fail || ("Error " + result + " executing " + item.cmd);
+		if (item.last === true)
+			break;
+		if (item.done)
+			return false;
+	}
+	return true;
+}
+
+function install_eval_cmd(ini_file, section, startup_dir)
+{
+	var list = ini_file.iniGetAllObjects("str", section);
+	for (var i = 0; i < list.length; i++) {
+		var item = list[i];
+		if (!item.cmd)
+			item.cmd = item.str; // the str can't contain [], so allow cmd to override
+		var prompt = "Evaluate: " + item.cmd;
+		if (item.prompt !== undefined)
+			prompt = item.prompt;
+		if (prompt && !confirm(prompt)) {
+			if (item.required == true)
+				return prompt + " is required to continue";
+			continue;
+		}
+		try {
+			var result = eval(item.cmd);
+		} catch(e) {
+			return e;
+		}
+		if (!result && item.required)
+			return item.fail || ("Truthful evaluation of '" + item.cmd + "' is required to continue");
+		if (item.last === true)
+			break;
+		if (item.done)
+			return false;
+	}
+	return true;
+}
+
 function install(ini_fname)
 {
 	ini_fname = fullpath(ini_fname);
@@ -254,6 +331,14 @@ function install(ini_fname)
 	var startup_dir = ini_fname.substr(0, Math.max(ini_fname.lastIndexOf("/"), ini_fname.lastIndexOf("\\"), 0));
 	startup_dir = relpath.get(system.ctrl_dir, startup_dir);
 
+	var result = install_exec_cmd(ini_file, "pre-exec:", startup_dir);
+	if(result !== true)
+		return result;
+
+	result = install_eval_cmd(ini_file, "pre-eval:", startup_dir);
+	if(result !== true)
+		return result;
+
 	const types = {
 		prog:	{ desc: "External Program", 	struct: "xtrn" },
 		event:	{ desc: "External Timed Event", struct: "event" },
@@ -276,7 +361,7 @@ function install(ini_fname)
 				return false;
 			if(item.last === true)
 				break;
-			done = item.done;
+			done = Boolean(item.done);
 		}
 	}
 	
@@ -318,7 +403,7 @@ function install(ini_fname)
 			return false;
 		if(item.last === true)
 			break;
-		done = item.done;
+		done = Boolean(item.done);
 	}
 
 	var services_ini = new File(file_cfgname(system.ctrl_dir, "services.ini"));
@@ -358,69 +443,21 @@ function install(ini_fname)
 			return false;
 		if(item.last === true)
 			break;
-		done = item.done;
+		done = Boolean(item.done);
 	}
 	
-	var list = ini_file.iniGetAllObjects("cmd", "exec:");
-	for (var i = 0; i < list.length && !done; i++) {
-		var item = list[i];
-		var js_args = item.cmd.split(/\s+/);
-		var js_file = js_args.shift();
-		
-		if (file_getext(js_file).toLowerCase() != ".js")
-			return "Only '.js' files may be executed: " + js_file;
-
-		if (item.note)
-			print(item.note);
-		
-		var prompt = "Execute: " + item.cmd;
-		if (item.prompt !== undefined)
-			prompt = item.prompt;
-	
-		if (prompt && !confirm(prompt)) {
-			if (item.required == true)
-				return prompt + " is required to continue";
-			continue;
-		}
-
-		if (item.startup_dir === undefined)
-			item.startup_dir = startup_dir;
-		if (!item.args)
-			item.args = "";
-		var result = js.exec.apply(null
-			,[js_file, item.startup_dir, {}].concat(js_args));
-		if (result !== 0 && item.required)
-			return "Error " + result + " executing " + item.cmd;
-		if(item.last === true)
-			return true;
-		done = item.done;
+	if(done === false) {
+		result = install_exec_cmd(ini_file, "exec:", startup_dir);
+		if(typeof result !== 'boolean')
+			return result;
+		done = !result;
 	}
-	
-	var list = ini_file.iniGetAllObjects("str", "eval:");
-	for (var i = 0; i < list.length && !done; i++) {
-		var item = list[i];
-		if (!item.cmd)
-			item.cmd = item.str; // the str can't contain [], so allow cmd to override
-		var prompt = "Evaluate: " + item.cmd;
-		if (item.prompt !== undefined)
-			prompt = item.prompt;
-		if (prompt && !confirm(prompt)) {
-			if (item.required == true)
-				return prompt + " is required to continue";
-			continue;
-		}
-		try {
-			var result = eval(item.cmd);
-		} catch(e) {
-			return e;
-		}
-		if (!result) {
-			if (item.required == true)
-				return "Truthful evaluation of '" + item.cmd + "' is required to continue";
-		}
-		if(item.last === true)
-			return true;
-		done = item.done;
+
+	if(done === false) {
+		result = install_eval_cmd(ini_file, "eval:", startup_dir);
+		if(typeof result !== 'boolean')
+			return result;
+		done = !result;
 	}
 
 	if (installed) {
diff --git a/exec/load/binkp.js b/exec/load/binkp.js
index 3cf4df88ce5978bfaa4451c2399ea03dfd1487ce..e2a05882cd7353f0e4b6e117343c0da9bc662d19 100644
--- a/exec/load/binkp.js
+++ b/exec/load/binkp.js
@@ -1,4 +1,4 @@
-const binkp_revision = 3;
+const binkp_revision = 4;
 
 require('sockdefs.js', 'SOCK_STREAM');
 require('fido.js', 'FIDO');
@@ -940,15 +940,18 @@ BinkP.prototype.close = function()
 				this.sendCmd(this.command.M_EOB);
 		}
 		// Attempt a super-duper graceful shutdown to prevent RST...
-		this.sock.is_writeable = false;
-		remain = this.timeout;
-		end = time() + remain;
-		do {
-			if (this.sock.recv(2048, remain) == 0)
-				break;
-			remain = end - time();
-		} while (remain > 0);
-		this.sock.close();
+		if (this.sock !== undefined) {
+			this.sock.is_writeable = false;
+			remain = this.timeout;
+			end = time() + remain;
+			do {
+				if (this.sock.recv(2048, remain) == 0)
+					break;
+				remain = end - time();
+			} while (remain > 0);
+			this.sock.close();
+			this.sock = undefined;
+		}
 	}
 	this.tx_queue.forEach(function(file) {
 		file.file.close();
@@ -1237,7 +1240,7 @@ BinkP.prototype.recvFrame = function(timeout)
 									log(LOG_WARNING, 'Peer ended their VER with " '+m[2]+'" instead of the required " binkp/1.1", but we\'re assuming binkp 1.1 anyway');
 								}
 								log(LOG_DEBUG, "Parsed BinkP version: " + binkp_ver);
-								this.ver_1_1 = binkp_ver >= 1.1;
+								this.ver1_1 = binkp_ver >= 1.1;
 							}
 							break;
 						case 'ZYZ':
diff --git a/exec/load/json-client.js b/exec/load/json-client.js
index a4341ba2c286efbbec4d652d73f26f32dd3cdb58..2552a4411c4db2d6de5cdb1ee1f2a6e3d040a704 100644
--- a/exec/load/json-client.js
+++ b/exec/load/json-client.js
@@ -17,8 +17,9 @@ load("json-sock.js");
 	-	JSONClient.read(scope,location,lock);
 	-	JSONClient.pop(scope,location,lock);
 	-	JSONClient.shift(scope,location,lock);
-	-	JSONClient.write(scope,location,lock);
-	-	JSONClient.push(scope,location,lock);
+	-	JSONClient.write(scope,location,data,lock);
+	-	JSONClient.push(scope,location,data,lock);
+	-	JSONClient.remove(scope,location,lock);
 	-	JSONClient.unshift(scope,location,lock);
 	-	JSONClient.splice(scope,location,start,end,data,lock)
 	-	JSONClient.slice(scope,location,start,end,lock)
diff --git a/exec/load/xtrnmenulib.js b/exec/load/xtrnmenulib.js
new file mode 100644
index 0000000000000000000000000000000000000000..e7cff9fab33e45554f2dd4c01a553cac101bcc5d
--- /dev/null
+++ b/exec/load/xtrnmenulib.js
@@ -0,0 +1,410 @@
+/**
+ * Custom External Program Menu Library for Custom External Program Menus
+ * by Michael Long mlong  innerrealmbbs.us
+ * 
+ * This provides common functionality for retrieving menus used by 
+ * the loadable module xtrnmenu.js and by the web interface
+ * 099-xtrnmenu-games.xjs
+ */
+
+"use strict";
+
+load("sbbsdefs.js", "K_NONE");
+
+/* text.dat entries */
+require("text.js", "XtrnProgLstFmt");
+
+function ExternalMenus() {
+	this.options = {};
+	this.xtrn_custommenu_options = {};
+	this.menuconfig = {};
+	
+	this.getOptions();
+	this.getMenuConfig();
+}
+
+ExternalMenus.prototype.getMenuConfig = function() {
+	var config_file = new File(system.ctrl_dir + "xtrnmenu.cfg");
+	var config_src;
+	this.menuconfig = undefined;
+	if (config_file.open('r+')) {
+		config_src = config_file.read();
+		config_file.close();
+	}
+	
+	if (typeof config_src !== "undefined") {
+		this.menuconfig = JSON.parse(config_src.toString());
+	}
+}
+
+ExternalMenus.prototype.getOptions = function(menutype, menuid) {
+	if (typeof menutype === "undefined") {
+		menutype = 'custommenu';
+	}
+	
+	// Get xtrn_sec options from modopts.ini [xtrn_sec]
+	if ((this.options = load({}, "modopts.js", "xtrn_sec")) == null) {
+		this.options = { multicolumn: true, sort: false };
+	}
+
+	// Get xtrn_custommenu options from modopts.ini [xtrn_custommenu]
+	if ((this.xtrn_custommenu_options = load({}, "modopts.js", "xtrnmenu")) == null) {
+		this.xtrn_custommenu_options = { };
+	}
+
+	// in all cases, we start with the xtrn_sec options as the base and set the defaults
+	if (this.options.multicolumn === undefined)
+		this.options.multicolumn = true;
+
+	if (this.options.multicolumn_separator === undefined)
+		this.options.multicolumn_separator = " ";
+
+	if (this.options.multicolumn_fmt === undefined)
+		this.options.multicolumn_fmt = system.text(XtrnProgLstFmt);
+	
+	if (this.options.singlecolumn_fmt === undefined)
+		this.options.singlecolumn_fmt = "\x01h\x01c%3u \xb3 \x01n\x01c%s\x01h ";
+
+	if (this.options.singlecolumn_margin == undefined)
+		this.options.singlecolumn_margin = 7;
+
+	if (typeof bbs !== "undefined") {
+		if (this.options.singlecolumn_height == undefined)
+			this.options.singlecolumn_height = console.screen_rows - this.options.singlecolumn_margin;
+
+		// override and turn off multicolumn if terminal width is less than 80
+		if (console.screen_columns < 80)
+			options.multicolumn = false;
+	}
+	
+	if (this.options.restricted_user_msg === undefined)
+		this.options.restricted_user_msg = system.text(R_ExternalPrograms);
+	
+	if (this.options.no_programs_msg === undefined) 
+		this.options.no_programs_msg =  system.text(NoXtrnPrograms);
+	
+	if (this.options.header_fmt === undefined)
+		this.options.header_fmt = system.text(XtrnProgLstHdr);
+
+	if (this.options.titles === undefined)
+		this.options.titles = system.text(XtrnProgLstTitles);
+
+	if (this.options.underline === undefined)
+		this.options.underline = system.text(XtrnProgLstUnderline);
+
+	if (this.options.which === undefined) 
+		this.options.which = system.text(WhichXtrnProg);
+
+	if (this.options.clear_screen === undefined)
+		this.options.clear_screen = true;
+
+	if (this.options.section_fmt === undefined)
+		this.options.section_fmt = "\x01y\x01g%3d:\x01n\x01g %s"
+
+	if (this.options.section_header_fmt === undefined)
+		this.options.section_header_fmt = "\x01-\x01gSelect \x01hExternal Program Section\x01-\x01g:"
+
+	if (this.options.section_which === undefined)
+		this.options.section_which = "\r\n\x01-\x01gWhich, \x01w\x01h~Q\x01n\x01guit or [1]: \x01h"
+
+	// if its a custom menu, then override with custommenu_options
+	if (menutype == 'custommenu') {
+		if (typeof this.xtrn_custommenu_options.multicolumn_fmt !== "undefined") {
+			this.options.multicolumn_fmt = this.xtrn_custommenu_options.multicolumn_fmt;
+		} else {
+			// cannot default to xtrn_sec multicolumn_fmt due to use of %u instead of %s
+			this.options.multicolumn_fmt = "\x01h\x01c%3s \xb3 \x01n\x01c%-32.32s\x01h ";
+		}
+
+		if (typeof this.xtrn_custommenu_options.singlecolumn_fmt !== "undefined") {
+			this.options.singlecolumn_fmt = this.xtrn_custommenu_options.singlecolumn_fmt;
+		} else {
+			// cannot default to xtrn_sec multicolumn_fmt due to use of %u instead of %s
+			this.options.singlecolumn_fmt = "\x01h\x01c%3s \xb3 \x01n\x01c%s\x01h ";
+		}
+
+		this.options.header_fmt = (typeof this.xtrn_custommenu_options.header_fmt !== "undefined") 
+			? this.xtrn_custommenu_options.header_fmt : this.options.header_fmt;
+
+		this.options.titles = (typeof this.xtrn_custommenu_options.titles !== "undefined")
+			? this.xtrn_custommenu_options.titles : this.options.titles;
+
+		this.options.which = (typeof this.xtrn_custommenu_options.which !== "undefined")
+			? this.xtrn_custommenu_options.which : this.options.which;
+
+		this.options.underline = (typeof this.xtrn_custommenu_options.underline !== "undefined")
+			? this.xtrn_custommenu_options.underline : this.options.underline;
+
+		this.options.multicolumn_separator = (typeof this.xtrn_custommenu_options.multicolumn_separator !== "undefined")
+			? this.xtrn_custommenu_options.multicolumn_separator : this.options.multicolumn_separator;
+
+		this.options.multicolumn = (typeof this.xtrn_custommenu_options.multicolumn !== "undefined")
+			? this.xtrn_custommenu_options.multicolumn : this.options.multicolumn;
+
+		this.options.sort = (typeof this.xtrn_custommenu_options.sort !== "undefined")
+			? this.xtrn_custommenu_options.sort : this.options.sort;
+
+		this.options.clear_screen = (typeof this.xtrn_custommenu_options.clear_screen !== "undefined")
+			? this.xtrn_custommenu_options.clear_screen : this.options.clear_screen;
+
+		this.options.singlecolumn_margin = (typeof this.xtrn_custommenu_options.singlecolumn_margin !== "undefined")
+			? this.xtrn_custommenu_options.singlecolumn_margin : this.options.singlecolumn_margin;
+
+		if (typeof bbs !== "undefined") {
+			this.options.singlecolumn_height = (typeof this.xtrn_custommenu_options.singlecolumn_height !== "undefined")
+				? this.xtrn_custommenu_options.singlecolumn_height : this.options.singlecolumn_height;
+		}
+
+		// no need to override restricted_user_msg or no_programs_msg - these
+		// will be the same for both types of menus
+		
+		// Allow overriding on a per-menu basis
+		var menuoptions = load({}, "modopts.js", "xtrnmenu:" + menuid);
+		if ((typeof menuid !== "undefined") && (menuoptions != null)) {
+			for (var m in menuoptions) {
+				this.options[m] = menuoptions[m];
+			}
+		}
+	}
+	
+	// these options only apply to terminals/consoles
+	
+	// the intention is to obtain all the mod_opts options for xtrn_sec, and
+	// override if a custom menu global setting is set
+
+	//// The following are used for the enhanced custom menu functionality
+	if (this.options.custom_menu_not_found_msg === undefined) {
+		this.options.custom_menu_not_found_msg = "Menu %MENUID% not found";
+	}
+	
+	if (this.options.custom_menu_program_not_found_msg === undefined) {
+		this.options.custom_menu_program_not_found_msg = "Program %PROGRAMID% not found";
+	}
+	
+	return this.options;
+}
+
+// return a custom menu object
+ExternalMenus.prototype.getMenu = function(menuid) {
+	// grab the specified menu, or get the main menu
+	if ((typeof menuid === "undefined") || !menuid) {
+		menuid = "main";
+	} else {
+		menuid = menuid.toLowerCase();
+	}
+
+	var menu;
+	
+	if ((typeof this.menuconfig !== "undefined") && (typeof this.menuconfig.menus !== "undefined")) {
+		this.menuconfig.menus.some(function (indmenu) {
+			if (indmenu.id.toLowerCase() == menuid) {
+				menu = indmenu;
+			}
+		});
+	}
+	
+	if (!menu && (menuid == "main")) {
+		// no custom menus defined, make one to mimic old behavior
+		
+		var menuitems = [];
+
+		var i;
+		xtrn_area.sec_list.forEach(function (sec) {
+			if (sec.can_access) {
+				menuitems.push({
+					"input": i,
+					"target": sec.code,
+					"type": "xtrnmenu",
+					"title": sec.name,
+					"access_string": sec.ars
+				});
+				i++;
+			}
+		});
+		
+		menu = {
+			"id": "main",
+			"title": "Main Menu",
+			"items": menuitems
+		};
+	}
+	return menu;
+}
+
+// return a section menu object (stock synchronet external door section)
+ExternalMenus.prototype.getSectionMenu = function(menuid) {
+
+	// grab the specified menu, or get the main menu
+	if ((typeof menuid === "undefined") || !menuid) {
+		return false;
+	}
+	
+	var menuitems = [];
+	var menu, title;
+	
+	xtrn_area.sec_list.some(function (sec) {
+		
+		if (sec.code.toLowerCase() == menuid.toLowerCase()) {
+			title = sec.name;
+
+			if (!sec.can_access || sec.prog_list.length < 1) {
+				return false;
+			}
+			
+			var i = 1;
+			sec.prog_list.some(function (prog) {
+				menuitems.push({
+					'input' : i,
+					'target': prog.code,
+					'title': prog.name,
+					'type': 'xtrnprog',
+					'access_string': prog.ars,
+					'cost': prog.cost
+				});
+				i++;
+			});
+			
+			if (menuitems.length > 0) {
+				menu = {
+					'id': menuid,
+					'title': title,
+					'items': menuitems,
+				};
+			}
+			return;
+		}
+	});
+
+	return menu;
+}
+
+// Sort the menu items according to options
+ExternalMenus.prototype.getSortedItems = function(menuobj) {
+	var sort_type;
+	
+	if ((typeof menuobj.sort_type !== "undefined") && menuobj.sort_type) {
+		sort_type = menuobj.sort_type; // "name" or "key"
+	} else {
+		sort_type = this.options.sort; // bool
+	}
+	
+	// first, build a new menu with only options they have access to
+	var menuitemsfiltered = [];
+	for (i in menuobj.items) {
+		switch (menuobj.items[i].type) {
+			case 'xtrnmenu':
+				for (j in xtrn_area.sec_list) {
+					if (xtrn_area.sec_list[j].code.toUpperCase() == menuobj.items[i].target.toUpperCase()) {
+						if (xtrn_area.sec_list[j].can_access) {
+							menuitemsfiltered.push(menuobj.items[i]);
+						}
+					}
+				}
+				break;
+			
+			case 'xtrnprog':
+				for (j in xtrn_area.sec_list) {
+					for (var k in xtrn_area.sec_list[j].prog_list) {
+						if (xtrn_area.sec_list[j].prog_list[k].code.toUpperCase() == menuobj.items[i].target.toUpperCase()) {
+							if (xtrn_area.sec_list[j].prog_list[k].can_access) {
+								menuitemsfiltered.push(menuobj.items[i]);
+							}
+						}
+					}
+				}
+				break;
+			
+			case 'custommenu':
+			default:
+				if ((typeof menuobj.items[i].access_string === "undefined") || !menuobj.items[i].access_string) {
+					// no access string defined, everyone gets access
+					menuitemsfiltered.push(menuobj.items[i]);
+				} else {
+					if (user.compare_ars(menuobj.items[i].access_string)) {
+						// they have access
+						menuitemsfiltered.push(menuobj.items[i]);
+					}
+				}
+				break;
+		}
+	}
+	
+	// if no custom input keys are specified for an input, then assign a numeric input
+	// this would mimic the built-in external section menu functionality
+	// but this is only assigned on sort_type key because if the sort type is for
+	// titles, we need to sort by title first before assigning the sequential numbers
+	var sortind = 0;
+	// make sure we only use the next available number for automatic assignment
+	for (i in menuitemsfiltered) {
+		if (!isNaN(menuitemsfiltered[i].input)) {
+			if (menuitemsfiltered[i].input > sortind) {
+				sortind = menuitemsfiltered[i].input;
+			}
+		}	
+	}
+	sortind++;
+	
+	if (sort_type == "key") {
+		for (i in menuitemsfiltered) {
+			if (!menuitemsfiltered[i].input) {
+				menuitemsfiltered[i].input = sortind;
+				sortind++;
+			}
+		}
+	}
+	
+	if (sort_type) {
+		switch (sort_type) {
+			case "key":
+				menuitemsfiltered.sort(this.sort_by_input);
+				break;
+			default:
+			case "title":
+			case "name":
+				menuitemsfiltered.sort(this.sort_by_title);
+				break;
+		}
+	}
+	
+	// if this is a sort by title and the key is empty, it will be assigned the next available number
+	// this is to support auto-generated inputs like is done on the built-in section menus
+	// however, to keep the numeric keys in order, the numbers could not be pre-assigned like it done
+	// on the sort_type key
+	for (i in menuitemsfiltered) {
+		if ((sort_type !== "key") && !menuitemsfiltered[i].input) {
+			menuitemsfiltered[i].input = sortind;
+			sortind++;
+		}
+	}
+	
+	return menuitemsfiltered;
+}
+
+// Original sort function used by external_program_menu and external_section_menu
+ExternalMenus.prototype.sort_by_name = function(a, b) {
+	if(a.name.toLowerCase()>b.name.toLowerCase()) return 1;
+	if(a.name.toLowerCase()<b.name.toLowerCase()) return -1;
+	return 0;
+}
+
+// Sort by title - used by external_section_menu_custom
+ExternalMenus.prototype.sort_by_title = function(a, b) {
+	if (a.title.toLowerCase() < b.title.toLowerCase()) {
+		return -1;
+	}
+	if (a.title.toLowerCase() > b.title.toLowerCase()) {
+		return 1;
+	}
+	return 0;
+}
+
+// Sort by input key - used by external_section_menu_custom
+ExternalMenus.prototype.sort_by_input = function(a, b) {
+	if (a.input.toString().toLowerCase() < b.input.toString().toLowerCase()) {
+		return -1;
+	}
+	if (a.input.toString().toLowerCase() > b.input.toString().toLowerCase()) {
+		return 1;
+	}
+	return 0;
+}
diff --git a/exec/sutils.js b/exec/sutils.js
index 1058a94da4cecb89b4eb21cb6375299ad284f840..ce837bf7c712d2ac9cbb921cc32139013481b3c8 100644
--- a/exec/sutils.js
+++ b/exec/sutils.js
@@ -1,4 +1,3 @@
-
 "use strict";
 
 const REVISION = "$Revision: 1.0 $".split(' ')[1];
@@ -29,7 +28,7 @@ while((!which || which < 1) && !aborted()) {
     if (typeof categories !== "undefined") {
         var x = 1;
         for (var catid in categories) {
-            printf("%2d. %s\n", x, categories[catid]);
+            printf("%2d. %s\r\n", x, categories[catid]);
             categoryitems[x] = catid;
             x++;
         };
@@ -73,7 +72,7 @@ function docategorymenu(catid, catname) {
             var x = 1;
             for (var iniid in utilitems) {
                 if (file_exists(js.exec_dir + utilitems[iniid].filename)) {
-                    printf("%2d. %-20s %s\n", x, utilitems[iniid].name, utilitems[iniid].desc);
+                    printf("%2d. %-20s %s\r\n", x, utilitems[iniid].name, utilitems[iniid].desc);
                     menuitems[x] = iniid;
                     x++;
                 }
diff --git a/exec/xtrn_sec.js b/exec/xtrn_sec.js
index e2dd5b6ef231cdd2313fb63bdad91eb54fb3d917..e2b7540d2160a126b4b5b3d9c4ac86d43b719aae 100644
--- a/exec/xtrn_sec.js
+++ b/exec/xtrn_sec.js
@@ -121,12 +121,15 @@ function external_program_menu(xsec)
 		if(options.clear_screen)
 			console.clear(LIGHTGRAY);
 
+		var show_header = true;
 		var secnum = xtrn_area.sec_list[xsec].number+1;
 		var seccode = xtrn_area.sec_list[xsec].code;
 		if(!bbs.menu("xtrn" + secnum + "_head", P_NOERROR) &&
 			!bbs.menu("xtrn" + seccode + "_head", P_NOERROR)) {
-			bbs.menu("xtrn_head", P_NOERROR);
+			show_header = !bbs.menu("xtrn_head", P_NOERROR);
 		}
+		else
+			show_header = false;
 		if(bbs.menu("xtrn" + secnum, P_NOERROR) || bbs.menu("xtrn" + seccode, P_NOERROR)) {
 			if(!bbs.menu("xtrn" + secnum + "_tail", P_NOERROR) &&
 				!bbs.menu("xtrn" + seccode + "_tail", P_NOERROR)) {
@@ -135,11 +138,14 @@ function external_program_menu(xsec)
 		}
 		else {
 			var multicolumn = options.multicolumn && prog_list.length > options.singlecolumn_height;
+			var center = options.center && !multicolumn;
+			var margin = center ? format("%*s", (console.screen_columns * 0.25) - 1, "") : "";
 			if(options.sort)
 				prog_list.sort(sort_by_name);
-			printf(options.header_fmt, xtrn_area.sec_list[xsec].name);
+			if(show_header)
+				write(margin, format(options.header_fmt, xtrn_area.sec_list[xsec].name));
 			if(options.titles.trimRight() != '')
-				write(options.titles);
+				write(margin, options.titles);
 			if(multicolumn) {
 				write(options.multicolumn_separator);
 				if (options.titles.trimRight() != '')
@@ -147,7 +153,7 @@ function external_program_menu(xsec)
 			}
 			if(options.underline.trimRight() != '') {
 				console.crlf();
-				write(options.underline);
+				write(margin, options.underline);
 			}
 			if(multicolumn) {
 				write(options.multicolumn_separator);
@@ -162,6 +168,7 @@ function external_program_menu(xsec)
 				n=prog_list.length;
 
 			for(i=0;i<n && !console.aborted;i++) {
+				write(margin);
 				console.add_hotspot(i+1);
 				printf(multicolumn ? options.multicolumn_fmt : options.singlecolumn_fmt
 					,i+1
@@ -185,7 +192,13 @@ function external_program_menu(xsec)
 				bbs.menu("xtrn_tail", P_NOERROR);
 			}
 			bbs.node_sync();
-			console.mnemonics(options.which);
+			if(margin) {
+				console.crlf();
+				write(margin);
+				console.mnemonics(options.which.trimLeft());
+			}
+			else
+				console.mnemonics(options.which);
 		}
 		system.node_list[bbs.node_num-1].aux=0; /* aux is 0, only if at menu */
 		bbs.node_action=NODE_XTRN;
@@ -202,6 +215,10 @@ function external_section_menu()
 {
     var i,j;
     var xsec=0;
+	var longest = 0;
+	for(i = 0; i < xtrn_area.sec_list.length; i++)
+		longest = Math.max(xtrn_area.sec_list[i].name.length, longest);
+	var margin = options.center ? format("%*s", ((console.screen_columns - longest)/2) - 5, "") : "";
 
     while(bbs.online) {
 
@@ -225,7 +242,7 @@ function external_section_menu()
 		if(options.clear_screen)
 			console.clear(LIGHTGRAY);
 
-		bbs.menu("xtrn_sec_head", P_NOERROR);
+		var show_header = !bbs.menu("xtrn_sec_head", P_NOERROR);
 
 		if(bbs.menu_exists("xtrn_sec")) {
 			bbs.menu("xtrn_sec");
@@ -235,16 +252,23 @@ function external_section_menu()
 			if(options.sort)
 				sec_list.sort(sort_by_name);
 
-			printf(options.section_header_fmt.replace('\x01l', ''), options.section_header_title);
+			if(show_header)
+				printf(margin + options.section_header_fmt.replace('\x01l', ''), options.section_header_title);
 			for (i = 0; i < sec_list.length; i++) {
 				console.add_hotspot(i+1);
-				printf(options.section_fmt, i + 1, sec_list[i].name);
+				printf(margin + options.section_fmt, i + 1, sec_list[i].name);
 			}
 
 			bbs.menu("xtrn_sec_tail", P_NOERROR);
 			
 			bbs.node_sync();
-			console.mnemonics(format(options.section_which, xsec + 1));
+			if(options.center) {
+				console.crlf();
+				write(margin);
+				console.mnemonics(format(options.section_which, xsec + 1).trimLeft());
+			}
+			else
+				console.mnemonics(format(options.section_which, xsec + 1));
 		}
 
 		bbs.node_sync();
diff --git a/exec/xtrnmenu.js b/exec/xtrnmenu.js
new file mode 100644
index 0000000000000000000000000000000000000000..7a32868f43ebfac10e18da3372c0a19a9279786f
--- /dev/null
+++ b/exec/xtrnmenu.js
@@ -0,0 +1,266 @@
+/**
+ * Xtrn Menu Mod
+ * Custom External Program Menus
+ * by Michael Long mlong  innerrealmbbs.us
+ *
+ * This is the loadable module that displays the custom external menus
+ * in terminal server (telnet/rlogin/ssh)
+ * 
+ * To jump to a specific menu, pass the ID as an argument
+ * 
+ * To set options, add to modopts.ini [xtrnmenu]
+ * 
+ * See instructions at http://wiki.synchro.net/module:xtrnmenu
+ */
+
+"use strict";
+
+require("sbbsdefs.js", "K_NONE");
+
+load("xtrnmenulib.js");
+
+var options, xsec = -1;
+
+//// Main
+var ExternalMenus = new ExternalMenus();
+const menuconfig = ExternalMenus.menuconfig;
+
+{
+	var i,j;
+	for(i in argv) {
+		for(j in xtrn_area.sec_list) {
+			if(argv[i].toLowerCase()==xtrn_area.sec_list[j].code)
+				xsec=j;
+		}
+	}
+}
+if (xsec > -1) {
+	// if its the id of a standard section menu, send it to the 
+	// stock menu
+	js.exec("xtrn_sec.js", {}, xsec);
+} else if (typeof argv[0] !== "undefined") {
+	// if its not a section menu, assume it is a custom menu
+	external_section_menu_custom(argv[0]);
+} else {
+	// main custom menu
+	external_section_menu_custom();
+}
+
+
+// Renders the top-level external menu
+function external_section_menu_custom(menuid)
+{
+	var i, menucheck, menuobj, item_multicolumn_fmt, item_singlecolumn_fmt,
+        cost, multicolumn, menuitemsfiltered = [];
+	var validkeys = []; // valid chars on menu selection
+	var keymax = 0; // max integer allowed on menu selection
+
+	var options = ExternalMenus.getOptions('custommenu', menuid);
+	
+	menuobj = ExternalMenus.getMenu(menuid);
+
+	// Allow overriding auto-format on a per-menu basis
+	var multicolumn_fmt = options.multicolumn_fmt;
+	var singlecolumn_fmt = options.singlecolumn_fmt;
+
+	while (bbs.online) {
+		console.aborted = false;
+
+		if (typeof menuobj === "undefined") {
+			doerror(options.custom_menu_not_found_msg.replace('%MENUID%', menuid));
+			break;
+		}
+
+		if (options.clear_screen) {
+			console.clear(LIGHTGRAY);
+		}
+
+		if (user.security.restrictions&UFLAG_X) {
+			write(options.restricted_user_msg);
+			break;
+		}
+
+		if (!xtrn_area.sec_list.length) {
+			write(options.no_programs_msg);
+			break;
+		}
+
+		var keyin;
+
+		system.node_list[bbs.node_num-1].aux = 0; /* aux is 0, only if at menu */
+		bbs.node_action = NODE_XTRN;
+		bbs.node_sync();
+
+        menuitemsfiltered = ExternalMenus.getSortedItems(menuobj);
+
+		if (!bbs.menu("xtrnmenu_head_" + menuid, P_NOERROR) && !bbs.menu("xtrnmenu_head", P_NOERROR)) {
+			bbs.menu("xtrn_head", P_NOERROR);
+	    }
+
+		// if file exists text/menu/xtrnmenu_(menuid).[rip|ans|mon|msg|asc], 
+		// then display that, otherwise dynamiic
+	    if (!bbs.menu("xtrnmenu_" + menuid, P_NOERROR)) {
+
+			// if no custom menu file in text/menu, create a dynamic one
+			multicolumn = options.multicolumn && menuitemsfiltered.length > options.singlecolumn_height;
+			
+			printf(options.header_fmt, menuobj.title);
+			if(options.titles.trimRight() != '')
+				write(options.titles);
+			if(multicolumn) {
+				write(options.multicolumn_separator);
+				if (options.titles.trimRight() != '')
+					write(options.titles);
+			}
+			if(options.underline.trimRight() != '') {
+				console.crlf();
+				write(options.underline);
+			}
+			if(multicolumn) {
+				write(options.multicolumn_separator);
+				if (options.underline.trimRight() != '')
+					write(options.underline);
+			}
+			console.crlf();
+			
+			// n is the number of items for the 1st column
+			var n;
+			if (multicolumn) {
+				n = Math.floor(menuitemsfiltered.length / 2) + (menuitemsfiltered.length & 1);
+			} else {
+				n = menuitemsfiltered.length;
+			}
+			
+            // j is the index for each menu item on 2nd column
+			var j = n; // start j at the first item for 2nd column
+			for (i = 0; i < n && !console.aborted; i++) {
+			    cost = "";
+			    if (menuitemsfiltered[i].type == "xtrnprog") {
+			        // if its an external program, get the cost
+                    cost = xtrn_area.prog[menuitemsfiltered[i].target.toLowerCase()].cost;
+                }
+
+                console.add_hotspot(menuitemsfiltered[i].input.toString());
+			    
+				validkeys.push(menuitemsfiltered[i].input.toString());
+				var intCheck = Number(menuitemsfiltered[i].input);
+				if (!intCheck.isNaN) {
+					if (intCheck > keymax) {
+						keymax = menuitemsfiltered[i].input;
+					}
+				}
+
+				// allow overriding format on a per-item basis
+                // great for featuring a specific game
+				var checkkey = menuitemsfiltered[i].target + '-multicolumn_fmt';
+				checkkey = checkkey.toLowerCase();
+				item_multicolumn_fmt = (typeof options[checkkey] !== "undefined") ?
+					options[checkkey] : options.multicolumn_fmt;
+
+				checkkey = menuitemsfiltered[i].target + '-singlecolumn_fmt'
+				checkkey = checkkey.toLowerCase();
+				item_singlecolumn_fmt = (typeof options[checkkey] !== "undefined") ? 
+					options[checkkey] : options.singlecolumn_fmt;
+
+				printf(multicolumn ? item_multicolumn_fmt : item_singlecolumn_fmt,
+                    menuitemsfiltered[i].input.toString().toUpperCase(),
+                    menuitemsfiltered[i].title,
+                    cost
+				);
+
+				if (multicolumn) {
+					if ((typeof(menuitemsfiltered[j]) !== "undefined")) {
+						validkeys.push(menuitemsfiltered[j].input.toString());
+
+						var intCheck = Number(menuitemsfiltered[j].input);
+						if (!intCheck.isNaN) {
+							if (intCheck > keymax) {
+								keymax = menuitemsfiltered[j].input;
+							}
+						}
+
+						// allow overriding format on a per-item basis
+						// great for featuring a specific game
+						var checkkey = menuitemsfiltered[j].target + '-multicolumn_fmt';
+						checkkey = checkkey.toLowerCase();
+						item_multicolumn_fmt = (typeof options[checkkey] !== "undefined") ?
+							options[checkkey] : options.multicolumn_fmt;
+
+						checkkey = menuitemsfiltered[j].target + '-singlecolumn_fmt'
+						checkkey = checkkey.toLowerCase();
+
+                        write(options.multicolumn_separator);
+						console.add_hotspot(menuitemsfiltered[j].input.toString());
+						printf(item_multicolumn_fmt,
+                            menuitemsfiltered[j].input.toString().toUpperCase(),
+							menuitemsfiltered[j].title,
+                            cost
+						);
+					} else {
+						write(options.multicolumn_separator);
+					}
+					j++;
+				}
+				console.crlf();
+			}
+
+			if (!bbs.menu("xtrnmenu_tail_" + menuid, P_NOERROR) && !bbs.menu("xtrnmenu_tail", P_NOERROR)) {
+				bbs.menu("xtrn_tail", P_NOERROR);
+			}
+
+			bbs.node_sync();
+			console.mnemonics(options.which);
+		}
+
+		validkeys.push('q');
+		keyin = console.getkeys(validkeys, keymax, K_NONE);
+		keyin = keyin.toString().toLowerCase();
+
+		if (keyin) {
+			// q for quit
+			if (keyin == "q") {
+				console.clear();
+				break; 
+			}
+			
+			menuitemsfiltered.some(function (menuitemfiltered) {
+				var menutarget = menuitemfiltered.target.toLowerCase();
+				var menuinput = menuitemfiltered.input.toString().toLowerCase();
+
+				if (menuinput == keyin) {
+					switch (menuitemfiltered.type) {
+						// custom menu, defined in xtrnmenu.json
+						case 'custommenu':
+							external_section_menu_custom(menutarget);
+							return true;
+						// external program section
+						case 'xtrnmenu':
+							js.exec("xtrn_sec.js", {}, menutarget);
+							//js.exec(js.exec_dir + 'xtrn_sec.js ' + menutarget)
+							return true;
+						// external program
+						case 'xtrnprog':
+							// run the external program
+							if (typeof xtrn_area.prog[menutarget] !== "undefined") {
+								bbs.exec_xtrn(menutarget);
+								return true;
+							} else {
+								doerror(options.custom_menu_program_not_found_msg.replace('%PROGRAMID%', menutarget));
+							}
+							break;
+					} //switch
+				} // if menu item matched keyin
+			}); // foreach menu item
+		} // if keyin
+	} // main bbs.online loop
+}
+
+
+// Display error message to console and save to log
+function doerror(msg)
+{
+	console.crlf();
+	log(LOG_ERR, msg);
+	console.putmsg('\x01+\x01h\x01r' +msg + ". The sysop has been notified." + '\x01-\r\n');
+}
+
diff --git a/exec/xtrnmenucfg.js b/exec/xtrnmenucfg.js
new file mode 100644
index 0000000000000000000000000000000000000000..21003b399a8234eb332b06e51f0809f76f1d5d2e
--- /dev/null
+++ b/exec/xtrnmenucfg.js
@@ -0,0 +1,828 @@
+"use strict"
+
+/**
+ * Menu editor for Custom External Program Menus
+ * by Michael Long mlong  innerrealmbbs.us
+ *
+ * This edits the file xtrnmenu.cfg
+ */
+
+load("sbbsdefs.js");
+load("uifcdefs.js");
+
+var log = function(msg) {
+    var f = new File("uifc.log");
+    f.open("a");
+    f.writeln(system.timestr() + ": " + msg);
+    f.close();
+}
+
+/**
+ * Write the menus out to a file
+ */
+var saveMenus = function() {
+
+    var filename = system.ctrl_dir + "xtrnmenu.cfg";
+
+    file_backup(filename, 5);
+
+    var config_file = new File(filename);
+    if (!config_file.open('w+')) {
+        uifc.msg("ERROR: Could not write to " + filename);
+        return;
+    }
+    config_file.write(JSON.stringify(menuconfig, null, '    '));
+    config_file.close();
+
+    uifc.msg("Config Saved");
+}
+
+/**
+ * Edit a custom menu
+ * @param menuid
+ */
+var editMenu = function(menuid) {
+    var menuindex, menu, menuindex, selection, selection2, editproperty;
+    var last = 0;
+    var selections = [], displayoptions = [], displayoptionids = [];
+    var menusize = menuconfig.menus.length;
+
+    // new menu but no code given, make one
+    if ((typeof menuid === "undefined") || (!menuid)) {
+        menuid = time();
+    }
+
+    // look for existing menu
+    for (var i in menuconfig.menus) {
+        if (menuconfig.menus[i].id == menuid) {
+            menu = menuconfig.menus[i];
+            menuindex = i;
+        }
+    }
+
+    if (typeof menu  === "undefined") {
+        menuindex = menusize;
+        menuconfig.menus[menuindex] = {
+            'id': menuid,
+            'title': "New Generated Menu " + menuid,
+            "sort_type": "title",
+            'items': []
+        };
+        menu = menuconfig.menus[menuindex];
+    }
+
+    while(1) {
+        uifc.help_text = word_wrap("This screen allows you to edit the configuration options for the custom menu.\r\n\r\nMost options default or are set in modopts.ini, but here you can define them on a per-menu basis.\r\n\r\nClick Edit Items to edit the individual entries (programs, menus, etc.)");
+
+        selections = [];
+        for (var j in menu.items) {
+            selections.push(menu.items.index);
+        }
+
+        displayoptions = [];
+        displayoptionids = [];
+
+        // setup display menu
+        displayoptions.push(format("%23s: %s", "id",
+            ("id" in menu ? menu.id : time())));
+        displayoptionids.push("id");
+
+        displayoptions.push(format("%23s: %s", "title",
+            ("title" in menu ? menu.title : "")));
+        displayoptionids.push("title");
+
+        displayoptions.push(format("%23s: %s", "sort_type",
+            ("sort_type" in menu ? menu.sort_type : "(default)")));
+        displayoptionids.push("sort_type");
+
+        displayoptions.push(format("%23s: %s", "Edit Items", "[...]"));
+        displayoptionids.push("items");
+        
+        selection = uifc.list(WIN_ORG|WIN_MID|WIN_ACT|WIN_ESC, 0, 0, 0, last, last,
+            menu.title + ": Options", displayoptions);
+
+        if (selection < 0) {
+            // escape key
+            break;
+        }
+
+        editproperty = displayoptionids[selection];
+
+        switch (editproperty) {
+            case 'id':
+                uifc.help_text = word_wrap("This is a unique ID for the menu, which can be used as the target to\r\ncall the menu from other menus.\r\n\r\nFor the top-level menu, the id should be 'main'.");
+                var selection2 = uifc.input(WIN_MID, "Menu ID", menu.id, 50, K_EDIT);
+                if ((selection2 < 0) || (selection2 == null)) {
+                    // escape key
+                    break;
+                }
+                menu.id = selection2;
+                break;
+            case 'title':
+                uifc.help_text = word_wrap("Title for the menu, to be shown at the top of the menu");
+                selection2 = uifc.input(WIN_MID, "Menu Title", menu.title, 255, K_EDIT);
+                if ((selection2 < 0) || (selection2 == null)) {
+                    // escape key
+                    break;
+                }
+                menu.title = selection2;
+                break;
+
+            case 'sort_type':
+                uifc.help_text = word_wrap("How to sort the menu:\r\nby input key\r\nby title\r\nnone (the items remain in the order they are in the config)");
+
+                switch (uifc.list(WIN_ORG | WIN_MID, "Sort Type", ["key", "title", "none"])) {
+                    case 0:
+                        menu.sort_type = "key";
+                        break;
+                    case 1:
+                        menu.sort_type = "title";
+                        break;
+                    case 2:
+                        delete menu.sort_type;
+                        break;
+                    default:
+                        //esc key
+                        break;
+                }
+
+            case 'items':
+                editItems(menuid);
+                break;
+
+            default:
+                // this isn't supposed to happen
+                uifc.msg("Unknown option");
+                break;
+        }
+
+        last = Math.max(selection, 0);
+    }
+}
+
+/**
+ * Edit menu items in a menu
+ * @param menuid
+ */
+var editItems = function(menuid) {
+    var menuindex, menu, selection, selection2, keyused, items = [], itemids = [];
+    var i, last = 0;
+
+    // cur bar top left width
+    var ctxm = new uifc.list.CTX(0, 0, 0, 0, 0);
+    
+    if (typeof menuid === "undefined") {
+        uifc.msg("Menu could not be found");
+        return;
+    } else {
+        for (i in menuconfig.menus) {
+            if (menuconfig.menus[i].id == menuid) {
+                menu = menuconfig.menus[i];
+                menuindex = i;
+            }
+        }
+    }
+    
+    if ((typeof menu.items == "undefined") || (menu.items.length == 0)) {
+        // no items, prompt them to make one
+        editItem(menu.id, 0);
+    }
+    
+
+    uifc.help_text = word_wrap("This menu allows editing the various items in this menu.\r\n\r\n"
+        + "If you leave input key blank, it will use an auto-generated number at display time.\r\n\r\n"
+        + "Choose a type first and the dropdown to choose tha target will allow you to select your target.\r\n\r\n"
+        + "Access string only applies to custom menu items. For external sections or external programs, use the access settings in scfg.\r\n\r\n");
+
+    while(1) {
+        items = [];
+        itemids = [];
+        for(i in menu.items) {
+            items.push(format(
+                "%6s %10s %s",
+                menu.items[i].input ? menu.items[i].input : '(auto)',
+                menu.items[i].type,
+                menu.items[i].title
+            ));
+            itemids.push(i);
+        }
+        // WIN_ORG = original menu
+        // WIN_MID = centered mid
+        // WIN_ACT = menu remains active after a selection
+        // WIN_ESC = screen is active when escape is pressed
+        // WIN_XTR = blank line to insert
+        // WIN_INS = insert key
+        // WIN_DEL = delete
+        // WIN_CUT = cut ctrl-x
+        // WIN_COPY = copy ctrl-c
+        // WIN_PUT = paste ctrl-v
+        // WIN_SAV = use context/save position
+        selection = uifc.list(
+            WIN_ORG|WIN_MID|WIN_ACT|WIN_ESC|WIN_XTR|WIN_INS|WIN_DEL|WIN_CUT|WIN_COPY|WIN_PASTE|WIN_SAV,
+            menu.title + ": Items",
+            items,
+            ctxm
+        );
+        
+        if (selection == -1) {
+            // esc key
+            break;
+        }
+
+        if ((selection & MSK_ON) == MSK_DEL) {
+            // delete item
+            selection &= MSK_OFF;
+            //renumber array so there are no gaps
+            var menuitems2 = [];
+            for (var i in menu.items) {
+                if (i != itemids[selection]) {
+                    menuitems2.push(menu.items[i]);
+                }
+            }
+            menu.items = menuitems2;
+        } else if (((selection & MSK_ON) == MSK_INS)) {
+            // new item from INSERT KEY
+            editItem(menuid, null);
+        } else if ((selection & MSK_ON) == MSK_COPY) {
+            // copy item
+            selection &= MSK_OFF;
+            copyitem = JSON.parse(JSON.stringify(menu.items[itemids[selection]])); // make copy
+        } else if ((selection & MSK_ON) == MSK_CUT) {
+            // cut item
+            selection &= MSK_OFF;
+            copyitem = menu.items[itemids[selection]];
+
+            //renumber array so there are no gaps
+            var menuitems2 = [];
+            for (var i in menu.items) {
+                if (i != itemids[selection]) {
+                    menuitems2.push(menu.items[i]);
+                }
+            }
+            menu.items = menuitems2;
+        } else if ((selection & MSK_ON) == MSK_PASTE) {
+            // paste item
+            selection &= MSK_OFF;
+
+            var oktopaste = true;
+
+            // only paste if there is an item copied
+            if ("type" in copyitem) {
+                // if item already exists in list, modify if since you can't have dupes (except empty input keys)
+                for (var i in menu.items) {
+                    if ((menu.items[i].input == copyitem.input) && (copyitem.input !== null) && (copyitem.input !== "")) {
+                        oktopaste = true;
+                        while(1) {
+                            selection2 = uifc.input(WIN_MID, "Enter New Input Key", "", 3, K_EDIT);
+                            if ((selection2 == -1) || (selection2 === "") || (selection2 === null)) {
+                                // escape key
+                                copyitem.input = null;
+                                break;
+                            }
+                            if (selection2 || selection2 === 0) {
+                                selection2 = selection2.toUpperCase();
+                                keyused = false;
+                                for (var j in menu.items) {
+                                    if (menu.items[j].input && (menu.items[j].input.toUpperCase() == selection2)) {
+                                        keyused = true;
+                                    }
+                                }
+                                if (keyused) {
+                                    uifc.msg("This input key is alread used for another item");
+                                    oktopaste = false;
+                                } else {
+                                    copyitem.input = selection2;
+                                    oktopaste = true;
+                                    break;
+                                }
+                            }
+                        }
+                        copyitem.input = selection2;
+                    }
+                }
+                if ((oktopaste) || (copyitem.input === "null") || (copyitem.input === "")) {
+                    var menuitems2 = [];
+                    for (i in menu.items) {
+                        menuitems2.push(menu.items[i]);
+                        // paste copied item after selected item
+						if (i == itemids[selection]) {
+							menuitems2.push(copyitem);
+                            ctxm.cur = i-1;
+						}
+                    }
+                    menu.items = menuitems2;
+                }
+            }
+        } else if (selection >= menu.items.length) {
+            // new item from blank line
+            editItem(menuid, null);
+        } else {
+            editItem(menuid, itemids[selection]);
+        }
+        last = Math.max(selection, 0);
+    }
+}
+
+/**
+ * Edit a specific menu item entry
+ * @param menuid
+ * @param itemindex
+ */
+var editItem = function(menuid, itemindex) {
+    var menu, menuindex, item;
+    var keyused, selection, selection2, i, last = 0;
+    var displayoptions = [], displayoptionids = [], newitems = [];
+    // used for building target selection
+    var custommenuitems = [], custommenuitemsids = [], custommenunames = [];
+
+    if (typeof menuid === "undefined") {
+        uifc.msg("Menu could not be found");
+        return;
+    } else {
+        for (i in menuconfig.menus) {
+            if (menuconfig.menus[i].id == menuid) {
+                menu = menuconfig.menus[i];
+                menuindex = i;
+            }
+        }
+    }
+
+    if (typeof menu.items[itemindex] === "undefined") {
+        // new item
+        menu.items.push({
+            "input": null,
+            "title": "New Item " + time(),
+            "type": null,
+            "target": null,
+            "access_string": null,
+        });
+        itemindex = menu.items.length - 1;
+        present_select_targettype(menu.items[itemindex]);
+    }
+    item = menu.items[itemindex];
+
+    var itemctx = new uifc.list.CTX(0,0,0,0,0);
+    while(1) {
+        displayoptions = [];
+        displayoptionids = [];
+
+        // setup display menu
+        displayoptions.push(format("%23s: %s", "input",
+            (("input" in item) && (item.input !== null) && (item.input !== "") ? item.input : "(auto)")));
+        displayoptionids.push("input");
+
+        displayoptions.push(format("%23s: %s", "title",
+            ("title" in item ? item.title : "")));
+        displayoptionids.push("title");
+
+        displayoptions.push(format("%23s: %s", "type",
+            ("type" in item ? item.type : "")));
+        displayoptionids.push("type");
+
+        displayoptions.push(format("%23s: %s", "target",
+            ("target" in item ? item.target : "")));
+        displayoptionids.push("target");
+
+        if (item.type == "custommenu") {
+            displayoptions.push(format("%23s: %s", "access_string",
+                ("access_string" in item ? item.access_string : "(default)")));
+            displayoptionids.push("access_string");
+        }
+
+        selection = uifc.list(WIN_ORG | WIN_MID | WIN_ACT | WIN_ESC,
+            menu.title + ": Item " + itemindex, displayoptions, itemctx);
+
+        if (selection < 0) {
+            if (!item.title || !item.type || !item.target) {
+                if (uifc.list(WIN_ORG | WIN_MID, "This item is missing required items.", ["Remove Item", "Edit Item"]) == 0) {
+                    // delete item and continue
+                    newitems = [];
+                    for (i in menu.items) {
+                        if (i != itemindex) {
+                            newitems.push(menu.items[i]);
+                        }
+                    }
+                    menu.items = newitems;
+                    break;
+                }
+            } else {
+                // leave menu
+                break;
+            }
+        }
+
+        switch (displayoptionids[selection]) {
+            
+            case 'input':
+                uifc.help_text = word_wrap("The input key to access this item. Can be anything except Q. Leave blank to auto-generate a number.");
+                selection2 = uifc.input(WIN_MID, "Input Key", item.input, 3, K_EDIT);
+                if ((selection2 < 0) || (selection2 == null)) {
+                    // escape key
+                    break;
+                }
+
+                if (selection2 !== "") {
+                    selection2 = selection2.toUpperCase();
+
+                    keyused = false;
+                    for (i in menu.items) {
+                        if ((menu.items[i].input === null) || (menu.items[i].input === "")) {
+                            // continue here as toUpperCase would break it, and they don't need to match
+                            // anyway because you can have multiple auto-assigned input items
+                            continue;
+                        }
+                        if ((menu.items[i].input.toUpperCase() == selection2) && (i != itemindex)) {
+                            keyused = true;
+                        }
+                    }
+
+                    if (keyused) {
+                        uifc.msg("This input key is already used by another item.");
+                    } else {
+                        item.input = selection2;
+                    }
+                } else {
+                    // save blank
+                    item.input = selection2;
+                }
+                break;
+
+            case 'title':
+                uifc.help_text = word_wrap("The menu item title.");
+                selection2 = uifc.input(WIN_MID, "Title", item.title, 255, K_EDIT);
+                if ((selection2 < 0) || (selection2 == null)) {
+                    // escape key
+                    break;
+                }
+                if (!selection2 && selection2 !== 0) {
+                    uifc.msg("Title is required.");
+                } else {
+                    item.title = selection2;
+                }
+                break;
+
+            case 'type':
+                present_select_targettype(item);
+                break;
+
+            case 'target':
+                present_select_target(item);
+                break;
+
+            case 'access_string':
+                uifc.help_text = word_wrap("The access string for the custom menu.\r\n\r\nOnly applies to custom menu items.\r\n\r\nExample: LEVEL 60");
+                selection2 = uifc.input(WIN_MID, "Access String", item.access_string, 255, K_EDIT);
+                if ((selection2 < 0) || (selection2 == null)) {
+                    // escape key
+                    break;
+                }
+                item.access_string = selection2;
+                break;
+                
+        }
+        last = Math.max(selection, 0);
+    }
+}
+
+function present_select_targettype(item)
+{
+    uifc.help_text = word_wrap(
+        "This is the type of target this item points to.\r\n\r\n"
+        + "custommenu is a custom menu defined in this tool.\r\n\r\n"
+        + "xtrnmenu is a standard Syncrhonet External Section Menu (refer to the scfg tool).\r\n\r\n"
+        + "xtrnprog is a direct link to an external program (refer to the scfg tool)");
+
+    var targetypectx = uifc.list.CTX(0, 0, 0, 0, 0);
+    if (typeof item.type !== "undefined") {
+        switch (item.type) {
+            case 'custommenu':
+                targetypectx.cur = 0;
+                targetypectx.bar = 0;
+                break;
+            case 'xtrnmenu':
+                targetypectx.cur = 1;
+                targetypectx.bar = 1;
+                break;
+            case 'xtrnprog':
+                targetypectx.cur = 2;
+                targetypectx.bar = 2;
+                break;
+        }
+    }
+    switch (uifc.list(WIN_ORG | WIN_MID | WIN_SAV,
+        "Target Type", ["custommenu", "xtrnmenu", "xtrnprog"], targetypectx)) {
+        case 0:
+            item.type = "custommenu";
+            break;
+        case 1:
+            item.type = "xtrnmenu"
+            break;
+        case 2:
+            item.type = "xtrnprog";
+            break;
+        default:
+            // includes escape key
+            break;
+    }
+
+    // convienence... enter target selection
+    present_select_target(item)    
+}
+
+function present_select_target(item)
+{
+    uifc.help_text = word_wrap("This is the ID of the custom menu, external program section, or external program to link to.");
+
+    var targetctx = uifc.list.CTX(0, 0, 0, 0, 0);
+
+    var custommenuitems = [];
+    var custommenuitemsids = [];
+    var custommenunames = [];
+    
+    var selection2;
+    
+    switch (item.type) {
+        case "custommenu":
+            // present list of custom menus
+            for (i in menuconfig.menus) {
+                custommenuitems.push(format("%23s: %s", menuconfig.menus[i].id, menuconfig.menus[i].title));
+                custommenuitemsids.push(menuconfig.menus[i].id);
+                custommenunames.push(menuconfig.menus[i].title);
+            }
+
+            if ((typeof item.target !== "undefined") && item.target) {
+                for (var p in custommenuitemsids) {
+                    if (custommenuitemsids[p] == item.target) {
+                        targetctx.cur = p;
+                        targetctx.bar = p;
+                    }
+                }
+            }
+
+            selection2 = uifc.list(WIN_ORG | WIN_MID | WIN_SAV, "Target", custommenuitems, targetctx);
+            if ((selection2 < 0) || (selection2 == null)) {
+                // escape key
+                break;
+            }
+
+            item.target = custommenuitemsids[selection2];
+
+            while(1) {
+                if (uifc.list(WIN_ORG | WIN_MID, "Replace item title with sections's name?", ["Yes", "No"]) == 0) {
+                    item.title = custommenunames[selection2]; // for external program, change title to program name
+                }
+                break;
+            }
+            break;
+
+        case "xtrnmenu":
+            // present list of external program sections
+            var seclist = [];
+            for (i in xtrn_area.sec_list) {
+                seclist.push({ code: xtrn_area.sec_list[i].code, name: xtrn_area.sec_list[i].name});
+            };
+            seclist.sort(sort_by_code);
+            
+            for (i in seclist) {
+                custommenuitems.push(format("%23s: %s", seclist[i].code, seclist[i].name));
+                custommenuitemsids.push(seclist[i].code);
+                custommenunames.push(seclist[i].name);
+            }
+
+            if ((typeof item.target !== "undefined") && item.target) {
+                for (var p in custommenuitemsids) {
+                    if (custommenuitemsids[p].toLowerCase() == item.target.toLowerCase()) {
+                        targetctx.cur = p;
+                        targetctx.bar = p;
+                    }
+                }
+            }
+
+            selection2 = uifc.list(WIN_ORG | WIN_MID | WIN_SAV, "Target", custommenuitems, targetctx);
+            if ((selection2 < 0) || (selection2 == null)) {
+                // escape key
+                break;
+            }
+
+            item.target = custommenuitemsids[selection2];
+
+            while(1) {
+                if (uifc.list(WIN_ORG | WIN_MID, "Replace item title with sections's name?", ["Yes", "No"]) == 0) {
+                    item.title = custommenunames[selection2]; // for external program, change title to program name
+                }
+                break;
+            }
+            break;
+
+        case "xtrnprog":
+
+            // present list of external programs
+            // create sorted list
+            var proglist = [];
+
+            for (i in xtrn_area.prog) {
+                proglist.push({ code: xtrn_area.prog[i].code, name: xtrn_area.prog[i].name});
+            };
+            proglist.sort(sort_by_code);
+            for (i in proglist) {
+                custommenuitems.push(format("%23s: %s", proglist[i].code, proglist[i].name));
+                custommenuitemsids.push(proglist[i].code);
+                custommenunames.push(proglist[i].name);
+            }
+
+            if ((typeof item.target !== "undefined") && item.target) {
+                for (var p in custommenuitemsids) {
+                    if (custommenuitemsids[p].toLowerCase() == item.target.toLowerCase()) {
+                        targetctx.cur = p;
+                        targetctx.bar = p;
+                    }
+                }
+            }
+
+            selection2 = uifc.list(WIN_ORG | WIN_MID | WIN_SAV, "Target", custommenuitems, targetctx);
+            if ((selection2 < 0) || (selection2 == null)) {
+                // escape key
+                break;
+            }
+            if (selection2 || selection2 === 0) {
+                item.target = custommenuitemsids[selection2];
+                while(1) {
+                    if (uifc.list(WIN_ORG | WIN_MID, "Replace item title with sections's name?", ["Yes", "No"]) == 0) {
+                        item.title = custommenunames[selection2]; // for external program, change title to program name
+                    }
+                    break;
+                }
+            }
+            break;
+
+        default:
+            selection2 = uifc.input(WIN_ORG | WIN_MID, "Target", item.target, 50, K_EDIT);
+            if ((selection2 < 0) || (selection2 == null)) {
+                // escape key
+                break;
+            }
+
+            item.target = selection2;
+            break;
+    }    
+}
+
+function sort_by_name(a, b)
+{
+    if (a.name.toLowerCase() > b.name.toLowerCase()) return 1;
+    if (a.name.toLowerCase() < b.name.toLowerCase()) return -1;
+    return 0;
+}
+
+function sort_by_code(a, b)
+{
+    if (a.code.toLowerCase() > b.code.toLowerCase()) return 1;
+    if (a.code.toLowerCase() < b.code.toLowerCase()) return -1;
+    return 0;
+}
+
+function sort_by_id(a, b)
+{
+    if (a.id.toLowerCase() > b.id.toLowerCase()) return 1;
+    if (a.id.toLowerCase() < b.id.toLowerCase()) return -1;
+    return 0;
+}
+
+
+// MAIN
+try {
+    var menuconfig = {};
+    var copyitem = {}; // for menu item copy/paste
+    var config_file = new File(file_cfgname(system.ctrl_dir, "xtrnmenu.cfg"));
+    if (config_file.open('r+')) {
+        var config_src = config_file.read();
+        try {
+            menuconfig = JSON.parse(config_src.toString());
+            if (!menuconfig) {
+                writeln("ERROR! Could not parse xtrnmenu.cfg. JSON may be invalid.");
+                exit();
+            }
+        } catch (e) {
+            writeln("ERROR! Could not parse xtrnmenu.cfg. JSON may be invalid. " + e.toString());
+            exit();
+        }
+    }
+    config_file.close();
+    
+    if (typeof menuconfig.menus === "undefined") {
+        menuconfig.menus = [];
+    }
+
+    uifc.init("Enhanced External Program Menus Configurator");
+    uifc.lightbar_color = 120;
+    uifc.background_color = 21; 
+    uifc.frame_color = 15;
+    js.on_exit("if (uifc.initialized) uifc.bail()");
+
+    // cur bar top left width
+    var ctx = new uifc.list.CTX(0, 0, 0, 0, 0);
+
+    while(1) {
+        uifc.help_text = word_wrap("This program allows managing the Enhanced External Program Menu feature.");
+
+        // no menus or no main menu
+        var mainmenufound = false;
+        for (var m in menuconfig.menus) {
+            if (menuconfig.menus[m].id.toLowerCase() == "main") {
+                mainmenufound = true;
+            }
+        }
+        if (!mainmenufound || (menuconfig.menus.length < 1)) {
+            uifc.msg("No menus defined and/or missing the main menu. Setting up now.");
+            editMenu("main");
+        }
+
+        // menus is array of menuconfig menu ids
+        var menus = [];
+        var menuTitles = [];
+        menuconfig.menus.sort(sort_by_id);
+        for (var m in menuconfig.menus) {
+            menus.push(menuconfig.menus[m].id);
+            menuTitles.push(format("%20s: %s", menuconfig.menus[m].id, menuconfig.menus[m].title));
+        }
+        
+        menuTitles.push(format("%20s  %s", '', "[Save Config Without Exit]"));
+        
+        // WIN_ORG = original menu, destroy valid screen area
+        // WIN_MID = place window in middle of screen
+        // WIN_XTR = add extra line at end for inserting at end
+        // WIN_DEL = allow user to use delete key
+        // WIN_ACT = menu remains active after a selection
+        // WIN_ESC = screen is active when escape is hit
+        // WIN_INS = allow user to use insert key
+        var selection = uifc.list(
+            WIN_ORG|WIN_MID|WIN_XTR|WIN_DEL|WIN_ACT|WIN_ESC|WIN_INS|WIN_SAV,
+            "Enhanced External Menus",
+            menuTitles,
+            ctx
+        );
+
+        if (selection == (menuTitles.length - 1)) {
+            // last item - save config
+            saveMenus();
+            uifc.pop("Config saved.");
+        } else if (selection < 0) {
+            while (1) {
+                var ret = uifc.list(WIN_ORG | WIN_MID, "Save Changes Before Exit?", ["Yes", "No", "Cancel"]);
+                if (ret == 0) {
+                    saveMenus();
+                    exit();
+                } else if (ret == 1) {
+                    // no - exit
+                    exit();
+                } else {
+                    // cancel
+                    break;
+                }
+            }
+
+        } else if ((selection & MSK_ON) == MSK_DEL) {
+            selection &= MSK_OFF;
+            var menus2 = [];
+            for (var m in menuconfig.menus) {
+                if (menuconfig.menus[m].id != menus[selection]) {
+                    menus2.push(menuconfig.menus[m]);
+                }
+            }
+            menuconfig.menus = menus2;
+            //selection--;
+        } else if (((selection & MSK_ON) == MSK_INS) || (selection >= menuconfig.menus.length)) {
+            // new menu
+            var newid = uifc.input(
+                WIN_MID,
+                "Enter a short unique id for the menu",
+                "",
+                0
+            );
+            if (typeof newid !== "undefined") {
+                var menufound = false;
+                for (var mf in menuconfig.menus) {
+                    if (menuconfig.menus[mf].id == newid) {
+                        menufound = true;
+                    }
+                }
+                if (menufound) {
+                    uifc.msg("That ID is already in use. Please choose another.");
+                } else {
+                    editMenu(newid);
+                }
+            }
+        } else {
+            editMenu(menuconfig.menus[selection].id);
+        }
+    }
+} catch(err) {
+    if ((typeof uifc !== "undefined") && uifc.initialized) {
+        uifc.bail();
+    }
+    writeln(err);
+    log(err);
+    if (typeof console !== "undefined") {
+        console.pause();
+    }
+}
diff --git a/src/sbbs3/.gitignore b/src/sbbs3/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..3467fa0e2eb31f7f349642e563cf3509f4a93a6c
--- /dev/null
+++ b/src/sbbs3/.gitignore
@@ -0,0 +1,2 @@
+git_branch.h
+git_hash.h
diff --git a/src/sbbs3/atcodes.cpp b/src/sbbs3/atcodes.cpp
index 971436da8b4ec55fb8f645bab7b93159a71fae61..f4384eaea74f410c31c4a93c25781a4af9b336f1 100644
--- a/src/sbbs3/atcodes.cpp
+++ b/src/sbbs3/atcodes.cpp
@@ -1,7 +1,4 @@
 /* Synchronet "@code" functions */
-// vi: tabstop=4
-
-/* $Id: atcodes.cpp,v 1.142 2020/05/10 20:12:35 rswindell Exp $ */
 
 /****************************************************************************
  * @format.tab-size 4		(Plain Text/Source Code File Header)			*
@@ -16,21 +13,9 @@
  * See the GNU General Public License for more details: gpl.txt or			*
  * http://www.fsf.org/copyleft/gpl.html										*
  *																			*
- * Anonymous FTP access to the most recent released source is available at	*
- * ftp://vert.synchro.net, ftp://cvs.synchro.net and ftp://ftp.synchro.net	*
- *																			*
- * Anonymous CVS access to the development source and modification history	*
- * is available at cvs.synchro.net:/cvsroot/sbbs, example:					*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs login			*
- *     (just hit return, no password is necessary)							*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs checkout src		*
- *																			*
  * For Synchronet coding style and modification guidelines, see				*
  * http://www.synchro.net/source.html										*
  *																			*
- * You are encouraged to submit any modifications (preferably in Unix diff	*
- * format) via e-mail to mods@synchro.net									*
- *																			*
  * Note: If this box doesn't appear square, then you need to fix your tabs.	*
  ****************************************************************************/
 
@@ -39,6 +24,7 @@
 #include "utf8.h"
 #include "unicode.h"
 #include "cp437defs.h"
+#include "ver.h"
 
 #if defined(_WINSOCKAPI_)
 	extern WSADATA WSAData;
@@ -411,6 +397,12 @@ const char* sbbs_t::atcode(char* sp, char* str, size_t maxlen, long* pmode, bool
 		return(str);
 	}
 
+	if(strcmp(sp, "GIT_HASH") == 0)
+		return git_hash;
+
+	if(strcmp(sp, "GIT_BRANCH") == 0)
+		return git_branch;
+
 	if(!strcmp(sp,"UPTIME")) {
 		extern volatile time_t uptime;
 		time_t up=0;
@@ -1126,6 +1118,21 @@ const char* sbbs_t::atcode(char* sp, char* str, size_t maxlen, long* pmode, bool
 		return(str);
 	}
 
+	if(strcmp(sp,"FREESPACEM") == 0) {
+		safe_snprintf(str,maxlen,"%lu",getfreediskspace(cfg.temp_dir, 1024 * 1024));
+		return(str);
+	}
+
+	if(strcmp(sp,"FREESPACEG") == 0) {
+		safe_snprintf(str,maxlen,"%lu",getfreediskspace(cfg.temp_dir, 1024 * 1024 * 1024));
+		return(str);
+	}
+
+	if(strcmp(sp,"FREESPACET") == 0) {
+		safe_snprintf(str,maxlen,"%lu",getfreediskspace(cfg.temp_dir, 1024 * 1024 * 1024) / 1024);
+		return(str);
+	}
+
 	if(!strcmp(sp,"UPBYTES")) {
 		safe_snprintf(str,maxlen,"%lu",useron.ulb);
 		return(str);
@@ -1578,6 +1585,16 @@ const char* sbbs_t::atcode(char* sp, char* str, size_t maxlen, long* pmode, bool
 		return(tp);
 	}
 
+	if(!strcmp(sp,"MAILR")) {
+		safe_snprintf(str,maxlen,"%u",getmail(&cfg,useron.number, /* Sent: */FALSE, /* attr: */MSG_READ));
+		return(str);
+	}
+
+	if(!strcmp(sp,"MAILU")) {
+		safe_snprintf(str,maxlen,"%u",getmail(&cfg,useron.number, /* Sent: */FALSE, /* attr: */~MSG_READ));
+		return(str);
+	}
+
 	if(!strcmp(sp,"MAILW")) {
 		safe_snprintf(str,maxlen,"%u",getmail(&cfg,useron.number, /* Sent: */FALSE, /* attr: */0));
 		return(str);
@@ -1593,6 +1610,16 @@ const char* sbbs_t::atcode(char* sp, char* str, size_t maxlen, long* pmode, bool
 		return(str);
 	}
 
+	if(!strncmp(sp,"MAILR:",6) || !strncmp(sp,"MAILR#",6)) {
+		safe_snprintf(str,maxlen,"%u",getmail(&cfg,atoi(sp+6), /* Sent: */FALSE, /* attr: */MSG_READ));
+		return(str);
+	}
+
+	if(!strncmp(sp,"MAILU:",6) || !strncmp(sp,"MAILU#",6)) {
+		safe_snprintf(str,maxlen,"%u",getmail(&cfg,atoi(sp+6), /* Sent: */FALSE, /* attr: */~MSG_READ));
+		return(str);
+	}
+
 	if(!strncmp(sp,"MAILW:",6) || !strncmp(sp,"MAILW#",6)) {
 		safe_snprintf(str,maxlen,"%u",getmail(&cfg,atoi(sp+6), /* Sent: */FALSE, /* attr: */0));
 		return(str);
@@ -1821,7 +1848,22 @@ const char* sbbs_t::atcode(char* sp, char* str, size_t maxlen, long* pmode, bool
 		return (current_msg != NULL && current_msg->user_voted == 1) ? text[PollAnswerChecked] : nulstr;
 	if(!strcmp(sp,"MSG_DOWNVOTED"))
 		return (current_msg != NULL && current_msg->user_voted == 2) ? text[PollAnswerChecked] : nulstr;
-
+	if(strcmp(sp, "MSG_THREAD_ID") == 0 && current_msg != NULL) {
+		safe_snprintf(str, maxlen, "%lu", (ulong)current_msg->hdr.thread_id);
+		return str;
+	}
+	if(strcmp(sp, "MSG_THREAD_BACK") == 0 && current_msg != NULL) {
+		safe_snprintf(str, maxlen, "%lu", (ulong)current_msg->hdr.thread_back);
+		return str;
+	}
+	if(strcmp(sp, "MSG_THREAD_NEXT") == 0 && current_msg != NULL) {
+		safe_snprintf(str, maxlen, "%lu", (ulong)current_msg->hdr.thread_next);
+		return str;
+	}
+	if(strcmp(sp, "MSG_THREAD_FIRST") == 0 && current_msg != NULL) {
+		safe_snprintf(str, maxlen, "%lu", (ulong)current_msg->hdr.thread_first);
+		return str;
+	}
 	if(!strcmp(sp,"SMB_AREA")) {
 		if(smb.subnum!=INVALID_SUB && smb.subnum<cfg.total_subs)
 			safe_snprintf(str,maxlen,"%s %s"
diff --git a/src/sbbs3/con_out.cpp b/src/sbbs3/con_out.cpp
index 0063c660b578461103ef0190c0d8206dd5b0625c..b0e05b9f9988bee062f7870666ca33387c2c5c6d 100644
--- a/src/sbbs3/con_out.cpp
+++ b/src/sbbs3/con_out.cpp
@@ -787,7 +787,7 @@ void sbbs_t::inc_row(int count)
 	}
 }
 
-void sbbs_t::center(char *instr, unsigned int columns)
+void sbbs_t::center(const char *instr, unsigned int columns)
 {
 	char str[256];
 	size_t len;
diff --git a/src/sbbs3/data.cpp b/src/sbbs3/data.cpp
index 52614df625c0419c8bd34b96c478eb793ca39121..4e4e587a79e6a0c17e29bdd63fc82e29de4f81d9 100644
--- a/src/sbbs3/data.cpp
+++ b/src/sbbs3/data.cpp
@@ -1,7 +1,5 @@
 /* Synchronet (oh, so old) data access routines */
 
-/* $Id: data.cpp,v 1.32 2020/04/27 07:42:23 rswindell Exp $ */
-
 /****************************************************************************
  * @format.tab-size 4		(Plain Text/Source Code File Header)			*
  * @format.use-tabs true	(see http://www.synchro.net/ptsc_hdr.html)		*
@@ -15,21 +13,9 @@
  * See the GNU General Public License for more details: gpl.txt or			*
  * http://www.fsf.org/copyleft/gpl.html										*
  *																			*
- * Anonymous FTP access to the most recent released source is available at	*
- * ftp://vert.synchro.net, ftp://cvs.synchro.net and ftp://ftp.synchro.net	*
- *																			*
- * Anonymous CVS access to the development source and modification history	*
- * is available at cvs.synchro.net:/cvsroot/sbbs, example:					*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs login			*
- *     (just hit return, no password is necessary)							*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs checkout src		*
- *																			*
  * For Synchronet coding style and modification guidelines, see				*
  * http://www.synchro.net/source.html										*
  *																			*
- * You are encouraged to submit any modifications (preferably in Unix diff	*
- * format) via e-mail to mods@synchro.net									*
- *																			*
  * Note: If this box doesn't appear square, then you need to fix your tabs.	*
  ****************************************************************************/
 
@@ -45,7 +31,7 @@
 /* Returns the number of the matched user or 0 if unsuccessful				*/
 /* Called from functions main_sec, useredit and readmailw					*/
 /****************************************************************************/
-uint sbbs_t::finduser(char *instr, bool silent_failure)
+uint sbbs_t::finduser(const char* instr, bool silent_failure)
 {
 	int file,i;
 	char str[128],str2[256],str3[256],ynq[25],c,pass=1;
diff --git a/src/sbbs3/ftpsrvr.c b/src/sbbs3/ftpsrvr.c
index 5460eb5080ec8ee70f2cbb13516f61691c7b8a51..87bf05a99b4375ebf9cd2efcd475d72691a67e94 100644
--- a/src/sbbs3/ftpsrvr.c
+++ b/src/sbbs3/ftpsrvr.c
@@ -43,6 +43,7 @@
 #include "cryptlib.h"
 #include "xpprintf.h"		// vasprintf
 #include "md5.h"
+#include "ver.h"
 
 /* Constants */
 
@@ -82,7 +83,6 @@ static protected_uint32_t thread_count;
 static volatile time_t	uptime=0;
 static volatile ulong	served=0;
 static volatile BOOL	terminate_server=FALSE;
-static char		revision[16];
 static char 	*text[TOTAL_TEXT];
 static str_list_t recycle_semfiles;
 static str_list_t shutdown_semfiles;
@@ -3112,8 +3112,8 @@ static void ctrl_thread(void* arg)
 	}
 
 	sockprintf(sock,sess,"220-%s (%s)",scfg.sys_name, server_host_name());
-	sockprintf(sock,sess," Synchronet FTP Server %s-%s Ready"
-		,revision,PLATFORM_DESC);
+	sockprintf(sock,sess," Synchronet FTP Server %s%c-%s Ready"
+		,VERSION, REVISION, PLATFORM_DESC);
 	sprintf(str,"%sftplogin.txt",scfg.text_dir);
 	if((fp=fopen(str,"rb"))!=NULL) {
 		while(!feof(fp)) {
@@ -5965,17 +5965,16 @@ const char* DLLCALL ftp_ver(void)
 
 	DESCRIBE_COMPILER(compiler);
 
-	sscanf("$Revision: 1.501 $", "%*s %s", revision);
-
-	sprintf(ver,"%s %s%s  "
-		"Compiled %s %s with %s"
+	safe_snprintf(ver, sizeof(ver), "%s %s%c%s  "
+		"Compiled %s/%s %s %s with %s"
 		,FTP_SERVER
-		,revision
+		,VERSION, REVISION
 #ifdef _DEBUG
 		," Debug"
 #else
 		,""
 #endif
+		,git_branch, git_hash
 		,__DATE__, __TIME__, compiler);
 
 	return(ver);
@@ -5999,8 +5998,6 @@ void DLLCALL ftp_server(void* arg)
 	char			client_ip[INET6_ADDRSTRLEN];
 	CRYPT_SESSION		none = -1;
 
-	ftp_ver();
-
 	startup=(ftp_startup_t*)arg;
 	SetThreadName("sbbs/ftpServer");
 
@@ -6024,7 +6021,7 @@ void DLLCALL ftp_server(void* arg)
 	}
 
 	ZERO_VAR(js_server_props);
-	SAFEPRINTF2(js_server_props.version,"%s %s",FTP_SERVER,revision);
+	SAFEPRINTF3(js_server_props.version,"%s %s%c", FTP_SERVER, VERSION, REVISION);
 	js_server_props.version_detail=ftp_ver();
 	js_server_props.clients=&active_clients.value;
 	js_server_props.options=&startup->options;
@@ -6061,8 +6058,8 @@ void DLLCALL ftp_server(void* arg)
 
 		memset(&scfg, 0, sizeof(scfg));
 
-		lprintf(LOG_INFO,"Synchronet FTP Server Revision %s%s"
-			,revision
+		lprintf(LOG_INFO,"Synchronet FTP Server Version %s%c%s"
+			,VERSION, REVISION
 #ifdef _DEBUG
 			," Debug"
 #else
@@ -6072,7 +6069,7 @@ void DLLCALL ftp_server(void* arg)
 
 		DESCRIBE_COMPILER(compiler);
 
-		lprintf(LOG_INFO,"Compiled %s %s with %s", __DATE__, __TIME__, compiler);
+		lprintf(LOG_INFO,"Compiled %s/%s %s %s with %s", git_branch, git_hash, __DATE__, __TIME__, compiler);
 
 		sbbs_srand();	/* Seed random number generator */
 
diff --git a/src/sbbs3/ftpsrvr.vcxproj b/src/sbbs3/ftpsrvr.vcxproj
index 84539c12fdf751e01c69ae5e86e8d4877fb329cb..7bcbbde8917079093e640d756a0de47baefb822c 100644
--- a/src/sbbs3/ftpsrvr.vcxproj
+++ b/src/sbbs3/ftpsrvr.vcxproj
@@ -176,6 +176,7 @@
       <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
       <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
     </ClCompile>
+    <ClCompile Include="ver.cpp" />
   </ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\xpdev\xpdev_mt.vcxproj">
diff --git a/src/sbbs3/getmail.c b/src/sbbs3/getmail.c
index a38d14390688dadd0182a0a5ad999cf1d4c8af9b..2dc5b48ce1081375dfa37da126d666b5ee5674bb 100644
--- a/src/sbbs3/getmail.c
+++ b/src/sbbs3/getmail.c
@@ -26,7 +26,7 @@
 /* If sent is non-zero, it returns the number of mail sent by usernumber    */
 /* If usernumber is 0, it returns all mail on the system                    */
 /****************************************************************************/
-int DLLCALL getmail(scfg_t* cfg, int usernumber, BOOL sent, uint16_t attr)
+int DLLCALL getmail(scfg_t* cfg, int usernumber, BOOL sent, int attr)
 {
     char    path[MAX_PATH+1];
     int     i=0;
@@ -55,7 +55,9 @@ int DLLCALL getmail(scfg_t* cfg, int usernumber, BOOL sent, uint16_t attr)
 			continue;
 		if(idx.attr&MSG_DELETE)
 			continue;
-		if((idx.attr&attr) != attr)
+		if(attr < 0 && (idx.attr & (~attr)) != 0)
+			continue;
+		if(attr > 0 && (idx.attr & attr) != attr)
 			continue;
 		if(usernumber == 0
 			|| (!sent && idx.to==usernumber)
diff --git a/src/sbbs3/getmail.h b/src/sbbs3/getmail.h
index 949db78e9d9144527fa0cf72926c3950591e7b3c..2a1f4b0e38944d23b79f69693cafc6ce324d3a1f 100644
--- a/src/sbbs3/getmail.h
+++ b/src/sbbs3/getmail.h
@@ -28,7 +28,7 @@
 extern "C" {
 #endif
 
-DLLEXPORT int		getmail(scfg_t* cfg, int usernumber, BOOL sent, uint16_t attr);
+DLLEXPORT int		getmail(scfg_t* cfg, int usernumber, BOOL sent, int attr);
 DLLEXPORT mail_t *	loadmail(smb_t* smb, uint32_t* msgs, uint usernumber
 							,int which, long mode);
 DLLEXPORT void		freemail(mail_t* mail);
@@ -37,4 +37,4 @@ DLLEXPORT BOOL		delfattach(scfg_t*, smbmsg_t*);
 #ifdef __cplusplus
 }
 #endif
-#endif /* Don't add anything after this line */
\ No newline at end of file
+#endif /* Don't add anything after this line */
diff --git a/src/sbbs3/getmsg.cpp b/src/sbbs3/getmsg.cpp
index e12351e8a44f279319e7a7ca6d9528b2554ed255..d0c98915b5a366126d99ca131587b1f8c57d4752 100644
--- a/src/sbbs3/getmsg.cpp
+++ b/src/sbbs3/getmsg.cpp
@@ -97,7 +97,8 @@ void sbbs_t::show_msgattr(smbmsg_t* msg)
 	uint32_t auxattr = msg->hdr.auxattr;
 	uint32_t netattr = msg->hdr.netattr;
 
-	bprintf(text[MsgAttr]
+	char attr_str[64];
+	safe_snprintf(attr_str, sizeof(attr_str), "%s%s%s%s%s%s%s%s%s%s%s%s%s%s"
 		,attr&MSG_PRIVATE	? "Private  "   :nulstr
 		,attr&MSG_SPAM		? "SPAM  "      :nulstr
 		,attr&MSG_READ		? "Read  "      :nulstr
@@ -112,11 +113,35 @@ void sbbs_t::show_msgattr(smbmsg_t* msg)
 		,attr&MSG_NOREPLY	? "NoReply  "	:nulstr
 		,poll == MSG_POLL	? "Poll  "		:nulstr
 		,poll == MSG_POLL && auxattr&POLL_CLOSED ? "(Closed)  "	:nulstr
-		,auxattr&(MSG_FILEATTACH|MSG_MIMEATTACH) ? "Attach  "   :nulstr
-		,netattr&MSG_SENT						 ? "Sent  "		:nulstr
-		,netattr&MSG_INTRANSIT					 ? "InTransit  ":nulstr
-		,netattr&MSG_KILLSENT					 ? "KillSent  " :nulstr
+	);
+
+	char auxattr_str[64];
+	safe_snprintf(auxattr_str, sizeof(auxattr_str), "%s%s%s%s%s%s%s"
+		,auxattr&MSG_FILEREQUEST? "FileRequest  "   :nulstr
+		,auxattr&MSG_FILEATTACH	? "FileAttach  "    :nulstr
+		,auxattr&MSG_MIMEATTACH	? "MimeAttach  "	:nulstr
+		,auxattr&MSG_KILLFILE	? "KillFile  "      :nulstr
+		,auxattr&MSG_RECEIPTREQ	? "ReceiptReq  "	:nulstr
+		,auxattr&MSG_CONFIRMREQ	? "ConfirmReq  "    :nulstr
+		,auxattr&MSG_NODISP		? "DontDisplay  "	:nulstr
 		);
+
+	char netattr_str[64];
+	safe_snprintf(netattr_str, sizeof(netattr_str), "%s%s%s%s%s%s%s%s"
+		,netattr&MSG_LOCAL		? "Local  "			:nulstr
+		,netattr&MSG_INTRANSIT	? "InTransit  "     :nulstr
+		,netattr&MSG_SENT		? "Sent  "			:nulstr
+		,netattr&MSG_KILLSENT	? "KillSent  "      :nulstr
+		,netattr&MSG_HOLD		? "Hold  "			:nulstr
+		,netattr&MSG_CRASH		? "Crash  "			:nulstr
+		,netattr&MSG_IMMEDIATE	? "Immediate  "		:nulstr
+		,netattr&MSG_DIRECT		? "Direct  "		:nulstr
+		);
+
+	bprintf(text[MsgAttr], attr_str, auxattr_str, netattr_str
+		,nulstr, nulstr, nulstr, nulstr, nulstr, nulstr, nulstr, nulstr, nulstr, nulstr, nulstr
+		,nulstr, nulstr, nulstr, nulstr, nulstr, nulstr, nulstr, nulstr, nulstr, nulstr, nulstr
+		,nulstr, nulstr, nulstr, nulstr, nulstr, nulstr, nulstr, nulstr, nulstr, nulstr, nulstr);
 }
 
 /* Returns a CP437 text.dat string converted to UTF-8, when appropriate */
@@ -247,19 +272,19 @@ bool sbbs_t::show_msg(smb_t* smb, smbmsg_t* msg, long p_mode, post_t* post)
 
 	show_msghdr(smb, msg);
 
+	int comments=0;
+	for(int i = 0; i < msg->total_hfields; i++)
+		if(msg->hfield[i].type == SMB_COMMENT) {
+			bprintf("%s\r\n", (char*)msg->hfield_dat[i]);
+			comments++;
+		}
+	if(comments)
+		CRLF;
+
 	if(msg->hdr.type == SMB_MSG_TYPE_POLL && post != NULL && smb->subnum < cfg.total_subs) {
 		char* answer;
 		int longest_answer = 0;
 
-		int comments=0;
-		for(int i = 0; i < msg->total_hfields; i++)
-			if(msg->hfield[i].type == SMB_COMMENT) {
-				bprintf("%s\r\n", (char*)msg->hfield_dat[i]);
-				comments++;
-			}
-		if(comments)
-			CRLF;
-
 		for(int i = 0; i < msg->total_hfields; i++) {
 			if(msg->hfield[i].type != SMB_POLL_ANSWER)
 				continue;
@@ -307,7 +332,7 @@ bool sbbs_t::show_msg(smb_t* smb, smbmsg_t* msg, long p_mode, post_t* post)
 			mnemonics(text[VoteInThisPollNow]);
 		return true;
 	}
-	if((txt=smb_getmsgtxt(smb, msg, 0)) == NULL)
+	if((txt=smb_getmsgtxt(smb, msg, GETMSGTXT_BODY_ONLY)) == NULL)
 		return false;
 	char* p = txt;
 	if(!(console&CON_RAW_IN)) {
diff --git a/src/sbbs3/gitinfo.bat b/src/sbbs3/gitinfo.bat
new file mode 100644
index 0000000000000000000000000000000000000000..63b9bec24281ecdfdae45c7e7155cf4a95a10f91
--- /dev/null
+++ b/src/sbbs3/gitinfo.bat
@@ -0,0 +1,4 @@
+@git log -1 HEAD --format="#define GIT_HASH \"%%h\"" > git_hash.h
+@echo #define GIT_BRANCH ^"| tr -d "\r\n" > git_branch.h
+@git rev-parse --abbrev-ref HEAD | tr -d "\n" >> git_branch.h
+@echo ^" >> git_branch.h
\ No newline at end of file
diff --git a/src/sbbs3/js_system.c b/src/sbbs3/js_system.c
index 6dea02881157acfb982f25dd10b6cf18f6bf566d..bd7039212ae503fa3b89a0a971a6b2a175b6785e 100644
--- a/src/sbbs3/js_system.c
+++ b/src/sbbs3/js_system.c
@@ -21,6 +21,7 @@
 
 #include "sbbs.h"
 #include "js_request.h"
+#include "ver.h"
 
 #ifdef JAVASCRIPT
 
@@ -529,6 +530,8 @@ static char* sys_prop_desc[] = {
 	,"Synchronet version notice (includes version and platform)"
 	,"Synchronet version number in decimal (e.g. 31301 for v3.13b)"
 	,"Synchronet version number in hexadecimal (e.g. 0x31301 for v3.13b)"
+	,"Synchronet Git repository branch name"
+	,"Synchronet Git repository commit hash"
 	,"platform description (e.g. 'Win32', 'Linux', 'FreeBSD')"
 	,"architecture description (e.g. 'i386', 'i686', 'x86_64')"
 	,"message base library version information"
@@ -1701,9 +1704,12 @@ js_new_user(JSContext *cx, uintN argc, jsval *arglist)
 		}
 	}
 	if(client!=NULL) {
-		SAFECOPY(user.modem,client->protocol);
-		SAFECOPY(user.comp,client->host);
-		SAFECOPY(user.ipaddr,client->addr);
+		if(client->protocol != NULL)
+			SAFECOPY(user.modem,client->protocol);
+		if(client->host != NULL)
+			SAFECOPY(user.comp,client->host);
+		if(client->addr != NULL)
+			SAFECOPY(user.ipaddr,client->addr);
 	}
 
 	user.sex=' ';
@@ -2472,6 +2478,10 @@ static JSBool js_system_resolve(JSContext *cx, JSObject *obj, jsid id)
 	LAZY_INTEGER("version_num", VERSION_NUM);
 	LAZY_INTEGER("version_hex", VERSION_HEX);
 
+	/* Git repo details */
+	LAZY_STRING("git_branch", git_branch);
+	LAZY_STRING("git_hash", git_hash);
+
 	LAZY_STRING("platform", PLATFORM_DESC);
 	LAZY_STRING("architecture", ARCHITECTURE_DESC);
 	LAZY_STRFUNC("msgbase_lib", sprintf(str,"SMBLIB %s",smb_lib_ver()), str);
diff --git a/src/sbbs3/js_user.c b/src/sbbs3/js_user.c
index f961a2b44e0ca449bed7d115ae7d19c3cff1be0f..39900e7d8463f837cc86d7ac8e2fc07a7b22940e 100644
--- a/src/sbbs3/js_user.c
+++ b/src/sbbs3/js_user.c
@@ -1,7 +1,4 @@
 /* Synchronet JavaScript "User" Object */
-// vi: tabstop=4
-
-/* $Id: js_user.c,v 1.119 2020/08/11 03:54:58 rswindell Exp $ */
 
 /****************************************************************************
  * @format.tab-size 4		(Plain Text/Source Code File Header)			*
@@ -16,21 +13,9 @@
  * See the GNU General Public License for more details: gpl.txt or			*
  * http://www.fsf.org/copyleft/gpl.html										*
  *																			*
- * Anonymous FTP access to the most recent released source is available at	*
- * ftp://vert.synchro.net, ftp://cvs.synchro.net and ftp://ftp.synchro.net	*
- *																			*
- * Anonymous CVS access to the development source and modification history	*
- * is available at cvs.synchro.net:/cvsroot/sbbs, example:					*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs login			*
- *     (just hit return, no password is necessary)							*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs checkout src		*
- *																			*
  * For Synchronet coding style and modification guidelines, see				*
  * http://www.synchro.net/source.html										*
  *																			*
- * You are encouraged to submit any modifications (preferably in Unix diff	*
- * format) via e-mail to mods@synchro.net									*
- *																			*
  * Note: If this box doesn't appear square, then you need to fix your tabs.	*
  ****************************************************************************/
 
@@ -88,6 +73,9 @@ enum {
 	,USER_PROP_ETODAY	
 	,USER_PROP_PTODAY
 	,USER_PROP_MAIL_WAITING
+	,USER_PROP_READ_WAITING
+	,USER_PROP_UNREAD_WAITING
+	,USER_PROP_SPAM_WAITING
 	,USER_PROP_MAIL_PENDING
 	,USER_PROP_ULB       
 	,USER_PROP_ULS       
@@ -391,7 +379,16 @@ static JSBool js_user_get(JSContext *cx, JSObject *obj, jsid id, jsval *vp)
 			val=scfg->level_freecdtperday[p->user->level];
 			break;
 		case USER_PROP_MAIL_WAITING:
-			val=getmail(scfg,p->user->number,/* sent? */FALSE, /* SPAM: */FALSE);
+			val=getmail(scfg,p->user->number,/* sent? */FALSE, /* attr: */0);
+			break;
+		case USER_PROP_READ_WAITING:
+			val=getmail(scfg,p->user->number,/* sent? */FALSE, /* attr: */MSG_READ);
+			break;
+		case USER_PROP_UNREAD_WAITING:
+			val=getmail(scfg,p->user->number,/* sent? */FALSE, /* attr: */~MSG_READ);
+			break;
+		case USER_PROP_SPAM_WAITING:
+			val=getmail(scfg,p->user->number,/* sent? */FALSE, /* attr: */MSG_SPAM);
 			break;
 		case USER_PROP_MAIL_PENDING:
 			val=getmail(scfg,p->user->number,/* sent? */TRUE, /* SPAM: */FALSE);
@@ -960,6 +957,9 @@ static jsSyncPropertySpec js_user_stats_properties[] = {
 	{	"files_downloaded"	,USER_PROP_DLS        	,USER_PROP_FLAGS,		310 },
 	{	"leech_attempts"	,USER_PROP_LEECH 	 	,USER_PROP_FLAGS,		310 },
 	{	"mail_waiting"		,USER_PROP_MAIL_WAITING	,USER_PROP_FLAGS,		312	},
+	{	"read_mail_waiting"	,USER_PROP_READ_WAITING	,USER_PROP_FLAGS,		31802 },
+	{	"unread_mail_waiting",USER_PROP_UNREAD_WAITING,USER_PROP_FLAGS,		31802 },
+	{	"spam_waiting"		,USER_PROP_SPAM_WAITING	,USER_PROP_FLAGS,		31802 },
 	{	"mail_pending"		,USER_PROP_MAIL_PENDING	,USER_PROP_FLAGS,		312	},
 	{0}
 };
@@ -984,7 +984,10 @@ static char* user_stats_prop_desc[] = {
 	,"total bytes downloaded"
 	,"total files downloaded"
 	,"suspected leech downloads"
-	,"number of e-mail messages currently waiting"
+	,"total number of e-mail messages currently waiting in inbox"
+	,"number of read e-mail messages currently waiting in inbox"
+	,"number of unread e-mail messages currently waiting in inbox"
+	,"number of SPAM e-mail messages currently waiting in inbox"
 	,"number of e-mail messages sent, currently pending deletion"
 	,NULL
 };
diff --git a/src/sbbs3/mailsrvr.c b/src/sbbs3/mailsrvr.c
index b90523cb51015b376861573b241a2b87d8e93cd3..90fbb8ae3f1ba61828af54740eded516379336a8 100644
--- a/src/sbbs3/mailsrvr.c
+++ b/src/sbbs3/mailsrvr.c
@@ -45,6 +45,7 @@
 #include "multisock.h"
 #include "ssl.h"
 #include "cryptlib.h"
+#include "ver.h"
 
 /* Constants */
 static const char*	server_name="Synchronet Mail Server";
@@ -84,7 +85,6 @@ static volatile BOOL	sendmail_running=FALSE;
 static volatile BOOL	terminate_server=FALSE;
 static volatile BOOL	terminate_sendmail=FALSE;
 static sem_t	sendmail_wakeup_sem;
-static char		revision[16];
 static volatile time_t	uptime;
 static str_list_t recycle_semfiles;
 static str_list_t shutdown_semfiles;
@@ -1176,8 +1176,8 @@ static void pop3_thread(void* arg)
 		safe_snprintf(challenge,sizeof(challenge),"<%x%x%lx%lx@%.128s>"
 			,rand(),socket,(ulong)time(NULL),(ulong)clock(), server_host_name());
 
-		sockprintf(socket,client.protocol,session,"+OK Synchronet %s Server %s-%s Ready %s"
-			,client.protocol, revision,PLATFORM_DESC,challenge);
+		sockprintf(socket,client.protocol,session,"+OK Synchronet %s Server %s%c-%s Ready %s"
+			,client.protocol, VERSION, REVISION, PLATFORM_DESC, challenge);
 
 		/* Requires USER or APOP command first */
 		for(i=5;i;i--) {
@@ -1192,7 +1192,7 @@ static void pop3_thread(void* arg)
 			else if (!stricmp(buf, "CAPA")) {
 				// Capabilities
 				sockprintf(socket,client.protocol,session, "+OK Capability list follows");
-				sockprintf(socket,client.protocol,session, "TOP\r\nUSER\r\nPIPELINING\r\nUIDL\r\nIMPLEMENTATION Synchronet POP3 Server %s-%s\r\n%s.", revision, PLATFORM_DESC, (session != -1 || get_ssl_cert(&scfg, NULL, NULL) == -1) ? "" : "STLS\r\n");
+				sockprintf(socket,client.protocol,session, "TOP\r\nUSER\r\nPIPELINING\r\nUIDL\r\nIMPLEMENTATION Synchronet POP3 Server %s%c-%s\r\n%s.", VERSION, REVISION, PLATFORM_DESC, (session != -1 || get_ssl_cert(&scfg, NULL, NULL) == -1) ? "" : "STLS\r\n");
 				i++;
 			}
 			else if (!stricmp(buf, "STLS")) {
@@ -1408,7 +1408,7 @@ static void pop3_thread(void* arg)
 			if(!stricmp(buf, "CAPA")) {
 				// Capabilities
 				sockprintf(socket,client.protocol,session, "+OK Capability list follows");
-				sockprintf(socket,client.protocol,session, "TOP\r\nUSER\r\nPIPELINING\r\nUIDL\r\nIMPLEMENTATION Synchronet POP3 Server %s-%s\r\n.", revision, PLATFORM_DESC);
+				sockprintf(socket,client.protocol,session, "TOP\r\nUSER\r\nPIPELINING\r\nUIDL\r\nIMPLEMENTATION Synchronet POP3 Server %s%c-%s\r\n.", VERSION, REVISION, PLATFORM_DESC);
 				continue;
 			}
 			if(!stricmp(buf, "QUIT")) {
@@ -3209,8 +3209,8 @@ static void smtp_thread(void* arg)
 
 	/* SMTP session active: */
 
-	sockprintf(socket,client.protocol,session,"220 %s Synchronet %s Server %s-%s Ready"
-		,server_host_name(), client.protocol, revision, PLATFORM_DESC);
+	sockprintf(socket,client.protocol,session,"220 %s Synchronet %s Server %s%c-%s Ready"
+		,server_host_name(), client.protocol, VERSION, REVISION, PLATFORM_DESC);
 	while(1) {
 		rd = sockreadline(socket, client.protocol, session, buf, sizeof(buf));
 		if(rd<0) 
@@ -3928,7 +3928,7 @@ static void smtp_thread(void* arg)
 
 					snprintf(hdrfield,sizeof(hdrfield),
 						"from %s (%s [%s%s])\r\n"
-						"          by %s [%s%s] (%s %s-%s) with %s\r\n"
+						"          by %s [%s%s] (%s %s%c-%s) with %s\r\n"
 						"          for %s; %s\r\n"
 						"          (envelope-from %s)"
 						,host_name,hello_name
@@ -3938,7 +3938,7 @@ static void smtp_thread(void* arg)
 						,server_addr.addr.sa_family==AF_INET6?"IPv6: ":""
 						,server_ip
 						,server_name
-						,revision,PLATFORM_DESC
+						,VERSION, REVISION, PLATFORM_DESC
 						,with_clauses[with_val]
 						,forward_path,msgdate(msg.hdr.when_imported,date)
 						,reverse_path);
@@ -6007,18 +6007,16 @@ const char* DLLCALL mail_ver(void)
 
 	DESCRIBE_COMPILER(compiler);
 
-	sscanf("$Revision: 1.735 $", "%*s %s", revision);
-
-	sprintf(ver,"%s %s%s  SMBLIB %s  "
-		"Compiled %s %s with %s"
+	sprintf(ver,"%s %s%c%s  "
+		"Compiled %s/%s %s %s with %s"
 		,server_name
-		,revision
+		,VERSION, REVISION
 #ifdef _DEBUG
 		," Debug"
 #else
 		,""
 #endif
-		,smb_lib_ver()
+		,git_branch, git_hash
 		,__DATE__, __TIME__, compiler
 		);
 
@@ -6049,8 +6047,6 @@ void DLLCALL mail_server(void* arg)
 	char*			servprot = "N/A";
 	CRYPT_SESSION	session = -1;
 
-	mail_ver();
-
 	startup=(mail_startup_t*)arg;
 
 #ifdef _THREAD_SUID_BROKEN
@@ -6073,7 +6069,7 @@ void DLLCALL mail_server(void* arg)
 	}
 
 	ZERO_VAR(js_server_props);
-	SAFEPRINTF2(js_server_props.version,"%s %s",server_name,revision);
+	SAFEPRINTF3(js_server_props.version,"%s %s%c",server_name, VERSION, REVISION);
 	js_server_props.version_detail=mail_ver();
 	js_server_props.clients=&active_clients.value;
 	js_server_props.options=&startup->options;
@@ -6113,9 +6109,9 @@ void DLLCALL mail_server(void* arg)
 
 		memset(&scfg, 0, sizeof(scfg));
 
-		lprintf(LOG_INFO,"%s Revision %s%s"
+		lprintf(LOG_INFO,"%s Version %s%c%s"
 			,server_name
-			,revision
+			,VERSION, REVISION
 #ifdef _DEBUG
 			," Debug"
 #else
@@ -6125,9 +6121,7 @@ void DLLCALL mail_server(void* arg)
 
 		DESCRIBE_COMPILER(compiler);
 
-		lprintf(LOG_INFO,"Compiled %s %s with %s", __DATE__, __TIME__, compiler);
-
-		lprintf(LOG_DEBUG,"SMBLIB %s (format %x.%02x)",smb_lib_ver(),smb_ver()>>8,smb_ver()&0xff);
+		lprintf(LOG_INFO,"Compiled %s/%s %s %s with %s", git_branch, git_hash, __DATE__, __TIME__, compiler);
 
 		sbbs_srand();
 
diff --git a/src/sbbs3/mailsrvr.vcxproj b/src/sbbs3/mailsrvr.vcxproj
index c4de8b7065f48a6f581ca237857fab69464fabc8..76235c4c64b250c992d461c3d2cb08a21f1ec27a 100644
--- a/src/sbbs3/mailsrvr.vcxproj
+++ b/src/sbbs3/mailsrvr.vcxproj
@@ -191,6 +191,7 @@
       <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
     </ClCompile>
     <ClCompile Include="nopen.c" />
+    <ClCompile Include="ver.cpp" />
   </ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\xpdev\xpdev_mt.vcxproj">
diff --git a/src/sbbs3/main.cpp b/src/sbbs3/main.cpp
index ddd9e1e2b057235b58233e4f97bad63e3fedb6a3..d8b5cf5124e6179e61355285244905e70caf6bfc 100644
--- a/src/sbbs3/main.cpp
+++ b/src/sbbs3/main.cpp
@@ -27,6 +27,7 @@
 #include "js_rtpool.h"
 #include "js_request.h"
 #include "ssl.h"
+#include "ver.h"
 #include <multisock.h>
 #include <limits.h>		// HOST_NAME_MAX
 
@@ -1289,7 +1290,7 @@ JSContext* sbbs_t::js_init(JSRuntime** runtime, JSObject** glob, const char* des
 					,uptime, server_host_name(), SOCKLIB_DESC	/* system */
 					,&js_callback								/* js */
 					,&startup->js
-					,&client, client_socket, -1					/* client */
+					,client_socket == INVALID_SOCKET ? NULL : &client, client_socket, -1 /* client */
 					,&js_server_props							/* server */
 					,glob
 			))
@@ -3532,6 +3533,19 @@ bool sbbs_t::init()
 		}
 		inet_addrtop(&addr, local_addr, sizeof(local_addr));
 		inet_addrtop(&client_addr, client_ipaddr, sizeof(client_ipaddr));
+		SAFEPRINTF(str, "%sclient.ini", cfg.node_dir);
+		FILE* fp = fopen(str, "wt");
+		if(fp != NULL) {
+			fprintf(fp, "sock=%d\n", client_socket);
+			fprintf(fp, "addr=%s\n", client.addr);
+			fprintf(fp, "host=%s\n", client.host);
+			fprintf(fp, "port=%u\n", (uint)client.port);
+			fprintf(fp, "time=%lu\n", (ulong)client.time);
+			fprintf(fp, "prot=%s\n", client.protocol);
+			fprintf(fp, "local_addr=%s\n", local_addr);
+			fprintf(fp, "local_port=%u\n", (uint)inet_addrport(&addr));
+			fclose(fp);
+		}
 		lprintf(LOG_INFO,"socket %u attached to local interface %s port %u"
 			,client_socket, local_addr, inet_addrport(&addr));
 		spymsg("Connected");
@@ -4453,7 +4467,7 @@ void sbbs_t::logoffstats()
 
 void node_thread(void* arg)
 {
-	char			str[128];
+	char			str[MAX_PATH + 1];
 	int				file;
 	uint			curshell=0;
 	node_t			node;
@@ -4554,6 +4568,15 @@ void node_thread(void* arg)
 	sbbs->logout();
 	sbbs->logoffstats();	/* Updates both system and node dsts.dab files */
 
+	SAFEPRINTF(str, "%sclient.ini", sbbs->cfg.node_dir);
+	FILE* fp = fopen(str, "at");
+	if(fp != NULL) {
+		fprintf(fp, "user=%u\n", sbbs->useron.number);
+		fprintf(fp, "name=%s\n", sbbs->useron.alias);
+		fprintf(fp, "done=%lu\n", (ulong)time(NULL));
+		fclose(fp);
+	}
+
 	if(sbbs->sys_status&SS_DAILY) {	// New day, run daily events/maintenance
 		sbbs->daily_maint();
 	}
@@ -4877,7 +4900,7 @@ const char* DLLCALL bbs_ver(void)
 	if(ver[0]==0) {	/* uninitialized */
 		DESCRIBE_COMPILER(compiler);
 
-		safe_snprintf(ver,sizeof(ver),"%s %s%c%s  SMBLIB %s  Compiled %s %s with %s"
+		safe_snprintf(ver,sizeof(ver),"%s %s%c%s  Compiled %s/%s %s %s with %s"
 			,TELNET_SERVER
 			,VERSION, REVISION
 #ifdef _DEBUG
@@ -4885,7 +4908,7 @@ const char* DLLCALL bbs_ver(void)
 #else
 			,""
 #endif
-			,smb_lib_ver()
+			,git_branch, git_hash
 			,__DATE__, __TIME__, compiler
 			);
 	}
@@ -5048,18 +5071,17 @@ void DLLCALL bbs_thread(void* arg)
 	char compiler[32];
 	DESCRIBE_COMPILER(compiler);
 
-	lprintf(LOG_INFO,"%s Version %s Revision %c%s"
+	lprintf(LOG_INFO,"%s Version %s%c%s"
 		,TELNET_SERVER
 		,VERSION
-		,toupper(REVISION)
+		,REVISION
 #ifdef _DEBUG
 		," Debug"
 #else
 		,""
 #endif
 		);
-	lprintf(LOG_INFO,"Compiled %s %s with %s", __DATE__, __TIME__, compiler);
-	lprintf(LOG_DEBUG,"SMBLIB %s (format %x.%02x)",smb_lib_ver(),smb_ver()>>8,smb_ver()&0xff);
+	lprintf(LOG_INFO,"Compiled %s/%s %s %s with %s", git_branch, git_hash, __DATE__, __TIME__, compiler);
 
 #ifdef _DEBUG
 	lprintf(LOG_DEBUG, "sizeof: int=%d, long=%d, off_t=%d, time_t=%d"
diff --git a/src/sbbs3/msg_id.c b/src/sbbs3/msg_id.c
index 26a5ad216f71fae427d8f7a1c3eeb90356dd9702..6dc987f255c29ee227a9cb647f54a1d810de63d7 100644
--- a/src/sbbs3/msg_id.c
+++ b/src/sbbs3/msg_id.c
@@ -21,6 +21,7 @@
 
 #include "msg_id.h"
 #include "smblib.h"
+#include "ver.h"
 
 static ulong msg_number(smbmsg_t* msg)
 {
@@ -242,8 +243,9 @@ char* DLLCALL msg_program_id(char* pid, size_t maxlen)
 	char compiler[64];
 
 	DESCRIBE_COMPILER(compiler);
-	snprintf(pid, maxlen, "%.10s %s%c-%s  %s %s"
+	snprintf(pid, maxlen, "%.10s %s%c-%s %s/%s %s %s"
 		,VERSION_NOTICE,VERSION,REVISION,PLATFORM_DESC
+		,git_branch, git_hash
 		,__DATE__,compiler);
 	return pid;
 }
diff --git a/src/sbbs3/netmail.cpp b/src/sbbs3/netmail.cpp
index eab9d211cddb2a309b1e3123f133de1841ad7a84..457ca30eaa02980ff84a712be10c3b0bf248d2c4 100644
--- a/src/sbbs3/netmail.cpp
+++ b/src/sbbs3/netmail.cpp
@@ -480,7 +480,7 @@ void sbbs_t::qwktonetmail(FILE *rep, char *block, char *into, uchar fromhub)
 				l+=strlen(str)+1;
 				cp=str;
 				while(*cp && *cp<=' ') cp++;
-				sprintf(senderaddr,"%s/%s",sender_id,cp);
+				safe_snprintf(senderaddr, sizeof(senderaddr), "%s/%s",sender_id,cp);
 				strupr(senderaddr);
 				smb_hfield(&msg,SENDERNETADDR,strlen(senderaddr),senderaddr); 
 			}
diff --git a/src/sbbs3/objects.mk b/src/sbbs3/objects.mk
index 1b4850a8924906d03b5546c72c2d99ce09adb823..e88c6d743488d9e3a29ff2054eb75978f297d2d5 100644
--- a/src/sbbs3/objects.mk
+++ b/src/sbbs3/objects.mk
@@ -174,6 +174,7 @@ SMBUTIL_OBJS = \
 
 SBBSECHO_OBJS = \
 			$(OBJODIR)$(DIRSEP)sbbsecho$(OFILE) \
+			$(OBJODIR)$(DIRSEP)ver$(OFILE) \
 			$(OBJODIR)$(DIRSEP)ars$(OFILE) \
 			$(OBJODIR)$(DIRSEP)date_str$(OFILE) \
 			$(OBJODIR)$(DIRSEP)load_cfg$(OFILE) \
diff --git a/src/sbbs3/prntfile.cpp b/src/sbbs3/prntfile.cpp
index 9419fcd00f810fd7604c905f0a31934eae41a4f1..d20aaa0d35afa4e21c7aa30e315e5e7355116a12 100644
--- a/src/sbbs3/prntfile.cpp
+++ b/src/sbbs3/prntfile.cpp
@@ -84,10 +84,6 @@ bool sbbs_t::printfile(const char* fname, long mode, long org_cols, JSObject* ob
 		sys_status&=~SS_ABORT; 
 	}
 
-	if(!(mode&P_NOCRLF) && row > 0 && !rip) {
-		newline();
-	}
-
 	if((stream=fnopen(&file,fpath,O_RDONLY|O_DENYNONE))==NULL) {
 		if(!(mode&P_NOERROR)) {
 			lprintf(LOG_NOTICE,"!Error %d (%s) opening: %s"
@@ -109,6 +105,10 @@ bool sbbs_t::printfile(const char* fname, long mode, long org_cols, JSObject* ob
 		return true;
 	}
 
+	if(!(mode&P_NOCRLF) && row > 0 && !rip) {
+		newline();
+	}
+
 	if((mode&P_OPENCLOSE) && length <= PRINTFILE_MAX_FILE_LEN) {
 		if((buf=(char*)malloc(length+1L))==NULL) {
 			fclose(stream);
@@ -196,9 +196,6 @@ bool sbbs_t::printtail(const char* fname, int lines, long mode, long org_cols, J
 		}
 		sys_status&=~SS_ABORT; 
 	}
-	if(!(mode&P_NOCRLF) && row > 0) {
-		newline();
-	}
 	if((fp=fnopen(&file,fpath,O_RDONLY|O_DENYNONE))==NULL) {
 		if(!(mode&P_NOERROR)) {
 			lprintf(LOG_NOTICE,"!Error %d (%s) opening: %s"
@@ -209,6 +206,9 @@ bool sbbs_t::printtail(const char* fname, int lines, long mode, long org_cols, J
 		}
 		return false; 
 	}
+	if(!(mode&P_NOCRLF) && row > 0) {
+		newline();
+	}
 	length=(long)filelength(file);
 	if(length<0) {
 		fclose(fp);
diff --git a/src/sbbs3/readmail.cpp b/src/sbbs3/readmail.cpp
index eb0dbfbacd38d07c5dc56540ea4c9c767106461b..92c437fb6a1f4b14ee17f4d81b5f2859a3a0f55d 100644
--- a/src/sbbs3/readmail.cpp
+++ b/src/sbbs3/readmail.cpp
@@ -1,9 +1,5 @@
-/* readmail.cpp */
-
 /* Synchronet private mail reading function */
 
-/* $Id: readmail.cpp,v 1.101 2020/05/11 05:01:01 rswindell Exp $ */
-
 /****************************************************************************
  * @format.tab-size 4		(Plain Text/Source Code File Header)			*
  * @format.use-tabs true	(see http://www.synchro.net/ptsc_hdr.html)		*
@@ -17,21 +13,9 @@
  * See the GNU General Public License for more details: gpl.txt or			*
  * http://www.fsf.org/copyleft/gpl.html										*
  *																			*
- * Anonymous FTP access to the most recent released source is available at	*
- * ftp://vert.synchro.net, ftp://cvs.synchro.net and ftp://ftp.synchro.net	*
- *																			*
- * Anonymous CVS access to the development source and modification history	*
- * is available at cvs.synchro.net:/cvsroot/sbbs, example:					*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs login			*
- *     (just hit return, no password is necessary)							*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs checkout src		*
- *																			*
  * For Synchronet coding style and modification guidelines, see				*
  * http://www.synchro.net/source.html										*
  *																			*
- * You are encouraged to submit any modifications (preferably in Unix diff	*
- * format) via e-mail to mods@synchro.net									*
- *																			*
  * Note: If this box doesn't appear square, then you need to fix your tabs.	*
  ****************************************************************************/
 
@@ -435,24 +419,20 @@ void sbbs_t::readmail(uint usernumber, int which, long lm_mode)
 			case 'F':  /* Forward last piece */
 				domsg=0;
 				bputs(text[ForwardMailTo]);
-				if(!getstr(str,LEN_ALIAS,cfg.uq&UQ_NOUPRLWR ? K_NONE:K_UPRLWR))
+				if(!getstr(str, sizeof(str) - 1, K_TRIM))
 					break;
-				i=finduser(str);
-				if(!i)
+				smb_getmsgidx(&smb,&msg);
+				if(!forwardmail(&msg, str))
 					break;
 				domsg=1;
 				if(smb.curmsg<smb.msgs-1) smb.curmsg++;
 				else done=1;
-				smb_getmsgidx(&smb,&msg);
-				forwardmail(&msg,i);
-				if(msg.hdr.attr&MSG_PERMANENT)
+				if(msg.hdr.attr&(MSG_PERMANENT | MSG_DELETE))
 					break;
 				SAFEPRINTF(str2,text[DeleteMailQ],msghdr_field(&msg, msg.from));
 				if(!yesno(str2))
 					break;
-				if(msg.total_hfields)
-					smb_freemsgmem(&msg);
-				msg.total_hfields=0;
+				smb_freemsgmem(&msg);
 				msg.idx.offset=0;
 				if(smb_locksmbhdr(&smb)==SMB_SUCCESS) {	/* Lock the entire base */
 					if(loadmsg(&msg,msg.idx.number) >= 0) {
@@ -465,7 +445,6 @@ void sbbs_t::readmail(uint usernumber, int which, long lm_mode)
 					}
 					smb_unlocksmbhdr(&smb);
 				}
-
 				break;
 			case 'H':
 				domsg=0;
diff --git a/src/sbbs3/sbbs.h b/src/sbbs3/sbbs.h
index 5b754d2c953907161f05aeddcfc17d2100aa026a..f12b8945a5999e4f2eb26f0d5ed7743e1b2af428 100644
--- a/src/sbbs3/sbbs.h
+++ b/src/sbbs3/sbbs.h
@@ -623,7 +623,7 @@ public:
 
 	void	reset_logon_vars(void);
 
-	uint	finduser(char *str, bool silent_failure = false);
+	uint	finduser(const char* str, bool silent_failure = false);
 
 	int 	sub_op(uint subnum);
 
@@ -681,7 +681,7 @@ public:
 	bool	msgabort(void);
 	bool	email(int usernumber, const char *top = NULL, const char *title = NULL
 				, long mode = WM_NONE, smb_t* resmb = NULL, smbmsg_t* remsg = NULL);
-	void	forwardmail(smbmsg_t* msg, int usernum);
+	bool	forwardmail(smbmsg_t* msg, const char* to, const char* subject = NULL, const char* comment = NULL);
 	void	removeline(char *str, char *str2, char num, char skip);
 	ulong	msgeditor(char *buf, const char *top, char *title);
 	bool	editfile(char *path, bool msg=false);
@@ -690,7 +690,7 @@ public:
 	void	editmsg(smbmsg_t* msg, uint subnum);
 	void	editor_inf(int xeditnum, const char *to, const char* from, const char *subj, long mode
 				,uint subnum, const char* tagfile);
-	void	copyfattach(uint to, uint from, char *title);
+	bool	copyfattach(uint to, uint from, const char* subj);
 	bool	movemsg(smbmsg_t* msg, uint subnum);
 	int		process_edited_text(char* buf, FILE* stream, long mode, unsigned* lines, unsigned maxlines);
 	int		process_edited_file(const char* src, const char* dest, long mode, unsigned* lines, unsigned maxlines);
@@ -759,7 +759,7 @@ public:
 	int		outchar(enum unicode_codepoint, const char* cp437_fallback = NULL);
 	void	inc_row(int count);
 	void	inc_column(int count);
-	void	center(char *str, unsigned int columns = 0);
+	void	center(const char *str, unsigned int columns = 0);
 	void	wide(const char*);
 	void	clearscreen(long term);
 	void	clearline(void);
@@ -1439,12 +1439,6 @@ extern char lastuseron[LEN_ALIAS+1];  /* Name of user last online */
 }
 #endif
 
-extern
-#ifdef __cplusplus
- "C"
-#endif
-	const char* beta_version;
-
 /* Global data */
 
 /* ToDo: These should be hunted down and killed */
diff --git a/src/sbbs3/sbbs.vcxproj b/src/sbbs3/sbbs.vcxproj
index e2f04731b4d8d16f830318bcecaaf25e379b4b9f..d71e501bee654dd794ee3e3d579668f54195658f 100644
--- a/src/sbbs3/sbbs.vcxproj
+++ b/src/sbbs3/sbbs.vcxproj
@@ -114,6 +114,9 @@
       <SuppressStartupBanner>true</SuppressStartupBanner>
       <OutputFile>.\msvc.win32.dll.debug/sbbs.bsc</OutputFile>
     </Bscmake>
+    <PreBuildEvent>
+      <Command>gitinfo.bat</Command>
+    </PreBuildEvent>
   </ItemDefinitionGroup>
   <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
     <Midl>
@@ -163,6 +166,9 @@
       <SuppressStartupBanner>true</SuppressStartupBanner>
       <OutputFile>.\msvc.win32.dll.release/sbbs.bsc</OutputFile>
     </Bscmake>
+    <PreBuildEvent>
+      <Command>gitinfo.bat</Command>
+    </PreBuildEvent>
   </ItemDefinitionGroup>
   <ItemGroup>
     <ClCompile Include="..\comio\comio.c" />
diff --git a/src/sbbs3/sbbscon.c b/src/sbbs3/sbbscon.c
index 205c69dff3798fc8693abc811bbd73fffc0914c6..4741e715a84c466a3654e1788a5e9657bf801950 100644
--- a/src/sbbs3/sbbscon.c
+++ b/src/sbbs3/sbbscon.c
@@ -1,8 +1,5 @@
 /* Synchronet vanilla/console-mode "front-end" */
 
-/* $Id: sbbscon.c,v 1.282 2020/08/17 00:48:28 rswindell Exp $ */
-// vi: tabstop=4
-
 /****************************************************************************
  * @format.tab-size 4		(Plain Text/Source Code File Header)			*
  * @format.use-tabs true	(see http://www.synchro.net/ptsc_hdr.html)		*
@@ -16,21 +13,9 @@
  * See the GNU General Public License for more details: gpl.txt or			*
  * http://www.fsf.org/copyleft/gpl.html										*
  *																			*
- * Anonymous FTP access to the most recent released source is available at	*
- * ftp://vert.synchro.net, ftp://cvs.synchro.net and ftp://ftp.synchro.net	*
- *																			*
- * Anonymous CVS access to the development source and modification history	*
- * is available at cvs.synchro.net:/cvsroot/sbbs, example:					*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs login			*
- *     (just hit return, no password is necessary)							*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs checkout src		*
- *																			*
  * For Synchronet coding style and modification guidelines, see				*
  * http://www.synchro.net/source.html										*
  *																			*
- * You are encouraged to submit any modifications (preferably in Unix diff	*
- * format) via e-mail to mods@synchro.net									*
- *																			*
  * Note: If this box doesn't appear square, then you need to fix your tabs.	*
  ****************************************************************************/
 
@@ -55,6 +40,7 @@
 #include "ftpsrvr.h"	/* ftp_startup_t, ftp_server */
 #include "mailsrvr.h"	/* mail_startup_t, mail_server */
 #include "services.h"	/* services_startup_t, services_thread */
+#include "ver.h"
 
 /* XPDEV headers */
 #include "conwrap.h"	/* kbhit/getch */
@@ -1570,8 +1556,6 @@ int main(int argc, char** argv)
 			continue;
 		}
 		if(stricmp(arg, "version") == 0) {
-			char revision[16];
-			sscanf("$Revision: 1.282 $", "%*s %s", revision);
 			char compiler[32];
 			DESCRIBE_COMPILER(compiler);
 			printf("%s\n", bbs_ver());
@@ -1579,13 +1563,14 @@ int main(int argc, char** argv)
 			printf("%s\n", ftp_ver());
 			printf("%s\n", web_ver());
 			printf("%s\n", services_ver());
-			printf("Synchronet Console %s%s  Compiled %s %s with %s\n"
-				,revision
+			printf("Synchronet Console %s%c%s  Compiled %s/%s %s %s with %s\n"
+				,VERSION, REVISION
 #ifdef _DEBUG
 				," Debug"
 #else
 				,""
 #endif
+				,git_branch, git_hash
 				,__DATE__, __TIME__, compiler);
 			return EXIT_SUCCESS;
 		}
diff --git a/src/sbbs3/sbbscon.vcxproj b/src/sbbs3/sbbscon.vcxproj
index 1f7ae09789f045d0b67dc576cfb2d9d94d1d6a5f..68e559e5c2726fc7e2509e66cdb0787b5c16a543 100644
--- a/src/sbbs3/sbbscon.vcxproj
+++ b/src/sbbs3/sbbscon.vcxproj
@@ -159,6 +159,7 @@
       <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
       <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
     </ClCompile>
+    <ClCompile Include="ver.cpp" />
   </ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\xpdev\xpdev_mt.vcxproj">
diff --git a/src/sbbs3/sbbsecho.c b/src/sbbs3/sbbsecho.c
index 83ee49abc685ba263123500d9c0383467c5180d7..ef34fff7211754133e0cbcaa9d9c1c7999b8541c 100644
--- a/src/sbbs3/sbbsecho.c
+++ b/src/sbbs3/sbbsecho.c
@@ -53,6 +53,7 @@
 #include "msg_id.h"
 #include "scfgsave.h"
 #include "getmail.h"
+#include "ver.h"
 
 #define MAX_OPEN_SMBS	10
 
@@ -91,7 +92,6 @@ str_list_t bad_areas;
 fidoaddr_t		sys_faddr = {1,1,1,0};		/* Default system address: 1:1/1.0 */
 sbbsecho_cfg_t	cfg;
 scfg_t			scfg;
-char			revision[16];
 char			compiler[32];
 
 bool pause_on_exit=false;
@@ -116,8 +116,8 @@ const char* sbbsecho_pid(void)
 {
 	static char str[256];
 
-	sprintf(str, "SBBSecho %u.%02u-%s r%s %s %s"
-		,SBBSECHO_VERSION_MAJOR,SBBSECHO_VERSION_MINOR,PLATFORM_DESC,revision,__DATE__,compiler);
+	sprintf(str, "SBBSecho %u.%02u-%s %s/%s %s %s"
+		,SBBSECHO_VERSION_MAJOR,SBBSECHO_VERSION_MINOR,PLATFORM_DESC,git_branch,git_hash,__DATE__,compiler);
 
 	return str;
 }
@@ -198,7 +198,7 @@ int fwrite_via_control_line(FILE* fp, fidoaddr_t* addr)
 	time_t t = time(NULL);
 	struct tm* tm = gmtime(&t);
 	return fprintf(fp,"\1Via %s @%04u%02u%02u.%02u%02u%02u.UTC "
-		"SBBSecho %u.%02u-%s r%s\r"
+		"SBBSecho %u.%02u-%s %s/%s\r"
 		,smb_faddrtoa(addr, NULL)
 		,tm->tm_year+1900
 		,tm->tm_mon+1
@@ -206,7 +206,7 @@ int fwrite_via_control_line(FILE* fp, fidoaddr_t* addr)
 		,tm->tm_hour
 		,tm->tm_min
 		,tm->tm_sec
-		,SBBSECHO_VERSION_MAJOR,SBBSECHO_VERSION_MINOR,PLATFORM_DESC,revision);
+		,SBBSECHO_VERSION_MAJOR,SBBSECHO_VERSION_MINOR,PLATFORM_DESC,git_branch,git_hash);
 }
 
 int fwrite_intl_control_line(FILE* fp, fmsghdr_t* hdr)
@@ -1262,10 +1262,6 @@ int create_netmail(const char *to, const smbmsg_t* msg, const char *subject, con
 		fprintf(fp, "\1CHRS: %s\r", charset);
 		if(msg->editor != NULL)
 			fprintf(fp, "\1NOTE: %s\r", msg->editor);
-		/* comment headers are part of text */
-		for(i=0; i<msg->total_hfields; i++)
-			if(msg->hfield[i].type == SMB_COMMENT)
-				fprintf(fp, "%s\r", (char*)msg->hfield_dat[i]);
 		if(subject != msg->subj)
 			fprintf(fp, "Subject: %s\r\r", msg->subj);
 	}
@@ -5142,7 +5138,7 @@ bool retoss_bad_echomail(void)
 			continue;
 		}
 
-		char* body = smb_getmsgtxt(&badsmb, &badmsg, GETMSGTXT_BODY_ONLY);
+		char* body = smb_getmsgtxt(&badsmb, &badmsg, GETMSGTXT_NO_TAILS);
 		if(body == NULL) {
 			smb_unlockmsghdr(&badsmb,&badmsg);
 			smb_freemsgmem(&badmsg);
@@ -6122,14 +6118,12 @@ int main(int argc, char **argv)
 		memset(&smb[i],0,sizeof(smb_t));
 	memset(&cfg,0,sizeof(cfg));
 
-	sscanf("$Revision: 3.179 $", "%*s %s", revision);
-
 	DESCRIBE_COMPILER(compiler);
 
-	printf("\nSBBSecho v%u.%02u-%s (rev %s) - Synchronet FidoNet EchoMail Tosser\n"
+	printf("\nSBBSecho v%u.%02u-%s (%s/%s) - Synchronet FidoNet EchoMail Tosser\n"
 		,SBBSECHO_VERSION_MAJOR, SBBSECHO_VERSION_MINOR
 		,PLATFORM_DESC
-		,revision
+		,git_branch, git_hash
 		);
 
 	cmdline[0]=0;
diff --git a/src/sbbs3/sbbsecho.vcxproj b/src/sbbs3/sbbsecho.vcxproj
index 9ddf9f7968d976c22da77f01a2a2004b6b9ebd0d..d78e6e0ad913ae0169c2b043cdc79db4b29eba20 100644
--- a/src/sbbs3/sbbsecho.vcxproj
+++ b/src/sbbs3/sbbsecho.vcxproj
@@ -187,6 +187,7 @@
       <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
       <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
     </ClCompile>
+    <ClCompile Include="ver.cpp" />
   </ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\smblib\smblib.vcxproj">
diff --git a/src/sbbs3/scfg/scfgsub.c b/src/sbbs3/scfg/scfgsub.c
index c4e5663c98da6f23f84def6232cc67b0b8efad66..5e14e49d0c6072b8ec26171cfe8adca09343e222 100644
--- a/src/sbbs3/scfg/scfgsub.c
+++ b/src/sbbs3/scfg/scfgsub.c
@@ -166,6 +166,7 @@ void sub_cfg(uint grpnum)
 		if(uifc.changes && cfg.grp[grpnum]->sort)
 			sort_subs(grpnum);
 		int maxlen = 0;
+		bool template_shown = false;
 		for(i=0,j=0;i<cfg.total_subs && j<MAX_OPTS;i++)
 			if(cfg.sub[i]->grp==grpnum) {
 				subnum[j]=i;
@@ -187,7 +188,9 @@ void sub_cfg(uint grpnum)
 					default:	/* Defeat stupid GCC warning */
 						break;
 				}
-				sprintf(str, "%-*s %c", name_len, name, cfg.sub[i]->misc&SUB_TEMPLATE ? '*' : ' ');
+				sprintf(str, "%-*s %c", name_len, name, (cfg.sub[i]->misc&SUB_TEMPLATE && !template_shown) ? '*' : ' ');
+				if(cfg.sub[i]->misc&SUB_TEMPLATE)
+					template_shown = true;
 				truncsp(str);
 				len += sprintf(opt[j] + strlen(opt[j]), "%s", str);
 				if(len > maxlen)
@@ -1370,8 +1373,10 @@ void sub_cfg(uint grpnum)
 								}
 								opt[n][0]=0;
 								n = uifc.list(WIN_RHT|WIN_SAV|WIN_ACT|WIN_INSACT, 0, 0, 0, &k, NULL, "FidoNet Address", opt);
-								if(n >= 0 && n < cfg.total_faddrs)
+								if(n >= 0 && n < cfg.total_faddrs) {
 									cfg.sub[i]->faddr = cfg.faddr[n];
+									uifc.changes = TRUE;
+								}
 								break;
 							}
 							case 8:
diff --git a/src/sbbs3/scfg/scfgxfr2.c b/src/sbbs3/scfg/scfgxfr2.c
index c85037852b64966b5001d61f0ecbe16be11350ed..380df8906906bf6b9cb0f0e7254ad41d49f891a7 100644
--- a/src/sbbs3/scfg/scfgxfr2.c
+++ b/src/sbbs3/scfg/scfgxfr2.c
@@ -979,6 +979,7 @@ void dir_cfg(uint libnum)
 		if(uifc.changes && cfg.lib[libnum]->sort)
 			sort_dirs(libnum);
 		int maxlen = 0;
+		bool template_shown = false;
 		for(i=0,j=0;i<cfg.total_dirs && j<MAX_OPTS;i++) {
 			if(cfg.dir[i]->lib != libnum)
 				continue;
@@ -996,7 +997,9 @@ void dir_cfg(uint libnum)
 				default:	/* Defeat stupid GCC warning */
 					break;
 			}
-			sprintf(str, "%-*s %c", name_len, name, cfg.dir[i]->misc&DIR_TEMPLATE ? '*' : ' ');
+			sprintf(str, "%-*s %c", name_len, name, (cfg.dir[i]->misc&DIR_TEMPLATE && !template_shown) ? '*' : ' ');
+			if(cfg.dir[i]->misc&DIR_TEMPLATE)
+				template_shown = true;
 			truncsp(str);
 			int len = sprintf(opt[j], "%s", str);
 			if(len > maxlen)
diff --git a/src/sbbs3/scfg/scfgxtrn.c b/src/sbbs3/scfg/scfgxtrn.c
index 6f788b5861392407f6a41d8dce17e2e88222450f..5bca78ce45b573eda7019641b120626c6e17bc28 100644
--- a/src/sbbs3/scfg/scfgxtrn.c
+++ b/src/sbbs3/scfg/scfgxtrn.c
@@ -2242,7 +2242,7 @@ int natvpgm_cfg()
 
 void xtrnsec_cfg()
 {
-	static int xtrnsec_dflt,xtrnsec_opt;
+	static int xtrnsec_dflt,xtrnsec_bar,xtrnsec_opt;
 	char str[128],code[128],done=0;
 	int j,k;
 	uint i;
@@ -2271,7 +2271,7 @@ void xtrnsec_cfg()
 			"\n"
 			"To configure an online program section, select it and hit ~ ENTER ~.\n"
 		;
-		i=uifc.list(j,0,0,45,&xtrnsec_dflt,0,"Online Program Sections",opt);
+		i=uifc.list(j,0,0,45,&xtrnsec_dflt,&xtrnsec_bar,"Online Program Sections",opt);
 		if((signed)i==-1)
 			return;
 		int msk = i & MSK_ON;
diff --git a/src/sbbs3/services.c b/src/sbbs3/services.c
index bf6ef2e11452d58f12f18db9a73851e0140c3923..8f99438eb9c8af4c5ca6317735d03f011908bfd4 100644
--- a/src/sbbs3/services.c
+++ b/src/sbbs3/services.c
@@ -47,6 +47,7 @@
 #include "js_socket.h"
 #include "multisock.h"
 #include "ssl.h"
+#include "ver.h"
 
 /* Constants */
 
@@ -59,7 +60,6 @@ static char*	text[TOTAL_TEXT];
 static volatile BOOL	terminated=FALSE;
 static time_t	uptime=0;
 static ulong	served=0;
-static char		revision[16];
 static str_list_t recycle_semfiles;
 static str_list_t shutdown_semfiles;
 static protected_uint32_t threads_pending_start;
@@ -817,8 +817,8 @@ js_initcx(JSRuntime* js_runtime, SOCKET sock, service_client_t* service_client,
 			break;
 
 		if(service_client->service->js_server_props.version[0]==0) {
-			SAFEPRINTF(service_client->service->js_server_props.version
-				,"Synchronet Services %s",revision);
+			SAFEPRINTF2(service_client->service->js_server_props.version
+				,"Synchronet Services %s%c", VERSION, REVISION);
 			service_client->service->js_server_props.version_detail=
 				services_ver();
 			service_client->service->js_server_props.clients=
@@ -1661,16 +1661,15 @@ const char* DLLCALL services_ver(void)
 
 	DESCRIBE_COMPILER(compiler);
 
-	sscanf("$Revision: 1.336 $", "%*s %s", revision);
-
-	sprintf(ver,"Synchronet Services %s%s  "
-		"Compiled %s %s with %s"
-		,revision
+	sprintf(ver,"Synchronet Services %s%c%s  "
+		"Compiled %s/%s %s %s with %s"
+		,VERSION, REVISION
 #ifdef _DEBUG
 		," Debug"
 #else
 		,""
 #endif
+		,git_branch, git_hash
 		,__DATE__, __TIME__, compiler
 		);
 
@@ -1735,8 +1734,6 @@ void DLLCALL services_thread(void* arg)
 	int			level;
 	BOOL			need_cert = FALSE;
 
-	services_ver();
-
 	startup=(services_startup_t*)arg;
 
     if(startup==NULL) {
@@ -1776,8 +1773,8 @@ void DLLCALL services_thread(void* arg)
 
 		memset(&scfg, 0, sizeof(scfg));
 
-		lprintf(LOG_INFO,"Synchronet Services Revision %s%s"
-			,revision
+		lprintf(LOG_INFO,"Synchronet Services Version %s%c%s"
+			,VERSION, REVISION
 #ifdef _DEBUG
 			," Debug"
 #else
@@ -1787,7 +1784,7 @@ void DLLCALL services_thread(void* arg)
 
 		DESCRIBE_COMPILER(compiler);
 
-		lprintf(LOG_INFO,"Compiled %s %s with %s", __DATE__, __TIME__, compiler);
+		lprintf(LOG_INFO,"Compiled %s/%s %s %s with %s", git_branch, git_hash, __DATE__, __TIME__, compiler);
 
 		protected_uint32_init(&threads_pending_start,0);
 
diff --git a/src/sbbs3/services.vcxproj b/src/sbbs3/services.vcxproj
index b84a464865002755a5cb4da625dedd9ded129b70..57beac0b879c45d42777eafddc1313bed0a962f5 100644
--- a/src/sbbs3/services.vcxproj
+++ b/src/sbbs3/services.vcxproj
@@ -176,6 +176,7 @@
       <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
       <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
     </ClCompile>
+    <ClCompile Include="ver.cpp" />
   </ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\xpdev\xpdev_mt.vcxproj">
diff --git a/src/sbbs3/sexyz.c b/src/sbbs3/sexyz.c
old mode 100644
new mode 100755
index 67e2339efbcf097f9f14d4d0ab9c0d4f3f34f0f2..92fe2eaa2d9f58cd1626addb4c1852a2fbdfe93c
--- a/src/sbbs3/sexyz.c
+++ b/src/sbbs3/sexyz.c
@@ -18,7 +18,7 @@
  *																			*
  * Note: If this box doesn't appear square, then you need to fix your tabs.	*
  ****************************************************************************/
-/* 
+/*
  * ZMODEM code based on zmtx/zmrx v1.02 (C) Mattheij Computer Service 1994
  * by Jacques Mattheij
  *
@@ -87,7 +87,7 @@
 /* Global Vars */
 /***************/
 long	mode=0;							/* Program mode 					*/
-long	zmode=0L;						/* Zmodem mode						*/
+long	zmode=0L;						/* ZMODEM mode						*/
 uchar	block[XMODEM_MAX_BLOCK_SIZE];	/* Block buffer 					*/
 ulong	block_num;						/* Block number 					*/
 char*	dszlog;
@@ -106,7 +106,7 @@ FILE*	errfp;
 FILE*	statfp;
 FILE*	logfp=NULL;
 
-char	revision[16];
+const char*	revision = "3.0";
 
 SOCKET	sock=INVALID_SOCKET;
 
@@ -604,7 +604,7 @@ int send_byte(void* unused, uchar ch, unsigned timeout)
 		fprintf(statfp,"\b\b\b\b    \b\b\b\b");
 		if(result!=WAIT_OBJECT_0) {
 			lprintf(LOG_WARNING
-				,"!TIMEOUT (%d) waiting for output buffer to flush (%u seconds, %u bytes)\n"
+				,"TIMEOUT (%d) waiting for output buffer to flush (%u seconds, %u bytes)"
 				,result, timeout, RingBufFull(&outbuf));
 			fprintf(statfp
 				,"\n!TIMEOUT (%d) waiting for output buffer to flush (%u seconds, %u bytes)\n"
@@ -972,7 +972,7 @@ static int send_files(char** fname, uint fnames)
 
 			if(mode&ZMODEM)
 				success=zmodem_send_file(&zm, path, fp, /* ZRQINIT? */fnum==0, &startfile, &sent_bytes);
-			else	/* X/Ymodem */
+			else	/* X/YMODEM */
 				success=xmodem_send_file(&xm, path, fp, &startfile, &sent_bytes);
 
 			fclose(fp);
@@ -1138,7 +1138,7 @@ static int receive_files(char** fname_list, int fnames)
 				lprintf(LOG_DEBUG,"YMODEM header (%u fields): %s", i, block+strlen((char*)block)+1);
 				SAFECOPY(fname,(char*)block);
 
-			} else {	/* Zmodem */
+			} else {	/* ZMODEM */
 				lprintf(LOG_INFO,"Waiting for ZMODEM sender...");
 
 				i=zmodem_recv_init(&zm);
@@ -1415,17 +1415,18 @@ static const char* usage=
 #endif
 	"\n"
 	"opts   = -y  allow overwriting of existing files when receiving\n"
-	"         -o  disable Zmodem CRC-32 mode (use CRC-16)\n"
-	"         -s  disable Zmodem streaming (Slow Zmodem)\n"
-	"         -k  enable X/Ymodem-1K send mode\n"
-    "         -c  enable Xmodem-CRC receive mode\n"
-	"         -g  enable X/Ymodem-G receive mode (no error recovery)\n"
-	"         -2  set maximum Zmodem block size to 2K\n"
-	"         -4  set maximum Zmodem block size to 4K\n"
-	"         -8  set maximum Zmodem block size to 8K (ZedZap)\n"
-	"         -m# set maximum receive file size to # bytes (0=unlimited, default=%u)\n"
-	"         -!  to pause after abnormal exit (error)\n"
+	"         -k  enable X/YMODEM-1K send mode\n"
+	"         -c  enable XMODEM-CRC receive mode\n"
+	"         -g  enable X/YMODEM-G receive mode (no error recovery)\n"
+	"         -o  disable ZMODEM CRC-32 mode (use CRC-16)\n"
+	"         -s  use segmented ZMODEM (disable streaming)\n"
+	"         -2  set maximum ZMODEM block size to 2K\n"
+	"         -4  set maximum ZMODEM block size to 4K\n"
+	"         -8  set maximum ZMODEM block size to 8K (ZedZap)\n"
+	"         -w# set maximum ZMODEM transmit window size (default=0, unlimited)\n"
+	"         -m# set maximum receive file size to # bytes (default=0, unlimited)\n"
 	"         -l  lowercase received filenames\n"
+	"         -!  to pause after abnormal exit (error)\n"
 #ifdef __unix__
 	"         -telnet to enable Telnet mode (the default except in stdio mode)\n"
 #else
@@ -1434,11 +1435,11 @@ static const char* usage=
 	"         -rlogin or -ssh or -raw to disable Telnet mode\n"
 	"\n"
 	"cmd    = v  to display detailed version information\n"
-	"         sx to send Xmodem     rx to receive Xmodem\n"
-	"         sX to send Xmodem-1K  rc to receive Xmodem-CRC\n"
-	"         sy to send Ymodem     ry to receive Ymodem\n"
-	"         sY to send Ymodem-1K  rg to receive Ymodem-G\n"
-	"         sz to send Zmodem     rz to receive Zmodem\n"
+	"         sx to send XMODEM     rx to receive XMODEM\n"
+	"         sX to send XMODEM-1K  rc to receive XMODEM-CRC\n"
+	"         sy to send YMODEM     ry to receive YMODEM\n"
+	"         sY to send YMODEM-1K  rg to receive YMODEM-G\n"
+	"         sz to send ZMODEM     rz to receive ZMODEM\n"
 	"\n"
 	"file   = filename to send or receive\n"
 	"path   = directory to receive files into\n"
@@ -1508,7 +1509,6 @@ int main(int argc, char **argv)
 	int		retval;
 	uint	fnames=0;
 	FILE*	fp;
-	BOOL	tcp_nodelay;
 	char	compiler[32];
 	BOOL	telnet_requested=FALSE;
 	str_list_t ini = strListInit();
@@ -1525,13 +1525,10 @@ int main(int argc, char **argv)
 	statfp=stdout;
 #endif
 
-	sscanf("$Revision: 2.10 $", "%*s %s", revision);
-
 	fprintf(statfp,"\nSynchronet External X/Y/ZMODEM  v%s-%s"
-		"  Copyright %s Rob Swindell\n\n"
+		"  Copyright Rob Swindell\n\n"
 		,revision
 		,PLATFORM_DESC
-		,&__DATE__[7]
 		);
 
 	xmodem_init(&xm,NULL,&mode,lputs,xmodem_progress,send_byte,recv_byte,is_connected,NULL,flush);
@@ -1555,8 +1552,6 @@ int main(int argc, char **argv)
 		fclose(fp);
 	}
 
-	tcp_nodelay				=iniGetBool(ini, ROOT_SECTION,"TCP_NODELAY",TRUE);
-
 	telnet					=iniGetBool(ini, ROOT_SECTION,"Telnet",TRUE);
 	debug_tx				=iniGetBool(ini, ROOT_SECTION,"DebugTx",FALSE);
 	debug_rx				=iniGetBool(ini, ROOT_SECTION,"DebugRx",FALSE);
@@ -1578,36 +1573,42 @@ int main(int argc, char **argv)
 	if(iniGetBool(ini, ROOT_SECTION,"Debug",FALSE))
 		log_level=LOG_DEBUG;
 
-	xm.send_timeout			=iniGetInteger(ini, "Xmodem","SendTimeout",xm.send_timeout);	/* seconds */
-	xm.recv_timeout			=iniGetInteger(ini, "Xmodem","RecvTimeout",xm.recv_timeout);	/* seconds */
-	xm.byte_timeout			=iniGetInteger(ini, "Xmodem","ByteTimeout",xm.byte_timeout);	/* seconds */
-	xm.ack_timeout			=iniGetInteger(ini, "Xmodem","AckTimeout",xm.ack_timeout);	/* seconds */
-	xm.block_size			=(ulong)iniGetBytes(ini, "Xmodem","BlockSize",1,xm.block_size);			/* 128 or 1024 */
-	xm.max_block_size		=(ulong)iniGetBytes(ini, "Xmodem","MaxBlockSize",1,xm.max_block_size);	/* 128 or 1024 */
-	xm.max_errors			=iniGetInteger(ini, "Xmodem","MaxErrors",xm.max_errors);
-	xm.g_delay				=iniGetInteger(ini, "Xmodem","G_Delay",xm.g_delay);
-	xm.crc_mode_supported	=iniGetBool(ini, "Xmodem","SendCRC",xm.crc_mode_supported);
-	xm.g_mode_supported		=iniGetBool(ini, "Xmodem","SendG",xm.g_mode_supported);
-
-	xm.fallback_to_xmodem	=iniGetInteger(ini, "Ymodem","FallbackToXmodem", xm.fallback_to_xmodem);
-
-	zm.init_timeout			=iniGetInteger(ini, "Zmodem","InitTimeout",zm.init_timeout);	/* seconds */
-	zm.send_timeout			=iniGetInteger(ini, "Zmodem","SendTimeout",zm.send_timeout);	/* seconds */
-	zm.recv_timeout			=iniGetInteger(ini, "Zmodem","RecvTimeout",zm.recv_timeout);	/* seconds */
-	zm.crc_timeout			=iniGetInteger(ini, "Zmodem","CrcTimeout",zm.crc_timeout);	/* seconds */
-	zm.block_size			=(ulong)iniGetBytes(ini, "Zmodem","BlockSize",1,zm.block_size);	/* 1024  */
-	zm.max_block_size		=(ulong)iniGetBytes(ini, "Zmodem","MaxBlockSize",1,zm.max_block_size); /* 1024 or 8192 */
-	zm.max_errors			=iniGetInteger(ini, "Zmodem","MaxErrors",zm.max_errors);
-	zm.recv_bufsize			=(ulong)iniGetBytes(ini, "Zmodem","RecvBufSize",1,0);
-	zm.no_streaming			=!iniGetBool(ini, "Zmodem","Streaming",TRUE);
-	zm.want_fcs_16			=!iniGetBool(ini, "Zmodem","CRC32",TRUE);
-	zm.escape_telnet_iac	=iniGetBool(ini, "Zmodem","EscapeTelnetIAC",TRUE);
-	zm.escape_8th_bit		=iniGetBool(ini, "Zmodem","Escape8thBit",FALSE);
-	zm.escape_ctrl_chars	=iniGetBool(ini, "Zmodem","EscapeCtrlChars",FALSE);
-
-	dszlog_path				=iniGetBool(ini, "DSZLOG","Path",TRUE);
-	dszlog_short			=iniGetBool(ini, "DSZLOG","Short",FALSE);
-	dszlog_quotes			=iniGetBool(ini, "DSZLOG","Quotes",FALSE);
+	const char* section = "XMODEM";
+	xm.send_timeout			=iniGetInteger(ini, section,"SendTimeout",xm.send_timeout);	/* seconds */
+	xm.recv_timeout			=iniGetInteger(ini, section,"RecvTimeout",xm.recv_timeout);	/* seconds */
+	xm.byte_timeout			=iniGetInteger(ini, section,"ByteTimeout",xm.byte_timeout);	/* seconds */
+	xm.ack_timeout			=iniGetInteger(ini, section,"AckTimeout",xm.ack_timeout);	/* seconds */
+	xm.block_size			=(ulong)iniGetBytes(ini, section,"BlockSize",1,xm.block_size);			/* 128 or 1024 */
+	xm.max_block_size		=(ulong)iniGetBytes(ini, section,"MaxBlockSize",1,xm.max_block_size);	/* 128 or 1024 */
+	xm.max_errors			=iniGetInteger(ini, section,"MaxErrors",xm.max_errors);
+	xm.g_delay				=iniGetInteger(ini, section,"G_Delay",xm.g_delay);
+	xm.crc_mode_supported	=iniGetBool(ini, section,"SendCRC",xm.crc_mode_supported);
+	xm.g_mode_supported		=iniGetBool(ini, section,"SendG",xm.g_mode_supported);
+
+	xm.fallback_to_xmodem	=iniGetInteger(ini, "YMODEM","FallbackToXmodem", xm.fallback_to_xmodem);
+
+	section = "ZMODEM";
+	zm.init_timeout			=iniGetInteger(ini, section,"InitTimeout",zm.init_timeout);	/* seconds */
+	zm.send_timeout			=iniGetInteger(ini, section,"SendTimeout",zm.send_timeout);	/* seconds */
+	zm.recv_timeout			=iniGetInteger(ini, section,"RecvTimeout",zm.recv_timeout);	/* seconds */
+	zm.crc_timeout			=iniGetInteger(ini, section,"CrcTimeout",zm.crc_timeout);	/* seconds */
+	zm.block_size			=(ulong)iniGetBytes(ini, section,"BlockSize",1,zm.block_size);	/* 1024  */
+	zm.max_block_size		=(ulong)iniGetBytes(ini, section,"MaxBlockSize",1,zm.max_block_size); /* 1024 or 8192 */
+	zm.max_errors			=iniGetInteger(ini, section,"MaxErrors",zm.max_errors);
+	zm.recv_bufsize			=(ulong)iniGetBytes(ini, section,"RecvBufSize",1,0);
+	zm.no_streaming			=!iniGetBool(ini, section,"Streaming",TRUE);
+	zm.want_fcs_16			=!iniGetBool(ini, section,"CRC32",TRUE);
+	zm.can_full_duplex		=iniGetBool(ini, section,"FullDuplex",TRUE);
+	zm.escape_telnet_iac	=iniGetBool(ini, section,"EscapeTelnetIAC",TRUE);
+	zm.escape_8th_bit		=iniGetBool(ini, section,"Escape8thBit",FALSE);
+	zm.escape_ctrl_chars	=iniGetBool(ini, section,"EscapeCtrlChars",FALSE);
+	zm.max_window_size		=(uint32_t)iniGetBytes(ini, section,"MaxWindowSize",1,0);
+	zm.target_window_size	=(unsigned)iniGetDuration(ini, section,"TargetWindowSize",0);
+
+	section = "DSZLOG";
+	dszlog_path				=iniGetBool(ini, section, "Path",TRUE);
+	dszlog_short			=iniGetBool(ini, section, "Short",FALSE);
+	dszlog_quotes			=iniGetBool(ini, section, "Quotes",FALSE);
 
 	if(zm.recv_bufsize > 0xffff)
 		zm.recv_bufsize = 0xffff;
@@ -1669,7 +1670,7 @@ int main(int argc, char **argv)
 					case 'Y':
 						mode|=(YMODEM|CRC);
 						break;
-					case 'k':	/* Ymodem-Checksum for debug/test purposes only */
+					case 'k':	/* YMODEM-Checksum for debug/test purposes only */
 						mode|=YMODEM;
 						break;
 					case 'g':
@@ -1682,7 +1683,7 @@ int main(int argc, char **argv)
 						break;
 					default:
 						fprintf(statfp,"Unrecognized command '%s'\n\n",argv[i]);
-						fprintf(statfp,usage,MAX_FILE_SIZE);
+						fprintf(statfp,usage);
 						bail(1); 
 						return -1;
 				} 
@@ -1729,11 +1730,11 @@ int main(int argc, char **argv)
 					dszlog_quotes=TRUE;
 					continue;
 				}
-				switch(toupper(*arg)) {
-					case 'K':	/* sz/rz compatible */
+				switch(*arg) {
+					case 'k':	/* sz/rz compatible */
 						xm.block_size=XMODEM_MAX_BLOCK_SIZE;
 						break;
-					case 'C':	/* sz/rz compatible */
+					case 'c':	/* sz/rz compatible */
 						mode|=CRC;
 						break;
 					case '2':
@@ -1745,25 +1746,28 @@ int main(int argc, char **argv)
 					case '8':	/* ZedZap */
 						zm.max_block_size=8192;
 						break;
-					case 'O':	/* disable Zmodem CRC-32 */
+					case 'o':	/* disable ZMODEM CRC-32 */
 						zm.want_fcs_16=TRUE;
 						break;
-					case 'S':	/* disable Zmodem streaming */
+					case 's':	/* disable ZMODEM streaming */
 						zm.no_streaming=TRUE;
 						break;
-					case 'G':	/* Ymodem-G or Xmodem-G (a.k.a. Qmodem-G) */
+					case 'w':	/* Max ZMODEM Transmit Window Size */
+						zm.max_window_size = parse_byte_count(arg + 1, /* units: */1);
+						break;
+					case 'g':	/* YMODEM-G or XMODEM-G (a.k.a. Qmodem-G) */
 						mode|=(GMODE|CRC);
 						break;
-					case 'Y':
+					case 'y':
 						mode|=OVERWRITE;
 						break;
 					case '!':
 						pause_on_abend=TRUE;
 						break;
-					case 'M':	/* MaxFileSize */
-						max_file_size=strtoul(arg + 1,NULL,0);	/* TODO: use strtoull() ? */
+					case 'm':	/* MaxFileSize */
+						max_file_size = parse_byte_count(arg + 1, /* units: */1);
 						break;
-					case 'L':	/* Lowercase received filenames */
+					case 'l':	/* Lowercase received filenames */
 						lc_filenames=TRUE;
 						break;
 				}
@@ -1819,14 +1823,14 @@ int main(int argc, char **argv)
 
 	if(!(mode&(SEND|RECV))) {
 		fprintf(statfp,"!No command specified\n\n");
-		fprintf(statfp,usage,MAX_FILE_SIZE);
+		fprintf(statfp,usage);
 		bail(1); 
 		return -1;
 	}
 
-	if(mode&(SEND|XMODEM) && !fnames) { /* Sending with any or recv w/Xmodem */
+	if(mode&(SEND|XMODEM) && !fnames) { /* Sending with any or recv w/XMODEM */
 		fprintf(statfp,"!Must specify filename or filelist\n\n");
-		fprintf(statfp,usage,MAX_FILE_SIZE);
+		fprintf(statfp,usage);
 		bail(1); 
 		return -1;
 	}
@@ -1845,7 +1849,7 @@ int main(int argc, char **argv)
 		init_stdio();
 #else
 		fprintf(statfp,"!No socket descriptor specified\n\n");
-		fprintf(errfp,usage,MAX_FILE_SIZE);
+		fprintf(errfp,usage);
 		bail(1);
 		return -1;
 #endif
@@ -1875,6 +1879,19 @@ int main(int argc, char **argv)
 		lprintf(LOG_DEBUG, "Setting socket options");
 		if(iniGetSocketOptions(ini, "sockopts", sock, error, sizeof(error)) != 0)
 			lprintf(LOG_ERR, "ERROR %s", error);
+		int value = 0;
+		socklen_t len = sizeof(value);
+		if(getsockopt(sock, SOL_SOCKET, SO_SNDBUF, (char*)&value, &len) == 0)
+			lprintf(LOG_DEBUG, "Socket send buffer length: %d bytes", value);
+		value = 0;
+		len = sizeof(value);
+		if(getsockopt(sock, SOL_SOCKET, SO_RCVBUF, (char*)&value, &len) == 0)
+			lprintf(LOG_DEBUG, "Socket receive buffer length: %d bytes", value);
+		value = 0;
+		len = sizeof(value);
+		if(getsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (char*)&value, &len) == 0)
+			lprintf(LOG_DEBUG, "Socket TCP_NODELAY: %d", value);
+
 #ifdef __unix__
 	}
 #endif
diff --git a/src/sbbs3/targets.mk b/src/sbbs3/targets.mk
index 7453e7044f3f18e1d7c9f90370bc645b26eb73c4..43a3aef500d2e3c7b67bccc0d0f0b9e81159115b 100644
--- a/src/sbbs3/targets.mk
+++ b/src/sbbs3/targets.mk
@@ -51,7 +51,9 @@ UTILS		= $(FIXSMB) $(CHKSMB) \
 			  $(SEXYZ) $(DSTSEDIT) $(READSAUCE) $(SHOWSTAT) \
 			  $(PKTDUMP) $(FMSGDUMP)
 
-all:	dlls utils console scfg uedit umonitor
+GIT_INFO	= git_hash.h git_branch.h
+
+all:	$(GIT_INFO) dlls utils console scfg uedit umonitor
 
 console:	$(JS_DEPS) xpdev-mt smblib \
 		$(MTOBJODIR) $(LIBODIR) $(EXEODIR) \
@@ -114,6 +116,21 @@ symlinks: all
 	ln -sfr */$(EXEODIR)/* $(SBBSEXEC)
 endif
 
+.PHONY: FORCE
+FORCE:
+
+ifneq ($(GIT), NO)
+git_hash.h: FORCE ../../.git
+	$(QUIET)echo '#define GIT_HASH "'`git log -1 HEAD --format=%h`\" > $@.tmp
+	$(QUIET)diff $@.tmp $@ || cp $@.tmp $@
+	$(QUIET)rm -f $@.tmp
+
+git_branch.h: FORCE ../../.git
+	$(QUIET)echo '#define GIT_BRANCH "'`git rev-parse --abbrev-ref HEAD`\" > $@.tmp
+	$(QUIET)diff $@.tmp $@ || cp $@.tmp $@
+	$(QUIET)rm -f $@.tmp
+endif
+
 ifeq ($(os),linux)
 .PHONY: setcap
 setcap: all
@@ -124,10 +141,10 @@ endif
 sexyz:	$(SEXYZ)
 
 .PHONY: jsdoor
-jsdoor: $(JS_DEPS) $(CRYPT_DEPS) $(XPDEV-MT_LIB) $(SMBLIB) $(UIFCLIB-MT) $(CIOLIB-MT) $(JSDOOR)
+jsdoor: $(GIT_INFO) $(JS_DEPS) $(CRYPT_DEPS) $(XPDEV-MT_LIB) $(SMBLIB) $(UIFCLIB-MT) $(CIOLIB-MT) $(JSDOOR)
 
 # Library dependencies
-$(SBBS): 
+$(SBBS):
 $(FTPSRVR): 
 $(WEBSRVR):
 $(MAILSRVR):
diff --git a/src/sbbs3/text_defaults.c b/src/sbbs3/text_defaults.c
index af7b5f1f0ee197272501e14a883df924fe3921b3..b6e55e09d86297de0ff613fd319639638f0a3ef2 100644
--- a/src/sbbs3/text_defaults.c
+++ b/src/sbbs3/text_defaults.c
@@ -66,10 +66,10 @@ const char * const text_defaults[TOTAL_TEXT]={
 	,"\x07\x01\x5f\x01\x77\x01\x68\x4e\x6f\x64\x65\x20\x25\x32\x64\x3a\x20\x01\x67\x25\x73\x01\x6e\x01\x67\x20\x73\x65\x6e\x74\x20\x79"
 		"\x6f\x75\x20\x45\x2d\x6d\x61\x69\x6c\x2e\x0d\x0a" // 038 EmailNodeMsg
 	,"\x01\x6e\x0d\x0a\x59\x6f\x75\x20\x63\x61\x6e\x27\x74\x20\x66\x6f\x72\x77\x61\x72\x64\x20\x6d\x61\x69\x6c\x2e\x0d\x0a" // 039 R_Forward
-	,"\x01\x6e\x01\x6d\x0d\x0a\x46\x6f\x72\x77\x61\x72\x64\x65\x64\x20\x62\x79\x20\x01\x68\x25\x73\x01\x6e\x01\x6d\x20\x6f\x6e\x20\x01"
-		"\x68\x25\x73\x01\x6e\x0d\x0a" // 040 ForwardedFrom
-	,"\x01\x6e\x01\x6d\x0d\x0a\x4d\x61\x69\x6c\x20\x66\x6f\x72\x77\x61\x72\x64\x65\x64\x20\x74\x6f\x20\x01\x68\x25\x73\x20\x01\x6e\x01"
-		"\x6d\x23\x25\x64\x2e\x01\x6e\x0d\x0a" // 041 Forwarded
+	,"\x01\x6e\x01\x6d\x46\x6f\x72\x77\x61\x72\x64\x65\x64\x20\x62\x79\x20\x01\x68\x25\x73\x01\x6e\x01\x6d\x20\x6f\x6e\x20\x01\x68\x25"
+		"\x73\x01\x6e\x0d\x0a" // 040 ForwardedFrom
+	,"\x01\x6e\x01\x6d\x0d\x0a\x4d\x61\x69\x6c\x20\x66\x6f\x72\x77\x61\x72\x64\x65\x64\x20\x74\x6f\x20\x01\x68\x25\x73\x01\x6e\x0d\x0a"
+		"" // 041 Forwarded
 	,"\x01\x62\x01\x68\x41\x75\x74\x6f\x20\x6d\x65\x73\x73\x61\x67\x65\x20\x62\x79\x3a\x20\x01\x63\x25\x73\x01\x62\x20\x6f\x6e\x20\x25"
 		"\x73\x01\x6e\x0d\x0a\x0d\x0a" // 042 AutoMsgBy
 	,"\x0d\x0a\x41\x75\x74\x6f\x20\x4d\x65\x73\x73\x61\x67\x65\x20\x2d\x20\x7e\x52\x65\x61\x64\x2c\x20\x7e\x57\x72\x69\x74\x65\x2c\x20"
diff --git a/src/sbbs3/umonitor/umonitor.c b/src/sbbs3/umonitor/umonitor.c
index bf9de1e8f00cc1df722ee9d69dd136ab10b3f216..8a5b9d81487ce88063b15bfad1a452076ffff03f 100644
--- a/src/sbbs3/umonitor/umonitor.c
+++ b/src/sbbs3/umonitor/umonitor.c
@@ -66,7 +66,7 @@
 uifcapi_t uifc; /* User Interface (UIFC) Library API */
 const char *YesStr="Yes";
 const char *NoStr="No";
-char* app_title = "Synchronet UNIX Monitor v" VERSION " " PLATFORM_DESC;
+char app_title[128];
 int	ciolib_mode=CIOLIB_MODE_AUTO;
 
 int lprintf(char *fmt, ...)
@@ -323,10 +323,12 @@ int drawstats(scfg_t *cfg, int nodenum, node_t *node, int *curp, int *barp) {
 	stats_t	nstats;
 	char	statbuf[6*78];		/* Buffer to hold the stats for passing to uifc.showbuf() */
 	char	str[4][4][12];
+	char	heading[128];
 	char	usrname[128];
+	char	tmp[128];
 	ulong	free;
 	uint	i,l,m;
-	time_t	t;
+	time_t	t, now;
 	int		shownode=1;
 
 	if(getnodedat(cfg,nodenum,node,FALSE,NULL)) {
@@ -338,16 +340,25 @@ int drawstats(scfg_t *cfg, int nodenum, node_t *node, int *curp, int *barp) {
 	username(cfg,node->useron,usrname);
 
 	getstats(cfg, 0, &sstats);
-	t=time(NULL);
-	strftime(str[0][0],12,"%b %e",localtime(&t));
-	free=getfreediskspace(cfg->temp_dir,1024);
-	if(free<1000) {
-		free=getfreediskspace(cfg->temp_dir,0);
-		getsizestr(str[0][1],free,TRUE);
-	}
-	else
-		getsizestr(str[0][1],free,FALSE);
+	now = time(NULL);
 	if(shownode) {
+		static client_t client;
+		time_t hangup = now;
+		getnodeclient(cfg, nodenum, &client, &hangup);
+		t = client.time ? client.time : now;
+		if(node->status != NODE_WFC && node->status != NODE_OFFLINE)
+			safe_snprintf(heading, sizeof(heading), "`Node #`%-3d %s `%s`: %s"
+				,nodenum
+				,sectostr(now - t, tmp)
+				,client.protocol
+				,client.addr);
+		else
+			safe_snprintf(heading, sizeof(heading), "`Node #`%-3d %s `%s`: %s `on` %.12s"
+				,nodenum
+				,sectostr(hangup - t, tmp)
+				,client.protocol
+				,client.addr
+				,ctime(&hangup) + 4);
 		snprintf(str[1][0],12,"%s/%s",getnumstr(str[3][2],nstats.ltoday),getnumstr(str[3][3],sstats.ltoday));
 		getnumstr(str[1][1],sstats.logons);
 		snprintf(str[1][2],12,"%s/%s",getnumstr(str[3][2],nstats.ttoday),getnumstr(str[3][3],sstats.ttoday));
@@ -367,6 +378,14 @@ int drawstats(scfg_t *cfg, int nodenum, node_t *node, int *curp, int *barp) {
 		getnumstr(str[3][3],sstats.dls);
 	}
 	else {
+		free=getfreediskspace(cfg->data_dir,1024);
+		if(free<1000) {
+			free=getfreediskspace(cfg->data_dir,0);
+			getsizestr(str[0][0],free,TRUE);
+		}
+		else
+			getsizestr(str[0][0],free,FALSE);
+		sprintf(heading, "`Space`: %s `in` %s", str[0][0], cfg->data_dir);
 		snprintf(str[1][0],12,"%s",getnumstr(str[3][3],sstats.ltoday));
 		getnumstr(str[1][1],sstats.logons);
 		snprintf(str[1][2],12,"%s",getnumstr(str[3][3],sstats.ttoday));
@@ -385,11 +404,11 @@ int drawstats(scfg_t *cfg, int nodenum, node_t *node, int *curp, int *barp) {
 		getsizestr(str[3][2],sstats.dlb,TRUE);
 		getnumstr(str[3][3],sstats.dls);
 	}
-	snprintf(statbuf,sizeof(statbuf),"`Node #`: %-3d %6s  `Space`: %s"
+	snprintf(statbuf,sizeof(statbuf),"%s"
 			"\n`Logons`: %-11s `Total`: %-11s `Timeon`: %-11s `Total`: %-11s"
 			"\n`Emails`: %-11s `Posts`: %-11s `Fbacks`: %-11s `Users`: %-11s"
 			"\n`Uloads`: %-11s `Files`: %-11s `Dloads`: %-11s `Files`: %-11s",
-			nodenum,str[0][0],str[0][1],
+			heading,
 			str[1][0],str[1][1],str[1][2],str[1][3],
 			str[2][0],str[2][1],str[2][2],str[2][3],
 			str[3][0],str[3][1],str[3][2],str[3][3]);
@@ -823,6 +842,8 @@ int main(int argc, char** argv)  {
 	time_t	last_semfile_check = time(NULL);
 	int		idle_sleep=100;
 
+	SAFEPRINTF2(app_title, "Synchronet UNIX Monitor v%s%c", VERSION, REVISION);
+
 	/******************/
 	/* Ini file stuff */
 	/******************/
@@ -875,7 +896,7 @@ int main(int argc, char** argv)  {
 		printf("ERROR! %s\n",str);
 		exit(1);
 	}
-	prep_dir(cfg.data_dir, cfg.temp_dir, sizeof(cfg.temp_dir));
+	prep_dir(cfg.ctrl_dir, cfg.temp_dir, sizeof(cfg.temp_dir));
 
 	memset(&uifc,0,sizeof(uifc));
 	uifc.mode|=UIFC_NOCTRL;
diff --git a/src/sbbs3/userdat.c b/src/sbbs3/userdat.c
index 883b5d7c1e6cb965f4116173f19f7ad8371264a3..44f6079cef58e0b753a7dc5541d7e7c10506ab84 100644
--- a/src/sbbs3/userdat.c
+++ b/src/sbbs3/userdat.c
@@ -1538,6 +1538,44 @@ int putnmsg(scfg_t* cfg, int num, char *strin)
 	return(0);
 }
 
+/* Return node's client's socket descriptor or negative on error */
+int getnodeclient(scfg_t* cfg, uint number, client_t* client, time_t* done)
+{
+	SOCKET sock = INVALID_SOCKET;
+	char path[MAX_PATH + 1];
+	char value[INI_MAX_VALUE_LEN];
+	char* p;
+	FILE* fp;
+
+	if(!VALID_CFG(cfg)
+		|| client == NULL || number < 1 || number > cfg->sys_nodes)
+		return -1;
+
+	if(client->size == sizeof(client)) {
+		free((char*)client->protocol);
+		free((char*)client->user);
+	}
+	memset(client, 0, sizeof(*client));
+	client->size = sizeof(client);
+	SAFEPRINTF(path, "%sclient.ini", cfg->node_path[number - 1]);
+	fp = iniOpenFile(path, /* create: */FALSE);
+	if(fp == NULL)
+		return -2;
+	sock = iniReadShortInt(fp, ROOT_SECTION, "sock", 0);
+	client->port = iniReadShortInt(fp, ROOT_SECTION, "port", 0);
+	client->time = iniReadInteger(fp, ROOT_SECTION, "time", 0);
+	client->usernum = iniReadInteger(fp, ROOT_SECTION, "user", 0);
+	SAFECOPY(client->addr, iniReadString(fp, ROOT_SECTION, "addr", "<none>", value));
+	SAFECOPY(client->host, iniReadString(fp, ROOT_SECTION, "host", "<none>", value));
+	if((p = iniReadString(fp, ROOT_SECTION, "prot", NULL, value)) != NULL)
+		client->protocol = strdup(p);
+	if((p = iniReadString(fp, ROOT_SECTION, "name", NULL, value)) != NULL)
+		client->user = strdup(p);
+	*done = iniReadInteger(fp, ROOT_SECTION, "done", client->time);
+	fclose(fp);
+	return sock;
+}
+
 static int getdirnum(scfg_t* cfg, char* code)
 {
 	size_t i;
diff --git a/src/sbbs3/userdat.h b/src/sbbs3/userdat.h
index 36c46244028272515a82ccd82081b6b460be3aab..3d78be70fee126a278b332544473294eda98c73f 100644
--- a/src/sbbs3/userdat.h
+++ b/src/sbbs3/userdat.h
@@ -76,6 +76,7 @@ DLLEXPORT char* getsmsg(scfg_t*, int usernumber);
 DLLEXPORT int	putsmsg(scfg_t*, int usernumber, char *strin);
 DLLEXPORT char* getnmsg(scfg_t*, int node_num);
 DLLEXPORT int	putnmsg(scfg_t*, int num, char *strin);
+DLLEXPORT int	getnodeclient(scfg_t*, uint number, client_t*, time_t*);
 
 DLLEXPORT uint	userdatdupe(scfg_t*, uint usernumber, uint offset, uint datlen, char *dat
 					,BOOL del, BOOL next, void (*progress)(void*, int, int), void* cbdata);
diff --git a/src/sbbs3/ver.cpp b/src/sbbs3/ver.cpp
index bc6e5d753c9cc6166dabdf5a0da1a7bb30d89b8e..594e9147104fabbc68499098d53b76ebdee9c96e 100644
--- a/src/sbbs3/ver.cpp
+++ b/src/sbbs3/ver.cpp
@@ -1,9 +1,4 @@
-/* ver.cpp */
-// vi: tabstop=4
-
-/* Synchronet version display */
-
-/* $Id: ver.cpp,v 1.31 2019/10/08 02:07:26 rswindell Exp $ */
+/* Synchronet version info */
 
 /****************************************************************************
  * @format.tab-size 4		(Plain Text/Source Code File Header)			*
@@ -18,29 +13,32 @@
  * See the GNU General Public License for more details: gpl.txt or			*
  * http://www.fsf.org/copyleft/gpl.html										*
  *																			*
- * Anonymous FTP access to the most recent released source is available at	*
- * ftp://vert.synchro.net, ftp://cvs.synchro.net and ftp://ftp.synchro.net	*
- *																			*
- * Anonymous CVS access to the development source and modification history	*
- * is available at cvs.synchro.net:/cvsroot/sbbs, example:					*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs login			*
- *     (just hit return, no password is necessary)							*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs checkout src		*
- *																			*
  * For Synchronet coding style and modification guidelines, see				*
  * http://www.synchro.net/source.html										*
  *																			*
- * You are encouraged to submit any modifications (preferably in Unix diff	*
- * format) via e-mail to mods@synchro.net									*
- *																			*
  * Note: If this box doesn't appear square, then you need to fix your tabs.	*
  ****************************************************************************/
 
 #include "sbbs.h"
+#ifdef SBBS
 #include "ssl.h"
+#endif
+#include "git_hash.h"
+#include "git_branch.h"
+#include "ver.h"
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+const char* git_hash = GIT_HASH;
+const char* git_branch = GIT_BRANCH;
 const char* beta_version = " "; /* Space if non-beta, " beta" otherwise */
 
+#ifdef __cplusplus
+}
+#endif
+
 #if defined(_WINSOCKAPI_)
 	extern WSADATA WSAData;
 	#define SOCKLIB_DESC WSAData.szDescription
@@ -71,7 +69,7 @@ char* socklib_version(char* str, char* winsock_ver)
 	return(str);
 }
 
-#ifndef JSDOOR
+#if defined(SBBS) && !defined(JSDOOR)
 void sbbs_t::ver()
 {
 	char str[128],compiler[32];
@@ -96,7 +94,10 @@ void sbbs_t::ver()
 	center(str);
 	CRLF;
 
-	sprintf(str,"%s - http://www.synchro.net", COPYRIGHT_NOTICE);
+	center("https://gitlab.synchro.net - " GIT_BRANCH "/" GIT_HASH); 
+	CRLF;
+
+	sprintf(str,"%s - http://synchro.net", COPYRIGHT_NOTICE);
 	center(str);
 	CRLF;
 
diff --git a/src/sbbs3/ver.h b/src/sbbs3/ver.h
new file mode 100644
index 0000000000000000000000000000000000000000..288605929394d995049c524bd3b57766f985e89b
--- /dev/null
+++ b/src/sbbs3/ver.h
@@ -0,0 +1,36 @@
+/* Synchronet version info */
+
+/****************************************************************************
+ * @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 _VER_H_
+#define _VER_H_
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+extern const char* git_hash;
+extern const char* git_branch;
+extern const char* beta_version;
+
+#ifdef __cplusplus
+}
+#endif
+#endif /* Don't add anything after this line */
\ No newline at end of file
diff --git a/src/sbbs3/websrvr.c b/src/sbbs3/websrvr.c
index 0a8cc52360bd689475db758847f6d06a70a90ad3..f34f9879d9d1eb5b0d589d54b387032d3eceb53e 100644
--- a/src/sbbs3/websrvr.c
+++ b/src/sbbs3/websrvr.c
@@ -65,6 +65,7 @@
 #include "xpprintf.h"
 #include "ssl.h"
 #include "fastcgi.h"
+#include "ver.h"
 
 static const char*	server_name="Synchronet Web Server";
 static const char*	newline="\r\n";
@@ -106,7 +107,6 @@ static volatile BOOL	terminate_server=FALSE;
 static volatile BOOL	terminated=FALSE;
 static volatile BOOL	terminate_http_logging_thread=FALSE;
 static struct xpms_set	*ws_set=NULL;
-static char		revision[16];
 static char		root_dir[MAX_PATH+1];
 static char		error_dir[MAX_PATH+1];
 static char		cgi_dir[MAX_PATH+1];
@@ -6758,17 +6758,16 @@ const char* DLLCALL web_ver(void)
 
 	DESCRIBE_COMPILER(compiler);
 
-	sscanf("$Revision: 1.720 $", "%*s %s", revision);
-
-	sprintf(ver,"%s %s%s  "
-		"Compiled %s %s with %s"
+	sprintf(ver,"%s %s%c%s  "
+		"Compiled %s/%s %s %s with %s"
 		,server_name
-		,revision
+		,VERSION, REVISION
 #ifdef _DEBUG
 		," Debug"
 #else
 		,""
 #endif
+		,git_branch, git_hash
 		,__DATE__, __TIME__, compiler);
 
 	return(ver);
@@ -6904,7 +6903,6 @@ void DLLCALL web_server(void* arg)
 	startup=(web_startup_t*)arg;
 
 	SetThreadName("sbbs/webServer");
-	web_ver();	/* get CVS revision */
 
     if(startup==NULL) {
     	sbbs_beep(100,500);
@@ -6926,7 +6924,7 @@ void DLLCALL web_server(void* arg)
 #endif
 
 	ZERO_VAR(js_server_props);
-	SAFEPRINTF2(js_server_props.version,"%s %s",server_name,revision);
+	SAFEPRINTF3(js_server_props.version,"%s %s%c",server_name, VERSION, REVISION);
 	js_server_props.version_detail=web_ver();
 	js_server_props.clients=&active_clients.value;
 	js_server_props.options=&startup->options;
@@ -6976,9 +6974,9 @@ void DLLCALL web_server(void* arg)
 
 		memset(&scfg, 0, sizeof(scfg));
 
-		lprintf(LOG_INFO,"%s Revision %s%s"
+		lprintf(LOG_INFO,"%s Version %s%c%s"
 			,server_name
-			,revision
+			,VERSION, REVISION
 #ifdef _DEBUG
 			," Debug"
 #else
@@ -6988,7 +6986,7 @@ void DLLCALL web_server(void* arg)
 
 		DESCRIBE_COMPILER(compiler);
 
-		lprintf(LOG_INFO,"Compiled %s %s with %s", __DATE__, __TIME__, compiler);
+		lprintf(LOG_INFO,"Compiled %s/%s %s %s with %s", git_branch, git_hash, __DATE__, __TIME__, compiler);
 
 		if(!winsock_startup()) {
 			cleanup(1);
diff --git a/src/sbbs3/websrvr.vcxproj b/src/sbbs3/websrvr.vcxproj
index 931bfdd3b13b51df583a0edf8598369faf04749b..94519cae70b333f6d7750821c019c15abdefb1f2 100644
--- a/src/sbbs3/websrvr.vcxproj
+++ b/src/sbbs3/websrvr.vcxproj
@@ -176,6 +176,7 @@
       <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
       <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
     </ClCompile>
+    <ClCompile Include="ver.cpp" />
     <ClCompile Include="websrvr.c">
       <AdditionalIncludeDirectories Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
       <PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
diff --git a/src/sbbs3/writemsg.cpp b/src/sbbs3/writemsg.cpp
index 2e3ae1a6d2c271d1b4b96b825798d41cb8851a01..189f32109ccafce6de8cc97ac68b63a049c61150 100644
--- a/src/sbbs3/writemsg.cpp
+++ b/src/sbbs3/writemsg.cpp
@@ -1,7 +1,4 @@
 /* Synchronet message creation routines */
-// vi: tabstop=4
-
-/* $Id: writemsg.cpp,v 1.175 2020/05/24 19:34:02 rswindell Exp $ */
 
 /****************************************************************************
  * @format.tab-size 4		(Plain Text/Source Code File Header)			*
@@ -16,21 +13,9 @@
  * See the GNU General Public License for more details: gpl.txt or			*
  * http://www.fsf.org/copyleft/gpl.html										*
  *																			*
- * Anonymous FTP access to the most recent released source is available at	*
- * ftp://vert.synchro.net, ftp://cvs.synchro.net and ftp://ftp.synchro.net	*
- *																			*
- * Anonymous CVS access to the development source and modification history	*
- * is available at cvs.synchro.net:/cvsroot/sbbs, example:					*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs login			*
- *     (just hit return, no password is necessary)							*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs checkout src		*
- *																			*
  * For Synchronet coding style and modification guidelines, see				*
  * http://www.synchro.net/source.html										*
  *																			*
- * You are encouraged to submit any modifications (preferably in Unix diff	*
- * format) via e-mail to mods@synchro.net									*
- *																			*
  * Note: If this box doesn't appear square, then you need to fix your tabs.	*
  ****************************************************************************/
 
@@ -758,11 +743,8 @@ bool sbbs_t::writemsg(const char *fname, const char *top, char *subj, long mode,
 
 void sbbs_t::editor_info_to_msg(smbmsg_t* msg, const char* editor, const char* charset)
 {
-	if(editor != NULL)
-		smb_hfield_str(msg, SMB_EDITOR, editor);
-
-	if(charset != NULL)
-		smb_hfield_str(msg, FIDOCHARSET, charset);
+	smb_hfield_string(msg, SMB_EDITOR, editor);
+	smb_hfield_string(msg, FIDOCHARSET, charset);
 
 	ushort useron_xedit = useron.xedit;
 
@@ -1312,11 +1294,12 @@ bool sbbs_t::editfile(char *fname, bool msg)
 /*************************/
 /* Copy file attachments */
 /*************************/
-void sbbs_t::copyfattach(uint to, uint from, char *title)
+bool sbbs_t::copyfattach(uint to, uint from, const char* subj)
 {
 	char str[128],str2[128],str3[128],*tp,*sp,*p;
+	bool result = false;
 
-	strcpy(str,title);
+	strcpy(str, subj);
 	tp=str;
 	while(1) {
 		p=strchr(tp,' ');
@@ -1328,96 +1311,219 @@ void sbbs_t::copyfattach(uint to, uint from, char *title)
 			,cfg.data_dir,to,tp);
 		SAFEPRINTF3(str3,"%sfile/%04u.in/%s"  /* str2 is path/fname */
 			,cfg.data_dir,from,tp);
-		if(strcmp(str2,str3))
-			mv(str3,str2,1);
+		if(strcmp(str2,str3)) {
+			if(mv(str3, str2, /* copy */true) != 0)
+				return false;
+			result = true;
+		}
 		if(!p)
 			break;
 		tp=p+1; 
 	}
+	return result;
 }
 
-
 /****************************************************************************/
-/* Forwards mail (fname) to usernumber                                      */
-/* Called from function readmail											*/
+/* Forwards mail 'orgmsg' to 'to' with optional 'comment'.					*/
+/* If comment is NULL, comment lines will be prompted for.					*/
+/* If comment is a zero-length string, no comments will be included.		*/
 /****************************************************************************/
-void sbbs_t::forwardmail(smbmsg_t *msg, int usernumber)
+bool sbbs_t::forwardmail(smbmsg_t* orgmsg, const char* to, const char* subject, const char* comment)
 {
 	char		str[256],touser[128];
 	char 		tmp[512];
-	int			i;
+	char		subj[LEN_TITLE + 1];
+	int			result;
+	smbmsg_t	msg;
 	node_t		node;
-	msghdr_t	hdr=msg->hdr;
-	idxrec_t	idx=msg->idx;
-	time32_t	now32;
+	uint usernumber = 0;
+
+	if(to == NULL)
+		return false;
+
+	uint16_t net_type = NET_NONE;
+	if(strchr(to, '@') != NULL)
+		net_type = smb_netaddr_type(to);
+	if(net_type == NET_NONE) {
+		usernumber = finduser(to);
+		if(usernumber < 1)
+			return false;
+	} else if(!is_supported_netmail_addr(&cfg, to)) {
+		bprintf(text[InvalidNetMailAddr], to);
+		return false;
+	}
 
 	if(useron.etoday>=cfg.level_emailperday[useron.level] && !SYSOP && !(useron.exempt&FLAG('M'))) {
 		bputs(text[TooManyEmailsToday]);
-		return; 
+		return false; 
 	}
 	if(useron.rest&FLAG('F')) {
 		bputs(text[R_Forward]);
-		return; 
+		return false;
 	}
 	if(usernumber==1 && useron.rest&FLAG('S')) {
 		bprintf(text[R_Feedback],cfg.sys_op);
-		return; 
+		return false;
 	}
 	if(usernumber!=1 && useron.rest&FLAG('E')) {
 		bputs(text[R_Email]);
-		return; 
+		return false;
+	}
+
+	if(subject == NULL) {
+		subject = subj;
+		SAFEPRINTF(subj, "Fwd: %s", orgmsg->subj);
+		bputs(text[SubjectPrompt]);
+		if(!getstr(subj, sizeof(subj) - 1, K_LINE | K_EDIT | K_AUTODEL | K_TRIM))
+			return false;
 	}
 
-	msg->idx.attr&=~(MSG_READ|MSG_DELETE);
-	msg->hdr.attr=msg->idx.attr;
+	memset(&msg, 0, sizeof(msg));
+	msg.hdr.auxattr = orgmsg->hdr.auxattr & (MSG_HFIELDS_UTF8 | MSG_MIMEATTACH);
+	msg.hdr.when_imported.time = time32(NULL);
+	msg.hdr.when_imported.zone = sys_timezone(&cfg);
+	msg.hdr.when_written = msg.hdr.when_imported;
 
+	smb_hfield_str(&msg, SUBJECT, subject);
+	add_msg_ids(&cfg, &smb, &msg, orgmsg);
 
-	smb_hfield_str(msg,SENDER,useron.alias);
+	smb_hfield_str(&msg,SENDER,useron.alias);
 	SAFEPRINTF(str,"%u",useron.number);
-	smb_hfield_str(msg,SENDEREXT,str);
+	smb_hfield_str(&msg,SENDEREXT,str);
 
 	/* Security logging */
-	msg_client_hfields(msg,&client);
-	smb_hfield_str(msg,SENDERSERVER, server_host_name());
-
-	username(&cfg,usernumber,touser);
-	smb_hfield_str(msg,RECIPIENT,touser);
-	SAFEPRINTF(str,"%u",usernumber);
-	smb_hfield_str(msg,RECIPIENTEXT,str);
-	msg->idx.to=usernumber;
-
-	now32=time32(NULL);
-	smb_hfield(msg,FORWARDED,sizeof(time32_t),&now32);
-
-
-	if((i=smb_open_da(&smb))!=SMB_SUCCESS) {
-		errormsg(WHERE,ERR_OPEN,smb.file,i,smb.last_error);
-		return; 
+	msg_client_hfields(&msg,&client);
+	smb_hfield_str(&msg,SENDERSERVER, server_host_name());
+
+	if(usernumber > 0) {
+		username(&cfg,usernumber,touser);
+		smb_hfield_str(&msg, RECIPIENT,touser);
+		SAFEPRINTF(str,"%u",usernumber);
+		smb_hfield_str(&msg, RECIPIENTEXT,str);
+	} else {
+		SAFECOPY(touser, to);
+		char* p;
+		if((p = strchr(touser, '@')) != NULL)
+			*p = '\0';
+		smb_hfield_str(&msg, RECIPIENT, touser);
+		SAFECOPY(touser, to);
+		const char* addr = touser;
+		if(net_type != NET_INTERNET && p != NULL)
+			addr = p + 1;
+		char fulladdr[128];
+		if(net_type == NET_QWK) {
+			usernumber = qwk_route(&cfg, addr, fulladdr, sizeof(fulladdr) - 1);
+			if(*fulladdr == '\0') {
+				bprintf(text[InvalidNetMailAddr], addr);
+				smb_freemsgmem(&msg);
+				return false; 
+			}
+			addr = fulladdr;
+			SAFEPRINTF(str, "%u", usernumber);
+			smb_hfield_str(&msg, RECIPIENTEXT, str);
+			usernumber = 0;
+		}
+		smb_hfield_bin(&msg, RECIPIENTNETTYPE, net_type);
+		smb_hfield_netaddr(&msg, RECIPIENTNETADDR, addr, &net_type);
+	}
+	if(orgmsg->mime_version != NULL) {
+		safe_snprintf(str, sizeof(str), "MIME-Version: %s", orgmsg->mime_version);
+		smb_hfield_str(&msg, RFC822HEADER, str);
+	}
+	if(orgmsg->content_type != NULL) {
+		safe_snprintf(str, sizeof(str), "Content-type: %s", orgmsg->content_type);
+		smb_hfield_str(&msg, RFC822HEADER, str);
+	}
+	// This header field not strictly required any more:
+	time32_t now32 = time32(NULL);
+	smb_hfield(&msg, FORWARDED, sizeof(now32), &now32);
+
+	const char* br = NULL;
+	const char* pg = nulstr;
+	const char* lt = "<";
+	const char* gt = ">";
+	if(orgmsg->text_subtype != NULL && stricmp(orgmsg->text_subtype, "html") == 0) {
+		lt = "&lt;";
+		gt = "&gt;";
+		br = "<br>";
+		pg = "<p>";
+	}
+
+	if(comment == NULL) {
+		while(online && !msgabort()) {
+			bputs(text[UeditComment]);
+			if(!getstr(str, 70, K_WRAP))
+				break;
+			smb_hfield_string(&msg, SMB_COMMENT, str);
+			smb_hfield_string(&msg, SMB_COMMENT, br);
+		}
+		if(!online || msgabort()) {
+			smb_freemsgmem(&msg);
+			return false; 
+		}
+	} else {
+		if(*comment)
+			smb_hfield_string(&msg, SMB_COMMENT, comment);
+	}
+	if(smb_get_hfield(&msg, SMB_COMMENT, NULL) != NULL)
+		smb_hfield_string(&msg, SMB_COMMENT, pg);
+	smb_hfield_string(&msg, SMB_COMMENT, "-----Forwarded Message-----");
+	smb_hfield_string(&msg, SMB_COMMENT, br);
+	if(orgmsg->from_net.addr != NULL)
+		safe_snprintf(str, sizeof(str), "From: %s %s%s%s"
+			,orgmsg->from, lt, smb_netaddrstr(&orgmsg->from_net, tmp), gt);
+	else
+		safe_snprintf(str, sizeof(str), "From: %s", orgmsg->from);
+	smb_hfield_string(&msg, SMB_COMMENT, str);
+	smb_hfield_string(&msg, SMB_COMMENT, br);
+	safe_snprintf(str, sizeof(str), "Date: %s", msgdate(orgmsg->hdr.when_written, tmp));
+	smb_hfield_string(&msg, SMB_COMMENT, str);
+	smb_hfield_string(&msg, SMB_COMMENT, br);
+	if(orgmsg->to_net.addr != NULL)
+		safe_snprintf(str, sizeof(str), "To: %s %s%s%s"
+			,orgmsg->to, lt, smb_netaddrstr(&orgmsg->to_net, tmp), gt);
+	else
+		safe_snprintf(str, sizeof(str), "To: %s", orgmsg->to);
+	smb_hfield_string(&msg, SMB_COMMENT, str);
+	smb_hfield_string(&msg, SMB_COMMENT, br);
+	safe_snprintf(str, sizeof(str), "Subject: %s", orgmsg->subj);
+	smb_hfield_string(&msg, SMB_COMMENT, str);
+	smb_hfield_string(&msg, SMB_COMMENT, pg);
+
+	// Re-use the original message's data
+	if((result = smb_open_da(&smb)) != SMB_SUCCESS) {
+		smb_freemsgmem(&msg);
+		errormsg(WHERE, ERR_OPEN, smb.file, result, smb.last_error);
+		return false;
 	}
-	if((i=smb_incmsg_dfields(&smb,msg,1))!=SMB_SUCCESS) {
-		errormsg(WHERE,ERR_WRITE,smb.file,i);
-		return; 
+	if((result = smb_incmsg_dfields(&smb, orgmsg, 1)) != SMB_SUCCESS) {
+		smb_freemsgmem(&msg);
+		errormsg(WHERE, ERR_WRITE, smb.file, result, smb.last_error);
+		return false;
 	}
 	smb_close_da(&smb);
 
+	msg.dfield = orgmsg->dfield;
+	msg.hdr.offset = orgmsg->hdr.offset;
+	msg.hdr.total_dfields = orgmsg->hdr.total_dfields;
 
-	if((i=smb_addmsghdr(&smb,msg,smb_storage_mode(&cfg, &smb)))!=SMB_SUCCESS) {
-		errormsg(WHERE,ERR_WRITE,smb.file,i,smb.last_error);
-		smb_freemsg_dfields(&smb,msg,1);
-		return; 
+	if(orgmsg->hdr.auxattr&MSG_FILEATTACH) {
+		copyfattach(usernumber, useron.number, orgmsg->subj);
+		msg.hdr.auxattr |= MSG_FILEATTACH;
 	}
 
-	if(msg->hdr.auxattr&MSG_FILEATTACH)
-		copyfattach(usernumber,useron.number,msg->subj);
+	result = smb_addmsghdr(&smb, &msg, smb_storage_mode(&cfg, &smb));
+	msg.dfield = NULL;
+	smb_freemsgmem(&msg);
+	if(result != SMB_SUCCESS) {
+		errormsg(WHERE, ERR_WRITE, smb.file, result, smb.last_error);
+		smb_freemsg_dfields(&smb, orgmsg, 1);
+		return false;
+	}
 
-	bprintf(text[Forwarded],username(&cfg,usernumber,str),usernumber);
-	SAFEPRINTF2(str,"forwarded mail to %s #%d"
-		,username(&cfg,usernumber,tmp)
-		,usernumber);
+	bprintf(text[Forwarded], touser, usernumber);
+	SAFEPRINTF(str, "forwarded mail to %s", touser);
 	logline("E+",str);
-	msg->idx=idx;
-	msg->hdr=hdr;
-
 
 	if(usernumber==1) {
 		useron.fbacks++;
@@ -1432,19 +1538,26 @@ void sbbs_t::forwardmail(smbmsg_t *msg, int usernumber)
 	useron.etoday++;
 	putuserrec(&cfg,useron.number,U_ETODAY,5,ultoa(useron.etoday,tmp,10));
 
-	for(i=1;i<=cfg.sys_nodes;i++) { /* Tell user, if online */
-		getnodedat(i,&node,0);
-		if(node.useron==usernumber && !(node.misc&NODE_POFF)
-			&& (node.status==NODE_INUSE || node.status==NODE_QUIET)) {
-			SAFEPRINTF2(str,text[EmailNodeMsg],cfg.node_num,useron.alias);
-			putnmsg(&cfg,i,str);
-			break; 
-		} 
-	}
-	if(i>cfg.sys_nodes) {	/* User wasn't online, so leave short msg */
-		SAFEPRINTF(str,text[UserSentYouMail],useron.alias);
-		putsmsg(&cfg,usernumber,str); 
+	if(usernumber > 0) {
+		int i;
+		for(i=1;i<=cfg.sys_nodes;i++) { /* Tell user, if online */
+			getnodedat(i,&node,0);
+			if(node.useron==usernumber && !(node.misc&NODE_POFF)
+				&& (node.status==NODE_INUSE || node.status==NODE_QUIET)) {
+				SAFEPRINTF2(str,text[EmailNodeMsg],cfg.node_num,useron.alias);
+				putnmsg(&cfg,i,str);
+				break; 
+			} 
+		}
+		if(i>cfg.sys_nodes) {	/* User wasn't online, so leave short msg */
+			SAFEPRINTF(str,text[UserSentYouMail],useron.alias);
+			putsmsg(&cfg,usernumber,str); 
+		}
+	} else {
+		if(net_type == NET_FIDO && cfg.netmail_sem[0])
+			ftouch(cmdstr(cfg.netmail_sem, nulstr, nulstr, NULL));
 	}
+	return true;
 }
 
 /****************************************************************************/
diff --git a/src/sbbs3/xmodem.c b/src/sbbs3/xmodem.c
index bbf6ba1e13f45f995f76ff1b97a174142a1b1a36..794002990cb811e0c130df87abc3a2aa4adbcd9c 100755
--- a/src/sbbs3/xmodem.c
+++ b/src/sbbs3/xmodem.c
@@ -1,8 +1,5 @@
-/* xmodem.c */
-
 /* Synchronet X/YMODEM Functions */
-
-/* $Id: xmodem.c,v 1.52 2019/08/31 22:39:24 rswindell Exp $ */
+/* Synchronet X/YMODEM Functions */
 
 /****************************************************************************
  * @format.tab-size 4		(Plain Text/Source Code File Header)			*
@@ -17,21 +14,9 @@
  * See the GNU General Public License for more details: gpl.txt or			*
  * http://www.fsf.org/copyleft/gpl.html										*
  *																			*
- * Anonymous FTP access to the most recent released source is available at	*
- * ftp://vert.synchro.net, ftp://cvs.synchro.net and ftp://ftp.synchro.net	*
- *																			*
- * Anonymous CVS access to the development source and modification history	*
- * is available at cvs.synchro.net:/cvsroot/sbbs, example:					*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs login			*
- *     (just hit return, no password is necessary)							*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs checkout src		*
- *																			*
  * For Synchronet coding style and modification guidelines, see				*
  * http://www.synchro.net/source.html										*
  *																			*
- * You are encouraged to submit any modifications (preferably in Unix diff	*
- * format) via e-mail to mods@synchro.net									*
- *																			*
  * Note: If this box doesn't appear square, then you need to fix your tabs.	*
  ****************************************************************************/
 
@@ -622,9 +607,7 @@ const char* xmodem_source(void)
 
 char* xmodem_ver(char *buf)
 {
-	sscanf("$Revision: 1.52 $", "%*s %s", buf);
-
-	return(buf);
+	return strcpy(buf, "2.0");
 }
 
 void xmodem_init(xmodem_t* xm, void* cbdata, long* mode
diff --git a/src/sbbs3/xtrn_sec.cpp b/src/sbbs3/xtrn_sec.cpp
index 60afb07da64bd285cc9eafbc6b26c1674dd03d5d..859e5e2b69626a8178037c95306e634964e2a415 100644
--- a/src/sbbs3/xtrn_sec.cpp
+++ b/src/sbbs3/xtrn_sec.cpp
@@ -151,6 +151,7 @@ void sbbs_t::xtrndat(const char *name, const char *dropdir, uchar type, ulong tl
 	struct tm tm;
 	struct tm tl;
 	stats_t stats;
+	long term = term_supports();
 
 	char	node_dir[MAX_PATH+1];
 	char	ctrl_dir[MAX_PATH+1];
@@ -212,8 +213,8 @@ void sbbs_t::xtrndat(const char *name, const char *dropdir, uchar type, ulong tl
 			,cfg.sys_nodes						/* Total system nodes */
 			,cfg.node_num						/* Current node */
 			,tleft								/* User Timeleft in seconds */
-			,term_supports(ANSI)				/* User ANSI ? (Yes/Mono/No) */
-				? term_supports(COLOR)
+			,(term & ANSI)						/* User ANSI ? (Yes/Mono/No) */
+				? (term & COLOR)
 				? "Yes":"Mono":"No"
 			,rows								/* User Screen lines */
 			,useron.cdt+useron.freecdt);		/* User Credits */
@@ -326,7 +327,7 @@ void sbbs_t::xtrndat(const char *name, const char *dropdir, uchar type, ulong tl
 		lfexpand(str,misc);
 		write(file,str,strlen(str));
 
-		safe_snprintf(str, sizeof(str), "%lu\n%s\n%lu\n%ld\n%u\n%u\n%u\n%ld\n%u\n"
+		safe_snprintf(str, sizeof(str), "%lu\n%s\n%lu\n%ld\n%u\n%u\n%u\n%d\n%u\n"
 			,useron.cdt+useron.freecdt			/* Gold */
 			,unixtodstr(&cfg,useron.laston,tmp)	/* User last on date */
 			,cols 								/* User screen width */
@@ -334,7 +335,7 @@ void sbbs_t::xtrndat(const char *name, const char *dropdir, uchar type, ulong tl
 			,useron.level						/* User SL */
 			,0									/* Cosysop? */
 			,SYSOP								/* Sysop? (1/0) */
-			,term_supports(ANSI)				/* ANSI ? (1/0) */
+			,INT_TO_BOOL(term & ANSI)			/* ANSI ? (1/0) */
 			,online==ON_REMOTE);				/* Remote (1/0) */
 		lfexpand(str,misc);
 		write(file,str,strlen(str));
@@ -416,8 +417,8 @@ void sbbs_t::xtrndat(const char *name, const char *dropdir, uchar type, ulong tl
 			,unixtodstr(&cfg,useron.laston,tmp)	/* 17: User last on date */
 			,tleft								/* 18: User time left in sec */
 			,tleft/60							/* 19: User time left in min */
-			,useron.misc&NO_EXASCII 			/* 20: GR if COLOR ANSI */
-				? "7E" : (useron.misc&(ANSI|COLOR))==(ANSI|COLOR) ? "GR" : "NG");
+			,(term & NO_EXASCII)				/* 20: GR if COLOR ANSI */
+				? "7E" : (term & (ANSI|COLOR)) == (ANSI|COLOR) ? "GR" : "NG");
 		lfexpand(str,misc);
 		write(file,str,strlen(str));
 
@@ -451,7 +452,7 @@ void sbbs_t::xtrndat(const char *name, const char *dropdir, uchar type, ulong tl
 
 		localtime_r(&ns_time,&tm);
 		safe_snprintf(str, sizeof(str), "%c\n%c\n%u\n%lu\n%02d/%02d/%02d\n"
-			,(useron.misc&(NO_EXASCII|ANSI|COLOR))==ANSI
+			,(term & (NO_EXASCII|ANSI|COLOR)) == ANSI
 				? 'Y':'N'                       /* 39: ANSI supported but NG mode */
 			,'Y'                                /* 40: Use record locking */
 			,cfg.color[clr_external]			/* 41: BBS default color */
@@ -522,11 +523,11 @@ void sbbs_t::xtrndat(const char *name, const char *dropdir, uchar type, ulong tl
 			*(p++)=0;
 		else
 			p=nulstr;
-		safe_snprintf(str, sizeof(str), "%s\n%s\n%s\n%ld\n%u\n%lu\n"
+		safe_snprintf(str, sizeof(str), "%s\n%s\n%s\n%d\n%u\n%lu\n"
 			,tmp								/* User's firstname */
 			,p									/* User's lastname */
 			,useron.location					/* User's city */
-			,term_supports(ANSI)				/* 1=ANSI 0=ASCII */
+			,INT_TO_BOOL(term & ANSI)			/* 1=ANSI 0=ASCII */
 			,useron.level						/* Security level */
 			,tleft/60); 						/* Time left in minutes */
 		strupr(str);
@@ -577,7 +578,7 @@ void sbbs_t::xtrndat(const char *name, const char *dropdir, uchar type, ulong tl
 		if(useron.misc&DELETED) c|=(1<<0);
 		if(useron.misc&CLRSCRN) c|=(1<<1);
 		if(useron.misc&UPAUSE)	 c|=(1<<2);
-		if(term_supports(ANSI))	c|=(1<<3);
+		if(term & ANSI)			c|=(1<<3);
 		if(useron.sex=='F')     c|=(1<<7);
 		write(file,&c,1);						/* Attrib */
 		write(file,&useron.flags1,4);			/* Flags */
@@ -655,7 +656,7 @@ void sbbs_t::xtrndat(const char *name, const char *dropdir, uchar type, ulong tl
 		write(file,&c,1);						/* ScreenClear */
 		c=useron.misc&UPAUSE ? 1:0;
 		write(file,&c,1);						/* MorePrompts */
-		c=useron.misc&NO_EXASCII ? 0:1;
+		c=(term & NO_EXASCII) ? 0:1;
 		write(file,&c,1);						/* GraphicsMode */
 		c=useron.xedit ? 1:0;
 		write(file,&c,1);						/* ExternEdit */
@@ -666,7 +667,7 @@ void sbbs_t::xtrndat(const char *name, const char *dropdir, uchar type, ulong tl
 		write(file,str,49); 					/* ChatReason */
 		c=0;
 		write(file,&c,1);						/* ExternLogoff */
-		c=(char)term_supports(ANSI);
+		c=(char)INT_TO_BOOL(term & ANSI);
 		write(file,&c,1);						/* ANSI_Capable */
 		close(file);
 	}
@@ -713,7 +714,7 @@ void sbbs_t::xtrndat(const char *name, const char *dropdir, uchar type, ulong tl
 			,useron.location					/* User location */
 			,useron.level						/* Security level */
 			,tleft/60							/* Time left in min */
-			,term_supports(ANSI) ? "COLOR":"MONO"  /* ANSI ??? */
+			,(term & ANSI) ? "COLOR":"MONO"		/* ANSI ??? */
 			,useron.pass						/* Password */
 			,useron.number);					/* User number */
 		lfexpand(str,misc);
@@ -801,8 +802,8 @@ void sbbs_t::xtrndat(const char *name, const char *dropdir, uchar type, ulong tl
 			,startup->answer_sound[0] ? -1:0	/* Caller Alarm on/off */
 			,' ' 								/* Sysop next flag */
 			,0									/* Error corrected */
-			,useron.misc&NO_EXASCII ? '7'       /* Graphics mode */
-				: (useron.misc&(COLOR|ANSI))==(COLOR|ANSI) ? 'Y':'N'
+			,(term & NO_EXASCII) ? '7'			/* Graphics mode */
+				: (term & (COLOR|ANSI)) == (COLOR|ANSI) ? 'Y':'N'
 			,'A'                                /* Node chat status */
 			,(uint)dte_rate 					/* DTE Port Speed */
 			,connection 						/* Connection description */
@@ -869,11 +870,11 @@ void sbbs_t::xtrndat(const char *name, const char *dropdir, uchar type, ulong tl
 		l=0L;
 		write(file,&l,4);						/* Memorized message number */
 
-		safe_snprintf(str, sizeof(str), "%d%c%c%ld%s%c%c%d%d%d%c%c"
+		safe_snprintf(str, sizeof(str), "%d%c%c%d%s%c%c%d%d%d%c%c"
 			,cfg.com_port						/* COM Port number */
 			,' ' 								/* Reserved */
 			,' ' 								/* "" */
-			,term_supports(ANSI)				/* 1=ANSI 0=NO ANSI */
+			,INT_TO_BOOL(term & ANSI)			/* 1=ANSI 0=NO ANSI */
 			,"01-01-80"                         /* last event date */
 			,0,0								/* last event minute */
 			,0									/* caller exited to dos */
@@ -1036,7 +1037,7 @@ void sbbs_t::xtrndat(const char *name, const char *dropdir, uchar type, ulong tl
 			"%s\n%s\n%lu\n%s\n%u\n%u\n%u\n%u\n%u\n%lu\n%u\n"
 			"%lu\n%lu\n%s\n%s\n"
 			,dropdir
-			,term_supports(ANSI) ? "TRUE":"FALSE"  /* ANSI ? True or False */
+			,(term & ANSI) ? "TRUE":"FALSE"		/* ANSI ? True or False */
 			,useron.level						/* Security level */
 			,useron.uls 						/* Total uploads */
 			,useron.dls 						/* Total downloads */
@@ -1102,10 +1103,10 @@ void sbbs_t::xtrndat(const char *name, const char *dropdir, uchar type, ulong tl
 			return; 
 		}
 
-		safe_snprintf(str, sizeof(str), "%s\n%ld\n%d\n%lu\n%lu\n%u\n%lu\n"
+		safe_snprintf(str, sizeof(str), "%s\n%d\n%d\n%lu\n%lu\n%u\n%lu\n"
 			,name								/* Complete name of user */
-			,term_supports(ANSI)	 			/* ANSI ? */
-			,term_supports(NO_EXASCII) ? 0:1	/* IBM characters ? */
+			,INT_TO_BOOL(term & ANSI)			/* ANSI ? */
+			,!INT_TO_BOOL(term & NO_EXASCII)	/* IBM characters ? */
 			,rows								/* Page length */
 			,dte_rate							/* Baud rate */
 			,online==ON_LOCAL ? 0:cfg.com_port	/* COM port */
@@ -1133,7 +1134,7 @@ void sbbs_t::xtrndat(const char *name, const char *dropdir, uchar type, ulong tl
 			,useron.pass						/* User's password */
 			,useron.level						/* User's level */
 			,useron.misc&EXPERT ? 'Y':'N'       /* Expert? */
-			,term_supports(ANSI) ? 'Y':'N'      /* ANSI? */
+			,(term & ANSI) ? 'Y':'N'			/* ANSI? */
 			,tleft/60							/* Minutes left */
 			,useron.phone						/* User's phone number */
 			,useron.location					/* User's city and state */
@@ -1170,7 +1171,7 @@ void sbbs_t::xtrndat(const char *name, const char *dropdir, uchar type, ulong tl
 		}
 
 		safe_snprintf(str, sizeof(str), "%d\n%d\n%lu\n%s%c\n%d\n%s\n%s\n%d\n%ld\n"
-			"%ld\n%d\n"
+			"%d\n%d\n"
 			,misc&(XTRN_STDIO|XTRN_CONIO) ? 0 /* Local */ : 2 /* Telnet */
 			,misc&(XTRN_STDIO|XTRN_CONIO) ? INVALID_SOCKET : client_socket_dup
 			,dte_rate
@@ -1180,7 +1181,7 @@ void sbbs_t::xtrndat(const char *name, const char *dropdir, uchar type, ulong tl
 			,name
 			,useron.level
 			,tleft/60
-			,term_supports(ANSI)
+			,INT_TO_BOOL(term & ANSI)
 			,cfg.node_num);
 		lfexpand(str,misc);
 		write(file,str,strlen(str));
diff --git a/src/sbbs3/zmodem.c b/src/sbbs3/zmodem.c
index 1270f77d2ab23971e1906c9cb43bc391873cfabf..cbde82b0977700538e3160ec2f91ff32381e1c13 100755
--- a/src/sbbs3/zmodem.c
+++ b/src/sbbs3/zmodem.c
@@ -2,8 +2,6 @@
 
 /* Synchronet ZMODEM Functions */
 
-/* $Id: zmodem.c,v 1.124 2019/08/25 03:05:34 rswindell Exp $ */
-
 /******************************************************************************/
 /* Project : Unite!       File : zmodem general        Version : 1.02         */
 /*                                                                            */
@@ -62,6 +60,8 @@
 
 #define BADSUBPKT	0x80
 
+#define SEND_SUCCESS	0
+
 #define HDRLEN     5	/* size of a zmodem header */
 
 static int lprintf(zmodem_t* zm, int level, const char *fmt, ...)
@@ -96,7 +96,7 @@ static BOOL is_cancelled(zmodem_t* zm)
 	return(zm->cancelled);
 }
 
-int zmodem_data_waiting(zmodem_t* zm, unsigned timeout)
+static BOOL is_data_waiting(zmodem_t* zm, unsigned timeout)
 {
 	if(zm->data_waiting)
 		return(zm->data_waiting(zm->cbdata, timeout));
@@ -111,7 +111,7 @@ static char *chr(int ch)
 		case TIMEOUT:			return("TIMEOUT");
 		case ABORTED:			return("ABORTED");
 		case SUBPKTOVERFLOW:	return "Subpacket Overflow";
-		case CRCFAILED:			return "CRC Failure";
+		case CRCFAILED:			return "CRC ERROR";
 		case INVALIDSUBPKT:		return "Invalid Subpacket";
 		case ZRQINIT:			return("ZRQINIT");
 		case ZRINIT:			return("ZRINIT");
@@ -241,7 +241,7 @@ int zmodem_send_raw(zmodem_t* zm, unsigned char ch)
 {
 	int	result;
 
-	if((result = zm->send_byte(zm->cbdata, ch, zm->send_timeout)) != 0)
+	if((result = zm->send_byte(zm->cbdata, ch, zm->send_timeout)) != SEND_SUCCESS)
 		lprintf(zm, LOG_ERR, "%s ERROR: %d", __FUNCTION__, result);
 	else
 		zm->last_sent = ch;
@@ -257,7 +257,7 @@ int zmodem_send_esc(zmodem_t* zm, unsigned char c)
 {
 	int	result;
 
-	if((result = zmodem_send_raw(zm, ZDLE)) != 0) {
+	if((result = zmodem_send_raw(zm, ZDLE)) != SEND_SUCCESS) {
 		lprintf(zm, LOG_ERR, "%s ERROR: %d", __FUNCTION__, result);
 		return result;
 	}
@@ -291,7 +291,7 @@ int zmodem_tx(zmodem_t* zm, unsigned char c)
 			break;
 		case TELNET_IAC:
 			if(zm->escape_telnet_iac) {
-				if((result=zmodem_send_raw(zm, ZDLE))!=0)
+				if((result=zmodem_send_raw(zm, ZDLE)) != SEND_SUCCESS)
 					return(result);
 				return zmodem_send_raw(zm, ZRUB1);
 			}
@@ -317,7 +317,7 @@ int zmodem_send_hex(zmodem_t* zm, uchar val)
 
 //	lprintf(zm, LOG_DEBUG, __FUNCTION__ " %02X",val);
 
-	if((result=zmodem_send_raw(zm, xdigit[val>>4]))!=0)
+	if((result=zmodem_send_raw(zm, xdigit[val>>4])) != SEND_SUCCESS)
 		return result;
 	return zmodem_send_raw(zm, xdigit[val&0xf]);
 }
@@ -326,9 +326,9 @@ int zmodem_send_padded_zdle(zmodem_t* zm)
 {
 	int result;
 
-	if((result=zmodem_send_raw(zm, ZPAD))!=0)
+	if((result=zmodem_send_raw(zm, ZPAD)) != SEND_SUCCESS)
 		return result;
-	if((result=zmodem_send_raw(zm, ZPAD))!=0)
+	if((result=zmodem_send_raw(zm, ZPAD)) != SEND_SUCCESS)
 		return result;
 	return zmodem_send_raw(zm, ZDLE);
 }
@@ -347,10 +347,10 @@ int zmodem_send_hex_header(zmodem_t* zm, unsigned char * p)
 
 //	lprintf(zm, LOG_DEBUG, __FUNCTION__ " %s", chr(type));
 
-	if((result=zmodem_send_padded_zdle(zm))!=0)
+	if((result=zmodem_send_padded_zdle(zm)) != SEND_SUCCESS)
 		return result;
 
-	if((result=zmodem_send_raw(zm, ZHEX))!=0)
+	if((result=zmodem_send_raw(zm, ZHEX)) != SEND_SUCCESS)
 		return result;
 
 	/*
@@ -364,7 +364,7 @@ int zmodem_send_hex_header(zmodem_t* zm, unsigned char * p)
 	 */
 
 	for(i=0;i<HDRLEN;i++) {
-		if((result=zmodem_send_hex(zm, *p))!=0)
+		if((result=zmodem_send_hex(zm, *p)) != SEND_SUCCESS)
 			return result;
 		crc = ucrc16(*p, crc);
 		p++;
@@ -378,18 +378,18 @@ int zmodem_send_hex_header(zmodem_t* zm, unsigned char * p)
 	 * transmit the crc
 	 */
 
-	if((result=zmodem_send_hex(zm, (uchar)(crc>>8)))!=0)
+	if((result=zmodem_send_hex(zm, (uchar)(crc>>8))) != SEND_SUCCESS)
 		return result;
-	if((result=zmodem_send_hex(zm, (uchar)(crc&0xff)))!=0)
+	if((result=zmodem_send_hex(zm, (uchar)(crc&0xff))) != SEND_SUCCESS)
 		return result;
 
 	/*
 	 * end of line sequence
 	 */
 
-	if((result=zmodem_send_raw(zm, '\r'))!=0)
+	if((result=zmodem_send_raw(zm, '\r')) != SEND_SUCCESS)
 		return result;
-	if((result=zmodem_send_raw(zm, '\n'))!=0)	/* FDSZ sends 0x8a instead of 0x0a */
+	if((result=zmodem_send_raw(zm, '\n')) != SEND_SUCCESS)	/* FDSZ sends 0x8a instead of 0x0a */
 		return result;
 
 	if(type!=ZACK && type!=ZFIN)
@@ -412,27 +412,27 @@ int zmodem_send_bin32_header(zmodem_t* zm, unsigned char * p)
 
 //	lprintf(zm, LOG_DEBUG, __FUNCTION__ " %s", chr(*p));
 
-	if((result=zmodem_send_padded_zdle(zm))!=0)
+	if((result=zmodem_send_padded_zdle(zm)) != SEND_SUCCESS)
 		return result;
 
-	if((result=zmodem_send_raw(zm, ZBIN32))!=0)
+	if((result=zmodem_send_raw(zm, ZBIN32)) != SEND_SUCCESS)
 		return result;
 
 	crc = 0xffffffffL;
 
 	for(i=0;i<HDRLEN;i++) {
 		crc = ucrc32(*p,crc);
-		if((result=zmodem_tx(zm, *p++))!=0)
+		if((result=zmodem_tx(zm, *p++)) != SEND_SUCCESS)
 			return result;
 	}
 
 	crc = ~crc;
 
-	if((result=	zmodem_tx(zm, (uchar)((crc      ) & 0xff)))!=0)
+	if((result=	zmodem_tx(zm, (uchar)((crc      ) & 0xff))) != SEND_SUCCESS)
 		return result;
-	if((result=	zmodem_tx(zm, (uchar)((crc >>  8) & 0xff)))!=0)
+	if((result=	zmodem_tx(zm, (uchar)((crc >>  8) & 0xff))) != SEND_SUCCESS)
 		return result;
-	if((result=	zmodem_tx(zm, (uchar)((crc >> 16) & 0xff)))!=0)
+	if((result=	zmodem_tx(zm, (uchar)((crc >> 16) & 0xff))) != SEND_SUCCESS)
 		return result;
 	return		zmodem_tx(zm, (uchar)((crc >> 24) & 0xff));
 }
@@ -445,21 +445,21 @@ int zmodem_send_bin16_header(zmodem_t* zm, unsigned char * p)
 
 //	lprintf(zm, LOG_DEBUG, __FUNCTION__ " %s", chr(*p));
 
-	if((result=zmodem_send_padded_zdle(zm))!=0)
+	if((result=zmodem_send_padded_zdle(zm)) != SEND_SUCCESS)
 		return result;
 
-	if((result=zmodem_send_raw(zm, ZBIN))!=0)
+	if((result=zmodem_send_raw(zm, ZBIN)) != SEND_SUCCESS)
 		return result;
 
 	crc = 0;
 
 	for(i=0;i<HDRLEN;i++) {
 		crc = ucrc16(*p,crc);
-		if((result=zmodem_tx(zm, *p++))!=0)
+		if((result=zmodem_tx(zm, *p++)) != SEND_SUCCESS)
 			return result;
 	}
 
-	if((result=	zmodem_tx(zm, (uchar)(crc >> 8)))!=0)
+	if((result=	zmodem_tx(zm, (uchar)(crc >> 8))) != SEND_SUCCESS)
 		return result;
 	return		zmodem_tx(zm, (uchar)(crc&0xff));
 }
@@ -494,25 +494,25 @@ int zmodem_send_data32(zmodem_t* zm, uchar subpkt_type, unsigned char * p, size_
 
 	while(l > 0) {
 		crc = ucrc32(*p,crc);
-		if((result=zmodem_tx(zm, *p++))!=0)
+		if((result=zmodem_tx(zm, *p++)) != SEND_SUCCESS)
 			return result;
 		l--;
 	}
 
 	crc = ucrc32(subpkt_type, crc);
 
-	if((result=zmodem_send_raw(zm, ZDLE))!=0)
+	if((result=zmodem_send_raw(zm, ZDLE)) != SEND_SUCCESS)
 		return result;
-	if((result=zmodem_send_raw(zm, subpkt_type))!=0)
+	if((result=zmodem_send_raw(zm, subpkt_type)) != SEND_SUCCESS)
 		return result;
 
 	crc = ~crc;
 
-	if((result=	zmodem_tx(zm, (uchar) ((crc      ) & 0xff)))!=0)
+	if((result=	zmodem_tx(zm, (uchar) ((crc      ) & 0xff))) != SEND_SUCCESS)
 		return result;
-	if((result=	zmodem_tx(zm, (uchar) ((crc >> 8 ) & 0xff)))!=0)
+	if((result=	zmodem_tx(zm, (uchar) ((crc >> 8 ) & 0xff))) != SEND_SUCCESS)
 		return result;
-	if((result=	zmodem_tx(zm, (uchar) ((crc >> 16) & 0xff)))!=0)
+	if((result=	zmodem_tx(zm, (uchar) ((crc >> 16) & 0xff))) != SEND_SUCCESS)
 		return result;
 	return		zmodem_tx(zm, (uchar) ((crc >> 24) & 0xff));
 }
@@ -528,42 +528,47 @@ int zmodem_send_data16(zmodem_t* zm, uchar subpkt_type,unsigned char * p, size_t
 
 	while(l > 0) {
 		crc = ucrc16(*p,crc);
-		if((result=zmodem_tx(zm, *p++))!=0)
+		if((result=zmodem_tx(zm, *p++)) != SEND_SUCCESS)
 			return result;
 		l--;
 	}
 
 	crc = ucrc16(subpkt_type,crc);
 
-	if((result=zmodem_send_raw(zm, ZDLE))!=0)
+	if((result=zmodem_send_raw(zm, ZDLE)) != SEND_SUCCESS)
 		return result;
-	if((result=zmodem_send_raw(zm, subpkt_type))!=0)
+	if((result=zmodem_send_raw(zm, subpkt_type)) != SEND_SUCCESS)
 		return result;
 
-	if((result=	zmodem_tx(zm, (uchar)(crc >> 8)))!=0)
+	if((result=	zmodem_tx(zm, (uchar)(crc >> 8))) != SEND_SUCCESS)
 		return result;
 	return		zmodem_tx(zm, (uchar)(crc&0xff));
 }
 
+BOOL zmodem_end_of_frame(int subpkt_type)
+{
+	return subpkt_type == ZCRCW || subpkt_type == ZCRCE;
+}
+
 /*
  * send a data subpacket using crc 16 or crc 32 as desired by the receiver
  */
 
-int zmodem_send_data_subpkt(zmodem_t* zm, uchar subpkt_type, unsigned char * p, size_t l)
+int zmodem_send_data_subpkt(zmodem_t* zm, uchar subpkt_type, unsigned char* data, size_t len)
 {
 	int result;
 
-	if(subpkt_type == ZCRCW || subpkt_type == ZCRCE)	/* subpacket indicating 'end-of-frame' */
+	if(zmodem_end_of_frame(subpkt_type))
 		zm->frame_in_transit=FALSE;
 	else	/* other subpacket (mid-frame) */
 		zm->frame_in_transit=TRUE;
 
 	if(!zm->want_fcs_16 && zm->can_fcs_32) {
-		if((result=zmodem_send_data32(zm, subpkt_type,p,l))!=0)
+		if((result=zmodem_send_data32(zm, subpkt_type, data, len)) != SEND_SUCCESS)
 			return result;
 	}
 	else {
-		if((result=zmodem_send_data16(zm, subpkt_type,p,l))!=0)
+		if((result=zmodem_send_data16(zm, subpkt_type, data, len)) != SEND_SUCCESS)
 			return result;
 	}
 
@@ -575,7 +580,7 @@ int zmodem_send_data_subpkt(zmodem_t* zm, uchar subpkt_type, unsigned char * p,
 	return result;
 }
 
-int zmodem_send_data(zmodem_t* zm, uchar subpkt_type, unsigned char * p, size_t len)
+int zmodem_send_data(zmodem_t* zm, uchar subpkt_type, unsigned char* data, size_t len)
 {
 	if(!zm->frame_in_transit)	{ /* Start of frame, include ZDATA header */
 		lprintf(zm, LOG_DEBUG, "%lu %s Start of frame: %s"
@@ -583,7 +588,7 @@ int zmodem_send_data(zmodem_t* zm, uchar subpkt_type, unsigned char * p, size_t
 		zmodem_send_pos_header(zm, ZDATA, (uint32_t)zm->current_file_pos, /* Hex? */ FALSE);
 	}
 
-	return zmodem_send_data_subpkt(zm, subpkt_type, p, len);
+	return zmodem_send_data_subpkt(zm, subpkt_type, data, len);
 }
 
 int zmodem_send_pos_header(zmodem_t* zm, int type, int32_t pos, BOOL hex)
@@ -862,7 +867,7 @@ int zmodem_recv_data32(zmodem_t* zm, unsigned char * p, unsigned maxlen, unsigne
 	rxd_crc |= zmodem_rx(zm) << 24;
 
 	if(rxd_crc != crc) {
-		lprintf(zm,LOG_WARNING, "%lu %s CRC ERROR (%08lX, expected: %08lX) Bytes=%u, subpacket type=%s"
+		lprintf(zm, LOG_DEBUG, "%lu %s CRC ERROR (%08lX, expected: %08lX) Bytes=%u, subpacket type=%s"
 			,(ulong)zm->ack_file_pos, __FUNCTION__, rxd_crc, crc, *len, chr(subpkt_type));
 		return CRCFAILED;
 	}
@@ -911,7 +916,7 @@ int zmodem_recv_data16(zmodem_t* zm, register unsigned char* p, unsigned maxlen,
 	rxd_crc |= zmodem_rx(zm);
 
 	if(rxd_crc != crc) {
-		lprintf(zm,LOG_WARNING, "%lu %s CRC ERROR (%04hX, expected: %04hX) Bytes=%u, subpacket type=%s"
+		lprintf(zm, LOG_DEBUG, "%lu %s CRC ERROR (%04hX, expected: %04hX) Bytes=%u, subpacket type=%s"
 			,(ulong)zm->ack_file_pos, __FUNCTION__, rxd_crc, crc, *len, chr(subpkt_type));
 		return CRCFAILED;
 	}
@@ -923,7 +928,7 @@ int zmodem_recv_data16(zmodem_t* zm, register unsigned char* p, unsigned maxlen,
 	return subpkt_type;
 }
 
-int zmodem_recv_data(zmodem_t* zm, unsigned char* p, size_t maxlen, unsigned* len, BOOL ack, int* type)
+int zmodem_recv_data(zmodem_t* zm, unsigned char* buf, size_t maxlen, unsigned* len, BOOL ack, int* type)
 {
 	int subpkt_type;
 	unsigned n=0;
@@ -940,15 +945,15 @@ int zmodem_recv_data(zmodem_t* zm, unsigned char* p, size_t maxlen, unsigned* le
 	*len = 0;
 
 	if(zm->receive_32bit_data) {
-		subpkt_type = zmodem_recv_data32(zm, p, maxlen, len, type);
+		subpkt_type = zmodem_recv_data32(zm, buf, maxlen, len, type);
 	}
 	else {
-		subpkt_type = zmodem_recv_data16(zm, p, maxlen, len, type);
+		subpkt_type = zmodem_recv_data16(zm, buf, maxlen, len, type);
 	}
 
 	if(subpkt_type <= 0) {	/* e.g. TIMEOUT, SUBPKTOVERFLOW, CRCFAILED */
-		lprintf(zm, LOG_WARNING, "%lu %s ERROR: %s (after %u bytes)"
-			,(ulong)zm->ack_file_pos, __FUNCTION__, chr(subpkt_type), *len);
+		lprintf(zm, LOG_WARNING, "%s data subpacket (%u bytes) %s"
+			,chr(*type), *len, chr(subpkt_type));
 		return(subpkt_type);
 	}
 
@@ -995,7 +1000,7 @@ BOOL zmodem_recv_subpacket(zmodem_t* zm, BOOL ack, int* type)
 
 	result = zmodem_recv_data(zm,zm->rx_data_subpacket, sizeof(zm->rx_data_subpacket), &len, ack, type);
 	if(result != FRAMEOK && result != ENDOFFRAME) {
-		lprintf(zm, LOG_ERR, "%lu %s ERROR: %s (subpacket type: %s, %u bytes)"
+		lprintf(zm, LOG_DEBUG, "%lu %s ERROR: %s (subpacket type: %s, %u bytes)"
 			,(ulong)zm->ack_file_pos, __FUNCTION__, chr(result), chr(*type), len);
 		zmodem_send_znak(zm);
 		return(FALSE);
@@ -1100,7 +1105,7 @@ BOOL zmodem_recv_bin16_header(zmodem_t* zm)
 //	lprintf(zm,LOG_DEBUG, "%lu %s GOOD CRC: %04hX", __FUNCTION__
 //		,(ulong)zm->ack_file_pos, __FUNCTION__, crc);
 
-	zm->rxd_header_len = 5;
+	zm->rxd_header_len = n;
 
 	return(TRUE);
 }
@@ -1143,7 +1148,7 @@ BOOL zmodem_recv_hex_header(zmodem_t* zm)
 
 	if(rxd_crc == crc) {
 //		lprintf(zm,LOG_DEBUG, "%s GOOD CRC: %04hX", __FUNCTION__, crc);
-		zm->rxd_header_len = 5;
+		zm->rxd_header_len = i;
 	}
 	else {
 		lprintf(zm,LOG_WARNING, "%s CRC ERROR: 0x%hX, expected: 0x%hX"
@@ -1203,7 +1208,7 @@ BOOL zmodem_recv_bin32_header(zmodem_t* zm)
 	}
 //	lprintf(zm,LOG_DEBUG, "%lu %s GOOD CRC: %08lX", (ulong)zm->ack_file_pos, __FUNCTION__, crc);
 
-	zm->rxd_header_len = 5;
+	zm->rxd_header_len = n;
 	return(TRUE);
 }
 
@@ -1369,7 +1374,7 @@ BOOL zmodem_recv_crc(zmodem_t* zm, uint32_t* crc)
 {
 	int type;
 
-	if(!zmodem_data_waiting(zm,zm->crc_timeout)) {
+	if(!is_data_waiting(zm,zm->crc_timeout)) {
 		lprintf(zm,LOG_ERR, "%lu %s Timeout waiting for response (%u seconds)"
 			,(ulong)zm->current_file_pos, __FUNCTION__, zm->crc_timeout);
 		return(FALSE);
@@ -1425,7 +1430,7 @@ int zmodem_get_zrinit(zmodem_t* zm)
 	zmodem_send_raw(zm,'\r');
 	zmodem_send_hex_header(zm,zrqinit_header);
 
-	if(!zmodem_data_waiting(zm,zm->init_timeout))
+	if(!is_data_waiting(zm,zm->init_timeout))
 		return(TIMEOUT);
 	return zmodem_recv_header(zm);
 }
@@ -1434,7 +1439,8 @@ int zmodem_send_zrinit(zmodem_t* zm)
 {
 	unsigned char zrinit_header[] = { ZRINIT, 0, 0, 0, 0 };
 
-	zrinit_header[ZF0] = ZF0_CANFDX;
+	if(zm->can_full_duplex)
+		zrinit_header[ZF0] = ZF0_CANFDX;
 
 	if(!zm->no_streaming)
 		zrinit_header[ZF0] |= ZF0_CANOVIO;
@@ -1452,7 +1458,7 @@ int zmodem_send_zrinit(zmodem_t* zm)
 		zrinit_header[ZF0] |= ZF0_ESC8;
 
 	if(zm->no_streaming && zm->recv_bufsize==0)
-		zm->recv_bufsize = sizeof(zm->rx_data_subpacket);
+		zm->recv_bufsize = zm->max_block_size;
 
 	zrinit_header[ZP0] = zm->recv_bufsize & 0xff;
 	zrinit_header[ZP1] = zm->recv_bufsize >> 8;
@@ -1474,7 +1480,7 @@ int zmodem_get_zfin(zmodem_t* zm)
 			result = zmodem_send_zabort(zm);
 		else
 			result = zmodem_send_zfin(zm);
-		if(result != 0)
+		if(result != SEND_SUCCESS)
 			return result;
 		if((type = zmodem_recv_header(zm)) == ZFIN)
 			break;
@@ -1496,9 +1502,10 @@ int zmodem_get_zfin(zmodem_t* zm)
 
 BOOL zmodem_handle_zrpos(zmodem_t* zm, uint64_t* pos)
 {
-	if(zm->rxd_header_pos <= zm->current_file_size) {
+	if(zm->rxd_header_pos < zm->current_file_size) {
 		if(*pos != zm->rxd_header_pos) {
 			*pos = zm->rxd_header_pos;
+			zm->ack_file_pos = (int32_t)*pos;
 			lprintf(zm, LOG_INFO, "%lu Resuming transfer from offset: %"PRIu64
 				,(ulong)zm->current_file_pos, *pos);
 		}
@@ -1510,10 +1517,11 @@ BOOL zmodem_handle_zrpos(zmodem_t* zm, uint64_t* pos)
 	return FALSE;
 }
 
-BOOL zmodem_handle_zack(zmodem_t* zm)
+BOOL zmodem_handle_zack(zmodem_t* zm, uint32_t min, uint32_t max)
 {
-	if(zm->rxd_header_pos == zm->current_file_pos) {
+	if(zm->rxd_header_pos >= min && zm->rxd_header_pos <= max) {
 		lprintf(zm, LOG_DEBUG, "%lu Received valid ZACK", zm->rxd_header_pos);
+		zm->ack_file_pos = zm->rxd_header_pos;
 		return TRUE;
 	}
 	lprintf(zm, LOG_WARNING, "%lu Received INVALID ZACK, offset: %lu"
@@ -1521,6 +1529,17 @@ BOOL zmodem_handle_zack(zmodem_t* zm)
 	return FALSE;
 }
 
+static unsigned new_window_size(zmodem_t* zm, time_t start, unsigned pos)
+{
+	time_t elapsed = time(NULL) - start;
+	if(elapsed < 1)
+		elapsed = 1;
+	unsigned cps = (unsigned)(pos / elapsed);
+	if(cps < 1)
+		cps = 1;
+	return cps * zm->target_window_size;
+}
+
 /*
  * send from the current position in the file
  * all the way to end of file or until something goes wrong.
@@ -1530,22 +1549,24 @@ BOOL zmodem_handle_zack(zmodem_t* zm)
 
 int zmodem_send_from(zmodem_t* zm, FILE* fp, uint64_t pos, uint64_t* sent)
 {
-	size_t n;
-	uchar type;
-	unsigned buf_sent=0;
-	unsigned subpkts_sent=0;
-
+	size_t len;
+	uchar tx_type;
+	unsigned buf_sent = 0;
+	unsigned subpkts_sent = 0;
+	unsigned backchannel_wait = 0;
+	time_t start = time(NULL);
+
+	lprintf(zm, LOG_DEBUG, "%lu %s", (ulong)pos, __FUNCTION__); 
 	if(sent!=NULL)
 		*sent=0;
 
-	if(fseeko(fp,(off_t)pos,SEEK_SET)!=0) {
+	if(fseeko(fp,(off_t)pos,SEEK_SET) != 0) {
 		lprintf(zm, LOG_ERR, "%s ERROR %d seeking to file offset %"PRIu64
 			,__FUNCTION__, errno, pos);
 		zmodem_send_pos_header(zm, ZFERR, (uint32_t)pos, /* Hex? */ TRUE);
 		return ZFERR;
 	}
-	zm->current_file_pos=pos;
-
+	zm->current_file_pos = pos;
 
 	/*
 	 * send the data in the file
@@ -1553,16 +1574,61 @@ int zmodem_send_from(zmodem_t* zm, FILE* fp, uint64_t pos, uint64_t* sent)
 
 	while(is_connected(zm)) {
 
+		/*
+		 * characters from the other side
+		 * check out that header
+		 */
+		if(zm->consecutive_errors && backchannel_wait == 0)
+			backchannel_wait = 1;
+		while(is_data_waiting(zm, backchannel_wait) && !is_cancelled(zm) && is_connected(zm)) {
+			int rx_type;
+			int c;
+			lprintf(zm, LOG_DEBUG, "Back-channel traffic detected");
+			if((c = zmodem_recv_raw(zm)) < 0) {
+				lprintf(zm, LOG_ERR, "Back-channel receive ERROR: %s", chr(c));
+				return(c);
+			}
+			if(c == ZPAD) {
+				rx_type = zmodem_recv_header(zm);
+				lprintf(zm,LOG_DEBUG, "Received back-channel data: %s", chr(rx_type));
+				if(rx_type == ZACK && zmodem_handle_zack(zm, zm->ack_file_pos, (uint32_t)zm->current_file_pos)) {
+					zm->current_window_size = zm->current_file_pos - zm->ack_file_pos;
+					lprintf(zm, LOG_DEBUG, "%lu Asynchronous acknowledgment (ZACK) of %lu bytes, new window: %lu"
+						,(ulong)zm->current_file_pos, (ulong)zm->ack_file_pos, (ulong)zm->current_window_size);
+					if(zm->max_window_size && zm->target_window_size) {
+						zm->max_window_size = new_window_size(zm, start, zm->rxd_header_pos);
+						lprintf(zm, LOG_DEBUG, "%lu New window size: %lu (%u seconds of data)"
+							,(ulong)zm->current_file_pos, (ulong)zm->max_window_size, zm->target_window_size);
+					}
+					continue;
+				}
+				else if(rx_type >= 0) {
+					zmodem_send_data(zm, ZCRCE, /* data: */NULL, /* len: */0);
+					return rx_type;
+				}
+			} else
+				lprintf(zm,LOG_INFO, "Unexpected back-channel data received: %s", chr(c));
+		}
+		if(is_cancelled(zm))
+			return(ZCAN);
+
 		/*
 		 * read a block from the file
 		 */
 		pos = zm->current_file_pos;
-		n = fread(zm->tx_data_subpacket,sizeof(BYTE),zm->block_size,fp);
+		if(zm->max_window_size && zm->current_window_size >= zm->max_window_size) {
+			lprintf(zm, LOG_WARNING, "%lu Transmit-Window management: %lu >= %lu"
+				,(ulong)zm->current_file_pos, (ulong)zm->current_window_size, (ulong)zm->max_window_size);
+			backchannel_wait = 1;
+			continue;
+		}
+
+		len = fread(zm->tx_data_subpacket,sizeof(BYTE),zm->block_size,fp);
 
 		if(zm->progress!=NULL)
 			zm->progress(zm->cbdata, ftello(fp));
 
-		type = ZCRCW;
+		tx_type = ZCRCW;
 
 		/** ZMODEM.DOC:
 			ZCRCW data subpackets expect a response before the next frame is sent.
@@ -1571,36 +1637,43 @@ int zmodem_send_from(zmodem_t* zm, FILE* fp, uint64_t pos, uint64_t* sent)
 			the receiver to write its buffer before sending more data.
 		***/
 		/* Note: we always use ZCRCW for the first frame */
-		if(subpkts_sent || n < zm->block_size) {
+		if(subpkts_sent || len < zm->block_size) {
 			/*  ZMODEM.DOC:
 				In the absence of fatal error, the sender eventually encounters end of
 				file.  If the end of file is encountered within a frame, the frame is
 				closed with a ZCRCE data subpacket which does not elicit a response
 				except in case of error.
 			*/
-			if(n < zm->block_size)
-				type = ZCRCE;
+			if(len < zm->block_size || zm->consecutive_errors)
+				tx_type = ZCRCE;
 			else {
-				if(zm->can_overlap_io && !zm->no_streaming && (zm->recv_bufsize==0 || buf_sent+n < zm->recv_bufsize))
-					type = ZCRCG;
+				if(zm->can_overlap_io && !zm->no_streaming && (zm->recv_bufsize==0 || buf_sent + len < zm->recv_bufsize)) {
+					if(zm->can_full_duplex && zm->max_window_size)
+						tx_type = (subpkts_sent % (zm->max_window_size / zm->block_size / 4)) == 0 ? ZCRCQ : ZCRCG;
+					else
+						tx_type = ZCRCG;
+				}
 				else	/* Send a ZCRCW frame */
 					buf_sent = 0;
 			}
 		}
 
-		/* Note: No support for sending ZCRCQ data subpackets here */
-
-		if(zmodem_send_data(zm, type, zm->tx_data_subpacket, n) != 0)
-			return(TIMEOUT);
+		lprintf(zm, LOG_DEBUG, "%lu Sending %s data subpacket (%u bytes) window: %lu / %lu"
+			,(ulong)pos, chr(tx_type), len, (ulong)zm->current_window_size, (ulong)zm->max_window_size);
+		if(zmodem_send_data(zm, tx_type, zm->tx_data_subpacket, len) != SEND_SUCCESS) {
+			zm->consecutive_errors++;
+			continue;
+		}
 
-		zm->current_file_pos += n;
+		zm->current_window_size += len;
+		zm->current_file_pos += len;
 		if(zm->current_file_pos > zm->current_file_size)
 			zm->current_file_size = zm->current_file_pos;
 		subpkts_sent++;
 
-		if(type == ZCRCW || type == ZCRCE) {
-			lprintf(zm, LOG_DEBUG, "%lu Sent end-of-frame (%s subpacket)",(ulong)pos, chr(type));
-			if(type == ZCRCW) {	/* ZACK expected */
+		if(zmodem_end_of_frame(tx_type)) {
+			lprintf(zm, LOG_DEBUG, "%lu Sent end-of-frame (%s subpacket)", (ulong)pos, chr(tx_type));
+			if(tx_type == ZCRCW) {	/* ZACK expected */
 				lprintf(zm, LOG_DEBUG, "%lu Waiting for ZACK", (ulong)pos);
 				while(is_connected(zm)) {
 					int ack;
@@ -1610,54 +1683,25 @@ int zmodem_send_from(zmodem_t* zm, FILE* fp, uint64_t pos, uint64_t* sent)
 					if(is_cancelled(zm))
 						return(ZCAN);
 
-					if(zmodem_handle_zack(zm))
+					if(zmodem_handle_zack(zm, (uint32_t)zm->current_file_pos, (uint32_t)zm->current_file_pos)) {
+						zm->current_window_size = 0;
 						break;
+					}
 				}
 			}
 		}
 
 		if(sent!=NULL)
-			*sent+=n;
+			*sent += len;
 
-		buf_sent+=n;
+		buf_sent += len;
 
-		if(n < zm->block_size) {
-			lprintf(zm,LOG_DEBUG, "%lu End of file (or read error) reached", (ulong)zm->current_file_pos);
+		if(len < zm->block_size) {
+			lprintf(zm, LOG_DEBUG, "%lu End of file (or read error) reached", (ulong)zm->current_file_pos);
 			zmodem_send_zeof(zm);
 			return zmodem_recv_header(zm);	/* If this is ZRINIT, Success */
 		}
-
-		/*
-		 * characters from the other side
-		 * check out that header
-		 */
-
-		while(zmodem_data_waiting(zm, zm->consecutive_errors ? 1:0)
-			&& !is_cancelled(zm) && is_connected(zm)) {
-			int rx_type;
-			int c;
-			lprintf(zm, LOG_DEBUG, "Back-channel traffic detected");
-			if((c = zmodem_recv_raw(zm)) < 0) {
-				lprintf(zm, LOG_ERR, "Back-channel receive ERROR: %s", chr(c));
-				return(c);
-			}
-			if(c == ZPAD) {
-				/* ZMODEM.DOC:
-					FULL STREAMING WITH SAMPLING
-					If one of these characters (CAN or ZPAD) is seen, an
-					empty ZCRCE data subpacket is sent.
-				*/
-				zmodem_send_data(zm, ZCRCE, NULL, 0);
-				rx_type = zmodem_recv_header(zm);
-				lprintf(zm,LOG_DEBUG, "Received back-channel data: %s", chr(rx_type));
-				if(rx_type >= 0) {
-					return rx_type;
-				}
-			} else
-				lprintf(zm,LOG_INFO, "Unexpected back-channel data received: %s", chr(c));
-		}
-		if(is_cancelled(zm))
-			return(ZCAN);
+		backchannel_wait = 0;
 
 		zm->consecutive_errors = 0;
 
@@ -1820,11 +1864,11 @@ BOOL zmodem_send_file(zmodem_t* zm, char* fname, FILE* fp, BOOL request_init, ti
 		lprintf(zm,LOG_DEBUG,"Sending ZFILE frame: '%s'"
 			,zm->tx_data_subpacket+strlen((char*)zm->tx_data_subpacket)+1);
 
-		if((i=zmodem_send_bin_header(zm,zfile_frame))!=0) {
+		if((i=zmodem_send_bin_header(zm,zfile_frame)) != SEND_SUCCESS) {
 			lprintf(zm,LOG_DEBUG,"zmodem_send_bin_header returned %d",i);
 			continue;
 		}
-		if((i=zmodem_send_data_subpkt(zm,ZCRCW,zm->tx_data_subpacket,p - zm->tx_data_subpacket))!=0) {
+		if((i=zmodem_send_data_subpkt(zm,ZCRCW,zm->tx_data_subpacket,p - zm->tx_data_subpacket)) != SEND_SUCCESS) {
 			lprintf(zm,LOG_DEBUG,"zmodem_send_data_subpkt returned %d",i);
 			continue;
 		}
@@ -1905,15 +1949,16 @@ BOOL zmodem_send_file(zmodem_t* zm, char* fname, FILE* fp, BOOL request_init, ti
 		if(type == ZRINIT)
 			return(TRUE);	/* Success */
 
-		if(type == ZACK && zmodem_handle_zack(zm)) {
-			pos += sent_bytes;
+		pos = zm->ack_file_pos;
+
+		if(type == ZACK && zmodem_handle_zack(zm, (uint32_t)pos, (uint32_t)zm->current_file_pos)) {
 			continue;
 		}
 
 		/* Error of some kind */
 
 		zm->errors++;
-		lprintf(zm, LOG_ERR, "%lu ERROR #%d: Received %s", (ulong)zm->current_file_pos, zm->errors, chr(type));
+		lprintf(zm, LOG_ERR, "%lu ERROR #%d: %s", (ulong)zm->current_file_pos, zm->errors, chr(type));
 
 		if(zm->block_size == zm->max_block_size && zm->max_block_size > ZBLOCKLEN)
 			zm->max_block_size /= 2;
@@ -2202,7 +2247,7 @@ unsigned zmodem_recv_file_data(zmodem_t* zm, FILE* fp, int64_t offset)
 	zm->transfer_start_pos=offset;
 	zm->transfer_start_time=time(NULL);
 
-	if(fseeko(fp, pos, SEEK_SET)!=0) {
+	if(fseeko(fp, pos, SEEK_SET) != 0) {
 		lprintf(zm,LOG_ERR, "%s ERROR %d seeking to file offset %"PRId64
 			,__FUNCTION__, errno, offset);
 		zmodem_send_pos_header(zm, ZFERR, (uint32_t)pos, /* Hex? */ TRUE);
@@ -2224,7 +2269,7 @@ unsigned zmodem_recv_file_data(zmodem_t* zm, FILE* fp, int64_t offset)
 		if(pos > zm->current_file_size)
 			zm->current_file_size = pos;
 
-		if(zm->max_file_size!=0 && pos >= zm->max_file_size) {
+		if(zm->max_file_size != 0 && pos >= zm->max_file_size) {
 			lprintf(zm, LOG_ERR, "%lu Specified maximum file size (%"PRId64" bytes) reached"
 				,(ulong)pos, zm->max_file_size);
 			zmodem_send_pos_header(zm, ZFERR, (uint32_t)pos, /* Hex? */ TRUE);
@@ -2338,9 +2383,7 @@ const char* zmodem_source(void)
 
 char* zmodem_ver(char *buf)
 {
-	sscanf("$Revision: 1.124 $", "%*s %s", buf);
-
-	return(buf);
+	return strcpy(buf, "2.0");
 }
 
 void zmodem_init(zmodem_t* zm, void* cbdata
@@ -2363,6 +2406,7 @@ void zmodem_init(zmodem_t* zm, void* cbdata
 	zm->block_size=ZBLOCKLEN;
 	zm->max_block_size=ZBLOCKLEN;
 	zm->max_errors=9;
+	zm->can_full_duplex=TRUE;
 
 	zm->cbdata=cbdata;
 	zm->lputs=lputs;
diff --git a/src/sbbs3/zmodem.h b/src/sbbs3/zmodem.h
index 6549e12f32c4721e1c31c7f2cbc3cb95eb839465..e4c46773ed1795bdac142c966deb7eae16d58067 100755
--- a/src/sbbs3/zmodem.h
+++ b/src/sbbs3/zmodem.h
@@ -281,6 +281,7 @@ typedef struct {
 	int32_t		crc_request;
 	unsigned	errors;
 	unsigned	consecutive_errors;
+	int64_t		current_window_size;	/* "current transmitter file offset - last reported receiver file offset" */
 
 	/* Configuration */
 	BOOL		escape_telnet_iac;
@@ -292,6 +293,8 @@ typedef struct {
 	unsigned	block_size;
 	unsigned	max_block_size;
 	int64_t		max_file_size;		/* 0 = unlimited */
+	int64_t		max_window_size;	/* 0 = unlimited */
+	unsigned	target_window_size;	/* Target Transmit Window Size, in Seconds, 0 = no-auto-adjustment of window size */
 	int			*log_level;
 
 	/* Callbacks */
diff --git a/src/smblib/smbdump.c b/src/smblib/smbdump.c
index ff1a86cb049108d3190fa27729048454603c3f2f..107bd66615deb190cb8f75dea4e9a064d17239b0 100644
--- a/src/smblib/smbdump.c
+++ b/src/smblib/smbdump.c
@@ -1,41 +1,25 @@
-/* smbdump.c */
-
 /* Synchronet message base (SMB) message header dumper */
 
-/* $Id: smbdump.c,v 1.19 2020/05/25 00:39:47 rswindell Exp $ */
-
 /****************************************************************************
  * @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				*
+ * This library is free software; you can redistribute it and/or			*
+ * modify it under the terms of the GNU Lesser 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										*
- *																			*
- * Anonymous FTP access to the most recent released source is available at	*
- * ftp://vert.synchro.net, ftp://cvs.synchro.net and ftp://ftp.synchro.net	*
- *																			*
- * Anonymous CVS access to the development source and modification history	*
- * is available at cvs.synchro.net:/cvsroot/sbbs, example:					*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs login			*
- *     (just hit return, no password is necessary)							*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs checkout src		*
+ * See the GNU Lesser General Public License for more details: lgpl.txt or	*
+ * http://www.fsf.org/copyleft/lesser.html									*
  *																			*
  * For Synchronet coding style and modification guidelines, see				*
  * http://www.synchro.net/source.html										*
  *																			*
- * You are encouraged to submit any modifications (preferably in Unix diff	*
- * format) via e-mail to mods@synchro.net									*
- *																			*
  * Note: If this box doesn't appear square, then you need to fix your tabs.	*
  ****************************************************************************/
 
-#include <time.h>		/* ctime */
+#include "datewrap.h"	/* ctime_r */
 #include <string.h>		/* strcat */
 #include "smblib.h"
 
@@ -104,6 +88,12 @@ str_list_t SMBCALL smb_msghdr_str_list(smbmsg_t* msg)
 					,smb_hfieldtype(msg->hfield[i].type)
 					,smb_netaddr(&msg->replyto_net));
 				break;
+			case FORWARDED:
+				tt = *(time32_t*)msg->hfield_dat[i];
+				strListAppendFormat(&list, HFIELD_NAME_FMT "%.24s"
+					,smb_hfieldtype(msg->hfield[i].type)
+					,ctime_r(&tt, tmp));
+				break;
 			default:
 				strListAppendFormat(&list, HFIELD_NAME_FMT "%s"
 					,smb_hfieldtype(msg->hfield[i].type)
@@ -116,12 +106,12 @@ str_list_t SMBCALL smb_msghdr_str_list(smbmsg_t* msg)
 	tt=msg->hdr.when_written.time;
 	strListAppendFormat(&list, HFIELD_NAME_FMT "%08X %04hX %.24s %s"	,"when_written"
 		,msg->hdr.when_written.time, msg->hdr.when_written.zone
-		,ctime(&tt)	
+		,ctime_r(&tt, tmp)
 		,smb_zonestr(msg->hdr.when_written.zone,NULL));
 	tt=msg->hdr.when_imported.time;
 	strListAppendFormat(&list, HFIELD_NAME_FMT "%08X %04hX %.24s %s"	,"when_imported"
 		,msg->hdr.when_imported.time, msg->hdr.when_imported.zone
-		,ctime(&tt)	
+		,ctime_r(&tt, tmp)
 		,smb_zonestr(msg->hdr.when_imported.zone,NULL));
 	strListAppendFormat(&list, HFIELD_NAME_FMT "%04Xh"			,"type"				,msg->hdr.type);
 	strListAppendFormat(&list, HFIELD_NAME_FMT "%04Xh"			,"version"			,msg->hdr.version);
@@ -154,14 +144,14 @@ str_list_t SMBCALL smb_msghdr_str_list(smbmsg_t* msg)
 			strListAppendFormat(&list, HFIELD_NAME_FMT "%"PRIu32,"times_downloaded"	,msg->hdr.times_downloaded);
 		if(msg->hdr.last_downloaded) {
 			tt=msg->hdr.last_downloaded;
-			strListAppendFormat(&list, HFIELD_NAME_FMT "%.24s"	,"last_downloaded"	,ctime(&tt));
+			strListAppendFormat(&list, HFIELD_NAME_FMT "%.24s"	,"last_downloaded"	,ctime_r(&tt, tmp));
 		}
 	}
 
 	/* convenience integers */
 	if(msg->expiration) {
 		tt=msg->expiration;
-		strListAppendFormat(&list, HFIELD_NAME_FMT "%.24s", "expiration", ctime(&tt));
+		strListAppendFormat(&list, HFIELD_NAME_FMT "%.24s", "expiration", ctime_r(&tt, tmp));
 	}
 	if(msg->cost)
 		strListAppendFormat(&list, HFIELD_NAME_FMT "%u", "cost", msg->cost);
diff --git a/src/smblib/smblib.c b/src/smblib/smblib.c
index b12d59177b1159ab49d257090ec78e26cd8889a4..9f77bbeb88ad2ad8064f2713c76e0545a978001b 100644
--- a/src/smblib/smblib.c
+++ b/src/smblib/smblib.c
@@ -1,8 +1,5 @@
 /* Synchronet message base (SMB) library routines */
 
-/* $Id: smblib.c,v 1.209 2020/05/07 19:30:22 rswindell Exp $ */
-// vi: tabstop=4
-
 /****************************************************************************
  * @format.tab-size 4		(Plain Text/Source Code File Header)			*
  * @format.use-tabs true	(see http://www.synchro.net/ptsc_hdr.html)		*
@@ -16,21 +13,9 @@
  * See the GNU Lesser General Public License for more details: lgpl.txt or	*
  * http://www.fsf.org/copyleft/lesser.html									*
  *																			*
- * Anonymous FTP access to the most recent released source is available at	*
- * ftp://vert.synchro.net, ftp://cvs.synchro.net and ftp://ftp.synchro.net	*
- *																			*
- * Anonymous CVS access to the development source and modification history	*
- * is available at cvs.synchro.net:/cvsroot/sbbs, example:					*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs login			*
- *     (just hit return, no password is necessary)							*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs checkout src		*
- *																			*
  * For Synchronet coding style and modification guidelines, see				*
  * http://www.synchro.net/source.html										*
  *																			*
- * You are encouraged to submit any modifications (preferably in Unix diff	*
- * format) via e-mail to mods@synchro.net									*
- *																			*
  * Note: If this box doesn't appear square, then you need to fix your tabs.	*
  ****************************************************************************/
 
@@ -728,32 +713,25 @@ static void set_convenience_ptr(smbmsg_t* msg, uint16_t hfield_type, void* hfiel
 {
 	switch(hfield_type) {	/* convenience variables */
 		case SENDER:
-			if(msg->from==NULL || *(msg->from)==0) {
-				msg->from=(char*)hfield_dat;
-				break; 
-			}
-		case FORWARDED: 	/* fall through */
-			msg->forwarded=TRUE;
+			msg->from=(char*)hfield_dat;
+			break; 
+		case FORWARDED:
+			msg->forwarded = TRUE;
 			break;
 		case SENDERAGENT:
-			if(!msg->forwarded)
-				msg->from_agent=*(uint16_t *)hfield_dat;
+			msg->from_agent=*(uint16_t *)hfield_dat;
 			break;
 		case SENDEREXT:
-			if(!msg->forwarded)
-				msg->from_ext=(char*)hfield_dat;
+			msg->from_ext=(char*)hfield_dat;
 			break;
 		case SENDERORG:
-			if(!msg->forwarded)
-				msg->from_org=(char*)hfield_dat;
+			msg->from_org=(char*)hfield_dat;
 			break;
 		case SENDERNETTYPE:
-			if(!msg->forwarded)
-				msg->from_net.type=*(uint16_t *)hfield_dat;
+			msg->from_net.type=*(uint16_t *)hfield_dat;
 			break;
 		case SENDERNETADDR:
-			if(!msg->forwarded)
-				msg->from_net.addr=(char*)hfield_dat;
+			msg->from_net.addr=(char*)hfield_dat;
 			break;
 		case SENDERIPADDR:
 			msg->from_ip=(char*)hfield_dat;
@@ -1240,13 +1218,23 @@ int	SMBCALL smb_hfield_add_list(smbmsg_t* msg, hfield_t** hfield_list, void** hf
 }
 
 /****************************************************************************/
-/* Convenience function to add an ASCIIZ string header field				*/
+/* Convenience function to add an ASCIIZ string header field (or blank)		*/
 /****************************************************************************/
 int SMBCALL smb_hfield_add_str(smbmsg_t* msg, uint16_t type, const char* str, BOOL insert)
 {
 	return smb_hfield_add(msg, type, str==NULL ? 0:strlen(str), (void*)str, insert);
 }
 
+/****************************************************************************/
+/* Convenience function to add an ASCIIZ string header field (NULL ignored)	*/
+/****************************************************************************/
+int SMBCALL smb_hfield_string(smbmsg_t* msg, uint16_t type, const char* str)
+{
+	if(str == NULL)
+		return SMB_ERR_HDR_FIELD;
+	return smb_hfield_add(msg, type, strlen(str), (void*)str, /* insert */FALSE);
+}
+
 /****************************************************************************/
 /* Convenience function to a network address header field to msg			*/
 /* Pass NULL for net_type to have the auto-detected net_type hfield	added	*/
diff --git a/src/smblib/smblib.h b/src/smblib/smblib.h
index 5c1c55e7a9340843ca0d8c69690c6ec1cd6d6309..fbe37ffbf99c2fc1101c72c210673328e7ba8063 100644
--- a/src/smblib/smblib.h
+++ b/src/smblib/smblib.h
@@ -160,6 +160,7 @@ SMBEXPORT int		SMBCALL smb_hfield_append(smbmsg_t* msg, uint16_t type, size_t le
 SMBEXPORT int		SMBCALL smb_hfield_append_str(smbmsg_t* msg, uint16_t type, const char* data);
 SMBEXPORT int		SMBCALL smb_hfield_add_list(smbmsg_t* msg, hfield_t** hfield_list, void** hfield_dat, BOOL insert);
 SMBEXPORT int		SMBCALL smb_hfield_add_netaddr(smbmsg_t* msg, uint16_t type, const char* str, uint16_t* nettype, BOOL insert);
+SMBEXPORT int		SMBCALL	smb_hfield_string(smbmsg_t*, uint16_t type, const char*);
 /* Convenience macro: */
 #define smb_hfield_bin(msg, type, data) smb_hfield_add(msg, type, sizeof(data), &(data), /* insert: */FALSE)
 /* Backward compatibility macros: */
diff --git a/src/smblib/smbtxt.c b/src/smblib/smbtxt.c
index 60794b91977ab98dd2dc710f7efc52d48e07b47e..0ffce0d1d5320d54dda792feee3625b5782544da 100644
--- a/src/smblib/smbtxt.c
+++ b/src/smblib/smbtxt.c
@@ -45,6 +45,7 @@
 char* SMBCALL smb_getmsgtxt(smb_t* smb, smbmsg_t* msg, ulong mode)
 {
 	char*	buf;
+	char*	preamble;
 	char*	lzhbuf;
 	char*	p;
 	char*	str;
@@ -107,6 +108,7 @@ char* SMBCALL smb_getmsgtxt(smb_t* smb, smbmsg_t* msg, ulong mode)
 			buf[l] = 0;
 		}
 	}
+	preamble = strdup(buf);
 
 	for(i=0;i<(uint)msg->hdr.total_dfields;i++) {
 		if(msg->dfield[i].length<=sizeof(xlat))
@@ -146,6 +148,7 @@ char* SMBCALL smb_getmsgtxt(smb_t* smb, smbmsg_t* msg, ulong mode)
 					,"%s malloc failure of %ld bytes for LZH buffer"
 					, __FUNCTION__, length);
 				free(buf);
+				free(preamble);
 				return(NULL);
 			}
 			if(smb_fread(smb,lzhbuf,length,smb->sdt_fp) != length) {
@@ -154,6 +157,7 @@ char* SMBCALL smb_getmsgtxt(smb_t* smb, smbmsg_t* msg, ulong mode)
 					, __FUNCTION__, length);
 				free(lzhbuf);
 				free(buf);
+				free(preamble);
 				return(NULL);
 			}
 			lzhlen=*(int32_t*)lzhbuf;
@@ -163,6 +167,7 @@ char* SMBCALL smb_getmsgtxt(smb_t* smb, smbmsg_t* msg, ulong mode)
 					, __FUNCTION__, l+lzhlen+3L);
 				free(lzhbuf);
 				free(buf);
+				free(preamble);
 				return(NULL);
 			}
 			buf=p;
@@ -176,6 +181,7 @@ char* SMBCALL smb_getmsgtxt(smb_t* smb, smbmsg_t* msg, ulong mode)
 					,"%s realloc failure of %ld bytes for text buffer"
 					, __FUNCTION__, l+length+3L);
 				free(buf);
+				free(preamble);
 				return(NULL);
 			}
 			buf=p;
@@ -196,9 +202,18 @@ char* SMBCALL smb_getmsgtxt(smb_t* smb, smbmsg_t* msg, ulong mode)
 
 	if(mode&GETMSGTXT_PLAIN) {
 		char* plaintext = smb_getplaintext(msg, buf);
-		if(plaintext != NULL)
-			return plaintext;
+		if(plaintext != NULL) {
+			buf = malloc(strlen(preamble) + strlen(plaintext) + 1);
+			if(buf == NULL)
+				buf = plaintext;
+			else {
+				strcpy(buf, preamble);
+				strcat(buf, plaintext);
+				free(plaintext);
+			}
+		}
 	}
+	free(preamble);
 	return(buf);
 }
 
@@ -480,7 +495,6 @@ char* SMBCALL smb_getplaintext(smbmsg_t* msg, char* buf)
 	const char*	txt;
 	enum content_transfer_encoding xfer_encoding = CONTENT_TRANFER_ENCODING_NONE;
 
-	FREE_AND_NULL(msg->text_subtype);
 	if(msg->mime_version == NULL || msg->content_type == NULL)	/* not MIME */
 		return NULL;
 	txt = mime_getcontent(buf, msg->content_type, "text/plain", 0, &xfer_encoding, &msg->text_charset
@@ -490,9 +504,12 @@ char* SMBCALL smb_getplaintext(smbmsg_t* msg, char* buf)
 			,/* attachment: */NULL, /* attachment_len: */0, /* index: */0);
 		if(txt == NULL)
 			return NULL;
+		free(msg->text_subtype);
 		msg->text_subtype = strdup("html");
-	} else
+	} else {
+		free(msg->text_subtype);
 		msg->text_subtype = strdup("plain");
+	}
 
 	memmove(buf, txt, strlen(txt)+1);
 	if(*buf == 0)	/* No decoding necessary */
diff --git a/src/xpdev/sockwrap.c b/src/xpdev/sockwrap.c
index fa22af5086ffbd20642e880f3ed92cc40215fcbf..0cf278787c931c400e58506ca4fac83b8ed68827 100644
--- a/src/xpdev/sockwrap.c
+++ b/src/xpdev/sockwrap.c
@@ -1,37 +1,21 @@
-/* sockwrap.c */
-
 /* Berkley/WinSock socket API wrappers */
 
-/* $Id: sockwrap.c,v 1.74 2020/08/09 02:13:57 rswindell Exp $ */
-
 /****************************************************************************
  * @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 library is free software; you can redistribute it and/or			*
- * modify it under the terms of the GNU Lesser General Public License		*
+ * 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 Lesser General Public License for more details: lgpl.txt or	*
- * http://www.fsf.org/copyleft/lesser.html									*
- *																			*
- * Anonymous FTP access to the most recent released source is available at	*
- * ftp://vert.synchro.net, ftp://cvs.synchro.net and ftp://ftp.synchro.net	*
- *																			*
- * Anonymous CVS access to the development source and modification history	*
- * is available at cvs.synchro.net:/cvsroot/sbbs, example:					*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs login			*
- *     (just hit return, no password is necessary)							*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs checkout src		*
+ * 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										*
  *																			*
- * You are encouraged to submit any modifications (preferably in Unix diff	*
- * format) via e-mail to mods@synchro.net									*
- *																			*
  * Note: If this box doesn't appear square, then you need to fix your tabs.	*
  ****************************************************************************/
 
@@ -524,14 +508,22 @@ DLLEXPORT char* socket_strerror(int error_number, char* buf, size_t buflen)
 #endif
 }
 
-#if defined(_WIN32) && !defined(_MSC_VER)
-DLLEXPORT int inet_pton(int af, const char *src, void *dst)
+DLLEXPORT void set_socket_errno(int err)
+{
+#if defined(_WINSOCKAPI_)
+	WSASetLastError(err);
+#else
+	errno = err;
+#endif
+}
+
+DLLEXPORT int xp_inet_pton(int af, const char *src, void *dst)
 {
 	struct addrinfo hints = {0};
 	struct addrinfo *res, *cur;
 
 	if (af != AF_INET && af != AF_INET6) {
-		// TODO: Should set socket_errno to EAFNOSUPPORT
+		set_socket_errno(EAFNOSUPPORT);
 		return -1;
 	}
 
@@ -558,4 +550,3 @@ DLLEXPORT int inet_pton(int af, const char *src, void *dst)
 	freeaddrinfo(res);
 	return 1;
 }
-#endif
diff --git a/src/xpdev/sockwrap.h b/src/xpdev/sockwrap.h
index 094701eae2fb77243ad9abe4652b7048cd7b31b3..73e7c90cced306b8d1b67ae9e6b9fc1b99d8bf75 100644
--- a/src/xpdev/sockwrap.h
+++ b/src/xpdev/sockwrap.h
@@ -1,37 +1,21 @@
-/* sockwrap.h */
-
 /* Berkley/WinSock socket API wrappers */
 
-/* $Id: sockwrap.h,v 1.61 2020/08/08 23:26:38 rswindell Exp $ */
-
 /****************************************************************************
  * @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 library is free software; you can redistribute it and/or			*
- * modify it under the terms of the GNU Lesser General Public License		*
+ * 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 Lesser General Public License for more details: lgpl.txt or	*
- * http://www.fsf.org/copyleft/lesser.html									*
- *																			*
- * Anonymous FTP access to the most recent released source is available at	*
- * ftp://vert.synchro.net, ftp://cvs.synchro.net and ftp://ftp.synchro.net	*
- *																			*
- * Anonymous CVS access to the development source and modification history	*
- * is available at cvs.synchro.net:/cvsroot/sbbs, example:					*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs login			*
- *     (just hit return, no password is necessary)							*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs checkout src		*
+ * 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										*
  *																			*
- * You are encouraged to submit any modifications (preferably in Unix diff	*
- * format) via e-mail to mods@synchro.net									*
- *																			*
  * Note: If this box doesn't appear square, then you need to fix your tabs.	*
  ****************************************************************************/
 
@@ -240,9 +224,10 @@ DLLEXPORT uint16_t inet_addrport(union xp_sockaddr *addr);
 DLLEXPORT void inet_setaddrport(union xp_sockaddr *addr, uint16_t port);
 DLLEXPORT BOOL inet_addrmatch(union xp_sockaddr* addr1, union xp_sockaddr* addr2);
 DLLEXPORT char* socket_strerror(int, char*, size_t);
-
-#if defined(_WIN32) && !defined(_MSC_VER)
-DLLEXPORT int inet_pton(int af, const char *src, void *dst);
+DLLEXPORT void set_socket_errno(int);
+DLLEXPORT int xp_inet_pton(int af, const char *src, void *dst);
+#if defined(_WIN32) // mingw and WinXP's WS2_32.DLL don't have inet_pton():
+	#define inet_pton	xp_inet_pton
 #endif
 
 #ifdef __cplusplus
diff --git a/src/xpdev/xpdev.props b/src/xpdev/xpdev.props
index 5966335cc74953e1bb909fbad211dab435e30dda..fc6a51f6e9261a9dc11cb7ef06858172c925cba3 100644
--- a/src/xpdev/xpdev.props
+++ b/src/xpdev/xpdev.props
@@ -11,7 +11,7 @@
       <AdditionalIncludeDirectories>$(MSBuildThisFileDirectory);%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
     </ClCompile>
     <Link>
-      <AdditionalDependencies>netapi32.lib;%(AdditionalDependencies)</AdditionalDependencies>
+      <AdditionalDependencies>netapi32.lib;ws2_32.lib;%(AdditionalDependencies)</AdditionalDependencies>
     </Link>
   </ItemDefinitionGroup>
   <ItemGroup />
diff --git a/webv4/lib/events/mail.js b/webv4/lib/events/mail.js
index 986b81875f53cb41f7035cd4ecaef5a9f0c21c33..4aca79415a478d83f9ad141f1cb6fbc471fcedfb 100644
--- a/webv4/lib/events/mail.js
+++ b/webv4/lib/events/mail.js
@@ -6,7 +6,7 @@ function cycle() {
     if (user.number < 1 || user.alias == settings.guest) return;
     if (time() - last_run <= frequency) return;
     last_run = time();
-    const count = user.stats.mail_waiting;
+    const count = user.stats.unread_mail_waiting;
     if (count > 0 || (count == 0 && last_count > 0)) {
         emit({ event: 'mail', data: JSON.stringify({ count: count })});
     }
diff --git a/webv4/lib/locale/en_us.ini b/webv4/lib/locale/en_us.ini
index c7391e85be75cd9fd9ea9fd21aabd2a8140ff285..655ad46eeb74c0d3a0420ad78ba2c0d42028886d 100644
--- a/webv4/lib/locale/en_us.ini
+++ b/webv4/lib/locale/en_us.ini
@@ -24,6 +24,7 @@ label_message_date = on
 label_message_subject = Subject
 label_tab_inbox = Inbox
 label_tab_sent = Sent
+label_new_message = New
 
 [page_register]
 title = Register
diff --git a/webv4/pages/000-mail.xjs b/webv4/pages/000-mail.xjs
index a4c71fec2a331b5b66b1ff92a44978278b0ed524..c7e662e58c3e922af0d08ff5b53f5662ec69ff94 100644
--- a/webv4/pages/000-mail.xjs
+++ b/webv4/pages/000-mail.xjs
@@ -18,6 +18,7 @@
 				<div class="checkbox">
 					<label class="checkbox-inline">
 						<input id="check-<? write(header.number); ?>" type="checkbox" class="mail-select">
+                        <? write(header.attr&MSG_READ ? '' : '<span class="badge new">' + locale.strings.page_mail.label_new_message + '</span>') ?>
 					</label>
 				</div>
 			</div>
diff --git a/webv4/pages/099-xtrnmenu-games.xjs b/webv4/pages/099-xtrnmenu-games.xjs
new file mode 100644
index 0000000000000000000000000000000000000000..c7106969f157d3872695bbe5d7e9b140abcde545
--- /dev/null
+++ b/webv4/pages/099-xtrnmenu-games.xjs
@@ -0,0 +1,149 @@
+<!--HIDDEN:Games-->
+<?xjs
+/**
+ * Web Display for Custom External Program Menus
+ * by Michael Long mlong  innerrealmbbs.us
+ * 
+ * See wiki at http://wiki.synchro.net/module:xtrnmenumod
+ */
+
+    load("ftelnethelper.js");
+    load('sbbsdefs.js');
+    load("xtrnmenulib.js");
+    load(settings.web_lib + 'ftelnet.js');
+    load(settings.web_lib + 'request.js');
+
+    var ExternalMenus = new ExternalMenus();
+
+    if (typeof settings.xtrn_blacklist === 'string') {
+        settings.xtrn_blacklist = settings.xtrn_blacklist.toLowerCase().split(',');
+    } else {
+        settings.xtrn_blacklist = [];
+    }
+
+    var menuitems = [];
+?>
+
+<style>.fTelnetStatusBar { display : none; }</style>
+
+<a name="fTelnet"></a>
+<div id="fTelnetContainer" class="fTelnetContainer" style="margin-bottom:1em;"></div>
+
+<div id="xtrn-list" class="list-group">
+
+<?xjs
+    var menuobj;
+    if ((Request.get_param('type') == 'xtrnmenu') && Request.has_param('target')) {
+        menuobj = ExternalMenus.getSectionMenu(Request.get_param('target'));
+    } else {
+        if (Request.has_param('target')) {
+            menuobj = ExternalMenus.getMenu(Request.get_param('target'));
+        } else {
+            menuobj = ExternalMenus.getMenu('main');
+        }
+    }
+
+    if ((typeof menuobj === "undefined") || !menuobj
+        || (typeof menuobj.items === "undefined") || (menuobj.items.length < 1)) {
+        writeln("<h4>" + ExternalMenus.options.no_programs_msg + "</h4>");
+    } else if (user.security.restrictions&UFLAG_X) {
+        writeln("<h4>" + options.restricted_user_msg + "</h4>");
+    } else {
+        // ok to display menu
+
+        writeln("<h4>" + menuobj.title + "</h4>");
+        var menuitemsfiltered = ExternalMenus.getSortedItems(menuobj);
+        menuitemsfiltered.forEach(function (menuitem) {
+            if (settings.xtrn_blacklist.indexOf(menuitem.target.toLowerCase()) > -1) {
+                return;
+            }
+            menuitems.push({
+                'itemtitle': menuitem.title,
+                'itemtype': menuitem.type,
+                'itemtarget': menuitem.target
+            });
+        });
+    }
+?>
+</div>
+
+<script id="fTelnetScript" src="<?xjs write(get_url()); ?>"></script>
+<script type="text/javascript">
+    var wsp = <?xjs write(settings.wsp || GetWebSocketServicePort()); ?>;
+    var wssp = <?xjs write(settings.wssp || GetWebSocketServicePort(true)); ?>;
+    var Options = new fTelnetOptions();
+    Options.BareLFtoCRLF = false;
+    Options.BitsPerSecond = 57600;
+    Options.ConnectionType = 'rlogin';
+    Options.Emulation = 'ansi-bbs';
+    Options.Enter = '\r';
+    Options.Font = 'CP437';
+    Options.ForceWss = false;
+    Options.Hostname = '<?xjs write(http_request.vhost); ?>';
+    Options.LocalEcho = false;
+    Options.Port = location.protocol == 'https:' ? wssp : wsp;
+    Options.RLoginClientUsername = '<?xjs write(user.security.password); ?>';
+    Options.RLoginServerUsername = '<?xjs write(user.alias); ?>';
+    Options.ScreenColumns = 80;
+    Options.ScreenRows = 25;
+    Options.SplashScreen = Options.SplashScreen = '<?xjs write(get_splash()); ?>';
+    Options.WebSocketUrlPath = '?Port=<?xjs write(GetRLoginPort()); ?>';
+    var fTelnet = new fTelnetClient('fTelnetContainer', Options);
+    fTelnet.OnConnectionClose = function () {
+        window.location.reload();
+    };
+
+    async function launchXtrn() {
+        var code = event.srcElement.id;
+        await v4_get('./api/system.ssjs?call=set-xtrn-intent&code=' + code);
+        fTelnet._Options.RLoginTerminalType = 'xtrn=' + code;
+        fTelnet.Connect();
+    }
+
+    var menuitems = <?xjs write(JSON.stringify(menuitems)); ?>;
+    var currentTarget = "<?xjs Request.write_param('target'); ?>";
+    var currentType = "<?xjs Request.write_param('type'); ?>";
+    var currentTitle = `<?xjs write(menuobj.title) ?>`;
+    if (currentTitle && !currentTarget) {
+        // main menu - store title for breadcrumb
+        sessionStorage.setItem("mainmenu", currentTitle);
+    }
+    var div = $('#xtrn-list');
+    menuitems.forEach(function (menuitem) {
+        var a = document.createElement('a');
+        $(a).addClass("list-group-item");
+        $(a).addClass("striped");
+        a.text = menuitem.itemtitle;
+
+        if (menuitem.itemtype == "xtrnprog") {
+            a.href = "#fTelnet";
+            a.id = menuitem.itemtarget;
+            a.onclick = function () { launchXtrn(); };
+        } else {
+            a.href = "/?page=<?xjs Request.write_param('page') ?>&type=" +menuitem.itemtype
+                + "&target=" + menuitem.itemtarget;
+            a.onclick = function () {
+                sessionStorage.setItem('prev:' + menuitem.itemtarget.toLowerCase(), '<?xjs Request.write_param('target')?>');
+                sessionStorage.setItem('prevtype:' + menuitem.itemtarget.toLowerCase(), '<?xjs Request.write_param('type')?>');
+                sessionStorage.setItem('prevtitle:' + menuitem.itemtarget.toLowerCase(), currentTitle);
+            };
+        }
+        $(div).append(a);
+    });
+
+    // breadcrumb
+    var prevTarget = sessionStorage.getItem('prev:' + currentTarget.toLowerCase());
+    var prevType = sessionStorage.getItem('prevtype:' + currentTarget.toLowerCase());
+    var prevTitle = sessionStorage.getItem('prevtitle:' + currentTarget.toLowerCase());
+    if (prevType && prevTarget && prevTitle) {
+        $('#xtrn-list').prepend('<ol class="breadcrumb"><a href="/?page=<?xjs Request.write_param('page') ?>&type=' + prevType
+        + '&target=' + prevTarget + '">' + prevTitle + '</a></ol>');
+    } else if (currentTarget) {
+        // level 2, not main menu
+        var mainmenuTitle = sessionStorage.getItem('mainmenu');
+        if (!mainmenuTitle) {
+            mainmenuTitle = 'Games';
+        }
+        $('#xtrn-list').prepend('<ol class="breadcrumb"><a href="/?page=<?xjs Request.write_param('page') ?>">' + mainmenuTitle + '</a></ol>');
+    }
+</script>
diff --git a/webv4/root/css/style.css b/webv4/root/css/style.css
index 7751bb1a8ed1e4a3274afa526dca9a58a355e007..7e49bcf0b1719cc5ecaf0b244f814668234257bf 100644
--- a/webv4/root/css/style.css
+++ b/webv4/root/css/style.css
@@ -17,6 +17,11 @@ a.unread {
 	background: #FFFFFF;
 }
 
+span.badge.new {
+        background: #2E9AFE;
+        color: #FFFFFF;
+}
+
 /* A read mail message in the list view. */
 a.read {
 	background: #E6E6E6;
@@ -174,4 +179,4 @@ animation: indicator-fade 3s ease 0s 1 alternate !important;
 
 .breadcrumb li {
 	display: inline;
-}
\ No newline at end of file
+}
diff --git a/xtrn/3rdp-install/bre.ini b/xtrn/3rdp-install/bre.ini
new file mode 100644
index 0000000000000000000000000000000000000000..1cdfa1b83ea9cd77ecd4665c8a0e072c2d1637b8
--- /dev/null
+++ b/xtrn/3rdp-install/bre.ini
@@ -0,0 +1,139 @@
+; Install instructions for Barren Realms Elite
+; see http://wiki.synchro.net/howto:door:bre
+Name: Barren Realms Elite
+Desc: Multi-player strategic war game set in a post-apocalyptic future
+By: Mehul Patel,John Dailey
+Cats: Games
+Subs: Multiplayer, Strategy, War
+exe: BRE.EXE
+
+[md5:127779656ab7564275c61bc3f3fad88b]
+ver = 0.988
+url = https://www.johndaileysoftware.com/download/?id=220BRE&fileName=brev988.exe
+
+[md5:6811a9565d1be70b7a7683cd791b4563]
+ver = 0.987
+url = http://breakintochat.com/files/doors/SRGames/BRE/distributions/bre0987.arj
+[md5:eb602a6238b686ed40ec8c6fc71b021a]
+ver = 0.986
+url = http://breakintochat.com/files/doors/SRGames/BRE/distributions/bre0986.arj
+[md5:2d8dbb97bc9a42536782127e530848aa]
+ver = 0.985
+url = http://breakintochat.com/files/doors/SRGames/BRE/distributions/bre0985.arj
+[md5:05273fda02fc52db2b2cc4cebd92e793]
+ver = 0.984
+url = http://breakintochat.com/files/doors/SRGames/BRE/distributions/bre0984.arj
+[md5: 6bdf120f19a41f48f2f0b532eda3ffaa]
+ver = 0.983
+url = http://breakintochat.com/files/doors/SRGames/BRE/distributions/bre0983.arj
+[md5:eb4af37fde3c8cf6baceacbb06a7e2fc]
+ver = 0.982
+url = http://breakintochat.com/files/doors/SRGames/BRE/distributions/bre0982.arj
+[md5:509bccf44ef158ea0c63b1e26e3ea3a9]
+ver = 0.981
+url = http://breakintochat.com/files/doors/SRGames/BRE/distributions/bre0981.arj
+[md5:8fadf439619d0ce52fd44426a1cdc0ef]
+ver = 0.980
+url = http://breakintochat.com/files/doors/SRGames/BRE/distributions/bre0980.arj
+[md5:660445e9c47eebcb98f5fdd6422cd3cd]
+ver = 0.979
+url = http://breakintochat.com/files/doors/SRGames/BRE/distributions/bre0979.arj
+[md5:4b6c85c8d88eecc4069142477cf5b57a]
+ver = 0.978
+url = http://breakintochat.com/files/doors/SRGames/BRE/distributions/bre0978.arj
+[md5:ba565786155d797b58b696f277f163b2]
+ver = 0.977
+url = http://breakintochat.com/files/doors/SRGames/BRE/distributions/bre0977.arj
+[md5:45ea1e2b739c216d250bad77b0d01797]
+ver = 0.976
+url = http://breakintochat.com/files/doors/SRGames/BRE/distributions/bre0976.arj
+[md5:e107667e1827c092a6bd0468f1134179]
+ver = 0.975
+url = http://breakintochat.com/files/doors/SRGames/BRE/distributions/bre0975.arj
+[md5:58167e18933109a2043d309b9e6ed47a]
+ver = 0.973
+url = http://breakintochat.com/files/doors/SRGames/BRE/distributions/bre0973.arj
+[md5:fb643b05fb10b955f414b53aff8bc4f6]
+ver = 0.972
+url = http://breakintochat.com/files/doors/SRGames/BRE/distributions/bre0972.arj
+[md5:3f4ad076d8fbc7cc7b670b99adbbd636]
+ver = 0.971
+url = http://breakintochat.com/files/doors/SRGames/BRE/distributions/bre0971.arj
+[md5:d867abd4f200bec4b8fdf66b3a143b5f]
+ver = 0.970
+url = http://breakintochat.com/files/doors/SRGames/BRE/distributions/bre0970.arj
+[md5:d8b4a33af532a17f59b65a826a3e2e72]
+ver = 0.964
+url = http://breakintochat.com/files/doors/SRGames/BRE/distributions/bre0964.arj
+[md5:cc3c935134a3bb27da156e96d8d5fb1c]
+ver = 0.963
+url = http://breakintochat.com/files/doors/SRGames/BRE/distributions/bre0963.arj
+[md5:a7dd8aef3b7d813fb284ce72aeb96672]
+ver = 0.962
+url = http://breakintochat.com/files/doors/SRGames/BRE/distributions/bre0962.arj
+[md5:4ac7ea354e51ff69c29848736e167afe]
+ver = 0.961
+url = http://breakintochat.com/files/doors/SRGames/BRE/distributions/bre0961.arj
+[md5:9e1845ec08bb2d0f56cb46ffc8a1bacd]
+ver = 0.960
+url = http://breakintochat.com/files/doors/SRGames/BRE/distributions/bre0960.zip
+[md5:4f3669cb3c378f47be3528ce6aed1d2d]
+ver = 0.956
+url = http://breakintochat.com/files/doors/SRGames/BRE/distributions/bre0956.zip
+[md5:b8fa5fb3d6fd22fdecb647bf396db6a5]
+ver = 0.953
+url = http://breakintochat.com/files/doors/SRGames/BRE/distributions/bre0953.arj
+[md5:f36d54acae29edc86d227e0450b4252f]
+ver = 0.910
+url = http://breakintochat.com/files/doors/SRGames/BRE/distributions/bre0910.arj
+[md5:c8c810778f03306d978dbed45684f540]
+ver = 0.904
+url = http://breakintochat.com/files/doors/SRGames/BRE/distributions/bre0904.arj
+[md5:7e556348d9ddcefdefaea6e58def25e3]
+ver = 0.901
+url = http://breakintochat.com/files/doors/SRGames/BRE/distributions/bre0901.zip
+[md5:66e2b24623aac433a6a87fe9677e68a5]
+ver = 0.875
+url = http://breakintochat.com/files/doors/SRGames/BRE/distributions/bre00875.zip
+[md5:7249263a11b956e269a3978f82df25e1]
+ver = 0.872
+url=http://breakintochat.com/files/doors/SRGames/BRE/distributions/bre00872.arj
+[md5:e70c862cbcb8a984e2313c431e6a4028]
+ver = 0.850
+url = http://breakintochat.com/files/doors/SRGames/BRE/distributions/bre00850.zip
+[md5:68fa2736115cca9e35d13306c1229d4f]
+ver = 0.601
+url = http://breakintochat.com/files/doors/SRGames/BRE/distributions/bre00601.arj
+
+[pre-eval:file_exists(startup_dir + 'RESOURCE.DAT')]
+prompt = false
+required = true
+fail = You must create RESOURCE.DAT by running BRE's INSTALL.EXE before you can continue installation.
+
+[prog:BRE]
+;      XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+name = Barren Realms Elite
+;      XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+cmd  = bre
+ars = DOS
+execution_ars  = NOT GUEST
+settings = XTRN_ANSI | XTRN_STARTUPDIR | XTRN_SH
+type = XTRN_SR
+clean_cmd = del inuse.flg
+
+[prog:BRERESET]
+name = Barren Realms Elite Reset
+cmd = bre RESET
+ars = DOS AND SYSOP
+execution_ars = SYSOP
+settings = XTRN_ANSI | XTRN_IO_INTS | XTRN_SH
+type = XTRN_NONE
+clean_cmd = del inuse.flg
+
+[event:BRESCORE]
+prompt = true
+cmd = bre score
+name = Barren Realms Elite Bulletins
+; all days
+days = 127
+freq = 4
diff --git a/xtrn/tw2/tw2.js b/xtrn/tw2/tw2.js
index bd0aa82409c7231480c43c07496b25e3b5d5b2c4..7f6cde73a24bd4b11cd159362e052fcfade60d05 100644
--- a/xtrn/tw2/tw2.js
+++ b/xtrn/tw2/tw2.js
@@ -41,7 +41,7 @@ var LOCK_READ=1;
 
 Settings=new GameSettings();
 if(db==undefined) {
-	alert("ERROR: Configuation invalid");
+	alert("ERROR: Configuration invalid");
 	exit(1);
 }