diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index e204c879710a6752cf6ad6d36c90cde4779300ab..825d202e34bb1291ad7755e9a64f12f593782a82 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -19,8 +19,9 @@ build-sbbs:
   artifacts:
     name: sbbs
     paths:
-      - "src/sbbs3/*.release/*"
-      - "src/sbbs3/*/*.release/*"
+      - "src/sbbs3/*.exe.release/*"
+      - "src/sbbs3/*.lib.release/*"
+      - "src/sbbs3/*/*.exe.release/*"
 
 build-sexpots:
   stage: build
@@ -34,7 +35,7 @@ build-sexpots:
   artifacts:
     name: sexpots
     paths:
-      - "src/sexpots/*.release/*"
+      - "src/sexpots/*.exe.release/*"
 
 build-syncterm:
   stage: build
@@ -48,7 +49,7 @@ build-syncterm:
   artifacts:
     name: syncterm
     paths:
-      - "src/syncterm/*.release/*"
+      - "src/syncterm/*.exe.release/*"
 
 # run tests using the binary built before
 #test:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 0000000000000000000000000000000000000000..eb48e4785049cf76ab210f68af16f9d051aed637
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,19 @@
+## Contributing changes to the Synchronet Source Repository
+
+Merge requests are considered and accepted at [gitlab.synchro.net](https://gitlab.synchro.net).
+Do not submit pull/merge requests in the Synchronet *mirror projects* on github.com or gitlab.com; they won't be considered.
+
+When submitting merge requests to existing files, unless you have prior agreement with the maintainers:
+* Use the dominant coding style of the file(s) being modified
+* Do not perform tab to space conversions (or vice-versa); incidental trimming of trailing whitespace is okay
+* Do not include changes that are not relevant to the merge request description/message
+* Keep merge requests to a single topical change (e.g. don't combine new features with bug fixes with typo-fixes and style changes)
+
+In general, if it's a large set of changes, your best bet of getting it accepted and merged into the repo would be to discuss the concept of the change with the developers in the [Synchronet Programming conference](http://web.synchro.net/?page=001-forum.ssjs&sub=syncprog) **first**.
+
+When modifying the C/C++ source files:
+* Do not call functions from `ctype.h` (e.g. `isprint`, `isspace`, `isdigit`, etc.) - use the `gen_defs.h IS_*` macros instead.
+
+If you were interested in contributing money, not code, then paypal to rob at synchro dot net.
+
+Thank you for contributing!
\ No newline at end of file
diff --git a/ctrl/sbbs.ini b/ctrl/sbbs.ini
index 0a717ce8c7ba306018b27ebcf7424c52b03ca0d7..2aef936d3a74828f941e4ed4c6a4f29a574359e9 100644
--- a/ctrl/sbbs.ini
+++ b/ctrl/sbbs.ini
@@ -172,6 +172,7 @@ Options = XTRN_MINIMIZED | ALLOW_RLOGIN | ALLOW_SSH
 	InboundSound = 
 	OutboundSound = 
 	ConnectTimeout = 30
+	MaxConcurrentConnections = 0
 ; Supported options (separated with |):
 ;	DEBUG_RX_HEADER		- Log header fields of received mail messages
 ;	DEBUG_RX_BODY		- Leave body text of received mail messages in temp directory forever
diff --git a/ctrl/sbbsecho.ini b/ctrl/sbbsecho.ini
index 48629697fb2affbef9b3e45d7b2a2387de66fd17..4d3fd036b04187e128f3b4bcdb8ad18a1bf283a7 100644
--- a/ctrl/sbbsecho.ini
+++ b/ctrl/sbbsecho.ini
@@ -153,6 +153,14 @@ DefaultRecipient =
 	Zones = 57
 	DNSSuffix = 
 	NodeList = 
+[domain:cnet]
+	Zones = 64
+	DNSSuffix =
+	NodeList =
+[domain:scinet]
+	Zones = 77
+	DNSSuffix =
+	NodeList =	
 [domain:retronet]
 	Zones = 80
 	DNSSuffix = 
@@ -173,6 +181,10 @@ DefaultRecipient =
 	Zones = 169
 	DNSSuffix = 
 	NodeList = 
+[domain:devnet]
+	Zones = 256
+	DNSSuffix =
+	NodeList =	
 [domain:pinet]
 	Zones = 314
 	DNSSuffix = 
@@ -189,6 +201,10 @@ DefaultRecipient =
 	Zones = 432
 	DNSSuffix = ftn.vkradio.com
 	NodeList = 
+[domain:musicnet]
+	Zones = 440
+	DNSSuffix =
+	NodeList =	
 [domain:justanet]
 	Zones = 510
 	DNSSuffix = 
@@ -229,6 +245,10 @@ DefaultRecipient =
 	Zones = 900
 	DNSSuffix = 
 	NodeList = 
+[domain:tqwnet]
+	Zones = 1337
+	DNSSuffix =
+	NodeLIst =	
 [domain:quartz]
 	Zones = 2547
 	DNSSuffix = 
diff --git a/ctrl/text.dat b/ctrl/text.dat
index 897bc0224f8bd393c32018a365a4f6fadeaebe7c..a2b4bf512a306bcace791e5175a661b764b99472 100644
--- a/ctrl/text.dat
+++ b/ctrl/text.dat
@@ -583,7 +583,7 @@
 "\1h\1bUser Settings for \1w%s #%d\1b:\r\n\r\n"                474 UserDefaultsHdr
 "\1n\1b[\1h\1wT\1n\1b] \1hTerminal Mode                \1n\1b\1\\: \1c%s\r\n"   475 UserDefaultsTerminal
 "\1n\1b[\1h\1wE\1n\1b] \1hExternal Editor              \1n\1b\1\\: \1c%s\r\n"   476 UserDefaultsXeditor
-"\1n\1b[\1h\1wL\1n\1b] \1hScreen Length                \1n\1b\1\\: \1c%s %s\r\n" 477 UserDefaultsRows
+"\1n\1b[\1h\1wL\1n\1b] \1hTerminal Dimensions          \1n\1b\1\\: \1c%s %s\r\n" 477 UserDefaultsRows
 "\1n\1b[\1h\1wX\1n\1b] \1hExpert Menu Mode             \1n\1b: \1c%s\r\n"   478 UserDefaultsMenuMode
 "\1n\1b[\1h\1wP\1n\1b] \1hScreen Pause                 \1n\1b: \1c%s\r\n"   479 UserDefaultsPause
 "\1n\1b[\1h\1wH\1n\1b] \1hHot Keys                     \1n\1b: \1c%s\r\n"   480 UserDefaultsHotKey
@@ -604,8 +604,8 @@
 "\r\n\1n\1h\1bWhich or [\1wQ\1b]uit: \1c"                   494 UserDefaultsWhich
 "On"                                                    495 On
 "Off"                                                   496 Off
-"\r\n\1_\1b\1h[\1c@CHECKMARK@\1b] \1yHow many rows on your monitor "\   497 HowManyRows
-	"[\1wAuto Detect\1y]: "
+"\1_\1b\1h[\1c@CHECKMARK@\1b] \1yTerminal rows "\       497 HowManyRows
+	"[\1wAuto\1y]: \1n"
 "\r\n\1_\1y\1hCurrent Password: \1w"                        498 CurrentPassword
 "Forward personal e-mail to network mail address"       499 ForwardMailQ
 "\1_\1b\1h[\1c@CHECKMARK@\1b] \1yNetwork mail address\1\\ "\      500 EnterNetMailAddress
@@ -1021,4 +1021,6 @@
 "\xda\xbf\xd9\xc0"                                        839 SpinningCursor6
 "\xdc\xde\xdf\xdd"                                        840 SpinningCursor7
 "\xdc\xdd\xdf\xde"                                        841 SpinningCursor8
-"\xfa\xf9\xfe\xf9"                                        842 SpinningCursor9
\ No newline at end of file
+"\xfa\xf9\xfe\xf9"                                        842 SpinningCursor9
+"\1_\1b\1h[\1c@CHECKMARK@\1b] \1yTerminal columns "\      843 HowManyColumns
+	"[\1wAuto\1y]: \1n"
\ No newline at end of file
diff --git a/exec/init-fidonet.ini b/exec/init-fidonet.ini
index e3c33868ba902c1398a9fb6d6bd9ca7c47cde2fc..0591376899a7cad042950b37ba3d27a2aaaca894 100644
--- a/exec/init-fidonet.ini
+++ b/exec/init-fidonet.ini
@@ -10,6 +10,7 @@
 ; name = Name of the network, usually the same as the 5D domain, <= 8 chars
 ; desc = Short description of the network
 ; info = URL to information about the network
+; pack = URL to info pack file
 ; coord = Name of the zone/network coordinator
 ; email = Internet email address of the zone/network coordinator
 ; fido  = FidoNet address of the zone/network coordinator (for NetMail)
@@ -46,11 +47,24 @@ desc  = Australasia
 [zone:4]
 name  = FidoNet
 desc  = Latin America (except Puerto Rico)
-	
+
+[zone:11]
+name  = WWIVnet
+desc  = WWIVnet
+info  = https://www.wwivbbs.org/docs/network/wwivnet.html
+coord = Mark Hofmann
+email = mark@weather-station.org
+addr  = 11:1/100
+host  = bbs.weather-station.org
+port  = 24555
+echolist = https://raw.githubusercontent.com/wwivbbs/wwivnet/master/wwivnet/wwivnet.na
+areatag_prefix = WWIV_
+
 [zone:21]
 name  = fsxNet
 desc  = Fun, Simple and eXperimental network
 info  = http://fsxnet.nz/
+pack = https://github.com/fsxnet/infopack/archive/master.zip
 coord = Paul Hayton
 email = avon@bbs.nz
 addr  = 21:1/100
@@ -76,32 +90,32 @@ areatitle_prefix = AmigaNet:
 [zone:40]
 name  = CyberNet
 desc  = Stock, Trading, Cybersecurity, OS, Technology, Hacking, Crypto
-info  = http://www.thebytexchange.com/cybernet/
+info  = telnet://bbs.thebytexchange.com:23
 coord = Chad Adams
 email = nugax@thebytexchange.com
 addr  = 40:1/1@cybernet
 fido  = 1:19/37
 host  = hub.cybernetbbs.net
-echolist = http://thebytexchange.com/cybernet/cybernet.na
 areatag_prefix = CN_
 
 [zone:44]
 name  = DoRENET
 desc  = BBS modifications, coding, ansi/asci etc
 info  = https://www.dreamlandbbs.org/dorenet/
+pack = https://www.dreamlandbbs.org/wp-content/uploads/2020/04/dorenet.zip
 coord = Dream Master
 addr  = 44:100/0
 fido  = 1:218/530
 host  = bbs.dreamlandbbs.org
 handles = true
 echolist = DORENET.NA
-areatag_prefix = DN_ 
+areatag_prefix = DN_
 areatag_exclude = netmail
 
 [zone:46]
 name  = Agoranet
 desc  = The Official Network of ACiD Productions
-info  = ftp://pharcyde.org/agoranet/AGN_INFO/agoranet.zip
+pack  = ftp://pharcyde.org/agoranet/AGN_INFO/agoranet.zip
 coord = Nicholas Boel
 email = accessd@pharcyde.org
 addr  = 46:1/100
@@ -129,31 +143,71 @@ areatag_prefix = CNT_
 name  = SciNet
 desc  = Active network for the BBS scene
 info  = https://scinet-ftn.org/
+pack = https://scinet-ftn.org/sciinfo.zip
 coord = Frank Linhares
 email = scinet@diskshop.ca
 addr  = 77:1/100
 fido  = 1:229/101
 dns   = scinet-ftn.org
+host  = bbs.diskshop.ca
 echolist = https://scinet-ftn.org/scinet.na
-areatag_prefix =
 areatitle_prefix = SciNet:
 
+[zone:256]
+name  = DevNet
+desc  = developer oriented network
+info = http://www.digitaldistortionbbs.com, Also available as QWK. See pack.
+pack  = ftp://digitaldistortionbbs.com/bbs/INFOPAKS/DevNet.zip
+coord = Tony Langdon
+email = vk3jed@vkradio.com
+addr  = 256:8/100
+host  = bridge.vkradio.com
+echolist = devnet.na
+areatag_prefix = DEV_
+
 [zone:432]
 name  = VKRadio
 desc  = hobbyist radio communication oriented network
-info  = https://vkradio.com/vkradio.zip
+info = https://vkradio.com/vkradio.zip
+pack  = https://vkradio.com/vkradio.zip
 coord = Tony Langdon
 email = vk3jed@vkradio.com
 addr  = 432:1/100
 fido  = 3:633/410
 dns   = ftn.vkradio.com
+host  = bridge.vkradio.com
 echolist = https://vkradio.com/vkradio.na
 areatag_prefix = VK_
 
+[zone:440]
+name  = MusicNet
+desc  = music oriented network
+info = http://www.digitaldistortionbbs.com, Also available as QWK. See pack.
+pack  = ftp://digitaldistortionbbs.com/bbs/INFOPAKS/MusicNet.zip
+coord = Tony Langdon
+email = vk3jed@vkradio.com
+addr  = 440:1/100
+host  = bridge.vkradio.com
+echolist = musicnet.na
+areatag_prefix = MUS_
+
+[zone:316]
+name  = Whisper
+desc  = Family-Oriented Bulletin Board Message/File Network
+info = http://www.cr1mson.org/whispernet.html
+pack  = http://www.cr1mson.org/uploads/1/4/5/3/14535704/whispnet.zip
+coord = Jon Justvig
+email = jonathanjustvig@gmail.com
+addr  = 316:36/1
+host  = vintagebbsing.com
+echolist = wenmbone.na
+areatag_prefix = WHISP_
+
 [zone:618]
-name  = Micronet 
+name  = Micronet
 desc  = laid-back, friendly and comfortable mail
 info  = https://minftn.net/
+pack = http://minftn.net/mininfo.zip
 coord = Sean Dennis
 email = zc@minftn.net
 fido  = 1:18/200
@@ -162,3 +216,15 @@ addr  = 618:618/2
 host  = phoenix.bnbbbs.net
 echolist = micronet.na
 areatag_prefix = MIN_
+
+[zone:1337]
+name = tqwNet
+desc = A network for having fun and international friendships
+info = https://www.erb.pw/
+pack = https://www.erb.pw/tqwinfo.zip
+coord = MeaTLoTioN
+email = ml@erb.pw
+addr = 1337:3/100
+host = hub.ca.erb.pw
+echolist =  tqwnet.na
+areatag_prefix = TQW_
diff --git a/exec/init-fidonet.js b/exec/init-fidonet.js
index a204eccf23b3b442721db8b73be6b8b7b4e79a4a..8f93b89a19291c0e4438db71f680517e45ba540a 100644
--- a/exec/init-fidonet.js
+++ b/exec/init-fidonet.js
@@ -22,15 +22,16 @@
 
 "use strict";
 
-const REVISION = "$Revision: 1.29 $".split(' ')[1];
+const REVISION = "$Revision: 1.30 $".split(' ')[1];
 require('sbbsdefs.js', 'SUB_NAME');
+const temp_node = 9999;
 var netname;
 var netdns;
 var netzone = parseInt(argv[0], 10);
 if(!netzone)
 	netname = argv[0];
 var echolist_url = argv[1];
-// If you want your Othernet listed here, please provide information
+// If you want your Othernet listed in init-fidonet.ini, please provide information
 // and an http[s] URL to your official EchoList
 var network;
 var fidoaddr = load({}, 'fidoaddr.js');
@@ -42,7 +43,7 @@ print("*************************************************************************
 
 var network_list = {};
 var file = new File(js.exec_dir + "init-fidonet.ini");
-if (file.open("r")) {
+if(file.open("r")) {
 	var list = file.iniGetSections("zone:", "zone");
 	for(var i in list)
 		network_list[list[i].substr(5)] = file.iniGetObject(list[i]);
@@ -59,15 +60,15 @@ function aborted()
 function exclude_strings(list, patterns, flags)
 {
 	patterns = [].concat(patterns);
-	if (flags === undefined)
+	if(flags === undefined)
 		flags = 'i';
 	return list.reduce(function (a, c) {
 		var matched = patterns.some(function (e) {
-			if (typeof e == 'string')
+			if(typeof e == 'string')
 				e = new RegExp(e, flags);
 			return c.match(e);
-			});
-		if (!matched)
+		});
+		if(!matched)
 			a.push(c);
 		return a;
 	}, []);
@@ -150,11 +151,13 @@ function send_app_netmail(destaddr)
 	body_text += "\r\n";
 	body_text += "My system is " + system.name + " at " + system.inet_addr + ".\r\n";
 	body_text += "\r\n";
-	body_text += "I am using Synchronet-" + system.platform + " v" + system.full_version 
+	body_text += "I am using Synchronet-" + system.platform + " v" + system.full_version
 		+ " with SBBSecho and BinkIT.\r\n";
 	body_text += "\r\n";
 	body_text += "My requested AreaFix password is: '" + link.AreaFixPwd + "'\r\n";
 	body_text += "My requested BinkP Session password is: '" + link.SessionPwd + "'\r\n";
+	if(link.TicFilePwd)
+		body_text += "My requested TIC Password is: '" + link.TicFilePwd  + "'\r\n";
 	body_text += "\r\n";
 	body_text += "I will be using 'Type-2+' (FSC-39) packets with no password.\r\n";
 	body_text += "Uncompressed or PKZIP-archived EchoMail bundles will work fine.\r\n";
@@ -187,13 +190,13 @@ function lookup_network(info)
 		if(result)
 			return result;
 	}
-	
+
 	var file = new File("sbbsecho.ini");
-	if (!file.open("r")) {
+	if(!file.open("r")) {
 		alert("Error " + file.error + " opening " + file.name);
 		return false;
 	}
-	
+
 	if(typeof info == "number") { // zone
 		var dns;
 		var domain_list = file.iniGetSections("domain:");
@@ -230,12 +233,52 @@ function lookup_network(info)
 	return result;
 }
 
-function get_linked_node(addr)
+function get_domain(zone)
+{
+	var file = new File("sbbsecho.ini");
+	if(!file.open("r")) {
+		alert("Error " + file.error + " opening " + file.name);
+		return false;
+	}
+
+	var domain_list = file.iniGetSections("domain:");
+	if(domain_list) {
+		var zonemap = {};
+		for(var i = 0; i < domain_list.length && !result; i++) {
+			var section = domain_list[i];
+			var netname = section.substr(7)
+			var zones = file.iniGetValue(section, "Zones");
+			if(!zones)
+				continue;
+			if(typeof zones == 'number') {
+				if(zone == zones) {
+					return netname;
+				}
+				continue;
+			}
+			zones = zones.split(',');
+			for(var j = 0; j < zones.length; j++) {
+				if(zone == zones[j]) {
+					return netname;
+				}
+			}
+		}
+		file.close();
+		return result;
+	}
+	return "";
+}
+
+function get_linked_node(addr, domain)
 {
 	var file = new File("sbbsecho.ini");
-	if (!file.open("r"))
+	if(!file.open("r"))
 		return false;
-	var result = file.iniGetObject("node:" + addr);
+	var result = false;
+	if(domain)
+		result = file.iniGetObject("node:" + addr + "@" + domain);
+	if (!result)
+		result = file.iniGetObject("node:" + addr);
 	file.close();
 	return result;
 }
@@ -243,14 +286,14 @@ function get_linked_node(addr)
 function get_binkp_sysop()
 {
 	var file = new File("sbbsecho.ini");
-	if (!file.open("r"))
+	if(!file.open("r"))
 		return false;
 	var result = file.iniGetValue("Binkp", "Sysop");
 	file.close();
 	return result;
 }
 
-function update_sbbsecho_ini(hub, link, echolist_fname, areamgr)
+function update_sbbsecho_ini(hub, link, domain, echolist_fname, areamgr)
 {
 	function makepath(path)
 	{
@@ -295,15 +338,36 @@ function update_sbbsecho_ini(hub, link, echolist_fname, areamgr)
 	if(!binkp) binkp = {};
 	binkp.sysop = sysop;
 	if(!file.iniSetObject("binkp", binkp)) {
-		return "Error" + file.error + " writign to " + file.name;
+		return "Error" + file.error + " writing to " + file.name;
 	}
+
+	var prefnode;
 	var section = "node:" + fidoaddr.to_str(hub);
-	if(!file.iniGetObject(section)
-		|| confirm("Overwrite hub [" + section + "] configuration in " + file.name)) {
-		if(!file.iniSetObject(section, link)) {
-			return "Error " + file.error + " writing to " + file.name;
+
+	if(domain) {
+		if(file.iniGetObject(section)) {
+			if(confirm("Migrate " + section + " to " + section + "@" + domain)) {
+				if(!file.iniSetObject(section + "@" + domain, link)) {
+					return "Error " + file.error + " writing to " + file.name;
+				} else {
+					file.iniRemoveSection(section);
+				}
+			}
+		} else {
+			if(!file.iniGetObject(section) || confirm("Overwrite hub [" + section + "@" + domain + "] configuration in " + file.name)) {
+				if(!file.iniSetObject(section + "@" + domain, link)) {
+					return "Error " + file.error + " writing to " + file.name;
+				}
+			}
+		}
+	} else {
+		if(!file.iniGetObject(section) || confirm("Overwrite hub [" + section + "] configuration in " + file.name)) {
+			if(!file.iniSetObject(section, link)) {
+				return "Error " + file.error + " writing to " + file.name;
+			}
 		}
 	}
+
 	var section = "node:" + hub.zone + ":ALL";
 	if(confirm("Route all zone " + hub.zone + " netmail through your hub")) {
 		if(!file.iniSetObject(section,
@@ -343,7 +407,6 @@ else if(netname) {
 	netzone = lookup_network(netname);
 	network = network_list[netzone];
 }
-
 if(!netzone) {
 	for(var zone in network_list) {
 		var desc = "";
@@ -358,7 +421,8 @@ if(!netzone) {
 				email = " <" + network_list[zone].email + ">";
 			if(network_list[zone].fido)
 				email += " " + network_list[zone].fido;
-			print("       coordinator: " + (network_list[zone].coord || "") + email);
+			// removed because screen is scrolling so much with so many networks
+			//print("       coordinator: " + (network_list[zone].coord || "") + email);
 		}
 	}
 	var which;
@@ -377,10 +441,13 @@ if(network)
 else
 	network = {};
 
+
+var domain = get_domain(netzone);
+
 if(netzone <= 6)
 	netname = "FidoNet";
 else {
-	while((!netname || netname.indexOf(' ') >= 0 || netname.length > 8 
+	while((!netname || netname.indexOf(' ') >= 0 || netname.length > 8
 		|| !confirm("Network name is '" + netname + "'")) && !aborted()) {
 		var str = prompt("Network name (no spaces or illegal filename chars) [" + netname + "]");
 		if(str)
@@ -391,7 +458,12 @@ if(netname) {
 	print("Network name: " + netname);
 	print("Network zone: " + netzone);
 	print("Network info: " + network.info);
-	print("Network coordinator: " + network.coord 
+	if(domain)
+		print("Network domain: " + domain);
+	if (network.pack) {
+		print("Network pack: " + network.pack);
+	}
+	print("Network coordinator: " + network.coord
 		+ (network.email ? (" <" + network.email + ">") : "")
 		+ (network.fido ? (" " + network.fido) : ""));
 	if(network.also)
@@ -403,12 +475,12 @@ if(netname) {
 print("Reading Message Area configuration file: msgs.cnf");
 var cnflib = load({}, "cnflib.js");
 var msgs_cnf = cnflib.read("msgs.cnf");
-if (!msgs_cnf) {
+if(!msgs_cnf) {
 	alert("Failed to read msgs.cnf");
 	exit(1);
 }
 
-var your = {zone: NaN, net: NaN, node: 9999, point: 0};
+var your = {zone: NaN, net: NaN, node: temp_node, point: 0};
 for(var i = 0; i < system.fido_addr_list.length; i++) {
 	var addr = fidoaddr.parse(system.fido_addr_list[i]);
 	if(!addr || addr.zone != netzone)
@@ -432,7 +504,7 @@ while(((isNaN(hub.zone) || hub.zone < 1)
 	hub = fidoaddr.parse(prompt("Your hub's address (zone:net/node)"));
 }
 
-var link = get_linked_node(fidoaddr.to_str(hub));
+var link = get_linked_node(fidoaddr.to_str(hub), domain);
 if(!link)
 	link = {};
 
@@ -477,11 +549,11 @@ while(!confirm("Your node address is " + fidoaddr.to_str(your)) && !aborted()) {
 	your.net = NaN;
 	your.node = NaN;
 	while((isNaN(your.zone) || your.zone < 1) && !aborted())
-		your.zone = parseInt(prompt("Your zone number (e.g. 1 for FidoNet North America)"));
+		your.zone = parseInt(prompt("Your zone number (e.g. " + hub.zone + ")"));
 	while((isNaN(your.net) || your.net < 1) && !aborted())
-		your.net = parseInt(prompt("Your network number (i.e. normally the same as your hub)"));
+		your.net = parseInt(prompt("Your network number (e.g. " + hub.net + ")"));
 	while((isNaN(your.node) || your.node < 1) && !aborted())
-		your.node = parseInt(prompt("Your node number (e.g. 9999 for temporary node)"));
+		your.node = parseInt(prompt("Your node number (e.g. " + temp_node + " for temporary node)"));
 	while((isNaN(your.point)) && !aborted())
 		your.point = parseInt(prompt("Your point number (i.e. 0 for a normal node)"));
 }
@@ -500,13 +572,15 @@ while((!sysop || !confirm("Your name is '" + sysop + "'")) && !aborted())
 /* Get/Confirm passwords */
 while((!link.AreaFixPwd || !confirm("Your AreaFix Password is '" + link.AreaFixPwd + "'")) && !aborted())
 	link.AreaFixPwd = prompt("Your AreaFix (a.k.a. Area Manager) Password (case in-sensitive)");
-while((!link.SessionPwd || !confirm("Your BinkP Session Passowrd is '" + link.SessionPwd + "'")) && !aborted())
+while((!link.SessionPwd || !confirm("Your BinkP Session Password is '" + link.SessionPwd + "'")) && !aborted())
 	link.SessionPwd = prompt("Your BinkP Session Password (case sensitive)");
+while(((!link.TicFilePwd && (link.TicFilePwd !== "")) || !confirm("Your TIC File Password is '" + (link.TicFilePwd ? link.TicFilePwd : "(not set)") + "'")) && !aborted())
+	link.TicFilePwd = prompt("Your TIC File Password (case sensitive) (optional)");
 
 /***********************************************/
 /* SEND NODE NUMBER REQUEST NETMAIL (Internet) */
 /***********************************************/
-if(your.node === 9999 && network.email && network.email.indexOf('@') > 0 
+if(your.node === temp_node && network.email && network.email.indexOf('@') > 0
 	&& confirm("Send a node number application to " + network.email)) {
 	var result = send_app_netmail(network.email);
 	if(typeof result !== 'boolean') {
@@ -517,7 +591,7 @@ if(your.node === 9999 && network.email && network.email.indexOf('@') > 0
 		exit(0);
 	if(confirm("Come back when you have your permanently-assigned node address")) {
 		if(confirm("Save changes to FidoNet configuration file: sbbsecho.ini")) {
-			var result = update_sbbsecho_ini(hub, link);
+			var result = update_sbbsecho_ini(hub, link, domain);
 			if (result != true) {
 				alert(result);
 				exit(1);
@@ -545,12 +619,12 @@ if(!msg_area.grp[netname]
 	&& confirm("Create " + netname + " message group in SCFG->Message Areas")) {
 	print("Adding Message Group: " + netname);
 	msgs_cnf.grp.push( {
-			"name": netname,
-			"description": netname,
-			"ars": "",
-			"code_prefix": network.areatag_prefix === undefined 
-				? (netname.toUpperCase() + "_") : network.areatag_prefix
-			});
+		"name": netname,
+		"description": netname,
+		"ars": "",
+		"code_prefix": network.areatag_prefix === undefined
+			? (netname.toUpperCase() + "_") : network.areatag_prefix
+	});
 }
 if(confirm("Save Changes to Message Area configuration file: msgs.cnf")) {
 	if(!cnflib.write("msgs.cnf", undefined, msgs_cnf)) {
@@ -564,11 +638,12 @@ if(confirm("Save Changes to Message Area configuration file: msgs.cnf")) {
 /* DOWNLOAD ECHOLIST */
 /*********************/
 var echolist_fname = file_getname(network.echolist);
-if(network.echolist 
+load("http.js");
+if(network.echolist
 	&& (network.echolist.indexOf("http://") == 0 || network.echolist.indexOf("https://") == 0)
 	&& confirm("Download " + netname + " EchoList: " + file_getname(network.echolist))) {
 	var echolist_url = network.echolist;
-	load("http.js");
+
 	while(!aborted()) {
 		while((!echolist_url || !confirm("Download from: " + echolist_url)) && !aborted()) {
 			echolist_url = prompt("Echolist URL");
@@ -589,8 +664,8 @@ if(network.echolist
 				break;
 			continue;
 		}
-		if(http_request.response_code == 200) {
-			print("Downloaded " + echolist_url + " to " + file.name);
+		if(http_request.response_code == http_request.status.ok) {
+			print("Downloaded " + echolist_url + " to " + system.ctrl_dir + file.name);
 			file.write(contents);
 			file.close();
 			break;
@@ -601,14 +676,65 @@ if(network.echolist
 		if(!confirm("Try again"))
 			break;
 	}
+} else if (network.pack
+	&& (network.pack.indexOf("http://") == 0 || network.pack.indexOf("https://") == 0)
+	&& confirm("Download " + netname + " Info Pack: " + network.pack)) {
+	while(!aborted()) {
+		var packdlfilename = file_getname(network.pack)
+		var file = new File(packdlfilename);
+		if(!file.open("w")) {
+			alert("Error " + file.error + " opening " + file.name);
+			exit(1);
+		}
+		var http_request = new HTTPRequest();
+		try {
+			var contents = http_request.Get(network.pack);
+		} catch(e) {
+			alert(e);
+			file.close();
+			file_remove(system.ctrl_dir + file.name);
+			if(!confirm("Try again"))
+				break;
+			continue;
+		}
+		if(http_request.response_code == http_request.status.ok) {
+			print("Downloaded " + network.pack + " to " + system.ctrl_dir + file.name);
+			file.write(contents);
+			file.close();
+
+			// try to extract
+			var prefix = "";
+			if(system.platform == "Win32")
+				prefix = system.exec_dir;
+			if (system.exec(prefix + "unzip -CLjo " + file_getname(network.pack) + " " + echolist_fname) !== 0) {
+				print("Please extract " + network.echolist + " from " + file.name + " into " + system.ctrl_dir);
+			}
+
+			break;
+		}
+		file.close();
+		file_remove(file.name);
+		alert("Error " + http_request.response_code + " downloading " + network.pack);
+		if(!confirm("Try again"))
+			break;
+	}
 }
 while(echolist_fname && !file_getcase(echolist_fname) && !aborted()) {
 	alert(system.ctrl_dir + echolist_fname + " does not exist");
-	if(!confirm("Install " + netname + " EchoList: " + echolist_fname))
-		break;
-	prompt("Download and extract " + echolist_fname + " now... Press enter to continue");
+	if ((network.echolist.indexOf("http://") == -1) && (network.echolist.indexOf("https://") == -1)
+		&& (network.pack)) {
+		if (!confirm("Please extract the " + echolist_fname + " file from the pack " + file_getname(network.pack) + " into " + system.ctrl_dir + ". Continue?")) {
+			break;
+		}
+	} else {
+		if (!confirm("Please place " + echolist_fname + " into " + system.ctrl_dir + ". Continue?")) {
+			break;
+		}
+	}
+
+	prompt(echolist_fname + " not found. Please put file into ctrl dir and press Enter.");
 }
-echolist_fname = file_getcase(echolist_fname)	
+echolist_fname = file_getcase(system.ctrl_dir + echolist_fname)
 if(echolist_fname && file_size(echolist_fname) > 0) {
 	if(network.areatag_exclude) {
 		print("Removing " + network.areatag_exclude + " from " + echolist_fname);
@@ -618,7 +744,7 @@ if(echolist_fname && file_size(echolist_fname) > 0) {
 	}
 	if(network.areatitle_prefix) {
 		print("Removing " + network.areatitle_prefix + " Title Prefixes from " + echolist_fname);
-		var result = remove_prefix_from_titles(echolist_fname, network.areatitle_prefix);
+		var result = remove_prefix_from_title(echolist_fname, network.areatitle_prefix);
 		if(result !== true)
 			alert(result);
 	}
@@ -628,11 +754,11 @@ if(echolist_fname && file_size(echolist_fname) > 0) {
 		if(!network.handles)
 			misc |= SUB_NAME;
 		system.exec(system.exec_dir + "scfg"
-			+ " -import=" + echolist_fname 
+			+ " -import=" + echolist_fname
 			+ " -g" + netname
 			+ " -faddr=" + fidoaddr.to_str(your)
 			+ " -misc=" + misc
-			);
+		);
 	}
 }
 
@@ -640,7 +766,7 @@ if(echolist_fname && file_size(echolist_fname) > 0) {
 /* UPDATE SBBSECHO.INI */
 /***********************/
 if(confirm("Save changes to FidoNet configuration file: sbbsecho.ini")) {
-	var result = update_sbbsecho_ini(hub, link, echolist_fname, network.areamgr);
+	var result = update_sbbsecho_ini(hub, link, domain, echolist_fname, network.areamgr);
 	if (result != true) {
 		alert(result);
 		exit(1);
@@ -665,7 +791,7 @@ if(!file_touch(system.ctrl_dir + "recycle"))
 /******************************************/
 /* SEND NODE NUMBER REQUEST NETMAIL (FTN) */
 /******************************************/
-if(your.node === 9999) {
+if(your.node === temp_node) {
 	if(confirm("Send a node number application to "	+ fidoaddr.to_str(hub))) {
 		var result = send_app_netmail(fidoaddr.to_str(hub));
 		if(typeof result !== 'boolean') {
@@ -682,13 +808,13 @@ if(your.node === 9999) {
 		}
 		if(aborted() || confirm("Come back when you have a permanently-assigned node address"))
 			exit(0);
-	}		
+	}
 }
 
 /************************/
 /* SEND AREAFIX NETMAIL */
 /************************/
-if(your.node !== 9999
+if(your.node !== temp_node
 	&& confirm("Send an AreaFix request to link EchoMail areas with "
 		+ fidoaddr.to_str(hub))) {
 	var msgbase = new MsgBase("mail");
@@ -701,12 +827,12 @@ if(your.node !== 9999
 		lines[i] = lines[i].split(/\s+/)[0];
 	}
 	if(!msgbase.save_msg({
-			to: network.areamgr || "AreaFix",
-			to_net_addr: fidoaddr.to_str(hub),
-			from: sysop,
-			from_ext: 1,
-			subject: link.AreaFixPwd
-		}, /* body text: */ lines.join('\r\n'))) {
+		to: network.areamgr || "AreaFix",
+		to_net_addr: fidoaddr.to_str(hub),
+		from: sysop,
+		from_ext: 1,
+		subject: link.AreaFixPwd
+	}, /* body text: */ lines.join('\r\n'))) {
 		alert("Error saving message: " + msgbase.last_error);
 		exit(1);
 	}
@@ -753,7 +879,7 @@ add_gfile("binkstats.ini", "Detailed BinkP (mail/file transfer) statistics");
 /***********************/
 print(netname + " initial setup completely successfully.");
 print();
-if(your.node == 9999) {
+if(your.node == temp_node) {
 	print("You used a temporary node address (" + fidoaddr.to_str(your) +
 		"). You will need to update your");
 	print("SCFG->Networks->FidoNet->Address once your permanent node address has been");
diff --git a/exec/init-tickit.ini b/exec/init-tickit.ini
index 1e3c6313d6476612097eaaf4c8a97781d2d076e2..9540144c3099a8c2d5530ceccabd5db77199b3e2 100644
--- a/exec/init-tickit.ini
+++ b/exec/init-tickit.ini
@@ -151,6 +151,11 @@ domain=scsinet
 nlmatch=scinet.*
 forcereplace=true
 
+[TQWNODE]
+domain=tqwnet
+nlmatch=tqwnet.*
+forcereplace=true
+
 [XOFCHUBSLST]
 forcereplace=true
 
diff --git a/exec/init-tickit.js b/exec/init-tickit.js
index 1b6be06ae58b40c266dbdd6a20c0e4540893c62a..7a17b6b9e7e05bd8025e45a5539d1243b072c4ed 100644
--- a/exec/init-tickit.js
+++ b/exec/init-tickit.js
@@ -3,13 +3,13 @@
  Nigel Reed - nigel@nigelreed.net sysop@endofthelinebbs.com
 
  This short crappy bit of code will create a tickit.ini file for you. You won't have
- edit it by hang any more and it's easy to add a new network or new file areas.
+ edit it by hand any more and it's easy to add a new network or new file areas.
  It will parse init-tickit.ini and automatically create /sbbs/ctrl/tickit.ini
 */
 
 "use strict";
 
-var init_ini = js.exec_dir + "init-tickit.ini";     
+var init_ini = js.exec_dir + "init-tickit.ini";
 var tickit_ini = system.ctrl_dir + "tickit.ini";
 
 var ini = [];
diff --git a/exec/lbshell.js b/exec/lbshell.js
index bc1411c87760c1cac56bf9d33e5b88029c8840da..ac6553264e0b173b30f65e289f88fd9e8b565980 100644
--- a/exec/lbshell.js
+++ b/exec/lbshell.js
@@ -715,15 +715,9 @@ while(bbs.online) {
     						console.writeln("DOORSCAN ERROR: "+e);
     						log("Error running "+xtrn_area.sec_list[curr_xtrnsec].prog_list[parseInt(x_prog)].code+" "+e);
 						}
-
-						if(xtrn_area.sec_list[curr_xtrnsec].prog_list[parseInt(x_prog)].settings & XTRN_PAUSE) {
-							console.pause();
-						}
 					}
 					else {
 						bbs.exec_xtrn(xtrn_area.sec_list[curr_xtrnsec].prog_list[parseInt(x_prog)].number);
-						if(xtrn_area.sec_list[curr_xtrnsec].prog_list[parseInt(x_prog)].settings & XTRN_PAUSE)
-							console.pause();
 					}
 					start_mouse();
 					draw_main(true);
diff --git a/exec/load/acmev2.js b/exec/load/acmev2.js
index 00ed4272f41c61c46fe7b3a6e425575f12d81db5..2a7af2ab18233eedc9a5b7751c0b86f04ab859be 100644
--- a/exec/load/acmev2.js
+++ b/exec/load/acmev2.js
@@ -40,7 +40,7 @@ require("http.js", "HTTPRequest");
 function ACMEv2(opts)
 {
 	if (opts.key === undefined)
-		throw('Need "key"!');
+		throw new Error('Need "key"!');
 
 	this.key = opts.key;
 	this.key_id = opts.key_id;
@@ -70,9 +70,9 @@ ACMEv2.prototype.get_terms_of_service = function()
 {
 	var dir = this.get_directory();
 	if (dir.meta === undefined)
-		throw('No "meta" in directory!');
+		throw new Error('No "meta" in directory!');
 	if (dir.meta.termsOfService === undefined)
-		throw('No "termsOfService" in directory metadata');
+		throw new Error('No "termsOfService" in directory metadata');
 	return dir.meta.termsOfService;
 };
 
@@ -84,7 +84,7 @@ ACMEv2.prototype.get_directory = function()
 		this.log_headers();
 		if (this.ua.response_code != 200) {
 			log(LOG_DEBUG, ret);
-			throw("Error fetching directory");
+			throw new Error("Error fetching directory");
 		}
 		this.update_nonce();
 		this.directory = JSON.parse(ret);
@@ -98,12 +98,12 @@ ACMEv2.prototype.create_new_account = function(opts)
 
 	if (this.ua.response_code != 201 && this.ua.response_code != 200) {
 		log(LOG_DEBUG, ret);
-		throw("newAccount returned "+this.ua.response_code+", not a 200 or 201 status!");
+		throw new Error("newAccount returned "+this.ua.response_code+", not a 200 or 201 status!");
 	}
 
 	if (this.ua.response_headers_parsed.Location === undefined) {
 		log(LOG_DEBUG, this.ua.response_headers.join("\n"));
-		throw("No Location header in newAccount response.");
+		throw new Error("No Location header in newAccount response.");
 	}
 	this.key_id = this.ua.response_headers_parsed.Location[0];
 	return JSON.parse(ret);
@@ -117,7 +117,7 @@ ACMEv2.prototype.update_account = function(opts)
 
 	if (this.ua.response_code != 201 && this.ua.response_code != 200) {
 		log(LOG_DEBUG, ret);
-		throw("update_account returned "+this.ua.response_code+", not a 200 or 201 status!");
+		throw new Error("update_account returned "+this.ua.response_code+", not a 200 or 201 status!");
 	}
 	return JSON.parse(ret);
 };
@@ -132,17 +132,17 @@ ACMEv2.prototype.create_new_order = function(opts)
 	var ret;
 
 	if (opts.identifiers === undefined)
-		throw("create_new_order() requires an identifier in opts");
+		throw new Error("create_new_order() requires an identifier in opts");
 	ret = this.post('newOrder', opts);
 	if (this.ua.response_code != 201) {
 		log(LOG_DEBUG, ret);
-		throw("newOrder responded with "+this.ua.response_code+" not 201");
+		throw new Error("newOrder responded with "+this.ua.response_code+" not 201");
 	}
 	ret = JSON.parse(ret);
 
 	if (this.ua.response_headers_parsed.Location === undefined) {
 		log(LOG_DEBUG, this.ua.response_headers.join("\n"));
-		throw("No Location header in 201 response.");
+		throw new Error("No Location header in 201 response.");
 	}
 	ret.Location=this.ua.response_headers_parsed.Location[0];
 
@@ -156,7 +156,7 @@ ACMEv2.prototype.accept_challenge = function(challenge)
 	var ret = this.post_url(challenge.url, opts);
 	if (this.ua.response_code != 200) {
 		log(LOG_DEBUG, ret);
-		throw("accept_challenge did not return 200");
+		throw new Error("accept_challenge did not return 200");
 	}
 	return JSON.parse(ret);
 };
@@ -187,18 +187,18 @@ ACMEv2.prototype.finalize_order = function(order, csr)
 	var opts = {};
 
 	if (order === undefined)
-		throw("Missing order");
+		throw new Error("Missing order");
 	if (csr === undefined)
-		throw("Missing csr");
+		throw new Error("Missing csr");
 	if (typeof(csr) != 'object' || csr.export_cert === undefined)
-		throw("Invalid csr");
+		throw new Error("Invalid csr");
 	opts.csr = this.base64url(csr.export_cert(CryptCert.FORMAT.CERTIFICATE));
 
 	log(LOG_DEBUG, "Finalizing order.");
 	var ret = this.post_url(order.finalize, opts);
 	if (this.ua.response_code != 200) {
 		log(LOG_DEBUG, ret);
-		throw("finalize_order did not return 200");
+		throw new Error("finalize_order did not return 200");
 	}
 
 	return JSON.parse(ret);
@@ -208,13 +208,13 @@ ACMEv2.prototype.poll_order = function(order)
 {
 	var loc = order.Location;
 	if (loc === undefined)
-		throw("No order location!");
+		throw new Error("No order location!");
 	log(LOG_DEBUG, "Polling oder.");
 	var ret = this.ua.Get(loc);
 	this.log_headers();
 	if (this.ua.response_code != 200) {
 		log(LOG_DEBUG, ret);
-		throw("order poll did not return 200");
+		throw new Error("order poll did not return 200");
 	}
 	this.update_nonce();
 
@@ -245,7 +245,7 @@ ACMEv2.prototype.get_jwk = function(key)
 	var ret = {};
 
 	if (key === undefined)
-		throw("change_key() requires a new key.");
+		throw new Error("change_key() requires a new key.");
 	/* Create the inner object signed with old key */
 	switch(key.algo) {
 		case CryptContext.ALGO.RSA:
@@ -271,13 +271,13 @@ ACMEv2.prototype.get_jwk = function(key)
 					ret.shalen = 512;
 					break;
 				default:
-					throw("Unhandled ECC curve size "+key.keysize);
+					throw new Error("Unhandled ECC curve size "+key.keysize);
 			}
 			ret.jwk.x = key.public_key.x;
 			ret.jwk.y = key.public_key.y;
 			break;
 		default:
-			throw("Unknown algorithm in new key");
+			throw new Error("Unknown algorithm in new key");
 	}
 	return ret;
 };
@@ -290,7 +290,7 @@ ACMEv2.prototype.change_key = function(new_key)
 	var jwk;
 
 	if (new_key === undefined)
-		throw("change_key() requires a new key.");
+		throw new Error("change_key() requires a new key.");
 	/* Create the inner object signed with old key */
 	jwk = this.get_jwk(new_key);
 	inner.protected.alg = jwk.alg;
@@ -304,7 +304,7 @@ ACMEv2.prototype.change_key = function(new_key)
 	ret = this.post('keyChange', inner);
 	if (this.ua.response_code != 200) {
 		log(LOG_DEBUG, ret);
-		throw("keyChange did not return 200");
+		throw new Error("keyChange did not return 200");
 	}
 	this.key = new_key;
 	return JSON.parse(ret);
@@ -357,7 +357,7 @@ ACMEv2.prototype.revoke = function(cert, reason)
 	ret = this.post('revokeCert', opts);
 	if (this.ua.response_code != 200) {
 		log(LOG_DEBUG, ret);
-		throw("revokeCert did not return 200");
+		throw new Error("revokeCert did not return 200");
 	}
 	return;
 };
@@ -392,13 +392,13 @@ ACMEv2.prototype.create_pkcs7 = function(cert)
 ACMEv2.prototype.get_cert = function(order)
 {
 	if (order.certificate === undefined)
-		throw("Order has no certificate!");
+		throw new Error("Order has no certificate!");
 	log(LOG_DEBUG, "Getting certificate.");
 	var cert = this.ua.Get(order.certificate);
 	this.log_headers();
 	if (this.ua.response_code != 200) {
 		log(LOG_DEBUG, cert);
-		throw("get_cert request did not return 200");
+		throw new Error("get_cert request did not return 200");
 	}
 	this.update_nonce();
 
@@ -417,7 +417,7 @@ ACMEv2.prototype.post = function(link, data)
 		post_method = 'post_full_jwt';
 	url = this.get_directory()[link];
 	if (url === undefined)
-		throw('Unknown link name: "'+link+'"');
+		throw new Error('Unknown link name: "'+link+'"');
 	log(LOG_DEBUG, "Calling "+link+".");
 	return this.post_url(url, data, post_method);
 };
@@ -444,7 +444,7 @@ ACMEv2.prototype.get_authorization = function(url)
 	this.log_headers();
 	if (this.ua.response_code != 200) {
 		log(LOG_DEBUG, ret);
-		throw("get_authorization request did not return 200");
+		throw new Error("get_authorization request did not return 200");
 	}
 	this.update_nonce();
 
@@ -588,7 +588,7 @@ ACMEv2.prototype.post_url = function(url, data, post_method)
 			protected.alg = jwk.alg;
 			protected.kid = this.key_id;
 			if (protected.kid === undefined)
-				throw("No key_id available!");
+				throw new Error("No key_id available!");
 			break;
 		case 'post_full_jwt':
 			protected.nonce = this.get_nonce();
diff --git a/exec/load/binkp.js b/exec/load/binkp.js
index 979b78d8597dfd32901e9041d2cca21d6acaca93..068f486989d1f8b816992da6bd72e7d1a4aba8f3 100644
--- a/exec/load/binkp.js
+++ b/exec/load/binkp.js
@@ -412,7 +412,7 @@ BinkP.prototype.connect = function(addr, password, auth_cb, port, inet_host, tls
 	this.in_keys = undefined;
 	this.out_keys = undefined;
 	if (addr === undefined)
-		throw("No address specified!");
+		throw new Error("No address specified!");
 	addr = FIDO.parse_addr(addr, this.default_zone, this.default_domain);
 
 	if (!password)
@@ -1041,7 +1041,7 @@ BinkP.prototype.recvFrame = function(timeout)
 		ret = new this.Frame();
 		i = this.sock.recv(1, timeout);
 		if (i === null) {
-			log(LOG_INFO, "Error in recv() of first byte of packet header");
+			log(LOG_INFO, "Error in recv() of first byte of packet header, timeout = " + timeout);
 			this.sock.close();
 			this.sock = undefined;
 			return undefined;
diff --git a/exec/load/fido.js b/exec/load/fido.js
index 813f97ffc6a9f93d50ef7c4e5840e8ca638e5d76..0702b7fd64f6b9b11603e4e19709060739bd1f26 100644
--- a/exec/load/fido.js
+++ b/exec/load/fido.js
@@ -74,9 +74,9 @@ var FIDO = {
 				set: function(val) {
 					net = parseInt(val, 10);
 					if (typeof net !== 'number')
-						throw('net is not a number!');
+						throw new Error('net is not a number!');
 					if (net < 0 || net > 65535)
-						throw('net out of range');
+						throw new Error('net out of range');
 				}
 			},
 			"node": {
@@ -85,7 +85,7 @@ var FIDO = {
 				set: function(val) {
 					node = parseInt(val, 10);
 					if (typeof node !== 'number')
-						throw('node is not a number!');
+						throw new Error('node is not a number!');
 					if (node < 0 || node > 65535)
 						throw ('node out of range');
 				}
@@ -103,9 +103,9 @@ var FIDO = {
 					else
 						zone = parseInt(val, 10);
 					if (typeof zone !== 'number')
-						throw('zone is not a number!');
+						throw new Error('zone is not a number!');
 					if (zone < -1 || zone > 65535)
-						throw('zone out of range');
+						throw new Error('zone out of range');
 				}
 			},
 			"point": {
@@ -121,7 +121,7 @@ var FIDO = {
 					else
 						point = parseInt(val, 10);
 					if (typeof point !== 'number')
-						throw('point is not a number!');
+						throw new Error('point is not a number!');
 					if (point < 0 || point > 65535)
 						throw ('point out of range');
 				}
@@ -139,7 +139,7 @@ var FIDO = {
 					else
 						domain = val.toString().toLowerCase().substr(0, 8);
 					if (typeof domain !== 'string')
-						throw('domain is not a string');
+						throw new Error('domain is not a string');
 				}
 			}
 		});
@@ -153,7 +153,7 @@ var FIDO = {
 		if(addr)
 			m = addr.toString().match(/^(?:([0-9]+):)?([0-9]+)\/([0-9]+)(?:\.([0-9]+))?(?:@(.*))?$/);
 		if (!m)
-			throw('invalid address '+addr);
+			throw new Error('invalid address '+addr);
 		zone = m[1];
 		domain = m[5];
 		if (zone == undefined)
@@ -174,14 +174,14 @@ var FIDO = {
 		var ext;
 
 		if (default_zone === undefined)
-			throw("Default zone unspecified");
+			throw new Error("Default zone unspecified");
 		m = path.match(/(?:\.([0-9a-f]{3,4})[\/\\])?([0-9a-f]{4})([0-9a-f]{4})\.(...)(?:[\/\\]([0-9a-f]{8})\.(...))?$/i);
 		if (m === null)
-			throw("Invalid flo file path");
+			throw new Error("Invalid flo file path");
 		ext = m[4];
 		if (m[5] != null) {
 			if (m[4].toUpperCase() !== 'PNT')
-				throw("Invalid flo file path");
+				throw new Error("Invalid flo file path");
 			ext = m[6];
 		}
 		switch(ext.toLowerCase()) {
@@ -200,7 +200,7 @@ var FIDO = {
 			case 'try':
 				break;
 			default:
-				throw("Invalid flo file path");
+				throw new Error("Invalid flo file path");
 		}
 		zone = m[1];
 		if (zone == null)
@@ -230,12 +230,12 @@ var FIDO = {
 			domain = '';
 
 		if (!f.open("r"))
-			throw("Unable to open '"+f.name+"'.");
+			throw new Error("Unable to open '"+f.name+"'.");
 
 		// Validate first line...
 		var line = f.readln(2048);
 		if (line == undefined)
-			throw("Unable to read first line in '"+f.name+"'");
+			throw new Error("Unable to read first line in '"+f.name+"'");
 		var m;
 		if ((m=line.match(/^;A (.*) Nodelist for (.*) -- Day number ([0-9]+) : ([0-9]{5})$/)) !== null) {
 			ret.domain = m[1];
@@ -502,7 +502,7 @@ Object.defineProperties(FIDO.Addr.prototype, {
 
 			// TODO: Use default zone from system.fido_addr_list[0]?
 			if (this.zone === undefined)
-				throw('zone is undefined');
+				throw new Error('zone is undefined');
 
 			// TODO: These don't need to be loaded into different objects since we're doing 5D
 			if (FIDO.FTNDomains.nodeListFN[this.domain] !== undefined && file_exists(FIDO.FTNDomains.nodeListFN[this.domain])) {
@@ -530,7 +530,7 @@ Object.defineProperties(FIDO.Addr.prototype, {
 		get: function() {
 			// TODO: Use default zone from system.fido_addr_list[0]?
 			if (this.zone === undefined)
-				throw('zone is undefined');
+				throw new Error('zone is undefined');
 
 			// TODO: These don't need to be loaded into different objects since we're doing 5D
 			if (FIDO.FTNDomains.nodeListFN[this.domain] !== undefined && file_exists(FIDO.FTNDomains.nodeListFN[this.domain])) {
@@ -643,7 +643,7 @@ FIDO.Packet.prototype.setBin = function(offset, len, val) {
 	var str = '';
 
 	if (typeof(val) !== 'number')
-		throw('Invalid setBin value type');
+		throw new Error('Invalid setBin value type');
 	for (i=0; i<len; i++) {
 		str += ascii(val & 0xff);
 		val >>= 8;
diff --git a/exec/load/fido_syscfg.js b/exec/load/fido_syscfg.js
index 3214b8109e0a0c11b52aae3dd7ebf7d1203c1949..1cdbd251cb8851c24fe066a37ba52a9ce8ec0518 100644
--- a/exec/load/fido_syscfg.js
+++ b/exec/load/fido_syscfg.js
@@ -40,7 +40,7 @@ function SBBSEchoCfg ()
 
 	ecfg = new File(file_cfgname(system.ctrl_dir, 'sbbsecho.ini'));
 	if (!ecfg.open("r"))
-		throw("Unable to open '"+ecfg.name+"'");
+		throw new Error("Unable to open '"+ecfg.name+"'");
 
 	this.inbound = backslash(ecfg.iniGetValue(null, "Inbound", "../fido/nonsecure"));
 	if (this.inbound !== null)
diff --git a/exec/load/fidocfg.js b/exec/load/fidocfg.js
index 84d7c6508ac07b0acadbf7351d73f29923087791..706b1c2dc1fa95662ffefdbd3ba2e2d918a618a0 100644
--- a/exec/load/fidocfg.js
+++ b/exec/load/fidocfg.js
@@ -66,7 +66,7 @@ function TickITCfg() {
 	}
 
 	if (!tcfg.open("r"))
-		throw("Unable to open '"+tcfg.name+"'");
+		throw new Error("Unable to open '"+tcfg.name+"'");
 	this.gcfg = tcfg.iniGetObject();
 	lcprops(this.gcfg);
 	if (this.gcfg.handler !== undefined) {
diff --git a/exec/load/frame.js b/exec/load/frame.js
index 515407c650a65d303ed860662789de3afce132b1..b221323a250ec37824e6f6ee65b0f271c382dc87 100644
--- a/exec/load/frame.js
+++ b/exec/load/frame.js
@@ -189,14 +189,14 @@ Frame.prototype.__defineSetter__("child", function(frame) {
 	if(frame instanceof Frame)
 		this.__relations__.child.push(frame);
 	else
-		throw("child not an instance of Frame()");
+		throw new Error("child not an instance of Frame()");
 });
 Frame.prototype.__defineGetter__("attr", function() {
 	return this.__properties__.attr;
 });
 Frame.prototype.__defineSetter__("attr", function(attr) {
 	if(attr !== undefined && isNaN(attr))
-		throw("invalid attribute: " + attr);
+		throw new Error("invalid attribute: " + attr);
 	this.__properties__.attr = attr;
 });
 Frame.prototype.__defineGetter__("x", function() {
@@ -208,7 +208,7 @@ Frame.prototype.__defineSetter__("x", function(x) {
 	if(x == undefined)
 		return;
 	if(!this.__checkX__(x))
-		throw("invalid x coordinate: " + x);
+		throw new Error("invalid x coordinate: " + x);
 	this.__properties__.x = Number(x);
 });
 Frame.prototype.__defineGetter__("y", function() {
@@ -220,7 +220,7 @@ Frame.prototype.__defineSetter__("y", function(y) {
 	if(y == undefined)
 		return;
 	if(!this.__checkY__(y))
-		throw("invalid y coordinate: " + y);
+		throw new Error("invalid y coordinate: " + y);
 	this.__properties__.y = Number(y);
 });
 Frame.prototype.__defineGetter__("width", function() {
@@ -232,7 +232,7 @@ Frame.prototype.__defineSetter__("width", function(width) {
 	if(width == undefined)
 		return;
 	if(!this.__checkWidth__(this.x,Number(width)))
-		throw("invalid width: " + width);
+		throw new Error("invalid width: " + width);
 	this.__properties__.width = Number(width);
 });
 Frame.prototype.__defineGetter__("height", function() {
@@ -244,7 +244,7 @@ Frame.prototype.__defineSetter__("height", function(height) {
 	if(height == undefined)
 		return;
 	if(!this.__checkHeight__(this.y,Number(height)))
-		throw("invalid height: " + height);
+		throw new Error("invalid height: " + height);
 	this.__properties__.height = Number(height);
 });
 
@@ -289,7 +289,7 @@ Frame.prototype.__defineSetter__("checkbounds", function(bool) {
 	if(typeof bool == "boolean")
 		this.__settings__.checkbounds=bool;
 	else
-		throw("non-boolean checkbounds: " + bool);
+		throw new Error("non-boolean checkbounds: " + bool);
 });
 Frame.prototype.__defineGetter__("transparent", function() {
 	return this.__settings__.transparent;
@@ -298,7 +298,7 @@ Frame.prototype.__defineSetter__("transparent", function(bool) {
 	if(typeof bool == "boolean")
 		this.__settings__.transparent=bool;
 	else
-		throw("non-boolean transparent: " + bool);
+		throw new Error("non-boolean transparent: " + bool);
 });
 Frame.prototype.__defineGetter__("lf_strict", function() {
 	return this.__settings__.lf_strict;
@@ -307,7 +307,7 @@ Frame.prototype.__defineSetter__("lf_strict", function(bool) {
 	if(typeof bool == "boolean")
 		this.__settings__.lf_strict=bool;
 	else
-		throw("non-boolean lf_strict: " + bool);
+		throw new Error("non-boolean lf_strict: " + bool);
 });
 Frame.prototype.__defineGetter__("scrollbars", function() {
 	return this.__settings__.scrollbars;
@@ -316,7 +316,7 @@ Frame.prototype.__defineSetter__("scrollbars", function(bool) {
 	if(typeof bool == "boolean")
 		this.__settings__.scrollbars=bool;
 	else
-		throw("non-boolean scrollbars: " + bool);
+		throw new Error("non-boolean scrollbars: " + bool);
 });
 Frame.prototype.__defineGetter__("v_scroll", function() {
 	return this.__settings__.v_scroll;
@@ -325,7 +325,7 @@ Frame.prototype.__defineSetter__("v_scroll", function(bool) {
 	if(typeof bool == "boolean")
 		this.__settings__.v_scroll=bool;
 	else
-		throw("non-boolean v_scroll: " + bool);
+		throw new Error("non-boolean v_scroll: " + bool);
 });
 Frame.prototype.__defineGetter__("word_wrap", function() {
 	return this.__settings__.word_wrap;
@@ -334,7 +334,7 @@ Frame.prototype.__defineSetter__("word_wrap", function(bool) {
 	if(typeof bool == "boolean")
 		this.__settings__.word_wrap=bool;
 	else
-		throw("non-boolean word_wrap: " + bool);
+		throw new Error("non-boolean word_wrap: " + bool);
 });
 Frame.prototype.__defineGetter__("h_scroll", function() {
 	return this.__settings__.h_scroll;
@@ -343,7 +343,7 @@ Frame.prototype.__defineSetter__("h_scroll", function(bool) {
 	if(typeof bool == "boolean")
 		this.__settings__.h_scroll=bool;
 	else
-		throw("non-boolean h_scroll: " + bool);
+		throw new Error("non-boolean h_scroll: " + bool);
 });
 Frame.prototype.__defineGetter__("is_open",function() {
 	return this.__properties__.open;
@@ -355,7 +355,7 @@ Frame.prototype.__defineSetter__("atcodes", function(bool) {
 	if(typeof bool == "boolean")
 		this.__settings__.atcodes=bool;
 	else
-		throw("non-boolean atcode: " + bool);
+		throw new Error("non-boolean atcode: " + bool);
 });
 
 /* public methods */
@@ -367,7 +367,7 @@ Frame.prototype.getData = function(x,y,use_offset) {
 		py += this.__position__.offset.y;
 	}
 	// if(!this.__properties__.data[py] || !this.__properties__.data[py][px])
-		// throw("Frame.getData() - invalid coordinates: " + px + "," + py);
+		// throw new Error("Frame.getData() - invalid coordinates: " + px + "," + py);
 	if(!this.__properties__.data[py] || !this.__properties__.data[py][px])
 		return new Char();
 	return this.__properties__.data[py][px];
@@ -381,7 +381,7 @@ Frame.prototype.setData = function(x,y,ch,attr,use_offset) {
 	}
 	//I don't remember why I did this, but it was probably important at the time
 	//if(!this.__properties__.data[py] || !this.__properties__.data[py][px])
-		// throw("Frame.setData() - invalid coordinates: " + px + "," + py);
+		// throw new Error("Frame.setData() - invalid coordinates: " + px + "," + py);
 	if(!this.__properties__.data[py])
 		this.__properties__.data[py] = [];
 	if(!this.__properties__.data[py][px])
@@ -820,13 +820,13 @@ Frame.prototype.load = function(filename,width,height) {
 			this.putmsg(lines.shift() + "\r\n");
 		break;
 	default:
-		throw("unsupported filetype");
+		throw new Error("unsupported filetype");
 		break;
 	}
 }
 Frame.prototype.load_bin = function(contents, width, height, offset) {
     if(width == undefined || height == undefined)
-        throw("unknown graphic dimensions");
+        throw new Error("unknown graphic dimensions");
     if(offset == undefined) offset = 0;
     for(var y=0; y<height; y++) {
         for(var x=0; x<width; x++) {
@@ -1516,7 +1516,7 @@ Display.prototype.__defineSetter__("x", function(x) {
 	if(x == undefined)
 		this.__properties__.x = 1;
 	else if(isNaN(x))
-		throw("invalid x coordinate: " + x);
+		throw new Error("invalid x coordinate: " + x);
 	else
 		this.__properties__.x = Number(x);
 });
@@ -1527,7 +1527,7 @@ Display.prototype.__defineSetter__("y", function(y) {
 	if(y == undefined)
 		this.__properties__.y = 1;
 	else if(isNaN(y) || y < 1 || y > console.screen_rows)
-		throw("invalid y coordinate: " + y);
+		throw new Error("invalid y coordinate: " + y);
 	else
 		this.__properties__.y = Number(y);
 });
@@ -1538,7 +1538,7 @@ Display.prototype.__defineSetter__("width", function(width) {
 	if(width == undefined)
 		this.__properties__.width = console.screen_columns;
 	else if(isNaN(width) || (this.x + Number(width) - 1) > (console.screen_columns))
-		throw("invalid width: " + width);
+		throw new Error("invalid width: " + width);
 	else
 		this.__properties__.width = Number(width);
 });
@@ -1549,7 +1549,7 @@ Display.prototype.__defineSetter__("height", function(height) {
 	if(height == undefined)
 		this.__properties__.height = console.screen_rows;
 	else if(isNaN(height) || (this.y + Number(height) - 1) > (console.screen_rows))
-		throw("invalid height: " + height);
+		throw new Error("invalid height: " + height);
 	else
 		this.__properties__.height = Number(height);
 });
@@ -1763,7 +1763,7 @@ function Cursor(x,y,frame) {
 	if(frame instanceof Frame)
 		this.__properties__.frame = frame;
 	else
-		throw("the frame is not a frame");
+		throw new Error("the frame is not a frame");
 
 	this.x = x;
 	this.y = y;
@@ -1774,7 +1774,7 @@ Cursor.prototype.__defineGetter__("x", function() {
 });
 Cursor.prototype.__defineSetter__("x", function(x) {
 	if(isNaN(x))
-		throw("invalid x coordinate: " + x);
+		throw new Error("invalid x coordinate: " + x);
 	this.__properties__.x = x;
 });
 Cursor.prototype.__defineGetter__("y", function() {
@@ -1782,7 +1782,7 @@ Cursor.prototype.__defineGetter__("y", function() {
 });
 Cursor.prototype.__defineSetter__("y", function(y) {
 	if(isNaN(y))
-		throw("invalid y coordinate: " + y);
+		throw new Error("invalid y coordinate: " + y);
 	this.__properties__.y = y;
 });
 
@@ -1799,7 +1799,7 @@ function Offset(x,y,frame) {
 	if(frame instanceof Frame)
 		this.__properties__.frame = frame;
 	else
-		throw("the frame is not a frame");
+		throw new Error("the frame is not a frame");
 
 	this.x = x;
 	this.y = y;
@@ -1810,7 +1810,7 @@ Offset.prototype.__defineGetter__("x", function() {
 });
 Offset.prototype.__defineSetter__("x", function(x) {
 	if(x == undefined)
-		throw("invalid x offset: " + x);
+		throw new Error("invalid x offset: " + x);
 	else if(x < 0)
 		x = 0;
 	this.__properties__.x = x;
@@ -1820,7 +1820,7 @@ Offset.prototype.__defineGetter__("y", function() {
 });
 Offset.prototype.__defineSetter__("y", function(y) {
 	if(y == undefined)
-		throw("invalid y offset: " + y);
+		throw new Error("invalid y offset: " + y);
 	else if(y < 0)
 		y = 0;
 	this.__properties__.y = y;
diff --git a/exec/load/ftn_nodelist.js b/exec/load/ftn_nodelist.js
index 05d7578e6166f75915000c618d8c74891f114cc5..c1fa6016e263bffba970f070281946a7efaad2ee 100644
--- a/exec/load/ftn_nodelist.js
+++ b/exec/load/ftn_nodelist.js
@@ -63,12 +63,12 @@ function NodeList(filename, warn)
 	});
 
 	if (!f.open("r"))
-		throw("Unable to open '"+f.name+"'.");
+		throw new Error("Unable to open '"+f.name+"'.");
 
 	// Validate first line...
 	var line = f.readln(2048);
 	if (line == undefined)
-		throw("Unable to read first line in '"+f.name+"'");
+		throw new Error("Unable to read first line in '"+f.name+"'");
 	var m;
 	if ((m=line.match(/^;A (.*) Nodelist for (.*) -- Day number ([0-9]+) : ([0-9]{5})$/)) !== null) {
 		this.domain = m[1];
diff --git a/exec/load/ftp.js b/exec/load/ftp.js
index f3fcbdcec91e35ae9acfead849194308dc11d43f..d8d70a165aaca304ee00ecc45047728ac1f15a7d 100644
--- a/exec/load/ftp.js
+++ b/exec/load/ftp.js
@@ -7,7 +7,7 @@ function FTP(host, user, pass, port, dport, bindhost, account)
 	var ret;
 
 	if (host === undefined)
-		throw("No hostname specified");
+		throw new Error("No hostname specified");
 	
 	this.revision = "JSFTP v" + "$Revision: 1.23 $".split(' ')[1];
 
@@ -40,19 +40,19 @@ function FTP(host, user, pass, port, dport, bindhost, account)
 	}
 	if (parseInt(response, 10) !== 220) {
 		this.socket.close();
-		throw("Invalid response from server: " + response);
+		throw new Error("Invalid response from server: " + response);
 	}
 	ret = parseInt(response = this.cmd("USER "+this.user, true), 10)
 	if (ret === 331)
 		ret = parseInt(response = this.cmd("PASS "+this.pass, true), 10);
 	if (ret === 332) {
 		if (this.account === undefined)
-			throw("Account required");
+			throw new Error("Account required");
 		ret = parseInt(response = this.cmd("ACCT "+this.account, true), 10);
 	}
 	if (ret !== 230 && ret != 202) {
 		this.socket.close();
-		throw("Login failed: " + response);
+		throw new Error("Login failed: " + response);
 	}
 	this.ascii = false;
 	this.passive = true;
@@ -298,7 +298,7 @@ FTP.prototype.do_sendfile = function(src, data_socket)
 	var f = new File(src);
 	if (!f.open("rb")) {
 		data_socket.close();
-		throw("Error " + f.error + " opening file '" + f.name + "'");
+		throw new Error("Error " + f.error + " opening file '" + f.name + "'");
 	}
 
 	do {
@@ -315,7 +315,7 @@ FTP.prototype.do_sendfile = function(src, data_socket)
 
 	rstr = this.cmd(undefined, true);
 	if (parseInt(rstr, 10) !== 226) {
-		throw("Data connection not closed: "+rstr);
+		throw new Error("Data connection not closed: "+rstr);
 	}
 	if (!error)
 		log(LOG_DEBUG, "Sent "+total+" bytes.");
@@ -333,7 +333,7 @@ FTP.prototype.cmd = function(cmd, needresp)
 	var done = false;
 
 	if (!this.socket.is_connected)
-		throw("Socket disconnected");
+		throw new Error("Socket disconnected");
 
 	if (cmd !== undefined) {
 		while (this.socket.data_waiting) {
@@ -343,7 +343,7 @@ FTP.prototype.cmd = function(cmd, needresp)
 		cmdline = cmd.replace(/\xff/g, "\xff\xff") + '\r\n';
 		log(LOG_DEBUG, "CMD: '"+cmd+"'");
 		if (this.socket.send(cmdline) != cmdline.length)
-			throw("Error " + this.socket.error + " sending command: '" + cmd + "'");
+			throw new Error("Error " + this.socket.error + " sending command: '" + cmd + "'");
 	}
 
 	if (needresp === true) {
@@ -355,7 +355,7 @@ FTP.prototype.cmd = function(cmd, needresp)
 				m = rd.match(/^([0-9]{3})([- ])/);
 				if (rsp === undefined) {
 					if (m === null) {
-						throw("Invalid response: "+rd);
+						throw new Error("Invalid response: "+rd);
 					}
 					rsp = m[1];
 					if (m[2] === ' ')
@@ -373,9 +373,9 @@ FTP.prototype.cmd = function(cmd, needresp)
 			}
 			else {
 				if(cmd)
-					throw("recvline timeout waiting for response to command: '" + cmd + "'");
+					throw new Error("recvline timeout waiting for response to command: '" + cmd + "'");
 				else
-					throw("recvline timeout waiting for additional response");
+					throw new Error("recvline timeout waiting for additional response");
 			}
 		} while(this.socket.is_connected && !done);
 		return ret;
@@ -399,7 +399,7 @@ FTP.prototype.data_socket = function(cmd)
 	else
 		rstr = this.cmd("TYPE I", true);
 	if (parseInt(rstr, 10) !== 200)
-		throw("Unable to create data socket: " + rstr);
+		throw new Error("Unable to create data socket: " + rstr);
 
 	ip6 = this.socket.local_ip_address.indexOf(':') !== -1;
 	if (this.passive) {
@@ -407,20 +407,20 @@ FTP.prototype.data_socket = function(cmd)
 		if (ip6) {
 			rstr = this.cmd("EPSV", true);
 			if (parseInt(rstr, 10) !== 229)
-				throw("EPSV Failed: " + rstr);
+				throw new Error("EPSV Failed: " + rstr);
 			m = rstr.match(/\(\|\|\|([0-9]+)\|\)/);
 			if (m === null)
-				throw("Unable to parse EPSV reply: " + rstr);
+				throw new Error("Unable to parse EPSV reply: " + rstr);
 			rhost = this.host;
 			rport = parseInt(m[1], 10);
 		}
 		else {
 			rstr = this.cmd("PASV", true);
 			if (parseInt(rstr, 10) !== 227)
-				throw("PASV Failed: " + rstr);
+				throw new Error("PASV Failed: " + rstr);
 			m = rstr.match(/\(([0-9]+),([0-9]+),([0-9]+),([0-9]+),([0-9]+),([0-9]+)\)/);
 			if (m === null)
-				throw("Unable to parse PASV reply: " + rstr);
+				throw new Error("Unable to parse PASV reply: " + rstr);
 			rhost = m[1] + '.' + m[2] + '.' + m[3] + '.' + m[4];
 			rport = (parseInt(m[5], 10) << 8) | parseInt(m[6], 10);
 		}
@@ -443,11 +443,11 @@ FTP.prototype.data_socket = function(cmd)
 			}
 		} catch(e) {
 			ds.close();
-			throw(e);
+			throw new Error(e);
 		}
 		if (parseInt(rstr, 10) !== 200) {
 			ds.close();
-			throw("EPRT/PORT rejected: " + rstr);
+			throw new Error("EPRT/PORT rejected: " + rstr);
 		}
 	}
 
@@ -461,14 +461,14 @@ FTP.prototype.data_socket = function(cmd)
 			// Fall-through
 		default:
 			ds.close();
-			throw(cmd+" failed: " + rstr);
+			throw new Error(cmd+" failed: " + rstr);
 	}
 
 	if (!this.passive) {
 		selret = socket_select([ds], this.timeout);
 		if (selret === null || selret.length === 0) {
 			ds.close();
-			throw("Timeout waiting for remote to connect");
+			throw new Error("Timeout waiting for remote to connect");
 		}
 		ts = ds.accept();
 		ds.close();
@@ -496,7 +496,7 @@ FTP.prototype.do_get = function(cmd, dest)
 		f = new File(dest);
 		if (!f.open("wb")) {
 			data_socket.close();
-			throw("Error " + f.error + " opening file '" + f.name + "'");
+			throw new Error("Error " + f.error + " opening file '" + f.name + "'");
 		}
 	}
 
@@ -510,14 +510,14 @@ FTP.prototype.do_get = function(cmd, dest)
 				f.write(rbuf);
 		}
 		else {
-			throw("recv timeout");
+			throw new Error("recv timeout");
 		}
 	} while(data_socket.is_connected && this.socket.is_connected);
 	data_socket.close();
 	if (f !== undefined)
 		f.close();
 	if (parseInt(this.cmd(undefined, true), 10) !== 226)
-		throw("Data connection not closed");
+		throw new Error("Data connection not closed");
 	log(LOG_DEBUG, "Received "+total+" bytes.");
 	if (dest === undefined)
 		return ret;
diff --git a/exec/load/graphic.js b/exec/load/graphic.js
index f5b8acbbf059a1125992a88b7df4a55f59423ef5..4e895b122a67c686298cb8928fb0910249ea4ae1 100644
--- a/exec/load/graphic.js
+++ b/exec/load/graphic.js
@@ -690,7 +690,7 @@ Graphic.prototype.load = function(filename, offset)
 	var l;
 
 	if(!file_type)
-		throw("unsupported file type: " + filename);
+		throw new Error("unsupported file type: " + filename);
 
 	switch(file_type.substr(1).toUpperCase()) {
 	case "ANS":
@@ -716,7 +716,7 @@ Graphic.prototype.load = function(filename, offset)
 			this.putmsg(undefined,undefined,l,true);
 		break;
 	default:
-		throw("unsupported file type: " + filename);
+		throw new Error("unsupported file type: " + filename);
 	}
 	return(true);
 };
diff --git a/exec/load/http.js b/exec/load/http.js
index 0af0a1970649dd6ff41b16157bd73e85cff812c9..40d5049bd86273712e4fc4a0dd3c910dcf31190c 100644
--- a/exec/load/http.js
+++ b/exec/load/http.js
@@ -114,39 +114,39 @@ HTTPRequest.prototype.SendRequest=function() {
 	port = this.url.port?this.url.port:(this.url.scheme=='http'?80:443);
 	if (js.global.ConnectedSocket != undefined) {
 		if ((this.sock = new ConnectedSocket(this.url.host, port)) == null)
-			throw(format("Unable to connect to %s:%u", this.url.host, this.url.port));
+			throw new Error(format("Unable to connect to %s:%u", this.url.host, this.url.port));
 	}
 	else {
 		if((this.sock=new Socket(SOCK_STREAM))==null)
-			throw("Unable to create socket");
+			throw new Error("Unable to create socket");
 		if(!this.sock.connect(this.url.host, port)) {
 			this.sock.close();
-			throw(format("Unable to connect to %s:%u", this.url.host, this.url.port));
+			throw new Error(format("Unable to connect to %s:%u", this.url.host, this.url.port));
 		}
 	}
 	if(this.url.scheme=='https')
 		this.sock.ssl_session=true;
 	if(!do_send(this.sock, this.request+"\r\n"))
-		throw("Unable to send request: " + this.request);
+		throw new Error("Unable to send request: " + this.request);
 	for(i in this.request_headers) {
 		if(!do_send(this.sock, this.request_headers[i]+"\r\n"))
-			throw("Unable to send headers");
+			throw new Error("Unable to send headers");
 	}
 	if(!do_send(this.sock, "\r\n"))
-		throw("Unable to terminate headers");
+		throw new Error("Unable to terminate headers");
 	if(this.body != undefined) {
 		if(!do_send(this.sock, this.body))
-			throw("Unable to send body");
+			throw new Error("Unable to send body");
 	}
 };
 
 HTTPRequest.prototype.ReadStatus=function() {
 	this.status_line=this.sock.recvline(4096);
 	if(this.status_line==null)
-		throw("Unable to read status");
+		throw new Error("Unable to read status");
 	var m = this.status_line.match(/^HTTP\/[0-9]+\.[0-9]+ ([0-9]{3})/);
 	if (m === null)
-		throw("Unable to parse status line '"+this.status_line+"'");
+		throw new Error("Unable to parse status line '"+this.status_line+"'");
 	this.response_code = parseInt(m[1], 10);
 };
 
@@ -159,7 +159,7 @@ HTTPRequest.prototype.ReadHeaders=function() {
 	for(;;) {
 		header=this.sock.recvline(4096, 120);
 		if(header==null)
-			throw("Unable to receive headers");
+			throw new Error("Unable to receive headers");
 		if(header=='')
 			return;
 		this.response_headers.push(header);
diff --git a/exec/load/json-chat.js b/exec/load/json-chat.js
index 1fcf59391988804e4dcf3064e1a77b41bc92014d..a71373ad7468ef559987a6010d2b2716861c7e20 100644
--- a/exec/load/json-chat.js
+++ b/exec/load/json-chat.js
@@ -38,7 +38,7 @@ function JSONChat(usernum,jsonclient,host,port) {
 			
 		if(!this.client) {
 			if(!host || isNaN(port))
-				throw("invalid client arguments");
+				throw new Error("invalid client arguments");
 			this.client = new JSONClient(host,port);
 		}
 		if(!this.client.connect()) {
@@ -48,7 +48,7 @@ function JSONChat(usernum,jsonclient,host,port) {
 		if(this.nick)
 			this.client.subscribe("chat","channels." + this.nick.name + ".messages");
 		else
-			throw("invalid user number");
+			throw new Error("invalid user number");
 		for(var c in this.channels) 
 			this.join(c.name);
 		return true;
diff --git a/exec/load/mouse_getkey.js b/exec/load/mouse_getkey.js
index 890cb093d28aba456884d88536ac27590e9f5144..6a368f8274291210c225ed67ae1206ee1cee9deb 100644
--- a/exec/load/mouse_getkey.js
+++ b/exec/load/mouse_getkey.js
@@ -18,7 +18,7 @@ function mouse_getkey(mode, timeout, enabled)
 	var safe_mode = mode & ~(K_UPPER|K_UPRLWR|K_NUMBER|K_ALPHA|K_NOEXASC);
 
 	if (safe_mode != mode) {
-		throw("Invalid mode "+mode+" for mouse_getkey()");
+		throw new Error("Invalid mode "+mode+" for mouse_getkey()");
 	}
 	
 	function mouse_enable(enable)
diff --git a/exec/load/recordfile.js b/exec/load/recordfile.js
index b788647c199b02de9b5c2e79f7d12fd1d0753317..52195269144ddd4ac6c07b619989837dc9055591 100644
--- a/exec/load/recordfile.js
+++ b/exec/load/recordfile.js
@@ -116,7 +116,7 @@ RecordFileRecord.prototype.flushRead = function(keeplocked)
 		}
 		this.parent.file.close();
 		if (!this.parent.file.open('rb+', true, this.parent.RecordLength)) {
-			throw('Unable to re-open '+this.parent.file.name);
+			throw new Error('Unable to re-open '+this.parent.file.name);
 		}
 		if (locked) {
 			this.lock();
diff --git a/exec/load/require.js b/exec/load/require.js
index 53b486b99910871a96d04019a6d0e41ea0f983ea..bcfbc2e25fd1ee1a5183b209b2f2dc1438e5d891 100644
--- a/exec/load/require.js
+++ b/exec/load/require.js
@@ -3,7 +3,7 @@ if (eval('typeof('+argv[2]+') !== "undefined" ? true : false'))
 	js.global.require_module_file = 'dummy.js';
 load(js.global.require_module_file);
 if (eval('typeof('+argv[2]+') == "undefined"'))
-	throw("ERROR: load()ing "+argv[1]+" didn't define symbol \""+argv[2]+"\"");
+	throw new Error("ERROR: load()ing "+argv[1]+" didn't define symbol \""+argv[2]+"\"");
 if (argv[0] === 'undefined') {
 	Object.defineProperties(this, {
 		"argv": {
diff --git a/exec/load/rss-atom.js b/exec/load/rss-atom.js
index 78dd53e563e2a26437714bc7f5d75934d2736c50..8400bbad13e66648760e71d967ec1cc605a9b497 100644
--- a/exec/load/rss-atom.js
+++ b/exec/load/rss-atom.js
@@ -119,11 +119,15 @@ const Item = function (i) {
 	this.title = i.title.length() ? i.title[0].toString() : ''; // uh ...
 	this.date = i.pubDate.length() ? i.pubDate[0].toString() : (i.updated.length() ? i.updated[0].toString() : '');
 	this.author = i.author.length() ? i.author.toString() : '';
-	this.body = i.description.length() ? i.description[0].toString() : (i.summary.length() ? i.summary[0].toString() : '');
+	this.body = '';
 	this.content = i.encoded.length() ? i.encoded.toString() : '';
 	this.link = i.link.length() ? skipsp(truncsp(i.link[0].toString())) : '';
 	this.enclosures = [];
 
+	if (i.description.length()) this.body += i.description[0].toString();
+	if (i.summary.length()) this.body += i.summary[0].toString();
+	if (i.content.length()) this.body += i.content[0].toString();
+
 	var enclosures = i.enclosure.length();
 	for (var n = 0; n < enclosures; n++) {
 		this.enclosures.push({
@@ -155,6 +159,14 @@ const Channel = function (c) {
 
 }
 
+function toLocal(x) {
+    for each(var e in x) {
+        e.setName(e.localName());
+        toLocal(e);
+    }
+    return x;
+}
+
 const Feed = function (url, follow_redirects) {
 
 	this.channels = [];
@@ -176,8 +188,8 @@ const Feed = function (url, follow_redirects) {
 			}
 			httpRequest = undefined;
 		}
-		var feed = new XML(doc.replace(/^<\?xml.*\?>/g, ""));
-		doc = undefined;
+        var feed = toLocal(new XML(doc.replace(/^<\?xml.*\?>/g, "")));
+        doc = undefined;
 		switch (feed.localName()) {
 			case "rss":
 				var channels = feed.channel.length();
diff --git a/exec/load/sbbsdefs.js b/exec/load/sbbsdefs.js
index 82d078dd197df3705a295a5ecfabc16e867edcd1..c8f071d887c8185c144ebc13e93117b18822970d 100644
--- a/exec/load/sbbsdefs.js
+++ b/exec/load/sbbsdefs.js
@@ -198,6 +198,7 @@ var   K_USEOFFSET	=(1<<20);	/* Use console.getstr_offset with getstr()	*/
 var   K_NOSPIN      =(1<<21);	/* Do not honor user's spinning cursor		*/
 var   K_ANSI_CPR	=(1<<22);	/* ANSI Cursor Position Report expected		*/
 var   K_TRIM        =(1<<23);   /* Trim white-space from both ends of str   */
+var   K_CTRLKEYS	=(1<<24);	/* No control-key handling/eating in inkey()*/
 					    		/********************************************/
 
 						    	/********************************************/
@@ -205,7 +206,7 @@ var   K_TRIM        =(1<<23);   /* Trim white-space from both ends of str   */
 							    /********************************************/
 var   P_NONE		=0;			/* No special behavior						*/
 var   P_NOABORT  	=(1<<0);	/* Disallows abortion of a message          */
-var   P_SAVEATR		=(1<<1);	/* Save the new current attributres after	*/
+var   P_SAVEATR		=(1<<1);	/* Save the new current attributes after	*/
 					    		/* msg has printed							*/
 var   P_NOATCODES	=(1<<2);	/* Don't allow @ codes                      */
 var   P_OPENCLOSE	=(1<<3);	/* Open and close the file					*/
@@ -221,6 +222,8 @@ var   P_WRAP        =(1<<12);   /* Wrap/split long-lines, ungracefully      */
 var   P_UTF8        =(1<<13);	/* Message is UTF-8 encoded                 */
 var   P_AUTO_UTF8	=(1<<14);	/* Message may be UTF-8, auto-detect		*/
 var   P_NOXATTRS	=(1<<15);	/* No "Extra Attribute Codes" supported		*/
+var   P_MARKUP		=(1<<16);	/* Support StyleCodes/Rich/StructuredText	*/
+var   P_HIDEMARKS	=(1<<17);	/* Hide the mark-up tags					*/
 							    /********************************************/
 
     							/********************************************/
diff --git a/exec/load/sprite.js b/exec/load/sprite.js
index dc9a1f8d7c56508797da5232ed21b6614b3b8195..da160c1e87432ee8414a8792c65cd1140729a706 100644
--- a/exec/load/sprite.js
+++ b/exec/load/sprite.js
@@ -436,10 +436,10 @@ Sprite.Aerial = function(fileName, parentFrame, x, y, bearing, position) {
 	if(!file_exists(fileName + ".ini"))
 		fileName = js.exec_dir + "sprites/" + fileName;
 	if(!file_exists(fileName + ".ini")) {
-		throw("Sprite file missing: " + fileName + ".ini");
+		throw new Error("Sprite file missing: " + fileName + ".ini");
 	}
 	if(!file_exists(fileName + ".bin")) {
-		throw("Sprite file missing: " + fileName + ".bin");
+		throw new Error("Sprite file missing: " + fileName + ".bin");
 	}
 
 	this.x = x;
@@ -958,10 +958,10 @@ Sprite.Profile = function(fileName, parentFrame, x, y, bearing, position) {
 	if(!file_exists(fileName + ".ini"))
 		fileName = js.exec_dir + "sprites/" + fileName;
 	if(!file_exists(fileName + ".ini")) {
-		throw("Sprite file missing: " + fileName + ".ini");
+		throw new Error("Sprite file missing: " + fileName + ".ini");
 	}
 	if(!file_exists(fileName + ".bin")) {
-		throw("Sprite file missing: " + fileName + ".bin");
+		throw new Error("Sprite file missing: " + fileName + ".bin");
 	}
 
 	this.x = x;
diff --git a/exec/load/text.js b/exec/load/text.js
index 0294ac23814cb84158dca9b7209e1f0c642d1407..247263d9ac2e5b1bf034aa7adb018b8b820bffed 100644
--- a/exec/load/text.js
+++ b/exec/load/text.js
@@ -849,7 +849,8 @@ var SpinningCursor6=839;
 var SpinningCursor7=840;
 var SpinningCursor8=841;
 var SpinningCursor9=842;
+var HowManyColumns=843;
 
-var TOTAL_TEXT=843;
+var TOTAL_TEXT=844;
 
 this;
diff --git a/exec/load/tree.js b/exec/load/tree.js
index e3ffbcaecb76c6b76f2f2494d58c999b03cf945a..44ca5cf8882c8735bb8a7ea6aa301dfc9823eb66 100644
--- a/exec/load/tree.js
+++ b/exec/load/tree.js
@@ -204,7 +204,7 @@ Tree.prototype.__defineSetter__("frame",function(frame) {
 		return true;
 	}
 	else {
-		throw("frame parameter must be a Frame() object");
+		throw new Error("frame parameter must be a Frame() object");
 	}
 });
 Tree.prototype.__defineGetter__("items",function() {
@@ -216,7 +216,7 @@ Tree.prototype.__defineSetter__("items",function(items) {
 		return true;
 	}
 	else {
-		throw("items parameter must be an array");
+		throw new Error("items parameter must be an array");
 	}
 });
 Tree.prototype.__defineGetter__("parent",function() {
diff --git a/exec/xtrn_sec.js b/exec/xtrn_sec.js
index 55d7f156b824d1f490cb08c58bf7b6d299628f84..f312d5e904e21e13c78e8f041bb96e021feff914 100644
--- a/exec/xtrn_sec.js
+++ b/exec/xtrn_sec.js
@@ -94,11 +94,6 @@ function exec_xtrn(prog)
 	load('fonts.js', 'default');
 	if(options.eval_after_exec)
 		eval(options.eval_after_exec);
-
-	if(prog.settings&XTRN_PAUSE)
-		console.pause();
-	else
-		console.line_counter=0;
 }
 
 function external_program_menu(xsec)
@@ -130,13 +125,20 @@ function external_program_menu(xsec)
 		if(options.clear_screen)
 			console.clear(LIGHTGRAY);
 
-		var secnum = xtrn_area.sec_list[xsec].number+1
+		var secnum = xtrn_area.sec_list[xsec].number+1;
+		var seccode = xtrn_area.sec_list[xsec].code;
 		if(bbs.menu_exists("xtrn" + secnum + "_head")) {
 			bbs.menu("xtrn" + secnum + "_head");
 		}
+		else if(bbs.menu_exists("xtrn" + seccode + "_head")) {
+			bbs.menu("xtrn" + seccode + "_head");
+		}
 		if(bbs.menu_exists("xtrn" + secnum)) {
 			bbs.menu("xtrn" + secnum);
 		}
+		else if(bbs.menu_exists("xtrn" + seccode)) {
+			bbs.menu("xtrn" + seccode);
+		}
 		else {
 			var multicolumn = options.multicolumn && prog_list.length > options.singlecolumn_height;
 			if(options.sort)
diff --git a/install/GNUmakefile b/install/GNUmakefile
index c944b26b8d765d8d05cea06cdeba2db008dac03d..3b7980c035075744ae872ab7e14eb1422ab9e6c8 100644
--- a/install/GNUmakefile
+++ b/install/GNUmakefile
@@ -160,13 +160,13 @@ baja:	run sbbs3
 	$(MAKE) -C $(SBBSDIR)/exec $(MKFLAGS) BAJAPATH=$(REPODIR)/src/sbbs3/$(CCPRE).$(machine).exe.$(BUILDPATH)/baja
 
 sbj:	run
-	$(MAKE) -C $(REPODIR)/xtrn/sbj $(MKFLAGS)
+	$(MAKE) -C $(SBBSDIR)/xtrn/sbj $(MKFLAGS)
 
 dpoker:	run
-	$(MAKE) -C $(REPODIR)/xtrn/dpoker $(MKFLAGS) SBBS_SRC=$(REPODIR)/src/sbbs3/ XPDEV=$(REPODIR)/src/xpdev/
+	$(MAKE) -C $(SBBSDIR)/xtrn/dpoker $(MKFLAGS) SBBS_SRC=$(REPODIR)/src/sbbs3/ XPDEV=$(REPODIR)/src/xpdev/
 
 tbd:	run
-	$(MAKE) -C $(REPODIR)/xtrn/tbd $(MKFLAGS) SBBS_SRC=$(REPODIR)/src/sbbs3/ XPDEV=$(REPODIR)/src/xpdev/
+	$(MAKE) -C $(SBBSDIR)/xtrn/tbd $(MKFLAGS) SBBS_SRC=$(REPODIR)/src/sbbs3/ XPDEV=$(REPODIR)/src/xpdev/
 
 gtkuseredit:	src
 ifdef USE_GLADE
diff --git a/src/build/Common.gmake b/src/build/Common.gmake
index fd6af34f9774aa0b22c5ceb1f99468826ba3b93e..71b5f0d0d82755900cde42d3fe7a61028dff8179 100644
--- a/src/build/Common.gmake
+++ b/src/build/Common.gmake
@@ -328,7 +328,7 @@ endif
 
 # PThread-specific flags
 ifeq ($(os),linux)    # Linux
- CFLAGS	+=	-DPOSIX_C_SOURCE=200809L -D_DEFAULT_SOURCE -D_BSD_SOURCE -DSPEED_MACROS_ONLY -D_GNU_SOURCE
+ CFLAGS	+=	-DPOSIX_C_SOURCE=200809L -D_DEFAULT_SOURCE -D_BSD_SOURCE -DSPEED_MACROS_ONLY -D_GNU_SOURCE -D_FILE_OFFSET_BITS=64
  ifndef THREADS_ACTUALLY_WORK
   CFLAGS    += -D_THREAD_SUID_BROKEN
  endif
diff --git a/src/conio/ansi_cio.c b/src/conio/ansi_cio.c
index 5f87ec5b9ba419daa587ec94a7c7f646f6680dc0..7d12107ce3b8aecfd3c0e2b97f972e026876738f 100644
--- a/src/conio/ansi_cio.c
+++ b/src/conio/ansi_cio.c
@@ -44,6 +44,16 @@
 	struct termios tio_default;				/* Initial term settings */
 #endif
 
+#ifdef _WIN32
+	#ifndef ENABLE_VIRTUAL_TERMINAL_INPUT
+	#define ENABLE_VIRTUAL_TERMINAL_INPUT 0x0200
+	#endif
+	#ifndef ENABLE_VIRTUAL_TERMINAL_PROCESSING
+	#define ENABLE_VIRTUAL_TERMINAL_PROCESSING 0x0004
+	#endif
+#endif
+
+
 #include "ciolib.h"
 #include "ansi_cio.h"
 
@@ -931,10 +941,12 @@ int ansi_initio_cb(void)
 {
 #ifdef _WIN32
 	if(isatty(fileno(stdin))) {
-		if(!SetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), 0))
+		if(!SetConsoleMode(GetStdHandle(STD_INPUT_HANDLE), ENABLE_VIRTUAL_TERMINAL_INPUT))
 			return(0);
 
-		if(!SetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), 0))
+		DWORD conmode = 0;
+		GetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), &conmode);
+		if(!SetConsoleMode(GetStdHandle(STD_OUTPUT_HANDLE), conmode | ENABLE_VIRTUAL_TERMINAL_PROCESSING))
 			return(0);
 	}
 	setmode(fileno(stdout),_O_BINARY);
diff --git a/src/sbbs3/addfiles.c b/src/sbbs3/addfiles.c
index 5dabc3ba37fd3d33ad653c5e483a955d496e421c..d087ae9f6b3c068cd1d6e53f07b60475d0ac6659 100644
--- a/src/sbbs3/addfiles.c
+++ b/src/sbbs3/addfiles.c
@@ -91,7 +91,7 @@ void prep_desc(char *str)
 			tmp[j++]=str[i];
 		else if(j && str[i]<=' ' && str[i] > 0&& tmp[j-1]==' ')
 			continue;
-		else if(i && !isalnum((uchar)str[i]) && str[i]==str[i-1])
+		else if(i && !IS_ALPHANUMERIC(str[i]) && str[i]==str[i-1])
 			continue;
 		else if(str[i]>=' ' || str[i]<0)
 			tmp[j++]=str[i];
@@ -248,7 +248,7 @@ bool get_file_diz(file_t* f, const char* filepath, char* ext)
 		sprintf(tmpext,"%.256s",ext);
 		prep_desc(tmpext);
 		for(i=0;tmpext[i];i++)
-			if(isalpha((uchar)tmpext[i]))
+			if(IS_ALPHA(tmpext[i]))
 				break;
 		sprintf(f->desc,"%.*s",LEN_FDESC,tmpext+i);
 		for(i=0;(f->desc[i]>=' ' || f->desc[i]<0) && i<LEN_FDESC;i++)
@@ -411,7 +411,7 @@ void addlist(char *inpath, file_t f, uint dskip, uint sskip)
 		else				   /* no space after filename? */
 			continue;
 #endif
-		if(!isalnum(*fname)) {	// filename doesn't begin with an alpha-numeric char?
+		if(!IS_ALPHANUMERIC(*fname)) {	// filename doesn't begin with an alpha-numeric char?
 			continue;
 		}
 		SAFEPRINTF2(filepath,"%s%s",cur_altpath ? scfg.altpath[cur_altpath-1]
@@ -756,7 +756,7 @@ int main(int argc, char **argv)
 		mode|=AUTO_ADD;
 		i=0;
 	} else {
-		if(!isalnum((uchar)argv[1][0]) && argc==2) {
+		if(!IS_ALPHANUMERIC(argv[1][0]) && argc==2) {
 			puts(usage);
 			return(1);
 		}
@@ -862,7 +862,7 @@ int main(int argc, char **argv)
 						return(1);
 			}
 		}
-		else if(isdigit((uchar)argv[j][0])) {
+		else if(IS_DIGIT(argv[j][0])) {
 			if(desc_offset==0)
 				desc_offset=atoi(argv[j]);
 			else
@@ -872,9 +872,9 @@ int main(int argc, char **argv)
 		else if(argv[j][0]=='+') {      /* filelist - FILES.BBS */
 			listgiven=1;
 			if(argc > j+1
-				&& isdigit((uchar)argv[j+1][0])) { /* skip x characters before description */
+				&& IS_DIGIT(argv[j+1][0])) { /* skip x characters before description */
 				if(argc > j+2
-					&& isdigit((uchar)argv[j+2][0])) { /* skip x characters before size */
+					&& IS_DIGIT(argv[j+2][0])) { /* skip x characters before size */
 					addlist(argv[j]+1,f,atoi(argv[j+1]),atoi(argv[j+2]));
 					j+=2;
 				}
diff --git a/src/sbbs3/allusers.c b/src/sbbs3/allusers.c
index 4d5ea513c7fdeaa837321149bf0ae6949b7dae62..ef8d3525f15357d3303a9ceac27d4f5848b220dd 100644
--- a/src/sbbs3/allusers.c
+++ b/src/sbbs3/allusers.c
@@ -182,22 +182,22 @@ int main(int argc, char **argv)
 				case 'F':                       /* Set required flags */
 					j=3;
 					set=1;
-					if(isdigit(argv[i][2]))
+					if(IS_DIGIT(argv[i][2]))
 						set=argv[i][2]&0xf;
 					else
 						j=2;
 					for(;argv[i][j];j++)
-						if(isalpha(argv[i][j]))
+						if(IS_ALPHA(argv[i][j]))
 							reqflags[set-1]|=FLAG(toupper(argv[i][j]));
 					break;
 				case 'R':                       /* Set required restrictions */
 					for(j=2;argv[i][j];j++)
-						if(isalpha(argv[i][j]))
+						if(IS_ALPHA(argv[i][j]))
 							reqrest|=FLAG(toupper(argv[i][j]));
 					break;
 				case 'E':                       /* Set required exemptions */
 					for(j=2;argv[i][j];j++)
-						if(isalpha(argv[i][j]))
+						if(IS_ALPHA(argv[i][j]))
 							reqexempt|=FLAG(toupper(argv[i][j]));
 					break;
 				default:						/* Unrecognized include */
@@ -210,7 +210,7 @@ int main(int argc, char **argv)
 				case 'F':   /* flags */
 					j=3;
 					set=1;
-					if(isdigit(argv[i][2]))
+					if(IS_DIGIT(argv[i][2]))
 						set=argv[i][2]&0xf;
 					else
 						j=2;
@@ -221,7 +221,7 @@ int main(int argc, char **argv)
 						sub=1; 
 					}
 					for(;argv[i][j];j++)
-						if(isalpha(argv[i][j]))
+						if(IS_ALPHA(argv[i][j]))
 							flags|=FLAG(toupper(argv[i][j]));
 					SAFEPRINTF(str,"%suser.dat",dir);
 					if(!fexistcase(str) || (file=sopen(str,O_RDWR|O_BINARY,SH_DENYNO))==-1) {
@@ -285,7 +285,7 @@ int main(int argc, char **argv)
 						sub=1; 
 					}
 					for(;argv[i][j];j++)
-						if(isalpha(argv[i][j]))
+						if(IS_ALPHA(argv[i][j]))
 							flags|=FLAG(toupper(argv[i][j]));
 					SAFEPRINTF(str,"%suser.dat",dir);
 					if(!fexistcase(str) || (file=sopen(str,O_RDWR|O_BINARY,SH_DENYNO))==-1) {
diff --git a/src/sbbs3/ansiterm.cpp b/src/sbbs3/ansiterm.cpp
index 7077b60af01568a484a39ebc17f69cadf7b28b43..d130acb177e9198cf99a35fc23ff5bfea2ea4f56 100644
--- a/src/sbbs3/ansiterm.cpp
+++ b/src/sbbs3/ansiterm.cpp
@@ -210,7 +210,8 @@ char* sbbs_t::ansi(int atr, int curatr, char* str)
 
 void sbbs_t::ansi_getlines()
 {
-	if(sys_status&SS_USERON && useron.misc&ANSI && !useron.rows /* Auto-detect rows */
+	if(sys_status&SS_USERON && useron.misc&ANSI
+		&& (useron.rows == TERM_ROWS_AUTO || useron.cols == TERM_COLS_AUTO)
 		&& online==ON_REMOTE) {									/* Remote */
 		SYNC;
 		putcom("\x1b[s\x1b[255B\x1b[255C\x1b[6n\x1b[u");
@@ -242,7 +243,7 @@ bool sbbs_t::ansi_getxy(int* x, int* y)
             	rsp++;
 				start=time(NULL);
 			}
-            else if(isdigit(ch) && rsp==2) {
+            else if(IS_DIGIT(ch) && rsp==2) {
 				if(y!=NULL) {
                		(*y)*=10;
 					(*y)+=(ch&0xf);
@@ -253,7 +254,7 @@ bool sbbs_t::ansi_getxy(int* x, int* y)
             	rsp++;
 				start=time(NULL);
 			}
-            else if(isdigit(ch) && rsp==3) {
+            else if(IS_DIGIT(ch) && rsp==3) {
 				if(x!=NULL) {
             		(*x)*=10;
 					(*x)+=(ch&0xf);
diff --git a/src/sbbs3/answer.cpp b/src/sbbs3/answer.cpp
index 38f3471164cd9b2ef92d3df069b4d159a535e9a4..f09930cda9d89eadcb3550712393274c02f1e719 100644
--- a/src/sbbs3/answer.cpp
+++ b/src/sbbs3/answer.cpp
@@ -109,7 +109,6 @@ bool sbbs_t::answer()
 				useron.number=matchuser(&cfg, rlogin_name, /* sysop_alias: */FALSE);
 			if(useron.number) {
 				getuserdat(&cfg,&useron);
-				useron.misc&=~TERM_FLAGS;
 				SAFEPRINTF(path,"%srlogin.cfg",cfg.ctrl_dir);
 				if(!findstr(client.addr,path)) {
 					SAFECOPY(tmp, rlogin_pass);
@@ -158,7 +157,7 @@ bool sbbs_t::answer()
 							badlogin(useron.alias, tmp);
 							bputs(text[InvalidLogon]);
 						}
-						lprintf(LOG_WARNING,"!CLIENT IP NOT LISTED in %s", path);
+						lprintf(LOG_DEBUG,"!CLIENT IP (%s) NOT LISTED in %s", client.addr, path);
 						useron.number=0;
 						hangup();
 					}
@@ -213,7 +212,6 @@ bool sbbs_t::answer()
 		useron.number=matchuser(&cfg, rlogin_name, /* sysop_alias: */FALSE);
 		if(useron.number) {
 			getuserdat(&cfg,&useron);
-			useron.misc&=~TERM_FLAGS;
 			for(i=0;i<3 && online;i++) {
 				if(stricmp(tmp,useron.pass)) {
 					if(cfg.sys_misc&SM_ECHO_PW)
@@ -390,6 +388,7 @@ bool sbbs_t::answer()
 		else
 			SAFECOPY(terminal,"DUMB");
 	}
+	update_nodeterm();
 
 	/* AutoLogon via IP or Caller ID here */
 	if(!useron.number && !(sys_status&SS_RLOGIN)
diff --git a/src/sbbs3/ars.c b/src/sbbs3/ars.c
index 8eb049dfbcbb6047f178b02fec6d1d119368e509..d03edddd557f28ffbce19795e5e698cc238ca55f 100644
--- a/src/sbbs3/ars.c
+++ b/src/sbbs3/ars.c
@@ -102,7 +102,7 @@ uchar* arstr(ushort* count, const char* str, scfg_t* cfg, uchar* ar_buf)
 		if(str[i]=='&')
 			continue;
 
-		if(isalpha(str[i])) {
+		if(IS_ALPHA(str[i])) {
 			p=np=str+i;
 			SKIP_ALPHA(np);
 			n=np-p;
@@ -248,7 +248,7 @@ uchar* arstr(ushort* count, const char* str, scfg_t* cfg, uchar* ar_buf)
 			continue; 
 		}
 
-		if(!arg_expected && isalpha(str[i])) {
+		if(!arg_expected && IS_ALPHA(str[i])) {
 			n=i;
 			if(!strnicmp(str+i,"AGE",3)) {
 				artype=AR_AGE;
@@ -550,7 +550,7 @@ uchar* arstr(ushort* count, const char* str, scfg_t* cfg, uchar* ar_buf)
 			ar[j++]=AR_EQUAL;
 		not=equal=0;
 
-		if(artype==AR_FLAG1 && isdigit(str[i])) {   /* flag set specified */
+		if(artype==AR_FLAG1 && IS_DIGIT(str[i])) {   /* flag set specified */
 			switch(str[i]) {
 				case '2':
 					artype=AR_FLAG2;
@@ -567,15 +567,15 @@ uchar* arstr(ushort* count, const char* str, scfg_t* cfg, uchar* ar_buf)
 
 		arg_expected=FALSE;
 
-		if(artype==AR_SUB && !isdigit(str[i]))
+		if(artype==AR_SUB && !IS_DIGIT(str[i]))
 			artype=AR_SUBCODE;
-		if(artype==AR_DIR && !isdigit(str[i]))
+		if(artype==AR_DIR && !IS_DIGIT(str[i]))
 			artype=AR_DIRCODE;
 
 		if(artype==AR_INVALID)
 			artype=AR_LEVEL;
 		ar[j++]=artype;
-		if(isdigit(str[i]) && !ar_string_arg(artype)) {
+		if(IS_DIGIT(str[i]) && !ar_string_arg(artype)) {
 			if(artype==AR_TIME) {
 				n=atoi(str+i)*60;
 				p=strchr(str+i,':');
@@ -583,7 +583,7 @@ uchar* arstr(ushort* count, const char* str, scfg_t* cfg, uchar* ar_buf)
 					n+=atoi(p+1);
 				*((short *)(ar+j))=n;
 				j+=2;
-				while(isdigit(str[i+1]) || str[i+1]==':') i++;
+				while(IS_DIGIT(str[i+1]) || str[i+1]==':') i++;
 				continue; 
 			}
 			n=atoi(str+i);
@@ -635,7 +635,7 @@ uchar* arstr(ushort* count, const char* str, scfg_t* cfg, uchar* ar_buf)
 					j--;
 					break; 
 			}
-			while(isdigit(str[i+1])) i++;
+			while(IS_DIGIT(str[i+1])) i++;
 			continue; 
 		}
 		maxlen=128;
@@ -681,7 +681,7 @@ uchar* arstr(ushort* count, const char* str, scfg_t* cfg, uchar* ar_buf)
 				}
 				else        /* Unknown sub-board */
 					j--;
-				while(isalpha(str[i+1])) i++;
+				while(IS_ALPHA(str[i+1])) i++;
 				break;
 			case AR_DIR:
 				for(n=0;n<(uint)cfg->total_dirs;n++)
@@ -693,7 +693,7 @@ uchar* arstr(ushort* count, const char* str, scfg_t* cfg, uchar* ar_buf)
 				}
 				else        /* Unknown directory */
 					j--;
-				while(isalpha(str[i+1])) i++;
+				while(IS_ALPHA(str[i+1])) i++;
 				break;
 			case AR_DAY:
 				if(toupper(str[i])=='S' 
@@ -712,7 +712,7 @@ uchar* arstr(ushort* count, const char* str, scfg_t* cfg, uchar* ar_buf)
 				else if(toupper(str[i])=='F')               /* Friday */
 					ar[j++]=5;
 				else ar[j++]=6;                             /* Saturday */
-				while(isalpha(str[i+1])) i++;
+				while(IS_ALPHA(str[i+1])) i++;
 				break;
 			default:	/* Badly formed ARS, digit expected */
 				j--;
diff --git a/src/sbbs3/atcodes.cpp b/src/sbbs3/atcodes.cpp
index b1ed9e9cb8cc4a9fbe9ed0eee27a5d6c801bb457..a51b5aa9c72b0e3abccbd6b774f11041e38aa678 100644
--- a/src/sbbs3/atcodes.cpp
+++ b/src/sbbs3/atcodes.cpp
@@ -52,7 +52,7 @@ static char* separate_thousands(const char* src, char *dest, size_t maxlen, char
 	if(strlen(src) * 1.3 > maxlen)
 		return (char*)src;
 	const char* tail = src;
-	while(*tail && isdigit(*tail))
+	while(*tail && IS_DIGIT(*tail))
 		tail++;
 	if(tail == src)
 		return (char*)src;
@@ -174,13 +174,13 @@ int sbbs_t::show_atcode(const char *instr, JSObject* obj)
 	if(p!=NULL) {
 		char* lp = p;
 		lp++;	// skip the '|' or '-'
-		while(*lp == '>'|| isalpha((uchar)*lp))
+		while(*lp == '>'|| IS_ALPHA(*lp))
 			lp++;
 		if(*lp)
 			width_specified = true;
-		while(*lp && !isdigit((uchar)*lp))
+		while(*lp && !IS_DIGIT(*lp))
 			lp++;
-		if(*lp && isdigit((uchar)*lp)) {
+		if(*lp && IS_DIGIT(*lp)) {
 			disp_len=atoi(lp);
 			width_specified = true;
 		}
diff --git a/src/sbbs3/baja.c b/src/sbbs3/baja.c
index b572083c842022d1409e77c45b3a8c84d572faa1..654a4dc0ff114c9fdd97b10a29f43595a2de5928 100644
--- a/src/sbbs3/baja.c
+++ b/src/sbbs3/baja.c
@@ -148,7 +148,7 @@ int32_t val(char *src, char *p)
 	static int inside;
 	int32_t l;
 
-	if(isdigit((uchar)*p) || *p=='-')      /* Dec, Hex, or Oct */
+	if(IS_DIGIT(*p) || *p=='-')      /* Dec, Hex, or Oct */
 		l=strtol(p,&p,0);
 	else if(*p=='\'') {  /* Char */
 		p++;
@@ -230,11 +230,11 @@ void writecstr(char *p)
 			continue; }
 		if(*p=='\\')    { /* escape */
 			p++;
-			if(isdigit((uchar)*p)) {
+			if(IS_DIGIT(*p)) {
 				sprintf(tmp,"%.3s",p);
 				str[j]=atoi(tmp); 		/* decimal, NOT octal */
-				if(isdigit((uchar)*(++p))) 	/* skip up to 3 digits */
-					if(isdigit((uchar)*(++p)))
+				if(IS_DIGIT(*(++p))) 	/* skip up to 3 digits */
+					if(IS_DIGIT(*(++p)))
 						p++;
 				j++;
 				continue; }
@@ -311,7 +311,7 @@ void newvar(char* src, char *in)
 	char name[128];
 	int32_t i,l;
 
-	if(isdigit((uchar)*in)) {
+	if(IS_DIGIT(*in)) {
 		printf("!SYNTAX ERROR (illegal variable name):\n");
 		printf(linestr,src,line,(char*)in);
 		bail(1); 
@@ -377,7 +377,7 @@ int32_t isvar(char *arg)
 	char name[128],*p;
 	int32_t i,l;
 
-	if(!arg || !(*arg) || isdigit((uchar)*arg))
+	if(!arg || !(*arg) || IS_DIGIT(*arg))
 		return(0);
 
 	sprintf(name,"%.80s",arg);
@@ -426,7 +426,7 @@ void expdefs(char *line)
 			continue; }
 
 		for(sp=p;*sp;sp++)
-			if(!isalnum((uchar)*sp) && *sp!='_')
+			if(!IS_ALPHANUMERIC(*sp) && *sp!='_')
 				break;
 		sav[0]=*sp; 		/* Save delimiter */
 		sav[1]=0;
@@ -1185,7 +1185,7 @@ void compile(char *src)
 
 		if(!stricmp(p,"COMPARE_INT_VAR") ||
 			(!stricmp(p,"COMPARE")
-				&& (isdigit((uchar)*arg2) || atol(arg2) || *arg2=='\'' || *arg2=='.'))) {
+				&& (IS_DIGIT(*arg2) || atol(arg2) || *arg2=='\'' || *arg2=='.'))) {
 			if(!(*arg)) break;
 
 			fputc(CS_VAR_INSTRUCTION,out);
@@ -1290,7 +1290,7 @@ void compile(char *src)
 
 		if(!stricmp(p,"ADD_INT_VAR")
 			|| (!stricmp(p,"ADD")
-				&& (isdigit((uchar)*arg2) || atol(arg2) || *arg2=='\'' || *arg2=='.'))) {
+				&& (IS_DIGIT(*arg2) || atol(arg2) || *arg2=='\'' || *arg2=='.'))) {
 			if(!(*arg)) break;
 			fputc(CS_VAR_INSTRUCTION,out);
 			fputc(ADD_INT_VAR,out);
@@ -1318,7 +1318,7 @@ void compile(char *src)
 
 		if(!stricmp(p,"SUB_INT_VAR")
 			|| (!stricmp(p,"SUB")
-				&& (isdigit((uchar)*arg2) || atol(arg2) || *arg2=='\'' || *arg2=='.'))) {
+				&& (IS_DIGIT(*arg2) || atol(arg2) || *arg2=='\'' || *arg2=='.'))) {
 			if(!(*arg)) break;
 			fputc(CS_VAR_INSTRUCTION,out);
 			fputc(SUB_INT_VAR,out);
@@ -1346,7 +1346,7 @@ void compile(char *src)
 
 		if(!stricmp(p,"MUL_INT_VAR")
 			|| (!stricmp(p,"MUL")
-				&& (isdigit((uchar)*arg2) || atol(arg2) || *arg2=='\'' || *arg2=='.'))) {
+				&& (IS_DIGIT(*arg2) || atol(arg2) || *arg2=='\'' || *arg2=='.'))) {
 			if(!(*arg)) break;
 			fputc(CS_VAR_INSTRUCTION,out);
 			fputc(MUL_INT_VAR,out);
@@ -1374,7 +1374,7 @@ void compile(char *src)
 
 		if(!stricmp(p,"DIV_INT_VAR")
 			|| (!stricmp(p,"DIV")
-				&& (isdigit((uchar)*arg2) || atol(arg2) || *arg2=='\'' || *arg2=='.'))) {
+				&& (IS_DIGIT(*arg2) || atol(arg2) || *arg2=='\'' || *arg2=='.'))) {
 			if(!(*arg)) break;
 			fputc(CS_VAR_INSTRUCTION,out);
 			fputc(DIV_INT_VAR,out);
@@ -1402,7 +1402,7 @@ void compile(char *src)
 
 		if(!stricmp(p,"MOD_INT_VAR")
 			|| (!stricmp(p,"MOD")
-				&& (isdigit((uchar)*arg2) || atol(arg2) || *arg2=='\'' || *arg2=='.'))) {
+				&& (IS_DIGIT(*arg2) || atol(arg2) || *arg2=='\'' || *arg2=='.'))) {
 			if(!(*arg)) break;
 			fputc(CS_VAR_INSTRUCTION,out);
 			fputc(MOD_INT_VAR,out);
@@ -1430,7 +1430,7 @@ void compile(char *src)
 
 		if(!stricmp(p,"AND_INT_VAR")
 			|| (!stricmp(p,"AND")
-				&& (isdigit((uchar)*arg2) || atol(arg2) || *arg2=='\'' || *arg2=='.'))) {
+				&& (IS_DIGIT(*arg2) || atol(arg2) || *arg2=='\'' || *arg2=='.'))) {
 			if(!(*arg)) break;
 			fputc(CS_VAR_INSTRUCTION,out);
 			fputc(AND_INT_VAR,out);
@@ -1474,7 +1474,7 @@ void compile(char *src)
 
 		if(!stricmp(p,"OR_INT_VAR")
 			|| (!stricmp(p,"OR")
-				&& (isdigit((uchar)*arg2) || atol(arg2) || *arg2=='\'' || *arg2=='.'))) {
+				&& (IS_DIGIT(*arg2) || atol(arg2) || *arg2=='\'' || *arg2=='.'))) {
 			if(!(*arg)) break;
 			fputc(CS_VAR_INSTRUCTION,out);
 			fputc(OR_INT_VAR,out);
@@ -1500,7 +1500,7 @@ void compile(char *src)
 
 		if(!stricmp(p,"NOT_INT_VAR")
 			|| (!stricmp(p,"NOT")
-				&& (isdigit((uchar)*arg2) || atol(arg2) || *arg2=='\'' || *arg2=='.'))) {
+				&& (IS_DIGIT(*arg2) || atol(arg2) || *arg2=='\'' || *arg2=='.'))) {
 			if(!(*arg)) break;
 			fputc(CS_VAR_INSTRUCTION,out);
 			fputc(NOT_INT_VAR,out);
@@ -1526,7 +1526,7 @@ void compile(char *src)
 
 		if(!stricmp(p,"XOR_INT_VAR")
 			|| (!stricmp(p,"XOR")
-				&& (isdigit((uchar)*arg2) || atol(arg2) || *arg2=='\'' || *arg2=='.'))) {
+				&& (IS_DIGIT(*arg2) || atol(arg2) || *arg2=='\'' || *arg2=='.'))) {
 			if(!(*arg)) break;
 			fputc(CS_VAR_INSTRUCTION,out);
 			fputc(XOR_INT_VAR,out);
@@ -1678,7 +1678,7 @@ void compile(char *src)
 			if(!(*arg)) break;
 
 			fputc(CS_FIO_FUNCTION,out);
-			if(!(*arg3) || isdigit((uchar)*arg3) || atoi(arg3))
+			if(!(*arg3) || IS_DIGIT(*arg3) || atoi(arg3))
 				fputc(FIO_READ,out);
 			else
 				fputc(FIO_READ_VAR,out);
@@ -1691,7 +1691,7 @@ void compile(char *src)
 			if(p)
 				*p=0;
 			writecrc(src,arg2); 		/* Variable */
-			if(isdigit((uchar)*arg3))
+			if(IS_DIGIT(*arg3))
 				i=val(src,arg3);	 /* Length */
 			else
 				i=0;
@@ -1703,7 +1703,7 @@ void compile(char *src)
 		if(!stricmp(p,"FWRITE")) {
 			if(!(*arg)) break;
 			fputc(CS_FIO_FUNCTION,out);
-			if(!(*arg3) || isdigit((uchar)*arg3) || atoi(arg3))
+			if(!(*arg3) || IS_DIGIT(*arg3) || atoi(arg3))
 				fputc(FIO_WRITE,out);
 			else
 				fputc(FIO_WRITE_VAR,out);
@@ -1716,7 +1716,7 @@ void compile(char *src)
 			if(p)
 				*p=0;
 			writecrc(src,arg2); 		/* Variable */
-			if(isdigit((uchar)*arg3))
+			if(IS_DIGIT(*arg3))
 				i=val(src,arg3);	 /* Length */
 			else
 				i=0;
@@ -1769,7 +1769,7 @@ void compile(char *src)
 		if(!stricmp(p,"FSET_POS") || !stricmp(p,"FSEEK")) {
 			if(!(*arg)) break;
 			fputc(CS_FIO_FUNCTION,out);
-			if(isdigit((uchar)*arg2) || atol(arg2))
+			if(IS_DIGIT(*arg2) || atol(arg2))
 				fputc(FIO_SEEK,out);
 			else
 				fputc(FIO_SEEK_VAR,out);
@@ -1781,7 +1781,7 @@ void compile(char *src)
 			p=strchr(arg2,' ');
 			if(p)
 				*p=0;
-			if(atol(arg2) || isdigit((uchar)*arg2)) {
+			if(atol(arg2) || IS_DIGIT(*arg2)) {
 				l=val(src,arg2);
 				fwrite(&l,4,1,out); }
 			else
@@ -1800,7 +1800,7 @@ void compile(char *src)
 		if(!stricmp(p,"FLOCK")) {
 			if(!(*arg)) break;
 			fputc(CS_FIO_FUNCTION,out);
-			if(isdigit((uchar)*arg2) || atol(arg2))
+			if(IS_DIGIT(*arg2) || atol(arg2))
 				fputc(FIO_LOCK,out);
 			else
 				fputc(FIO_LOCK_VAR,out);
@@ -1809,7 +1809,7 @@ void compile(char *src)
 				break;
 			*p=0;
 			writecrc(src,arg);			/* File handle */
-			if(atol(arg2) || isdigit((uchar)*arg2)) {
+			if(atol(arg2) || IS_DIGIT(*arg2)) {
 				l=val(src,arg2);
 				if(!l)
 					break;
@@ -1820,7 +1820,7 @@ void compile(char *src)
 		if(!stricmp(p,"FUNLOCK")) {
 			if(!(*arg)) break;
 			fputc(CS_FIO_FUNCTION,out);
-			if(isdigit((uchar)*arg2) || atol(arg2))
+			if(IS_DIGIT(*arg2) || atol(arg2))
 				fputc(FIO_UNLOCK,out);
 			else
 				fputc(FIO_UNLOCK_VAR,out);
@@ -1829,7 +1829,7 @@ void compile(char *src)
 				break;
 			*p=0;
 			writecrc(src,arg);			/* File handle */
-			if(atol(arg2) || isdigit((uchar)*arg2)) {
+			if(atol(arg2) || IS_DIGIT(*arg2)) {
 				l=val(src,arg2);
 				if(!l)
 					break;
@@ -1840,7 +1840,7 @@ void compile(char *src)
 		if(!stricmp(p,"FSET_LENGTH")) {
 			if(!(*arg)) break;
 			fputc(CS_FIO_FUNCTION,out);
-			if(isdigit((uchar)*arg2) || atol(arg2))
+			if(IS_DIGIT(*arg2) || atol(arg2))
 				fputc(FIO_SET_LENGTH,out);
 			else
 				fputc(FIO_SET_LENGTH_VAR,out);
@@ -1849,7 +1849,7 @@ void compile(char *src)
 				break;
 			*p=0;
 			writecrc(src,arg);			/* File handle */
-			if(atol(arg2) || isdigit((uchar)*arg2)) {
+			if(atol(arg2) || IS_DIGIT(*arg2)) {
 				l=val(src,arg2);
 				fwrite(&l,4,1,out); }
 			else
@@ -2548,7 +2548,7 @@ void compile(char *src)
 		if(!stricmp(p,"GETSTR")) {
 			p=strchr(arg,' ');
 			if(p) *p=0;
-			if((!(*arg) || isdigit((uchar)*arg) || !stricmp(arg,"STR")) && !(*arg3))
+			if((!(*arg) || IS_DIGIT(*arg) || !stricmp(arg,"STR")) && !(*arg3))
 				fprintf(out,"%c%c",CS_GETSTR,atoi(arg) ? atoi(arg)
 					: *arg2 ? atoi(arg2) : 128);
 			else {
@@ -2577,7 +2577,7 @@ void compile(char *src)
 			if(!(*arg)) break;
 			p=strchr(arg,' ');
 			if(p) *p=0;
-			if(isdigit((uchar)*arg)) {
+			if(IS_DIGIT(*arg)) {
 				i=val(src,arg);
 				fprintf(out,"%c",CS_GETNUM);
 				fwrite(&i,2,1,out); }
@@ -2612,7 +2612,7 @@ void compile(char *src)
 		if(!stricmp(p,"GETLINE")) {
 			p=strchr(arg,' ');
 			if(p) *p=0;
-			if(!(*arg) || isdigit((uchar)*arg))
+			if(!(*arg) || IS_DIGIT(*arg))
 				fprintf(out,"%c%c",CS_GETLINE,*arg ? atoi(arg) :128);
 			else {
 				if((l=isvar(arg2))!=0) {
@@ -2632,7 +2632,7 @@ void compile(char *src)
 		if(!stricmp(p,"GETSTRUPR")) {
 			p=strchr(arg,' ');
 			if(p) *p=0;
-			if(!(*arg) || isdigit((uchar)*arg))
+			if(!(*arg) || IS_DIGIT(*arg))
 				fprintf(out,"%c%c",CS_GETSTRUPR,*arg ? atoi(arg) :128);
 			else {
 				if((l=isvar(arg2))!=0) {
@@ -2652,7 +2652,7 @@ void compile(char *src)
 		if(!stricmp(p,"GETNAME")) {
 			p=strchr(arg,' ');
 			if(p) *p=0;
-			if(!(*arg) || isdigit((uchar)*arg))
+			if(!(*arg) || IS_DIGIT(*arg))
 				fprintf(out,"%c%c",CS_GETNAME,*arg ? atoi(arg) :25);
 			else {
 				if((l=isvar(arg2))!=0) {
@@ -2673,7 +2673,7 @@ void compile(char *src)
 			if(!(*arg)) break;
 			p=strchr(arg,' ');
 			if(p) *p=0;
-			if(isdigit((uchar)*arg))
+			if(IS_DIGIT(*arg))
 				fprintf(out,"%c%c",CS_SHIFT_STR,atoi(arg));
 			else {
 				if((l=isvar(arg2))!=0) {
diff --git a/src/sbbs3/build.bat b/src/sbbs3/build.bat
index 160182e46a6dfa3af7cbdaa9a269cf863379e25b..dd8dde46a9146cbd2d10d81862ed4c63b0f3f7e0 100755
--- a/src/sbbs3/build.bat
+++ b/src/sbbs3/build.bat
@@ -2,5 +2,5 @@
 setlocal
 rem *** Requires Microsoft Visual C++ 2019 ***
 call "%VS160COMNTOOLS%\VsMSBuildCmd.bat"
-msbuild sbbs3.sln /p:Platform="Win32" %1 %2 %3 %4 %5
+msbuild sbbs3.sln /p:Platform="Win32" %*
 if errorlevel 1 echo. & echo !ERROR(s) occurred & exit /b 1
\ No newline at end of file
diff --git a/src/sbbs3/chat.cpp b/src/sbbs3/chat.cpp
index 1d68f5209226aa858c2fbfce1ac8bc275adffa17..321c907bd00b5c7822e40a09cd5a56d5ab15c67a 100644
--- a/src/sbbs3/chat.cpp
+++ b/src/sbbs3/chat.cpp
@@ -1571,7 +1571,7 @@ void sbbs_t::guruchat(char* line, char* gurubuf, int gurunum, char* last_answer)
 	j=strlen(line);
 	k=0;
 	for(i=0;i<j;i++) {
-		if(line[i]<0 || !isalnum((uchar)line[i])) {
+		if(line[i]<0 || !IS_ALPHANUMERIC(line[i])) {
 			if(!k)	/* beginning non-alphanumeric */
 				continue;
 			if(line[i]==line[i+1])	/* redundant non-alnum */
@@ -1584,7 +1584,7 @@ void sbbs_t::guruchat(char* line, char* gurubuf, int gurunum, char* last_answer)
 	cstr[k]=0;
 	while(k) {
 		k--;
-		if(!isalnum((uchar)cstr[k]))
+		if(!IS_ALPHANUMERIC(cstr[k]))
 			continue;
 		break; 
 	}
@@ -1786,8 +1786,8 @@ void sbbs_t::guruchat(char* line, char* gurubuf, int gurunum, char* last_answer)
 			if(action!=NODE_MCHT) {
 				for(i=0;i<k;i++) {
 					if(i && mistakes && theanswer[i]!=theanswer[i-1] &&
-						((!isalnum((uchar)theanswer[i]) && !sbbs_random(100))
-						|| (isalnum((uchar)theanswer[i]) && !sbbs_random(30)))) {
+						((!IS_ALPHANUMERIC(theanswer[i]) && !sbbs_random(100))
+						|| (IS_ALPHANUMERIC(theanswer[i]) && !sbbs_random(30)))) {
 						c=j=((uint)sbbs_random(3)+1);	/* 1 to 3 chars */
 						if(c<strcspn(theanswer+(i+1),"\0., "))
 							c=j=1;
@@ -1909,7 +1909,7 @@ bool sbbs_t::guruexp(char **ptrptr, char *line)
 		if((**ptrptr)==')')
 			break;
 		c=0;
-		while((**ptrptr) && isspace(**ptrptr))
+		while((**ptrptr) && IS_WHITESPACE(**ptrptr))
 			(*ptrptr)++;
 		while((**ptrptr)!='|' && (**ptrptr)!='&' && (**ptrptr)!=')' &&(**ptrptr)) {
 			str[c++]=(**ptrptr);
@@ -1948,13 +1948,13 @@ bool sbbs_t::guruexp(char **ptrptr, char *line)
 		else {
 			cp=strstr(line,str);
 			if(cp && c) {
-				if(cp!=line || isalnum((uchar)*(cp+strlen(str))))
+				if(cp!=line || IS_ALPHANUMERIC(*(cp+strlen(str))))
 					cp=0; 
 			}
 			else {	/* must be isolated word */
 				while(cp)
-					if((cp!=line && isalnum((uchar)*(cp-1)))
-						|| isalnum((uchar)*(cp+strlen(str))))
+					if((cp!=line && IS_ALPHANUMERIC(*(cp-1)))
+						|| IS_ALPHANUMERIC(*(cp+strlen(str))))
 						cp=strstr(cp+strlen(str),str);
 					else
 						break; 
diff --git a/src/sbbs3/clean.bat b/src/sbbs3/clean.bat
new file mode 100644
index 0000000000000000000000000000000000000000..9eb3574c9fcaf174f306859a78bc0b093520f449
--- /dev/null
+++ b/src/sbbs3/clean.bat
@@ -0,0 +1,4 @@
+@echo off
+setlocal
+call build.bat /t:Clean
+call release.bat /t:Clean
\ No newline at end of file
diff --git a/src/sbbs3/con_out.cpp b/src/sbbs3/con_out.cpp
index 1c2ed232e850608ca3a279e2f4d66d0bc1280434..fb1f43b15cb8cf06ac6bf77de93f0a1c6b13129e 100644
--- a/src/sbbs3/con_out.cpp
+++ b/src/sbbs3/con_out.cpp
@@ -178,7 +178,7 @@ size_t sbbs_t::bstrlen(const char *str, long mode)
 /* Perform PETSCII terminal output translation (from ASCII/CP437) */
 unsigned char cp437_to_petscii(unsigned char ch)
 {
-	if(isalpha(ch))
+	if(IS_ALPHA(ch))
 		return ch ^ 0x20;	/* swap upper/lower case */
 	switch(ch) {
 		case '\1':		return '@';
@@ -260,7 +260,7 @@ int sbbs_t::petscii_to_ansibbs(unsigned char ch)
 {
 	if((ch&0xe0) == 0xc0)	/* "Codes $60-$7F are, actually, copies of codes $C0-$DF" */
 		ch = 0x60 | (ch&0x1f);
-	if(isalpha(ch))
+	if(IS_ALPHA(ch))
 		return outchar(ch ^ 0x20);	/* swap upper/lower case */
 	switch(ch) {
 		case '\r':					newline();		break;
@@ -556,6 +556,32 @@ const char* sbbs_t::term_charset(long term)
 	return "CP437";
 }
 
+/****************************************************************************/
+/* For node spying purposes													*/
+/****************************************************************************/
+bool sbbs_t::update_nodeterm(void)
+{
+	str_list_t	ini = strListInit();
+	iniSetInteger(&ini, ROOT_SECTION, "cols", cols, NULL);
+	iniSetInteger(&ini, ROOT_SECTION, "rows", rows, NULL);
+	iniSetString(&ini, ROOT_SECTION, "type", term_type(), NULL);
+	iniSetString(&ini, ROOT_SECTION, "chars", term_charset(), NULL);
+	iniSetHexInt(&ini, ROOT_SECTION, "flags", term_supports(), NULL);
+	iniSetHexInt(&ini, ROOT_SECTION, "mouse", mouse_mode, NULL);
+	iniSetHexInt(&ini, ROOT_SECTION, "console", console, NULL);
+
+	char path[MAX_PATH + 1];
+	SAFEPRINTF(path, "%sterminal.ini", cfg.node_dir);
+	FILE* fp = iniOpenFile(path, /* create: */TRUE);
+	bool result = false;
+	if(fp != NULL) {
+		result = iniWriteFile(fp, ini);
+		iniCloseFile(fp);
+	}
+	strListFree(&ini);
+	return result;
+}
+
 /****************************************************************************/
 /* Outputs character														*/
 /* Performs terminal translations (e.g. EXASCII-to-ASCII, FF->ESC[2J)		*/
@@ -1038,7 +1064,7 @@ void sbbs_t::ctrl_a(char x)
 		cursor_right((uchar)x-0x7f);
 		return;
 	}
-	if(isdigit(x)) {	/* background color */
+	if(IS_DIGIT(x)) {	/* background color */
 		atr &= (BG_BRIGHT|BLINK|0x0f);
 	}
 	switch(toupper(x)) {
diff --git a/src/sbbs3/ctrl/MailCfgDlgUnit.cpp b/src/sbbs3/ctrl/MailCfgDlgUnit.cpp
index 7b862b42ea88054820a8d6137c8882c1599bfa71..96b40c132259fb6b28dda9b0bc783f360ae7b0c3 100644
--- a/src/sbbs3/ctrl/MailCfgDlgUnit.cpp
+++ b/src/sbbs3/ctrl/MailCfgDlgUnit.cpp
@@ -95,10 +95,10 @@ void __fastcall TMailCfgDlg::FormShow(TObject *Sender)
         MaxMsgsWaitingEdit->Text="N/A";
     else
         MaxMsgsWaitingEdit->Text=AnsiString(MainForm->mail_startup.max_msgs_waiting);
-    if(MainForm->mail_startup.lines_per_yield == 0)
-        LinesPerYieldEdit->Text="N/A";
+    if(MainForm->mail_startup.max_concurrent_connections == 0)
+        MaxConConEdit->Text="N/A";
     else
-        LinesPerYieldEdit->Text=AnsiString(MainForm->mail_startup.lines_per_yield);
+        MaxConConEdit->Text=AnsiString((int)MainForm->mail_startup.max_concurrent_connections);
 
     AutoStartCheckBox->Checked=MainForm->MailAutoStart;
     LogFileCheckBox->Checked=MainForm->MailLogFile;
@@ -234,7 +234,7 @@ void __fastcall TMailCfgDlg::OKBtnClick(TObject *Sender)
     MainForm->mail_startup.max_msgs_waiting=MaxMsgsWaitingEdit->Text.ToIntDef(0);
     MainForm->mail_startup.max_delivery_attempts=DeliveryAttemptsEdit->Text.ToIntDef(MAIL_DEFAULT_MAX_DELIVERY_ATTEMPTS);
     MainForm->mail_startup.rescan_frequency=RescanFreqEdit->Text.ToIntDef(MAIL_DEFAULT_RESCAN_FREQUENCY);
-    MainForm->mail_startup.lines_per_yield=LinesPerYieldEdit->Text.ToIntDef(0);
+    MainForm->mail_startup.max_concurrent_connections=MaxConConEdit->Text.ToIntDef(0);
 
     SAFECOPY(MainForm->mail_startup.default_charset
         ,DefCharsetEdit->Text.c_str());
diff --git a/src/sbbs3/ctrl/MailCfgDlgUnit.dfm b/src/sbbs3/ctrl/MailCfgDlgUnit.dfm
index c4533d5f35d5a95610a64f481648befabe93bee9..41200ee03aea661eba9300870d56dcc5ce561c64 100644
--- a/src/sbbs3/ctrl/MailCfgDlgUnit.dfm
+++ b/src/sbbs3/ctrl/MailCfgDlgUnit.dfm
@@ -1,6 +1,6 @@
 object MailCfgDlg: TMailCfgDlg
-  Left = 1213
-  Top = 393
+  Left = 1274
+  Top = 822
   BorderStyle = bsDialog
   Caption = 'Mail Server Configuration'
   ClientHeight = 246
@@ -53,8 +53,8 @@ object MailCfgDlg: TMailCfgDlg
     Top = 3
     Width = 278
     Height = 199
-    ActivePage = POP3TabSheet
-    TabIndex = 2
+    ActivePage = GeneralTabSheet
+    TabIndex = 0
     TabOrder = 3
     object GeneralTabSheet: TTabSheet
       Caption = 'General'
@@ -84,13 +84,13 @@ object MailCfgDlg: TMailCfgDlg
         AutoSize = False
         Caption = 'Max Inactivity'
       end
-      object LinesPerYieldLabel: TLabel
+      object MaxConConLabel: TLabel
         Left = 7
         Top = 140
         Width = 85
         Height = 19
         AutoSize = False
-        Caption = 'Lines Per Yield'
+        Caption = 'Max Con-Conn'
       end
       object MaxMsgsLabel: TLabel
         Left = 7
@@ -178,14 +178,12 @@ object MailCfgDlg: TMailCfgDlg
         ShowHint = True
         TabOrder = 10
       end
-      object LinesPerYieldEdit: TEdit
+      object MaxConConEdit: TEdit
         Left = 92
         Top = 140
         Width = 39
         Height = 21
-        Hint = 
-          'Number of lines of message text sent/received between time-slice' +
-          ' yields'
+        Hint = 'Maximum Concurrent Connections from same IP (0=unlimited)'
         ParentShowHint = False
         ShowHint = True
         TabOrder = 6
diff --git a/src/sbbs3/ctrl/MailCfgDlgUnit.h b/src/sbbs3/ctrl/MailCfgDlgUnit.h
index c6598a4cdade958402ed0a30c4bc81e53aacb29b..258490faee238dae494057ce0f7eb58620f551b7 100644
--- a/src/sbbs3/ctrl/MailCfgDlgUnit.h
+++ b/src/sbbs3/ctrl/MailCfgDlgUnit.h
@@ -112,8 +112,8 @@ __published:
 	TEdit *BLHeaderEdit;
 	TLabel *BLSubjectLabel;
 	TLabel *BLHeaderLabel;
-	TEdit *LinesPerYieldEdit;
-	TLabel *LinesPerYieldLabel;
+    TEdit *MaxConConEdit;
+    TLabel *MaxConConLabel;
 	TLabel *MaxRecipientsLabel;
 	TEdit *MaxRecipientsEdit;
 	TButton *DNSBLExemptionsButton;
diff --git a/src/sbbs3/ctrl/TelnetCfgDlgUnit.cpp b/src/sbbs3/ctrl/TelnetCfgDlgUnit.cpp
index ed2f1b368524f207722c3aab289368f1254a5f06..60e9ab7ff82f5896cd480ac2f6c23697e6c03ebc 100644
--- a/src/sbbs3/ctrl/TelnetCfgDlgUnit.cpp
+++ b/src/sbbs3/ctrl/TelnetCfgDlgUnit.cpp
@@ -83,7 +83,10 @@ void __fastcall TTelnetCfgDlg::FormShow(TObject *Sender)
 
 	FirstNodeEdit->Text=AnsiString((int)MainForm->bbs_startup.first_node);
 	LastNodeEdit->Text=AnsiString((int)MainForm->bbs_startup.last_node);
-    MaxConConEdit->Text=AnsiString((int)MainForm->bbs_startup.max_concurrent_connections);
+    if(MainForm->bbs_startup.max_concurrent_connections == 0)
+        MaxConConEdit->Text="N/A";
+    else
+        MaxConConEdit->Text=AnsiString((int)MainForm->bbs_startup.max_concurrent_connections);
     AutoStartCheckBox->Checked=MainForm->SysAutoStart;
     AnswerSoundEdit->Text=AnsiString(MainForm->bbs_startup.answer_sound);
     HangupSoundEdit->Text=AnsiString(MainForm->bbs_startup.hangup_sound);
diff --git a/src/sbbs3/dat_rec.c b/src/sbbs3/dat_rec.c
index 0cec87f4adc5ee05577fc93a5767f606254234ba..b8ccaf78a08d6b4e7c57ba847c292eb703d9000e 100644
--- a/src/sbbs3/dat_rec.c
+++ b/src/sbbs3/dat_rec.c
@@ -41,7 +41,7 @@
 /* Places into 'strout' CR or ETX terminated string starting at             */
 /* 'start' and ending at 'start'+'length' or terminator from 'strin'        */
 /****************************************************************************/
-void DLLCALL getrec(const char *strin,int start,int length,char *strout)
+int DLLCALL getrec(const char *strin,int start,int length,char *strout)
 {
     int i=0,stop;
 
@@ -52,6 +52,7 @@ void DLLCALL getrec(const char *strin,int start,int length,char *strout)
 		strout[i++]=strin[start++]; 
 	}
 	strout[i]=0;
+	return i;
 }
 
 /****************************************************************************/
diff --git a/src/sbbs3/dat_rec.h b/src/sbbs3/dat_rec.h
index e60dd510291d8152f4de56b09f5808aeef8cc62a..b266c0bd049ba9e7bcf5c863b9303218d4b80a72 100644
--- a/src/sbbs3/dat_rec.h
+++ b/src/sbbs3/dat_rec.h
@@ -70,7 +70,7 @@
 extern "C" {
 #endif
 
-DLLEXPORT void	DLLCALL getrec(const char *instr,int start,int length,char *outstr); /* Retrieve a record from a string */
+DLLEXPORT int	DLLCALL getrec(const char *instr,int start,int length,char *outstr); /* Retrieve a record from a string */
 DLLEXPORT void	DLLCALL putrec(char *outstr,int start,int length,char *instr); /* Place a record into a string */
 
 #ifdef __cplusplus
diff --git a/src/sbbs3/date_str.c b/src/sbbs3/date_str.c
index 310aff910194e4a4e1c8ce95c249824e6869505e..c2f2b4d38422e8678c18ce410ad51858017c8049 100644
--- a/src/sbbs3/date_str.c
+++ b/src/sbbs3/date_str.c
@@ -54,18 +54,18 @@ time32_t DLLCALL dstrtounix(scfg_t* cfg, const char *instr)
 	if(!instr[0] || !strncmp(instr,"00/00/00",8))
 		return(0);
 
-	if(isdigit(instr[0]) && isdigit(instr[1])
-		&& isdigit(instr[3]) && isdigit(instr[4])
-		&& isdigit(instr[6]) && isdigit(instr[7]))
+	if(IS_DIGIT(instr[0]) && IS_DIGIT(instr[1])
+		&& IS_DIGIT(instr[3]) && IS_DIGIT(instr[4])
+		&& IS_DIGIT(instr[6]) && IS_DIGIT(instr[7]))
 		p=instr;	/* correctly formatted */
 	else {
 		p=instr;	/* incorrectly formatted */
-		while(*p && isdigit(*p)) p++;
+		while(*p && IS_DIGIT(*p)) p++;
 		if(*p==0)
 			return(0);
 		p++;
 		day=p;
-		while(*p && isdigit(*p)) p++;
+		while(*p && IS_DIGIT(*p)) p++;
 		if(*p==0)
 			return(0);
 		p++;
diff --git a/src/sbbs3/download.cpp b/src/sbbs3/download.cpp
index e953f8ebfb0c2e7a3cc36963628873b7e2ec651b..cabc7730049567086e01385d74fc21929518c4db 100644
--- a/src/sbbs3/download.cpp
+++ b/src/sbbs3/download.cpp
@@ -264,7 +264,8 @@ int sbbs_t::protocol(prot_t* prot, enum XFER_TYPE type
 /****************************************************************************/
 void sbbs_t::autohangup()
 {
-    char	a,c,k;
+    int		a,c;
+	char	k;
 	char 	tmp[512];
 
 	if(online!=ON_REMOTE)
diff --git a/src/sbbs3/exec.cpp b/src/sbbs3/exec.cpp
index 770aabcc377cf1c0710fb81b071e96d7402a6c95..f864952c7177a32fe42d350b3c1402ee063478d6 100644
--- a/src/sbbs3/exec.cpp
+++ b/src/sbbs3/exec.cpp
@@ -696,13 +696,11 @@ long sbbs_t::js_execfile(const char *cmd, const char* startup_dir, JSObject* sco
 	JS_ExecuteScript(js_cx, js_scope, js_script, &rval);
 	sys_status &=~ SS_ABORT;
 
-	if(scope==NULL) {
-		JS_GetProperty(js_cx, js_scope, "exit_code", &rval);
-		if(rval!=JSVAL_VOID)
-			JS_ValueToInt32(js_cx,rval,&result);
+	JS_GetProperty(js_cx, js_scope, "exit_code", &rval);
+	if(rval!=JSVAL_VOID)
+		JS_ValueToInt32(js_cx,rval,&result);
 
-		js_EvalOnExit(js_cx, js_scope, &js_callback);
-	}
+	js_EvalOnExit(js_cx, js_scope, &js_callback);
 
 	JS_ReportPendingException(js_cx);	/* Added Dec-4-2005, rswindell */
 
@@ -1523,9 +1521,9 @@ int sbbs_t::exec(csi_t *csi)
 				csi->logic=*csi->ip++;
 				return(0);
 			case CS_CMDKEY:
-				if( ((*csi->ip)==CS_DIGIT && isdigit(csi->cmd))
+				if( ((*csi->ip)==CS_DIGIT && IS_DIGIT(csi->cmd))
 					|| ((*csi->ip)==CS_EDIGIT && csi->cmd&0x80
-					&& isdigit(csi->cmd&0x7f))) {
+					&& IS_DIGIT(csi->cmd&0x7f))) {
 					csi->ip++;
 					return(0); 
 				}
@@ -1599,9 +1597,9 @@ int sbbs_t::exec(csi_t *csi)
 					memmove(csi->str,csi->str+i,j+1);
 				return(0);
 			case CS_COMPARE_KEY:
-				if( ((*csi->ip)==CS_DIGIT && isdigit(csi->cmd))
+				if( ((*csi->ip)==CS_DIGIT && IS_DIGIT(csi->cmd))
 					|| ((*csi->ip)==CS_EDIGIT && csi->cmd&0x80
-					&& isdigit(csi->cmd&0x7f))) {
+					&& IS_DIGIT(csi->cmd&0x7f))) {
 					csi->ip++;
 					csi->logic=LOGIC_TRUE; 
 				}
@@ -1630,7 +1628,7 @@ int sbbs_t::exec(csi_t *csi)
 				}
 				switch(*(csi->ip++)) {
 					case USER_STRING_ALIAS:
-						if(!isalpha(csi->str[0]) || trashcan(csi->str,"name"))
+						if(!IS_ALPHA(csi->str[0]) || trashcan(csi->str,"name"))
 							break;
 						i=matchuser(&cfg,csi->str,TRUE /*sysop_alias*/);
 						if(i && i!=useron.number)
diff --git a/src/sbbs3/execfile.cpp b/src/sbbs3/execfile.cpp
index 061bd073f236442017f0b22499eabba0a32a22a6..a27f220ea66a2db584613c5ba9e1755d598fdab4 100644
--- a/src/sbbs3/execfile.cpp
+++ b/src/sbbs3/execfile.cpp
@@ -127,7 +127,7 @@ int sbbs_t::exec_file(csi_t *csi)
 			if((ch&0xf)*10U<=usrdirs[curlib] && (ch&0xf) && usrlibs) {
 				i=(ch&0xf)*10;
 				ch=getkey(K_UPPER);
-				if(!isdigit(ch) && ch!=CR) {
+				if(!IS_DIGIT(ch) && ch!=CR) {
 					ungetkey(ch);
 					curdir[curlib]=(i/10)-1;
 					return(0); 
@@ -142,7 +142,7 @@ int sbbs_t::exec_file(csi_t *csi)
 				if(i*10<=usrdirs[curlib]) { 	/* 100+ dirs */
 					i*=10;
 					ch=getkey(K_UPPER);
-					if(!isdigit(ch) && ch!=CR) {
+					if(!IS_DIGIT(ch) && ch!=CR) {
 						ungetkey(ch);
 						curdir[curlib]=(i/10)-1;
 						return(0); 
@@ -179,7 +179,7 @@ int sbbs_t::exec_file(csi_t *csi)
 			if((ch&0xf)*10U<=usrlibs && (ch&0xf)) {
 				i=(ch&0xf)*10;
 				ch=getkey(K_UPPER);
-				if(!isdigit(ch) && ch!=CR) {
+				if(!IS_DIGIT(ch) && ch!=CR) {
 					ungetkey(ch);
 					curlib=(i/10)-1;
 					return(0); 
diff --git a/src/sbbs3/execmsg.cpp b/src/sbbs3/execmsg.cpp
index e5952d8600d9210c825eeb721672a585d9278377..ce327dd3b912816637699c05ded2f851cf0c9f17 100644
--- a/src/sbbs3/execmsg.cpp
+++ b/src/sbbs3/execmsg.cpp
@@ -123,7 +123,7 @@ int sbbs_t::exec_msg(csi_t *csi)
 			if(usrgrps && (ch&0xf)*10U<=usrsubs[curgrp] && (ch&0xf)) {
 				i=(ch&0xf)*10;
 				ch=getkey(K_UPPER);
-				if(!isdigit((uchar)ch) && ch!=CR) {
+				if(!IS_DIGIT(ch) && ch!=CR) {
 					ungetkey(ch);
 					cursub[curgrp]=(i/10)-1;
 					return(0); 
@@ -138,7 +138,7 @@ int sbbs_t::exec_msg(csi_t *csi)
 				if(i*10<=usrsubs[curgrp]) { 	/* 100+ subs */
 					i*=10;
 					ch=getkey(K_UPPER);
-					if(!isdigit((uchar)ch) && ch!=CR) {
+					if(!IS_DIGIT(ch) && ch!=CR) {
 						ungetkey(ch);
 						cursub[curgrp]=(i/10)-1;
 						return(0); 
@@ -175,7 +175,7 @@ int sbbs_t::exec_msg(csi_t *csi)
 			if((ch&0xf)*10U<=usrgrps && (ch&0xf)) {
 				i=(ch&0xf)*10;
 				ch=getkey(K_UPPER);
-				if(!isdigit((uchar)ch) && ch!=CR) {
+				if(!IS_DIGIT(ch) && ch!=CR) {
 					ungetkey(ch);
 					curgrp=(i/10)-1;
 					return(0); 
diff --git a/src/sbbs3/filedat.c b/src/sbbs3/filedat.c
index 0940ec962e9ddcbb43916a066460e21458b7ee2b..a47c1cc5ac4f683e30346272043ebf93a0466c61 100644
--- a/src/sbbs3/filedat.c
+++ b/src/sbbs3/filedat.c
@@ -659,6 +659,7 @@ void DLLCALL putextdesc(scfg_t* cfg, uint dirnum, ulong datoffset, char *ext)
 	char str[MAX_PATH+1],nulbuf[F_EXBSIZE];
 	int file;
 
+	strip_ansi(ext);
 	strip_invalid_attr(ext);	/* eliminate bogus ctrl-a codes */
 	memset(nulbuf,0,sizeof(nulbuf));
 	SAFEPRINTF2(str,"%s%s.exb",cfg->dir[dirnum]->data_dir,cfg->dir[dirnum]->code);
diff --git a/src/sbbs3/ftpsrvr.c b/src/sbbs3/ftpsrvr.c
index 9e9065a6787332b2e131db7e88798c6c8ffcbc9d..ba9d9b77362899efee793113af5a5716def3a117 100644
--- a/src/sbbs3/ftpsrvr.c
+++ b/src/sbbs3/ftpsrvr.c
@@ -1887,8 +1887,8 @@ static void receive_thread(void* arg)
 							SAFECOPY(desc,ext);
 							strip_exascii(desc, desc);	/* strip extended ASCII chars */
 							prep_file_desc(desc, desc);	/* strip control chars and dupe chars */
-							for(i=0;desc[i];i++)	/* find approprate first char */
-								if(isalnum(desc[i]))
+							for(i=0;desc[i];i++)	/* find appropriate first char */
+								if(IS_ALPHANUMERIC(desc[i]))
 									break;
 							SAFECOPY(f.desc,desc+i); 
 						}
@@ -5080,9 +5080,14 @@ static void ctrl_thread(void* arg)
 				if(!fexistcase(qwkfile)) {
 					lprintf(LOG_INFO,"%04d <%s> creating QWK packet...",sock,user.alias);
 					sprintf(str,"%spack%04u.now",scfg.data_dir,user.number);
-					if(!ftouch(str))
-						lprintf(LOG_ERR,"%04d <%s> !ERROR creating semaphore file: %s"
-							,sock, user.alias, str);
+					if(!fmutex(str, startup->host_name, /* max_age: */60 * 60)) {
+						lprintf(LOG_WARNING, "%04d <%s> !ERROR %d creating mutex-semaphore file: %s"
+							,sock, user.alias, errno, str);
+						sockprintf(sock,sess,"451 Packet creation already in progress (are you logged-in concurrently?)");
+						filepos=0;
+						continue;
+					}
+
 					t=time(NULL);
 					while(fexist(str) && !terminate_server) {
 						if(!socket_check(sock,NULL,NULL,0))
diff --git a/src/sbbs3/getkey.cpp b/src/sbbs3/getkey.cpp
index f4a0258ccadebaea4cfc092591c0efe10862aad4..b7c1a3e56ee3ef21adeb9b5c115cbbef29b773c0 100644
--- a/src/sbbs3/getkey.cpp
+++ b/src/sbbs3/getkey.cpp
@@ -91,9 +91,9 @@ char sbbs_t::getkey(long mode)
 			return(0);
 		now=time(NULL);
 		if(ch) {
-			if(mode&K_NUMBER && isprint(ch) && !isdigit(ch))
+			if(mode&K_NUMBER && IS_PRINTABLE(ch) && !IS_DIGIT(ch))
 				continue;
-			if(mode&K_ALPHA && isprint(ch) && !isalpha(ch))
+			if(mode&K_ALPHA && IS_PRINTABLE(ch) && !IS_ALPHA(ch))
 				continue;
 			if(mode&K_NOEXASC && ch&0x80)
 				continue;
@@ -365,7 +365,7 @@ long sbbs_t::getkeys(const char *keys, ulong max, long mode)
 			lncntr=0;
 			return(-1); 
 		}
-		if(ch && !n && ((keys == NULL && !isdigit(ch)) || (strchr(str,ch)))) {  /* return character if in string */
+		if(ch && !n && ((keys == NULL && !IS_DIGIT(ch)) || (strchr(str,ch)))) {  /* return character if in string */
 			if(ch > ' ') {
 				if(!(mode&K_NOECHO))
 					outchar(ch);
@@ -411,7 +411,7 @@ long sbbs_t::getkeys(const char *keys, ulong max, long mode)
 			i/=10;
 			n--; 
 		}
-		else if(max && isdigit(ch) && (i*10)+(ch&0xf)<=max && (ch!='0' || n)) {
+		else if(max && IS_DIGIT(ch) && (i*10)+(ch&0xf)<=max && (ch!='0' || n)) {
 			i*=10;
 			n++;
 			i+=ch&0xf;
@@ -475,13 +475,14 @@ void sbbs_t::pause()
 /****************************************************************************/
 void sbbs_t::ungetkey(char ch, bool insert)
 {
+	char dbg[2] = {};
 #if 0	/* this way breaks ansi_getxy() */
 	RingBufWrite(&inbuf,(uchar*)&ch,sizeof(uchar));
 #else
 	if(keybuf_space()) {
 		char* p = c_escape_char(ch);
 		if(p == NULL) {
-			char dbg[2] = { ch, 0 };
+			dbg[0] = ch;
 			p = dbg;
 		}
 		lprintf(LOG_DEBUG, "%s key into keybuf: %02X (%s)", insert ? "insert" : "append", ch, p);
diff --git a/src/sbbs3/getstr.cpp b/src/sbbs3/getstr.cpp
index bee868bdac1ff61465c5f4be5a0456cd762bde52..92308498de39dea168df3553ccf20afb2142d456 100644
--- a/src/sbbs3/getstr.cpp
+++ b/src/sbbs3/getstr.cpp
@@ -99,7 +99,7 @@ size_t sbbs_t::getstr(char *strout, size_t maxlen, long mode, const str_list_t h
 	if(mode&K_AUTODEL && str1[0] && !(mode&K_NOECHO)) {
 		ch=getkey(mode|K_GETSTR);
 		attr(atr);
-		if(isprint(ch) || ch==DEL) {
+		if(IS_PRINTABLE(ch) || ch==DEL) {
 			for(i=0;i<l;i++)
 				backspace();
 			i=l=0; 
@@ -689,7 +689,7 @@ long sbbs_t::getnum(ulong max, ulong dflt)
 			i/=10;
 			n--; 
 		}
-		else if(isdigit(ch) && (i*10UL)+(ch&0xf)<=max && (dflt || ch!='0' || n)) {
+		else if(IS_DIGIT(ch) && (i*10UL)+(ch&0xf)<=max && (dflt || ch!='0' || n)) {
 			i*=10L;
 			n++;
 			i+=ch&0xf;
diff --git a/src/sbbs3/inkey.cpp b/src/sbbs3/inkey.cpp
index 082e6b19bf01889ca2a49e1d7715a0bd5b690995..089006e87ba26a6c535fde6a925e64a1398dfc19 100644
--- a/src/sbbs3/inkey.cpp
+++ b/src/sbbs3/inkey.cpp
@@ -74,7 +74,7 @@ int kbincom(sbbs_t* sbbs, unsigned long timeout)
 			}
 			if((ch&0xe0) == 0xc0)	/* "Codes $60-$7F are, actually, copies of codes $C0-$DF" */
 				ch = 0x60 | (ch&0x1f);
-			if(isalpha((unsigned char)ch))
+			if(IS_ALPHA(ch))
 				ch ^= 0x20;	/* Swap upper/lower case */
 		}
 
@@ -116,7 +116,7 @@ char sbbs_t::inkey(long mode, unsigned long timeout)
 	this->timeout=time(NULL);
 
 	/* Is this a control key */
-	if(ch<' ') {
+	if(!(mode & K_CTRLKEYS) && ch < ' ') {
 		if(cfg.ctrlkey_passthru&(1<<ch))	/*  flagged as passthru? */
 			return(ch);						/* do not handle here */
 		return(handle_ctrlkey(ch,mode));
@@ -416,7 +416,7 @@ char sbbs_t::handle_ctrlkey(char ch, long mode)
 							return 0;
 						}
 						str[i++] = byte;
-						if(isalpha(byte))
+						if(IS_ALPHA(byte))
 							break;
 					}
 					str[i] = 0;
@@ -487,7 +487,7 @@ char sbbs_t::handle_ctrlkey(char ch, long mode)
 	#endif
 					return 0;
 				}
-				if(ch!=';' && !isdigit((uchar)ch) && ch!='R') {    /* other ANSI */
+				if(ch!=';' && !IS_DIGIT(ch) && ch!='R') {    /* other ANSI */
 					str[i]=0;
 					switch(ch) {
 						case 'A':
@@ -533,15 +533,17 @@ char sbbs_t::handle_ctrlkey(char ch, long mode)
 					return(ESC); 
 				}
 				if(ch=='R') {       /* cursor position report */
-					if(mode&K_ANSI_CPR && i && !(useron.rows)) {	/* auto-detect rows */
+					if(mode&K_ANSI_CPR && i) {	/* auto-detect rows */
 						int	x,y;
 						str[i]=0;
 						if(sscanf(str,"%u;%u",&y,&x)==2) {
 							lprintf(LOG_DEBUG,"received ANSI cursor position report: %ux%u"
 								,x, y);
 							/* Sanity check the coordinates in the response: */
-							if(x >= TERM_COLS_MIN && x <= TERM_COLS_MAX) cols=x;
-							if(y >= TERM_ROWS_MIN && y <= TERM_ROWS_MAX) rows=y;
+							if(useron.cols == TERM_COLS_AUTO && x >= TERM_COLS_MIN && x <= TERM_COLS_MAX) cols=x;
+							if(useron.rows == TERM_ROWS_AUTO && y >= TERM_ROWS_MIN && y <= TERM_ROWS_MAX) rows=y;
+							if(useron.cols == TERM_COLS_AUTO || useron.rows == TERM_ROWS_AUTO)
+								update_nodeterm();
 						}
 					}
 					return(0); 
diff --git a/src/sbbs3/js_bbs.cpp b/src/sbbs3/js_bbs.cpp
index 0e1121e74bd12c466f4ce5a0e2f5088953c1d762..e21ddaf9b2da36364a9da629bae103f4bdc0f252 100644
--- a/src/sbbs3/js_bbs.cpp
+++ b/src/sbbs3/js_bbs.cpp
@@ -1662,7 +1662,7 @@ js_atcode(JSContext *cx, uintN argc, jsval *arglist)
 	else if((p=strstr(instr,"-R"))!=NULL)
 		padded_right=true;
 	if(p!=NULL) {
-		if(*(p+2) && isdigit(*(p+2)))
+		if(*(p+2) && IS_DIGIT(*(p+2)))
 			disp_len=atoi(p+2);
 		*p=0;
 	}
diff --git a/src/sbbs3/js_console.cpp b/src/sbbs3/js_console.cpp
index efc8894b05ec01489b3109533b3326cccbdcd1a0..972135757a7b430b09e76204a1355fa5e890ab64 100644
--- a/src/sbbs3/js_console.cpp
+++ b/src/sbbs3/js_console.cpp
@@ -2099,6 +2099,22 @@ js_term_supports(JSContext *cx, uintN argc, jsval *arglist)
     return(JS_TRUE);
 }
 
+static JSBool
+js_term_updated(JSContext *cx, uintN argc, jsval *arglist)
+{
+	sbbs_t*		sbbs;
+	jsrefcount	rc;
+
+	if((sbbs=(sbbs_t*)js_GetClassPrivate(cx, JS_THIS_OBJECT(cx, arglist), &js_console_class))==NULL)
+		return JS_FALSE;
+
+	rc=JS_SUSPENDREQUEST(cx);
+	JS_SET_RVAL(cx, arglist, BOOLEAN_TO_JSVAL(sbbs->update_nodeterm()));
+	JS_RESUMEREQUEST(cx, rc);
+
+    return JS_TRUE;
+}
+
 static jsSyncMethodSpec js_console_functions[] = {
 	{"inkey",			js_inkey,			0, JSTYPE_STRING,	JSDOCSTR("[mode=<tt>K_NONE</tt>] [,timeout=<tt>0</tt>]")
 	,JSDOCSTR("get a single key with optional <i>timeout</i> in milliseconds (defaults to 0, for no wait).<br>"
@@ -2314,6 +2330,10 @@ static jsSyncMethodSpec js_console_functions[] = {
 		"<i>terminal_flags</i> (numeric bit-field) if no <i>terminal_flags</i> were specified")
 	,314
 	},
+	{"term_updated",	js_term_updated,	1, JSTYPE_BOOLEAN,	JSDOCSTR("")
+	,JSDOCSTR("update the node's <tt>terminal.ini</tt> file to match the current terminal settings")
+	,31802
+	},
 	{"backspace",		js_backspace,		0, JSTYPE_VOID,		JSDOCSTR("[count=<tt>1</tt>]")
 	,JSDOCSTR("send a destructive backspace sequence")
 	,315
diff --git a/src/sbbs3/js_file.c b/src/sbbs3/js_file.c
index b6d6ab25947f464b2b93f92c4ed7866568a648f2..646d48ef058a814f88f74bf196ebed9e8199777d 100644
--- a/src/sbbs3/js_file.c
+++ b/src/sbbs3/js_file.c
@@ -714,7 +714,7 @@ static jsval get_value(JSContext *cx, char* value)
 	for(p=value;*p;p++) {
 		if(*p=='.' && !f)
 			f=TRUE;
-		else if(!isdigit((uchar)*p))
+		else if(!IS_DIGIT(*p))
 			break;
 	}
 	if(*p==0) {
diff --git a/src/sbbs3/js_global.c b/src/sbbs3/js_global.c
index 2ee6631ea13c61c6dd3576b90a85baacf7256d45..9ee3956cce5062efc38592d7cb2b0e916cc9974e 100644
--- a/src/sbbs3/js_global.c
+++ b/src/sbbs3/js_global.c
@@ -1115,6 +1115,37 @@ js_strip_ctrl(JSContext *cx, uintN argc, jsval *arglist)
 	return(JS_TRUE);
 }
 
+static JSBool
+js_strip_ansi(JSContext *cx, uintN argc, jsval *arglist)
+{
+	jsval *argv=JS_ARGV(cx, arglist);
+	char*		buf = NULL;
+	JSString*	js_str;
+	jsrefcount	rc;
+
+	JS_SET_RVAL(cx, arglist, JSVAL_VOID);
+
+	if(argc==0 || JSVAL_IS_VOID(argv[0]))
+		return JS_TRUE;
+
+	JSVALUE_TO_MSTRING(cx, argv[0], buf, NULL)
+	HANDLE_PENDING(cx, buf);
+	if(buf==NULL) 
+		return JS_TRUE;
+
+	rc=JS_SUSPENDREQUEST(cx);
+	strip_ansi(buf);
+	JS_RESUMEREQUEST(cx, rc);
+
+	js_str = JS_NewStringCopyZ(cx, buf);
+	free(buf);
+	if(js_str==NULL)
+		return JS_FALSE;
+
+	JS_SET_RVAL(cx, arglist, STRING_TO_JSVAL(js_str));
+	return JS_TRUE;
+}
+
 static JSBool
 js_strip_exascii(JSContext *cx, uintN argc, jsval *arglist)
 {
@@ -1855,7 +1886,7 @@ js_html_encode(JSContext *cx, uintN argc, jsval *arglist)
 				ansi_seq[MAX_ANSI_SEQ]=0;
 				for(lastparam=ansi_seq;*lastparam;lastparam++)
 				{
-					if(isalpha(*lastparam))
+					if(IS_ALPHA(*lastparam))
 					{
 						*(++lastparam)=0;
 						break;
@@ -1865,16 +1896,16 @@ js_html_encode(JSContext *cx, uintN argc, jsval *arglist)
 				param=ansi_seq;
 				if(*param=='?')		/* This is to fix ESC[?7 whatever that is */
 					param++;
-				if(isdigit(*param))
+				if(IS_DIGIT(*param))
 					ansi_param[k++]=atoi(ansi_seq);
-				while(isspace(*param) || isdigit(*param))
+				while(IS_WHITESPACE(*param) || IS_DIGIT(*param))
 					param++;
 				lastparam=param;
 				while((param=strchr(param,';'))!=NULL)
 				{
 					param++;
 					ansi_param[k++]=atoi(param);
-					while(isspace(*param) || isdigit(*param))
+					while(IS_WHITESPACE(*param) || IS_DIGIT(*param))
 						param++;
 					lastparam=param;
 				}
@@ -4574,6 +4605,10 @@ static jsSyncMethodSpec js_global_functions[] = {
 	,JSDOCSTR("strip control characters from string, returns modified string")
 	,310
 	},		
+	{"strip_ansi",		js_strip_ansi,		1,	JSTYPE_STRING,	JSDOCSTR("text")
+	,JSDOCSTR("strip all ANSI terminal control sequences from string, returns modified string")
+	,31802
+	},		
 	{"strip_exascii",	js_strip_exascii,	1,	JSTYPE_STRING,	JSDOCSTR("text")
 	,JSDOCSTR("strip all extended-ASCII characters from string, returns modified string")
 	,310
diff --git a/src/sbbs3/js_server.c b/src/sbbs3/js_server.c
index 55ddba7d5f023238b6d2ceffbac0155a64a79f9d..de81d138afac15b16f6b857780a24185077b7a86 100644
--- a/src/sbbs3/js_server.c
+++ b/src/sbbs3/js_server.c
@@ -150,7 +150,7 @@ static void remove_port_part(char *host)
 {
 	char *p=strchr(host, 0)-1;
 
-	if (isdigit(*p)) {
+	if (IS_DIGIT(*p)) {
 		/*
 		 * If the first and last : are not the same, and it doesn't
 		 * start with '[', there's no port part.
@@ -164,7 +164,7 @@ static void remove_port_part(char *host)
 				*p = 0;
 				break;
 			}
-			if (!isdigit(*p))
+			if (!IS_DIGIT(*p))
 				break;
 		}
 	}
diff --git a/src/sbbs3/js_socket.c b/src/sbbs3/js_socket.c
index a987cf41f1b7a94efe419ca28c1085b3c95fff7d..e47b0335b5b491a0f263205d9e4d340122ab49bd 100644
--- a/src/sbbs3/js_socket.c
+++ b/src/sbbs3/js_socket.c
@@ -458,7 +458,7 @@ static ushort js_port(JSContext* cx, jsval val, int type)
 	if(JSVAL_IS_STRING(val)) {
 		str = JS_ValueToString(cx,val);
 		JSSTRING_TO_ASTRING(cx, str, cp, 16, NULL);
-		if(isdigit(*cp))
+		if(IS_DIGIT(*cp))
 			return((ushort)strtol(cp,NULL,0));
 		rc=JS_SUSPENDREQUEST(cx);
 		serv = getservbyname(cp,type==SOCK_STREAM ? "tcp":"udp");
diff --git a/src/sbbs3/js_user.c b/src/sbbs3/js_user.c
index 0eca3f60a95812c2f825ad30ff8c2f5688ff5269..f961a2b44e0ca449bed7d115ae7d19c3cff1be0f 100644
--- a/src/sbbs3/js_user.c
+++ b/src/sbbs3/js_user.c
@@ -102,7 +102,8 @@ enum {
 	,USER_PROP_FLAGS4	
 	,USER_PROP_EXEMPT	
 	,USER_PROP_REST		
-	,USER_PROP_ROWS		
+	,USER_PROP_ROWS
+	,USER_PROP_COLS
 	,USER_PROP_SEX		
 	,USER_PROP_MISC		
 	,USER_PROP_LEECH 	
@@ -314,6 +315,9 @@ static JSBool js_user_get(JSContext *cx, JSObject *obj, jsid id, jsval *vp)
 		case USER_PROP_ROWS:
 			val=p->user->rows;
 			break;
+		case USER_PROP_COLS:
+			val=p->user->cols;
+			break;
 		case USER_PROP_SEX:
 			sprintf(tmp,"%c",p->user->sex);
 			s=tmp;
@@ -538,6 +542,10 @@ static JSBool js_user_set(JSContext *cx, JSObject *obj, jsid id, JSBool strict,
 			p->user->rows=atoi(str);
 			putuserrec(scfg,p->user->number,U_ROWS,0,str);	/* base 10 */
 			break;
+		case USER_PROP_COLS:	
+			p->user->cols=atoi(str);
+			putuserrec(scfg,p->user->number,U_COLS,0,str);	/* base 10 */
+			break;
 		case USER_PROP_SEX:		 
 			p->user->sex=toupper(str[0]);
 			putuserrec(scfg,p->user->number,U_SEX,0,strupr(str));	/* single char */
@@ -785,6 +793,7 @@ static jsSyncPropertySpec js_user_properties[] = {
 	{	"connection"		,USER_PROP_MODEM      	,USER_PROP_FLAGS,		310},
 	{	"modem"				,USER_PROP_MODEM      	,USER_PROP_FLAGS,		310},
 	{	"screen_rows"		,USER_PROP_ROWS		 	,USER_PROP_FLAGS,		310},
+	{	"screen_columns"	,USER_PROP_COLS		 	,USER_PROP_FLAGS,		31802},
 	{	"gender"			,USER_PROP_SEX		 	,USER_PROP_FLAGS,		310},
 	{	"cursub"			,USER_PROP_CURSUB	 	,USER_PROP_FLAGS,		310},
 	{	"curdir"			,USER_PROP_CURDIR	 	,USER_PROP_FLAGS,		310},
@@ -829,8 +838,9 @@ static char* user_prop_desc[] = {
 	,"calculated age in years - <small>READ ONLY</small>"
 	,"connection type (protocol)"
 	,"AKA connection"
-	,"terminal rows (lines)"
-	,"gender type (e.g. M or F)"
+	,"terminal rows (0 = auto-detect)"
+	,"terminal columns (0 = auto-detect)"
+	,"gender type (e.g. M or F or any single-character)"
 	,"current/last message sub-board (internal code)"
 	,"current/last file directory (internal code)"
 	,"current/last external program (internal code) run"
diff --git a/src/sbbs3/jsexec.c b/src/sbbs3/jsexec.c
index 636887a47f9b5caf65ef0b31246eca86e18093dc..cfe931314da8c44d638b72f64231739e3875c4e9 100644
--- a/src/sbbs3/jsexec.c
+++ b/src/sbbs3/jsexec.c
@@ -1111,7 +1111,7 @@ int parseLogLevel(const char* p)
 	str_list_t logLevelStringList=iniLogLevelStringList();
 	int i;
 
-	if(isdigit(*p))
+	if(IS_DIGIT(*p))
 		return strtol(p,NULL,0);
 
 	/* Exact match */
diff --git a/src/sbbs3/load_cfg.c b/src/sbbs3/load_cfg.c
index ee0ecb55f86c5b35c9528dff1c344e4e17a96b55..1c6ced9ba6200d84b9f8d6cd373c89dd6e6a8cc1 100644
--- a/src/sbbs3/load_cfg.c
+++ b/src/sbbs3/load_cfg.c
@@ -474,7 +474,7 @@ char* prep_code(char *str, const char* prefix)
 	strcpy(str,tmp);
 	if(j>LEN_CODE) {	/* Extra chars? Strip symbolic chars */
 		for(i=j=0;str[i];i++)
-			if(isalnum(str[i]))
+			if(IS_ALPHANUMERIC(str[i]))
 				tmp[j++]=str[i];
 		tmp[j]=0;
 		strcpy(str,tmp);
diff --git a/src/sbbs3/login.cpp b/src/sbbs3/login.cpp
index d5ee19f77ef314db9d4c135d89b83b5a4868db17..7432b23b30cd2b59b555f41f62c5108429263e75 100644
--- a/src/sbbs3/login.cpp
+++ b/src/sbbs3/login.cpp
@@ -61,7 +61,7 @@ int sbbs_t::login(char *username, char *pw_prompt, const char* user_pw, const ch
 	useron.number=0;
 	username = parse_login(username);
 
-	if(!(cfg.node_misc&NM_NO_NUM) && isdigit((uchar)username[0])) {
+	if(!(cfg.node_misc&NM_NO_NUM) && IS_DIGIT(username[0])) {
 		useron.number=atoi(username);
 		getuserdat(&cfg,&useron);
 		if(useron.number && useron.misc&(DELETED|INACTIVE))
@@ -71,7 +71,7 @@ int sbbs_t::login(char *username, char *pw_prompt, const char* user_pw, const ch
 	if(!useron.number) {
 		useron.number=matchuser(&cfg,username,FALSE);
 		if(!useron.number && (uchar)username[0]<0x7f && username[1]
-			&& isalpha(username[0]) && strchr(username,' ') && cfg.node_misc&NM_LOGON_R)
+			&& IS_ALPHA(username[0]) && strchr(username,' ') && cfg.node_misc&NM_LOGON_R)
 			useron.number=userdatdupe(0,U_NAME,LEN_NAME,username);
 		if(useron.number) {
 			getuserdat(&cfg,&useron);
diff --git a/src/sbbs3/logon.cpp b/src/sbbs3/logon.cpp
index 55754b7191a0aefa7159db5b1745e1f24822fc68..31b23906360f55c4ab4c406121f5203d078e33b2 100644
--- a/src/sbbs3/logon.cpp
+++ b/src/sbbs3/logon.cpp
@@ -79,7 +79,8 @@ bool sbbs_t::logon()
 
 	if(useron.rest&FLAG('G')) {     /* Guest account */
 		useron.misc=(cfg.new_misc&(~ASK_NSCAN));
-		useron.rows=0;
+		useron.rows = TERM_ROWS_AUTO;
+		useron.cols = TERM_COLS_AUTO;
 		useron.misc &= ~TERM_FLAGS;
 		useron.misc|=autoterm;
 		if(!(useron.misc&(ANSI|PETSCII)) && text[AnsiTerminalQ][0] && yesno(text[AnsiTerminalQ]))
@@ -215,8 +216,11 @@ bool sbbs_t::logon()
 	}
 
 	bputs(text[LoggingOn]);
-	if(useron.rows)
-		rows=useron.rows;
+	if(useron.rows != TERM_ROWS_AUTO)
+		rows = useron.rows;
+	if(useron.cols != TERM_COLS_AUTO)
+		cols = useron.cols;
+	update_nodeterm();
 	if(tm.tm_mon + 1 == getbirthmonth(&cfg, useron.birth) && tm.tm_mday == getbirthday(&cfg, useron.birth)
 		&& !(useron.rest&FLAG('Q'))) {
 		if(text[HappyBirthday][0]) {
@@ -240,7 +244,7 @@ bool sbbs_t::logon()
 			c=0;
 			while(c < RAND_PASS_LEN) { 				/* Create random password */
 				str[c]=sbbs_random(43)+'0';
-				if(isalnum(str[c]))
+				if(IS_ALPHANUMERIC(str[c]))
 					c++; 
 			}
 			str[c]=0;
diff --git a/src/sbbs3/mailsrvr.c b/src/sbbs3/mailsrvr.c
index 24fd1bd8bd2ad7195c7e6e4c28380c938c56a5c0..c4e01ca6de136938b8112848c77c94129821a467 100644
--- a/src/sbbs3/mailsrvr.c
+++ b/src/sbbs3/mailsrvr.c
@@ -1,6 +1,6 @@
 /* Synchronet Mail (SMTP/POP3) server and sendmail threads */
 
-/* $Id: mailsrvr.c,v 1.734 2020/08/08 19:04:06 rswindell Exp $ */
+/* $Id: mailsrvr.c,v 1.735 2020/10/22 19:04:06 rswindell Exp $ */
 // vi: tabstop=4
 
 /****************************************************************************
@@ -40,7 +40,6 @@
 #include <stdlib.h>			/* ltoa in GNU C lib */
 #include <stdarg.h>			/* va_list */
 #include <string.h>			/* strrchr */
-#include <ctype.h>			/* isdigit */
 #include <fcntl.h>			/* Open flags */
 #include <errno.h>			/* errno */
 
@@ -71,11 +70,11 @@ static const char*	server_name="Synchronet Mail Server";
 int dns_getmx(char* name, char* mx, char* mx2
 			  ,DWORD intf, DWORD ip_addr, BOOL use_tcp, int timeout);
 
-#define pop_err			"-ERR"
+#define pop_error		"-ERR System Error: %s, try again later"
+#define pop_auth_error	"-ERR Authentication failure"
 #define ok_rsp			"250 OK"
 #define auth_ok			"235 User Authenticated"
-#define sys_error		"421 System error"
-#define sys_unavail		"421 System unavailable, try again later"
+#define smtp_error		"421 System Error: %s, try again later"
 #define insuf_stor		"452 Insufficient system storage"
 #define badarg_rsp 		"501 Bad argument"
 #define badseq_rsp		"503 Bad sequence of commands"
@@ -84,6 +83,7 @@ int dns_getmx(char* name, char* mx, char* mx2
 
 #define TIMEOUT_THREAD_WAIT		60		/* Seconds */
 #define DNSBL_THROTTLE_VALUE	1000	/* Milliseconds */
+#define SMTP_MAX_BAD_CMDS		9
 
 #define STATUS_WFC	"Listening"
 
@@ -105,11 +105,22 @@ static str_list_t recycle_semfiles;
 static str_list_t shutdown_semfiles;
 static int		mailproc_count;
 static js_server_props_t js_server_props;
+static link_list_t current_logins;
+static link_list_t current_connections;
+static bool savemsg_mutex_created = false;
+static pthread_mutex_t savemsg_mutex;
+
+static const char* servprot_smtp = "SMTP";
+static const char* servprot_submission = "SMTP";
+static const char* servprot_submissions = "SMTPS";
+static const char* servprot_pop3 = "POP3";
+static const char* servprot_pop3s = "POP3S";
 
 struct {
 	volatile ulong	sockets;
 	volatile ulong	errors;
 	volatile ulong	crit_errors;
+	volatile ulong	connections_exceeded;
 	volatile ulong	connections_ignored;
 	volatile ulong	connections_refused;
 	volatile ulong	connections_served;
@@ -207,7 +218,7 @@ static int lprintf(int level, const char *fmt, ...)
 		return(0);
 #endif
 
-    return(startup->lputs(startup->cbdata,level,sbuf));
+    return(startup->lputs(startup->cbdata, level, condense_whitespace(sbuf)));
 }
 
 #ifdef _WINSOCKAPI_
@@ -250,12 +261,15 @@ static void update_clients(void)
 
 static void client_on(SOCKET sock, client_t* client, BOOL update)
 {
+	if(!update)
+		listAddNodeData(&current_connections, client->addr, strlen(client->addr)+1, sock, LAST_NODE);
 	if(startup!=NULL && startup->client_on!=NULL)
 		startup->client_on(startup->cbdata,TRUE,sock,client,update);
 }
 
 static void client_off(SOCKET sock)
 {
+	listRemoveTaggedNode(&current_connections, sock, /* free_data */TRUE);
 	if(startup!=NULL && startup->client_on!=NULL)
 		startup->client_on(startup->cbdata,FALSE,sock,NULL,FALSE);
 }
@@ -630,6 +644,23 @@ void originator_info(SOCKET socket, const char* protocol, CRYPT_SESSION sess, sm
 			);
 }
 
+char* angle_bracket(char* buf, size_t max, const char* addr)
+{
+	if(*addr == '<')
+		safe_snprintf(buf, max, "%s", addr);
+	else
+		safe_snprintf(buf, max, "<%s>", addr);
+	return buf;
+}
+
+int compare_addrs(const char* addr1, const char* addr2)
+{
+	char tmp1[256];
+	char tmp2[256];
+
+	return strcmp(angle_bracket(tmp1, sizeof(tmp1), addr1), angle_bracket(tmp2, sizeof(tmp2), addr2));
+}
+
 /* RFC822: The maximum total length of a text line including the
    <CRLF> is 1000 characters (but not counting the leading
    dot duplicated for transparency). 
@@ -645,6 +676,7 @@ static ulong sockmimetext(SOCKET socket, const char* prot, CRYPT_SESSION sess, s
 	char		fromaddr[256]="";
 	char		fromhost[256];
 	char		msgid[256];
+	char		tmp[256];
 	char		date[64];
 	char*		p;
 	char*		np;
@@ -692,10 +724,7 @@ static ulong sockmimetext(SOCKET socket, const char* prot, CRYPT_SESSION sess, s
 			SAFECOPY(fromaddr,(char*)msg->from_net.addr);
 		else 
 			usermailaddr(&scfg,fromaddr,msg->from);
-		if(fromaddr[0]=='<')
-			s=sockprintf(socket,prot,sess,"From: %s %s",fromname,fromaddr);
-		else
-			s=sockprintf(socket,prot,sess,"From: %s <%s>",fromname,fromaddr);
+		s = sockprintf(socket,prot,sess,"From: %s %s", fromname, angle_bracket(tmp, sizeof(tmp), fromaddr));
 	}
 	if(!s)
 		return(0);
@@ -916,7 +945,7 @@ static u_long resolve_ip(const char *inaddr)
 		return((u_long)INADDR_NONE);
 
 	for(p=addr;*p;p++)
-		if(*p!='.' && !isdigit((uchar)*p))
+		if(*p!='.' && !IS_DIGIT(*p))
 			break;
 	if(!(*p))
 		return(inet_addr(addr));
@@ -969,6 +998,7 @@ static void pop3_thread(void* arg)
 	char		buf[512];
 	char		host_name[128];
 	char		host_ip[INET6_ADDRSTRLEN];
+	char		server_ip[INET6_ADDRSTRLEN];
 	char		username[128];
 	char		password[128];
 	char		challenge[256];
@@ -1001,6 +1031,7 @@ static void pop3_thread(void* arg)
 	char *estr;
 	int level;
 	int stat;
+	union xp_sockaddr	server_addr;
 
 	SetThreadName("sbbs/pop3");
 	thread_up(TRUE /* setuid */);
@@ -1019,22 +1050,32 @@ static void pop3_thread(void* arg)
 		PlaySound(startup->pop3_sound, NULL, SND_ASYNC|SND_FILENAME);
 #endif
 
+	socklen_t addr_len = sizeof(server_addr);
+	if((i=getsockname(socket, &server_addr.addr, &addr_len))!=0) {
+		lprintf(LOG_CRIT,"%04d %s !ERROR %d (%d) getting address/port"
+			,socket, client.protocol, i, ERROR_VALUE);
+		mail_close_socket(&socket, &session);
+		thread_down();
+		return;
+	}
+
 	inet_addrtop(&pop3.client_addr, host_ip, sizeof(host_ip));
+	inet_addrtop(&server_addr, server_ip, sizeof(server_ip));
 
 	if(startup->options&MAIL_OPT_DEBUG_POP3)
-		lprintf(LOG_INFO,"%04d %s connection accepted from: %s port %u"
-			,socket, client.protocol, host_ip, inet_addrport(&pop3.client_addr));
+		lprintf(LOG_INFO,"%04d %s [%s] connection accepted on %s port %u from port %u"
+			,socket, client.protocol, host_ip, server_ip, inet_addrport(&server_addr), inet_addrport(&pop3.client_addr));
 
 	SAFECOPY(host_name, STR_NO_HOSTNAME);
 	if(!(startup->options&MAIL_OPT_NO_HOST_LOOKUP)) {
 		getnameinfo(&pop3.client_addr.addr, pop3.client_addr_len, host_name, sizeof(host_name), NULL, 0, NI_NAMEREQD);
 		if(startup->options&MAIL_OPT_DEBUG_POP3)
-			lprintf(LOG_INFO,"%04d %s Hostname: %s [%s]", socket, client.protocol, host_name, host_ip);
+			lprintf(LOG_INFO,"%04d %s [%s] Hostname: %s", socket, client.protocol, host_ip, host_name);
 	}
 	if (pop3.tls_port) {
 		if (get_ssl_cert(&scfg, &estr, &level) == -1) {
 			if (estr) {
-				lprintf(level, "%04d %s !Failure getting certificate: %s", socket, client.protocol, estr);
+				lprintf(level, "%04d %s [%s] !Failure getting certificate: %s", socket, client.protocol, host_ip, estr);
 				free_crypt_attrstr(estr);
 			}
 			mail_close_socket(&socket, &session);
@@ -1094,11 +1135,11 @@ static void pop3_thread(void* arg)
 	if(banned || trashcan(&scfg,host_ip,"ip")) {
 		if(banned) {
 			char ban_duration[128];
-			lprintf(LOG_NOTICE, "%04d %s !TEMPORARY BAN of %s (%lu login attempts, last: %s) - remaining: %s"
+			lprintf(LOG_NOTICE, "%04d %s [%s] !TEMPORARY BAN (%lu login attempts, last: %s) - remaining: %s"
 				,socket, client.protocol, host_ip, attempted.count-attempted.dupes, attempted.user, seconds_to_str(banned, ban_duration));
 		}
 		else
-			lprintf(LOG_NOTICE,"%04d %s !CLIENT IP ADDRESS BLOCKED: %s",socket, client.protocol, host_ip);
+			lprintf(LOG_NOTICE,"%04d %s [%s] !CLIENT IP ADDRESS BLOCKED",socket, client.protocol, host_ip);
 		sockprintf(socket,client.protocol,session,"-ERR Access denied.");
 		mail_close_socket(&socket, &session);
 		thread_down();
@@ -1106,8 +1147,8 @@ static void pop3_thread(void* arg)
 	}
 
 	if(trashcan(&scfg,host_name,"host")) {
-		lprintf(LOG_NOTICE,"%04d %s !CLIENT HOSTNAME BLOCKED: %s"
-			,socket, client.protocol, host_name);
+		lprintf(LOG_NOTICE,"%04d %s [%s] !CLIENT HOSTNAME BLOCKED: %s"
+			,socket, client.protocol, host_ip, host_name);
 		sockprintf(socket,client.protocol,session,"-ERR Access denied.");
 		mail_close_socket(&socket, &session);
 		thread_down();
@@ -1132,7 +1173,7 @@ static void pop3_thread(void* arg)
 
 	if(startup->login_attempt.throttle
 		&& (login_attempts=loginAttempts(startup->login_attempt_list, &pop3.client_addr)) > 1) {
-		lprintf(LOG_DEBUG,"%04d %s Throttling suspicious connection from: %s (%lu login attempts)"
+		lprintf(LOG_DEBUG,"%04d %s [%s] Throttling suspicious connection (%lu login attempts)"
 			,socket, client.protocol, host_ip, login_attempts);
 		mswait(login_attempts*startup->login_attempt.throttle);
 	}
@@ -1172,7 +1213,7 @@ static void pop3_thread(void* arg)
 			else if (!stricmp(buf, "STLS")) {
 				if (get_ssl_cert(&scfg, &estr, &level) == -1) {
 					if (estr) {
-						lprintf(level, "%04d %s !TLS Failure getting certificate: %s", socket, client.protocol, estr);
+						lprintf(level, "%04d %s [%s] !TLS Failure getting certificate: %s", socket, client.protocol, host_ip, estr);
 						free_crypt_attrstr(estr);
 					}
 					sockprintf(socket,client.protocol,session,"-ERR STLS command not supported");
@@ -1261,19 +1302,18 @@ static void pop3_thread(void* arg)
 			else
 				lprintf(LOG_NOTICE,"%04d %s [%s] !UNKNOWN USER: '%s'"
 					,socket, client.protocol, host_ip, username);
-			badlogin(socket, session, client.protocol, pop_err, username, password, host_name, &pop3.client_addr);
+			badlogin(socket, session, client.protocol, pop_auth_error, username, password, host_name, &pop3.client_addr);
 			break;
 		}
 		if((i=getuserdat(&scfg, &user))!=0) {
 			lprintf(LOG_ERR,"%04d %s [%s] !ERROR %d getting data on user (%s)"
 				,socket, client.protocol, host_ip, i, username);
-			badlogin(socket, session, client.protocol, pop_err, NULL, NULL, NULL, NULL);
 			break;
 		}
 		if(user.misc&(DELETED|INACTIVE)) {
 			lprintf(LOG_NOTICE,"%04d %s [%s] !DELETED or INACTIVE user #%u (%s)"
 				,socket, client.protocol, host_ip, user.number, username);
-			badlogin(socket, session, client.protocol, pop_err, NULL, NULL, NULL, NULL);
+			badlogin(socket, session, client.protocol, pop_auth_error, username, password, NULL, NULL);
 			break;
 		}
 		if(apop) {
@@ -1289,7 +1329,7 @@ static void pop3_thread(void* arg)
 				lprintf(LOG_DEBUG,"%04d !POP3 calc digest: %s",socket,str);
 				lprintf(LOG_DEBUG,"%04d !POP3 resp digest: %s",socket,response);
 #endif
-				badlogin(socket, session, client.protocol, pop_err, username, response, host_name, &pop3.client_addr);
+				badlogin(socket, session, client.protocol, pop_auth_error, username, response, host_name, &pop3.client_addr);
 				break;
 			}
 		} else if(stricmp(password,user.pass)) {
@@ -1299,12 +1339,14 @@ static void pop3_thread(void* arg)
 			else
 				lprintf(LOG_NOTICE,"%04d %s [%s] !FAILED Password attempt for user %s"
 					,socket, client.protocol, host_ip, username);
-			badlogin(socket, session, client.protocol, pop_err, username, password, host_name, &pop3.client_addr);
+			badlogin(socket, session, client.protocol, pop_auth_error, username, password, host_name, &pop3.client_addr);
 			break;
 		}
 
-		if(user.pass[0])
+		if(user.pass[0]) {
 			loginSuccess(startup->login_attempt_list, &pop3.client_addr);
+			listAddNodeData(&current_logins, client.addr, strlen(client.addr) + 1, socket, LAST_NODE);
+		}
 
 		putuserrec(&scfg,user.number,U_COMP,LEN_COMP,host_name);
 		putuserrec(&scfg,user.number,U_IPADDR,LEN_IPADDR,host_ip);
@@ -1315,8 +1357,8 @@ static void pop3_thread(void* arg)
 		client_on(socket,&client,TRUE /* update */);
 		activity=FALSE;
 
-		if(startup->options&MAIL_OPT_DEBUG_POP3)		
-			lprintf(LOG_INFO,"%04d %s [%s] %s logged in %s", socket, client.protocol, host_ip, user.alias, apop ? "via APOP":"");
+		if(startup->options&MAIL_OPT_DEBUG_POP3)
+			lprintf(LOG_INFO,"%04d %s [%s] %s logged-in %s", socket, client.protocol, host_ip, user.alias, apop ? "via APOP":"");
 		SAFEPRINTF2(str,"%s: %s", client.protocol, user.alias);
 		status(str);
 
@@ -1378,6 +1420,12 @@ static void pop3_thread(void* arg)
 				sockprintf(socket,client.protocol,session,"+OK");
 				continue;
 			}
+			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);
+				continue;
+			}
 			if(!stricmp(buf, "QUIT")) {
 				sockprintf(socket,client.protocol,session,"+OK");
 				break;
@@ -1429,7 +1477,7 @@ static void pop3_thread(void* arg)
 			if(!strnicmp(buf, "LIST",4) || !strnicmp(buf,"UIDL",4)) {
 				p=buf+4;
 				SKIP_WHITESPACE(p);
-				if(isdigit((uchar)*p)) {
+				if(IS_DIGIT(*p)) {
 					msgnum=strtoul(p, NULL, 10);
 					if(msgnum<1 || msgnum>msgs) {
 						lprintf(LOG_NOTICE,"%04d %s <%s> !INVALID message #%" PRIu32
@@ -1686,11 +1734,11 @@ static void pop3_thread(void* arg)
 
 	if(activity) {
 		if(user.number)
-			lprintf(LOG_INFO,"%04d %s <%s> logged out from port %u on %s [%s]"
+			lprintf(LOG_INFO,"%04d %s <%s> logged-out from port %u on %s [%s]"
 				,socket, client.protocol, user.alias, inet_addrport(&pop3.client_addr), host_name, host_ip);
 		else
-			lprintf(LOG_INFO,"%04d %s client disconnected from port %u on %s [%s]"
-				,socket, client.protocol, inet_addrport(&pop3.client_addr), host_name, host_ip);
+			lprintf(LOG_INFO,"%04d %s [%s] client disconnected from port %u on %s"
+				,socket, client.protocol, host_ip, inet_addrport(&pop3.client_addr), host_name);
 	}
 
 	status(STATUS_WFC);
@@ -1702,6 +1750,7 @@ static void pop3_thread(void* arg)
 	smb_freemsgmem(&msg);
 	smb_close(&smb);
 
+	listRemoveTaggedNode(&current_logins, socket, /* free_data */TRUE);
 	protected_uint32_adjust(&active_clients, -1);
 	update_clients();
 	client_off(socket);
@@ -1709,8 +1758,8 @@ static void pop3_thread(void* arg)
 	{ 
 		int32_t remain = thread_down();
 		if(startup->options&MAIL_OPT_DEBUG_POP3)
-			lprintf(LOG_DEBUG,"%04d %s session thread terminated (%u threads remain, %lu clients served)"
-				,socket, client.protocol, remain, ++stats.pop3_served);
+			lprintf(LOG_DEBUG,"%04d %s [%s] session thread terminated (%u threads remain, %lu clients served)"
+				,socket, client.protocol, host_ip, remain, ++stats.pop3_served);
 	}
 
 	/* Must be last */
@@ -1856,8 +1905,8 @@ static BOOL chk_email_addr(SOCKET socket, const char* prot, char* p, char* host_
 	if(!trashcan(&scfg,addr,"email"))
 		return(TRUE);
 
-	lprintf(LOG_NOTICE,"%04d %s !BLOCKED %s e-mail address: %s"
-		,socket, prot, source, addr);
+	lprintf(LOG_NOTICE,"%04d %s [%s] !BLOCKED %s e-mail address: %s"
+		,socket, prot, host_ip, source, addr);
 	SAFEPRINTF2(tmp,"Blocked %s e-mail address: %s", source, addr);
 	spamlog(&scfg, (char*)prot, "REFUSED", tmp, host_name, host_ip, to, from);
 
@@ -1883,30 +1932,24 @@ static BOOL email_addr_is_exempt(const char* addr)
 }
 
 static void exempt_email_addr(const char* comment
-							  ,const char* fromname, const char* fromext, const char* fromaddr
+							  ,const char* sender_info
 							  ,const char* toaddr)
 {
 	char	fname[MAX_PATH+1];
-	char	to[128];
+	char	to[256];
 	char	tmp[128];
 	FILE*	fp;
 
-	if(*toaddr == '<')
-		SAFECOPY(to, toaddr);
-	else
-		SAFEPRINTF(to,"<%s>",toaddr);
+	angle_bracket(to, sizeof(to), toaddr);
 	if(!email_addr_is_exempt(to)) {
 		SAFEPRINTF(fname,"%sdnsbl_exempt.cfg",scfg.ctrl_dir);
 		if((fp=fopen(fname,"a"))==NULL)
 			lprintf(LOG_ERR,"0000 !Error opening file: %s", fname);
 		else {
 			lprintf(LOG_INFO,"0000 %s: %s", comment, to);
-			fprintf(fp,"\n;%s from \"%s\""
-				,comment, fromname);
-			if(fromext!=NULL)
-				fprintf(fp,"%s",fromext);
-			fprintf(fp," %s on %s\n%s\n"
-				,fromaddr, timestr(&scfg,time32(NULL),tmp), to);
+			fprintf(fp,"\n;%s from %s on %s\n%s\n"
+				,comment, sender_info
+				,timestr(&scfg,time32(NULL),tmp), to);
 			fclose(fp);
 		}
 	}
@@ -2384,7 +2427,7 @@ char* mimehdr_q_decode(char* buf)
 	for(;*p != 0; p++) {
 		if(*p == '=') {
 			p++;
-			if(isxdigit(*p) && isxdigit(*(p + 1))) {
+			if(IS_HEXDIGIT(*p) && IS_HEXDIGIT(*(p + 1))) {
 				uchar ch = HEX_CHAR_TO_INT(*p) << 4;
 				p++;
 				ch |= HEX_CHAR_TO_INT(*p);
@@ -2611,7 +2654,7 @@ static int chk_received_hdr(SOCKET socket,const char* prot,const char *buf,IN_AD
 		if(*p==0)
 			break;
 		p2=host_name;
-		for(;*p && !isspace((unsigned char)*p) && p2<host_name+126;p++)  {
+		for(;*p && !IS_WHITESPACE(*p) && p2<host_name+126;p++)  {
 			*p2++=*p;
 		}
 		*p2=0;
@@ -2645,8 +2688,8 @@ static int chk_received_hdr(SOCKET socket,const char* prot,const char *buf,IN_AD
 		}
 
 		if((dnsbl_result->s_addr=dns_blacklisted(socket,prot,&addr,host_name,dnsbl,dnsbl_ip))!=0)
-				lprintf(LOG_NOTICE,"%04d %s BLACKLISTED SERVER on %s: %s [%s] = %s"
-					,socket, prot, dnsbl, host_name, ip, inet_ntoa(*dnsbl_result));
+				lprintf(LOG_NOTICE,"%04d %s [%s] BLACKLISTED SERVER on %s: %s = %s"
+					,socket, prot, ip, dnsbl, host_name, inet_ntoa(*dnsbl_result));
 	} while(0);
 	free(fromstr);
 	return(dnsbl_result->s_addr);
@@ -2710,7 +2753,7 @@ static char* qp_decode(char* buf)
 			p++;
 			if(*p==0) 	/* soft link break */
 				break;
-			if(isxdigit(*p) && isxdigit(*(p+1))) {
+			if(IS_HEXDIGIT(*p) && IS_HEXDIGIT(*(p+1))) {
 				char hex[3];
 				hex[0]=*p;
 				hex[1]=*(p+1);
@@ -2819,11 +2862,9 @@ static void smtp_thread(void* arg)
 	char		rcpt_name[128];
 	char		rcpt_addr[128];
 	char		sender[128];
-	char		sender_ext[128];
 	char		sender_addr[128];
 	char		hello_name[128];
 	char		user_name[128];
-	char		user_pass[128];
 	char		relay_list[MAX_PATH+1];
 	char		domain_list[MAX_PATH+1];
 	char		spam_bait[MAX_PATH+1];
@@ -2888,6 +2929,7 @@ static void smtp_thread(void* arg)
 	user_t		relay_user;
 	node_t		node;
 	client_t	client;
+	char		client_id[128];
 	smtp_t		smtp=*(smtp_t*)arg;
 	union xp_sockaddr	server_addr;
 	IN_ADDR		dnsbl_result;
@@ -3012,7 +3054,7 @@ static void smtp_thread(void* arg)
 	if((i=getsockname(socket, &server_addr.addr, &addr_len))!=0) {
 		lprintf(LOG_CRIT,"%04d %s !ERROR %d (%d) getting address/port"
 			,socket, client.protocol, i, ERROR_VALUE);
-		sockprintf(socket,client.protocol,session,sys_error);
+		sockprintf(socket,client.protocol,session,smtp_error, "getsockname failure");
 		mail_close_socket(&socket, &session);
 		thread_down();
 		return;
@@ -3020,7 +3062,7 @@ static void smtp_thread(void* arg)
 
 	if((mailproc_to_match=malloc(sizeof(BOOL)*mailproc_count))==NULL) {
 		lprintf(LOG_CRIT,"%04d %s !ERROR allocating memory for mailproc_to_match", socket, client.protocol);
-		sockprintf(socket,client.protocol,session,sys_error);
+		sockprintf(socket,client.protocol,session,smtp_error, "malloc failure");
 		mail_close_socket(&socket, &session);
 		thread_down();
 		return;
@@ -3034,14 +3076,16 @@ static void smtp_thread(void* arg)
 	memset(&relay_user,0,sizeof(relay_user));
 
 	inet_addrtop(&smtp.client_addr,host_ip,sizeof(host_ip));
+	inet_addrtop(&server_addr,server_ip,sizeof(server_ip));
 
-	lprintf(LOG_INFO,"%04d %s Connection accepted on port %u from: %s port %u"
-		,socket, client.protocol, inet_addrport(&server_addr), host_ip, inet_addrport(&smtp.client_addr));
+	lprintf(LOG_INFO,"%04d %s [%s] Connection accepted on %s port %u from port %u"
+		,socket, client.protocol, host_ip, server_ip, inet_addrport(&server_addr), inet_addrport(&smtp.client_addr));
+	SAFEPRINTF(client_id, "[%s]", host_ip);
 
 	SAFECOPY(host_name, STR_NO_HOSTNAME);
 	if(!(startup->options&MAIL_OPT_NO_HOST_LOOKUP)) {
 		getnameinfo(&smtp.client_addr.addr, smtp.client_addr_len, host_name, sizeof(host_name), NULL, 0, NI_NAMEREQD);
-		lprintf(LOG_INFO,"%04d %s Hostname: %s [%s]", socket, client.protocol, host_name, host_ip);
+		lprintf(LOG_INFO,"%04d %s %s Hostname: %s", socket, client.protocol, client_id, host_name);
 	}
 	protected_uint32_adjust(&active_clients, 1);
 	update_clients();
@@ -3052,8 +3096,6 @@ static void smtp_thread(void* arg)
 	SAFEPRINTF(spam_block,"%sspamblock.cfg",scfg.ctrl_dir);
 	SAFEPRINTF(spam_block_exemptions,"%sspamblock_exempt.cfg",scfg.ctrl_dir);
 
-	inet_addrtop(&server_addr,server_ip,sizeof(server_ip));
-
 	if(strcmp(server_ip, host_ip)==0) {
 		/* local connection */
 		dnsbl_result.s_addr=0;
@@ -3061,8 +3103,8 @@ static void smtp_thread(void* arg)
 		ulong banned = loginBanned(&scfg, startup->login_attempt_list, socket, host_name, startup->login_attempt, &attempted);
 		if(banned) {
 			char ban_duration[128];
-			lprintf(LOG_NOTICE, "%04d !TEMPORARY BAN of %s (%lu login attempts, last: %s) - remaining: %s"
-				,socket, host_ip, attempted.count-attempted.dupes, attempted.user, seconds_to_str(banned, ban_duration));
+			lprintf(LOG_NOTICE, "%04d %s [%s] !TEMPORARY BAN (%lu login attempts, last: %s) - remaining: %s"
+				,socket, client.protocol, host_ip, attempted.count-attempted.dupes, attempted.user, seconds_to_str(banned, ban_duration));
 			mail_close_socket(&socket, &session);
 			thread_down();
 			protected_uint32_adjust(&active_clients, -1);
@@ -3086,8 +3128,8 @@ static void smtp_thread(void* arg)
 		}
 
 		if(trashcan(&scfg,host_name,"host")) {
-			lprintf(LOG_NOTICE,"%04d %s !CLIENT HOSTNAME BLOCKED: %s (%lu total)"
-				,socket, client.protocol, host_name, ++stats.sessions_refused);
+			lprintf(LOG_NOTICE,"%04d %s [%s] !CLIENT HOSTNAME BLOCKED: %s (%lu total)"
+				,socket, client.protocol, host_ip, host_name, ++stats.sessions_refused);
 			sockprintf(socket,client.protocol,session,"550 CLIENT HOSTNAME BLOCKED: %s", host_name);
 			mail_close_socket(&socket, &session);
 			thread_down();
@@ -3100,8 +3142,8 @@ static void smtp_thread(void* arg)
 		/*  SPAM Filters (mail-abuse.org) */
 		dnsbl_result.s_addr = dns_blacklisted(socket,client.protocol,&smtp.client_addr,host_name,dnsbl,dnsbl_ip);
 		if(dnsbl_result.s_addr) {
-			lprintf(LOG_NOTICE,"%04d %s BLACKLISTED SERVER on %s: %s [%s] = %s"
-				,socket, client.protocol, dnsbl, host_name, dnsbl_ip, inet_ntoa(dnsbl_result));
+			lprintf(LOG_NOTICE,"%04d %s [%s] BLACKLISTED SERVER on %s: %s = %s"
+				,socket, client.protocol, dnsbl_ip, dnsbl, host_name, inet_ntoa(dnsbl_result));
 			if(startup->options&MAIL_OPT_DNSBL_REFUSE) {
 				SAFEPRINTF2(str,"Listed on %s as %s", dnsbl, inet_ntoa(dnsbl_result));
 				spamlog(&scfg, (char*)client.protocol, "SESSION REFUSED", str, host_name, dnsbl_ip, NULL, NULL);
@@ -3122,9 +3164,9 @@ static void smtp_thread(void* arg)
 
 	SAFEPRINTF(smb.file,"%smail",scfg.data_dir);
 	if(smb_islocked(&smb)) {
-		lprintf(LOG_WARNING,"%04d %s !MAIL BASE LOCKED: %s"
-			,socket, client.protocol, smb.last_error);
-		sockprintf(socket,client.protocol,session,sys_unavail);
+		lprintf(LOG_WARNING,"%04d %s [%s] !MAIL BASE LOCKED: %s"
+			,socket, client.protocol, host_ip, smb.last_error);
+		sockprintf(socket,client.protocol,session, smtp_error, "mail base locked");
 		mail_close_socket(&socket, &session);
 		thread_down();
 		protected_uint32_adjust(&active_clients, -1);
@@ -3139,16 +3181,16 @@ static void smtp_thread(void* arg)
 	srand((unsigned int)(time(NULL) ^ (time_t)GetCurrentThreadId()));	/* seed random number generator */
 	rand();	/* throw-away first result */
 	SAFEPRINTF4(session_id,"%x%x%x%lx",getpid(),socket,rand(),(long)clock());
-	lprintf(LOG_DEBUG,"%04d %s Session ID=%s", socket, client.protocol, session_id);
+	lprintf(LOG_DEBUG,"%04d %s [%s] Session ID=%s", socket, client.protocol, host_ip, session_id);
 	SAFEPRINTF3(msgtxt_fname,"%sSBBS_%s.%s.msg", scfg.temp_dir, client.protocol, session_id);
 	SAFEPRINTF3(newtxt_fname,"%sSBBS_%s.%s.new", scfg.temp_dir, client.protocol, session_id);
 	SAFEPRINTF3(logtxt_fname,"%sSBBS_%s.%s.log", scfg.temp_dir, client.protocol, session_id);
 	SAFEPRINTF3(rcptlst_fname,"%sSBBS_%s.%s.lst", scfg.temp_dir, client.protocol, session_id);
 	rcptlst=fopen(rcptlst_fname,"w+");
 	if(rcptlst==NULL) {
-		lprintf(LOG_CRIT,"%04d %s !ERROR %d creating recipient list: %s"
-			,socket, client.protocol, errno, rcptlst_fname);
-		sockprintf(socket,client.protocol,session,sys_error);
+		lprintf(LOG_CRIT,"%04d %s [%s] !ERROR %d creating recipient list: %s"
+			,socket, client.protocol, host_ip, errno, rcptlst_fname);
+		sockprintf(socket,client.protocol,session,smtp_error, "fopen error");
 		mail_close_socket(&socket, &session);
 		thread_down();
 		protected_uint32_adjust(&active_clients, -1);
@@ -3203,23 +3245,23 @@ static void smtp_thread(void* arg)
 				cmd=SMTP_CMD_NONE;
 
 				if(msgtxt==NULL) {
-					lprintf(LOG_ERR,"%04d %s !NO MESSAGE TEXT FILE POINTER?", socket, client.protocol);
+					lprintf(LOG_ERR,"%04d %s %s !NO MESSAGE TEXT FILE POINTER?", socket, client.protocol, client_id);
 					sockprintf(socket,client.protocol,session,"554 No message text");
 					continue;
 				}
 
 				if(ftell(msgtxt)<1) {
-					lprintf(LOG_ERR,"%04d %s !INVALID MESSAGE LENGTH: %ld (%lu lines)"
-						, socket, client.protocol, ftell(msgtxt), lines);
+					lprintf(LOG_ERR,"%04d %s %s !INVALID MESSAGE LENGTH: %ld (%lu lines)"
+						, socket, client.protocol, client_id, ftell(msgtxt), lines);
 					sockprintf(socket,client.protocol,session,"554 No message text");
 					continue;
 				}
 
-				lprintf(LOG_INFO,"%04d %s End of message (body: %lu lines, %lu bytes, header: %lu lines, %lu bytes)"
-					, socket, client.protocol, lines, ftell(msgtxt)-hdr_len, hdr_lines, hdr_len);
+				lprintf(LOG_INFO,"%04d %s %s End of message (body: %lu lines, %lu bytes, header: %lu lines, %lu bytes)"
+					, socket, client.protocol, client_id, lines, ftell(msgtxt)-hdr_len, hdr_lines, hdr_len);
 
 				if(!socket_check(socket, NULL, NULL, 0)) {
-					lprintf(LOG_WARNING,"%04d %s !sender disconnected (premature evacuation)", socket, client.protocol);
+					lprintf(LOG_WARNING,"%04d %s %s !Sender disconnected (premature evacuation)", socket, client.protocol, client_id);
 					continue;
 				}
 
@@ -3228,8 +3270,8 @@ static void smtp_thread(void* arg)
 				/* Twit-listing (sender's name and e-mail addresses) here */
 				SAFEPRINTF(path,"%stwitlist.cfg",scfg.ctrl_dir);
 				if(fexist(path) && (findstr(sender,path) || findstr(sender_addr,path))) {
-					lprintf(LOG_NOTICE,"%04d %s !FILTERING TWIT-LISTED SENDER: %s <%s> (%lu total)"
-						,socket, client.protocol, sender, sender_addr, ++stats.msgs_refused);
+					lprintf(LOG_NOTICE,"%04d %s %s !FILTERING TWIT-LISTED SENDER: %s <%s> (%lu total)"
+						,socket, client.protocol, client_id, sender, sender_addr, ++stats.msgs_refused);
 					SAFEPRINTF2(tmp,"Twit-listed sender: %s <%s>", sender, sender_addr);
 					spamlog(&scfg, (char*)client.protocol, "REFUSED", tmp, host_name, host_ip, rcpt_addr, reverse_path);
 					sockprintf(socket,client.protocol,session, "554 Sender not allowed.");
@@ -3271,17 +3313,17 @@ static void smtp_thread(void* arg)
 							,head,sender_addr,host_ip,host_name,tail);
 					else
 						safe_snprintf(str,sizeof(str),"%s%s%s",head,sender_addr,tail);
-					
+
 					if((telegram_buf=(char*)malloc(length+strlen(str)+1))==NULL) {
-						lprintf(LOG_CRIT,"%04d %s !ERROR allocating %lu bytes of memory for telegram from %s"
-							,socket, client.protocol,length+strlen(str)+1,sender_addr);
+						lprintf(LOG_CRIT,"%04d %s %s !ERROR allocating %lu bytes of memory for telegram from %s"
+							,socket, client.protocol, client_id, length+strlen(str)+1,sender_addr);
 						sockprintf(socket,client.protocol,session, insuf_stor);
 						continue; 
 					}
 					strcpy(telegram_buf,str);	/* can't use SAFECOPY here */
 					if(fread(telegram_buf+strlen(str),1,length,msgtxt)!=length) {
-						lprintf(LOG_ERR,"%04d %s !ERROR reading %lu bytes from telegram file"
-							,socket, client.protocol,length);
+						lprintf(LOG_ERR,"%04d %s %s !ERROR reading %lu bytes from telegram file"
+							,socket, client.protocol, client_id, length);
 						sockprintf(socket,client.protocol,session, insuf_stor);
 						free(telegram_buf);
 						continue; 
@@ -3301,11 +3343,11 @@ static void smtp_thread(void* arg)
 						SAFECOPY(rcpt_addr,iniReadString(rcptlst,section	,smb_hfieldtype(RECIPIENTNETADDR),rcpt_to,value));
 
 						if((i=putsmsg(&scfg,usernum,telegram_buf))==0)
-							lprintf(LOG_INFO,"%04d %s Created telegram (%ld/%lu bytes) from %s to %s <%s>"
-								,socket, client.protocol, length, (ulong)strlen(telegram_buf), sender_addr, rcpt_to, rcpt_addr);
+							lprintf(LOG_INFO,"%04d %s %s Created telegram (%ld/%lu bytes) from %s to %s <%s>"
+								,socket, client.protocol, client_id, length, (ulong)strlen(telegram_buf), sender_addr, rcpt_to, rcpt_addr);
 						else
-							lprintf(LOG_ERR,"%04d %s !ERROR %d creating telegram from %s to %s <%s>"
-								,socket, client.protocol, i, sender_addr, rcpt_to, rcpt_addr);
+							lprintf(LOG_ERR,"%04d %s %s !ERROR %d creating telegram from %s to %s <%s>"
+								,socket, client.protocol, client_id, i, sender_addr, rcpt_to, rcpt_addr);
 					}
 					iniFreeStringList(sec_list);
 					free(telegram_buf);
@@ -3351,18 +3393,18 @@ static void smtp_thread(void* arg)
 							,host_name, host_ip, relay_user.number
 							,rcpt_addr
 							,sender, sender_addr, reverse_path, str);
-						lprintf(LOG_INFO,"%04d %s Executing external mail processor: %s"
-							,socket, client.protocol, mp->name);
+						lprintf(LOG_INFO,"%04d %s %s Executing external mail processor: %s"
+							,socket, client.protocol, client_id, mp->name);
 
 						if(mp->native) {
-							lprintf(LOG_DEBUG,"%04d %s Executing external command: %s"
-								,socket, client.protocol, str);
+							lprintf(LOG_DEBUG,"%04d %s %s Executing external command: %s"
+								,socket, client.protocol, client_id, str);
 							if((j=system(str))!=0) {
-								lprintf(LOG_NOTICE,"%04d %s system(%s) returned %d (errno: %d)"
-									,socket, client.protocol, str, j, errno);
+								lprintf(LOG_NOTICE,"%04d %s %s system(%s) returned %d (errno: %d)"
+									,socket, client.protocol, client_id, str, j, errno);
 								if(mp->ignore_on_error) {
-									lprintf(LOG_WARNING,"%04d %s !IGNORED MAIL due to mail processor (%s) error: %d"
-										,socket, client.protocol, mp->name, j);
+									lprintf(LOG_WARNING,"%04d %s %s !IGNORED MAIL due to mail processor (%s) error: %d"
+										,socket, client.protocol, client_id, mp->name, j);
 									msg_handled=TRUE;
 								}
 							}
@@ -3393,8 +3435,8 @@ static void smtp_thread(void* arg)
 								if(!fgets(str,sizeof(str),proc_out))
 									break;
 								truncsp(str);
-								lprintf(LOG_DEBUG,"%04d %s External mail processor (%s) debug: %s"
-									,socket, client.protocol, mp->name, str);
+								lprintf(LOG_DEBUG,"%04d %s %s External mail processor (%s) debug: %s"
+									,socket, client.protocol, client_id, mp->name, str);
 							}
 							fclose(proc_out);
 						}
@@ -3407,15 +3449,15 @@ static void smtp_thread(void* arg)
 					}
 					if(flength(proc_err_fname)>0 
 						&& (proc_out=fopen(proc_err_fname,"r"))!=NULL) {
-						lprintf(LOG_WARNING,"%04d %s !External mail processor (%s) created: %s"
-								,socket, client.protocol, mailproc->name, proc_err_fname);
+						lprintf(LOG_WARNING,"%04d %s %s !External mail processor (%s) created: %s"
+								,socket, client.protocol, client_id, mailproc->name, proc_err_fname);
 						while(!feof(proc_out)) {
 							int n;
 							if(!fgets(str,sizeof(str),proc_out))
 								break;
 							truncsp(str);
-							lprintf(LOG_WARNING,"%04d %s !External mail processor (%s) error: %s"
-								,socket, client.protocol, mailproc->name, str);
+							lprintf(LOG_WARNING,"%04d %s %s !External mail processor (%s) error: %s"
+								,socket, client.protocol, client_id, mailproc->name, str);
 							n=atoi(str);
 							if(n>=100 && n<1000)
 								sockprintf(socket,client.protocol,session,"%s", str);
@@ -3428,8 +3470,8 @@ static void smtp_thread(void* arg)
 						msg_handled=TRUE;
 					}
 					else if(!fexist(msgtxt_fname) || !fexist(rcptlst_fname)) {
-						lprintf(LOG_NOTICE,"%04d %s External mail processor (%s) removed %s file"
-							,socket, client.protocol
+						lprintf(LOG_NOTICE,"%04d %s %s External mail processor (%s) removed %s file"
+							,socket, client.protocol, client_id
 							,mailproc->name
 							,fexist(msgtxt_fname)==FALSE ? "message text" : "recipient list");
 						sockprintf(socket,client.protocol,session,ok_rsp);
@@ -3444,23 +3486,23 @@ static void smtp_thread(void* arg)
 				/* We must do this before continuing for handled msgs */
 				/* to prevent freopen(NULL) and orphaned temp files */
 				if((rcptlst=fopen(rcptlst_fname,fexist(rcptlst_fname) ? "r":"w+"))==NULL) {
-					lprintf(LOG_ERR,"%04d %s !ERROR %d re-opening recipient list: %s"
-						,socket, client.protocol, errno, rcptlst_fname);
+					lprintf(LOG_ERR,"%04d %s %s !ERROR %d re-opening recipient list: %s"
+						,socket, client.protocol, client_id, errno, rcptlst_fname);
 					if(!msg_handled)
-						sockprintf(socket,client.protocol,session,sys_error);
+						sockprintf(socket,client.protocol,session,smtp_error, "fopen error");
 					continue;
 				}
 			
 				if(!msg_handled && subnum==INVALID_SUB && iniReadSectionCount(rcptlst,NULL) < 1) {
-					lprintf(LOG_DEBUG,"%04d %s No recipients in recipient list file (message handled by external mail processor?)"
-						,socket, client.protocol);
+					lprintf(LOG_DEBUG,"%04d %s %s No recipients in recipient list file (message handled by external mail processor?)"
+						,socket, client.protocol, client_id);
 					sockprintf(socket,client.protocol,session,ok_rsp);
 					msg_handled=TRUE;
 				}
 				if(msg_handled) {
 					if(mailproc!=NULL)
-						lprintf(LOG_NOTICE,"%04d %s Message handled by external mail processor (%s, %lu total)"
-							,socket, client.protocol, mailproc->name, ++mailproc->handled);
+						lprintf(LOG_NOTICE,"%04d %s %s Message handled by external mail processor (%s, %lu total)"
+							,socket, client.protocol, client_id, mailproc->name, ++mailproc->handled);
 					continue;
 				}
 
@@ -3472,9 +3514,9 @@ static void smtp_thread(void* arg)
 					remove(newtxt_fname);
 
 				if((msgtxt=fopen(msgtxt_fname,"rb"))==NULL) {
-					lprintf(LOG_ERR,"%04d %s !ERROR %d re-opening message file: %s"
-						,socket, client.protocol, errno, msgtxt_fname);
-					sockprintf(socket,client.protocol,session,sys_error);
+					lprintf(LOG_ERR,"%04d %s %s !ERROR %d re-opening message file: %s"
+						,socket, client.protocol, client_id, errno, msgtxt_fname);
+					sockprintf(socket,client.protocol,session,smtp_error, "fopen error");
 					continue;
 				}
 
@@ -3501,8 +3543,8 @@ static void smtp_thread(void* arg)
 							/* SPAM Filtering/Logging */
 							if(relay_user.number==0) {
 								if(trashcan(&scfg,p,"subject")) {
-									lprintf(LOG_NOTICE,"%04d %s !BLOCKED SUBJECT (%s) from: %s (%lu total)"
-										,socket, client.protocol, p, reverse_path, ++stats.msgs_refused);
+									lprintf(LOG_NOTICE,"%04d %s %s !BLOCKED SUBJECT (%s) from: %s (%lu total)"
+										,socket, client.protocol, client_id, p, reverse_path, ++stats.msgs_refused);
 									SAFEPRINTF2(tmp,"Blocked subject (%s) from: %s"
 										,p, reverse_path);
 									spamlog(&scfg, (char*)client.protocol, "REFUSED"
@@ -3516,8 +3558,8 @@ static void smtp_thread(void* arg)
 										,(int)sizeof(str)/2, startup->dnsbl_tag
 										,(int)sizeof(str)/2, p);
 									p=str;
-									lprintf(LOG_NOTICE,"%04d %s TAGGED MAIL SUBJECT from blacklisted server with: %s"
-										,socket, client.protocol, startup->dnsbl_tag);
+									lprintf(LOG_NOTICE,"%04d %s %s TAGGED MAIL SUBJECT from blacklisted server with: %s"
+										,socket, client.protocol, client_id, startup->dnsbl_tag);
 									msg.hdr.attr |= MSG_SPAM;
 								}
 							}
@@ -3541,11 +3583,11 @@ static void smtp_thread(void* arg)
 					}
 					if((smb_error=parse_header_field((char*)buf, &msg, &hfield_type))!=SMB_SUCCESS) {
 						if(smb_error==SMB_ERR_HDR_LEN)
-							lprintf(LOG_WARNING,"%04d %s !MESSAGE HEADER EXCEEDS %u BYTES"
-								,socket, client.protocol, SMB_MAX_HDR_LEN);
+							lprintf(LOG_WARNING,"%04d %s %s !MESSAGE HEADER EXCEEDS %u BYTES"
+								,socket, client.protocol, client_id, SMB_MAX_HDR_LEN);
 						else
-							lprintf(LOG_ERR,"%04d %s !ERROR %d adding header field: %s"
-								,socket, client.protocol, smb_error, buf);
+							lprintf(LOG_ERR,"%04d %s %s !ERROR %d adding header field: %s"
+								,socket, client.protocol, client_id, smb_error, buf);
 						break;
 					}
 				}
@@ -3594,8 +3636,8 @@ static void smtp_thread(void* arg)
 							,startup->dnsbl_hdr, dnsbl_ip
 							,dnsbl, inet_ntoa(dnsbl_result));
 						smb_hfield_str(&msg, RFC822HEADER, str);
-						lprintf(LOG_NOTICE,"%04d %s TAGGED MAIL HEADER from blacklisted server with: %s"
-							,socket, client.protocol, startup->dnsbl_hdr);
+						lprintf(LOG_NOTICE,"%04d %s %s TAGGED MAIL HEADER from blacklisted server with: %s"
+							,socket, client.protocol, client_id, startup->dnsbl_hdr);
 					}
 					if(startup->dnsbl_hdr[0] || startup->dnsbl_tag[0]) {
 						SAFEPRINTF2(str,"Listed on %s as %s", dnsbl, inet_ntoa(dnsbl_result));
@@ -3604,21 +3646,36 @@ static void smtp_thread(void* arg)
 				}
 				if(dnsbl_recvhdr)			/* DNSBL-listed IP found in Received header? */
 					dnsbl_result.s_addr=0;	/* Reset DNSBL look-up result between messages */
-				
+
 				if((scfg.sys_misc&SM_DELREADM)
 					|| ((startup->options&MAIL_OPT_KILL_READ_SPAM) && (msg.hdr.attr&MSG_SPAM)))
 					msg.hdr.attr |= MSG_KILLREAD;
 
 				if(sender[0]==0) {
-					lprintf(LOG_WARNING,"%04d %s !MISSING mail header 'FROM' field (%lu total)"
-						,socket, client.protocol, ++stats.msgs_refused);
+					lprintf(LOG_WARNING,"%04d %s %s !MISSING mail header 'FROM' field (%lu total)"
+						,socket, client.protocol, client_id, ++stats.msgs_refused);
 					sockprintf(socket,client.protocol,session, "554 Mail header missing 'FROM' field");
 					subnum=INVALID_SUB;
 					continue;
 				}
+				if(relay_user.number == 0
+					&& strchr(sender, '@') != NULL
+					&& compare_addrs(sender, sender_addr) != 0) {
+					lprintf(LOG_WARNING,"%04d %s %s !FORGED mail header 'FROM' field (%lu total)"
+						,socket, client.protocol, client_id, ++stats.msgs_refused);
+					sockprintf(socket,client.protocol,session, "554 Mail header contains mismatched 'FROM' field");
+					subnum=INVALID_SUB;
+					continue;
+				}
+				char sender_info[512];
 				if(relay_user.number) {
 					SAFEPRINTF(str,"%u",relay_user.number);
 					smb_hfield_str(&msg, SENDEREXT, str);
+					SAFEPRINTF2(sender_info, "'%s' #%u", sender, relay_user.number);
+				} else if(compare_addrs(sender, sender_addr) == 0) {
+					angle_bracket(sender_info, sizeof(sender_info), sender_addr);
+				} else {
+					safe_snprintf(sender_info, sizeof(sender_info), "'%s' %s", sender, angle_bracket(tmp, sizeof(tmp), sender_addr));
 				}
 				if(relay_user.number && subnum!=INVALID_SUB) {
 					nettype=NET_NONE;
@@ -3676,8 +3733,8 @@ static void smtp_thread(void* arg)
 				length=filelength(fileno(msgtxt))-ftell(msgtxt);
 
 				if(startup->max_msg_size && length>startup->max_msg_size) {
-					lprintf(LOG_WARNING,"%04d %s !Message size (%lu) exceeds maximum: %u bytes"
-						,socket, client.protocol,length,startup->max_msg_size);
+					lprintf(LOG_WARNING,"%04d %s %s !Message size (%lu) from %s to <%s> exceeds maximum: %u bytes"
+						,socket, client.protocol, client_id, length, sender_info, rcpt_addr, startup->max_msg_size);
 					sockprintf(socket,client.protocol,session, "552 Message size (%lu) exceeds maximum: %u bytes"
 						,length,startup->max_msg_size);
 					subnum=INVALID_SUB;
@@ -3686,8 +3743,8 @@ static void smtp_thread(void* arg)
 				}
 
 				if((msgbuf=(char*)malloc(length+1))==NULL) {
-					lprintf(LOG_CRIT,"%04d %s !ERROR allocating %lu bytes of memory"
-						,socket, client.protocol,length+1);
+					lprintf(LOG_CRIT,"%04d %s %s !ERROR allocating %lu bytes of memory"
+						,socket, client.protocol, client_id, length+1);
 					sockprintf(socket,client.protocol,session, insuf_stor);
 					subnum=INVALID_SUB;
 					continue;
@@ -3703,8 +3760,8 @@ static void smtp_thread(void* arg)
 						memset(&relay_user,0,sizeof(relay_user));
 
 					if(!can_user_post(&scfg,subnum,&relay_user,&client,&reason)) {
-						lprintf(LOG_WARNING,"%04d %s !%s (user #%u) cannot post on %s (reason: %u)"
-							,socket, client.protocol, sender_addr, relay_user.number
+						lprintf(LOG_WARNING,"%04d %s %s !%s (user #%u) cannot post on %s (reason: %u)"
+							,socket, client.protocol, client_id, sender_addr, relay_user.number
 							,scfg.sub[subnum]->sname, reason + 1);
 						sockprintf(socket,client.protocol,session,"550 Insufficient access");
 						subnum=INVALID_SUB;
@@ -3718,13 +3775,13 @@ static void smtp_thread(void* arg)
 
 					smb.subnum=subnum;
 					if((i=savemsg(&scfg, &smb, &msg, &client, server_host_name(), msgbuf, /* remsg: */NULL))!=SMB_SUCCESS) {
-						lprintf(LOG_WARNING,"%04d %s !ERROR %d (%s) posting message to %s (%s)"
-							,socket, client.protocol, i, smb.last_error, scfg.sub[subnum]->sname, smb.file);
+						lprintf(LOG_WARNING,"%04d %s %s !ERROR %d (%s) %s posting message to %s (%s)"
+							,socket, client.protocol, client_id, i, smb.last_error, sender_info, scfg.sub[subnum]->sname, smb.file);
 						sockprintf(socket,client.protocol,session, "452 ERROR %d (%s) posting message"
 							,i,smb.last_error);
 					} else {
-						lprintf(LOG_INFO,"%04d %s %s posted a message on %s (%s)"
-							,socket, client.protocol, sender_addr, scfg.sub[subnum]->sname, smb.file);
+						lprintf(LOG_INFO,"%04d %s %s %s posted a message on %s (%s)"
+							,socket, client.protocol, client_id, sender_info, scfg.sub[subnum]->sname, smb.file);
 						sockprintf(socket,client.protocol,session,ok_rsp);
 						if(relay_user.number != 0)
 							user_posted_msg(&scfg, &relay_user, 1);
@@ -3745,49 +3802,50 @@ static void smtp_thread(void* arg)
 					if((dnsbl_recvhdr || dnsbl_result.s_addr) && startup->options&MAIL_OPT_DNSBL_SPAMHASH)
 						is_spam=TRUE;
 
-					lprintf(LOG_DEBUG,"%04d %s Calculating message hashes (sources=%lx, msglen=%lu)"
-						,socket, client.protocol, sources, (ulong)strlen(msgbuf));
+					lprintf(LOG_DEBUG,"%04d %s %s Calculating message hashes (sources=%lx, msglen=%lu)"
+						,socket, client.protocol, client_id, sources, (ulong)strlen(msgbuf));
 					if((hashes=smb_msghashes(&msg, (uchar*)msgbuf, sources)) != NULL) {
 						hash_t	found;
 
 						for(i=0;hashes[i];i++)
-							lprintf(LOG_DEBUG,"%04d %s Message %s crc32=%x flags=%x length=%u"
-								,socket, client.protocol, smb_hashsourcetype(hashes[i]->source)
+							lprintf(LOG_DEBUG,"%04d %s %s Message %s crc32=%x flags=%x length=%u"
+								,socket, client.protocol, client_id, smb_hashsourcetype(hashes[i]->source)
 								,hashes[i]->crc32, hashes[i]->flags, hashes[i]->length);
 
-						lprintf(LOG_DEBUG, "%04d %s Searching SPAM database for a match", socket, client.protocol);
+						lprintf(LOG_DEBUG, "%04d %s %s Searching SPAM database for a match", socket, client.protocol, client_id);
 						if((i=smb_findhash(&spam, hashes, &found, sources, /* Mark: */TRUE))==SMB_SUCCESS) {
 							SAFEPRINTF3(str,"%s (%s) found in SPAM database (added on %s)"
 								,smb_hashsourcetype(found.source)
 								,smb_hashsource(&msg,found.source)
 								,timestr(&scfg,found.time,tmp)
 								);
-							lprintf(LOG_NOTICE,"%04d %s Message %s", socket, client.protocol, str);
+							lprintf(LOG_NOTICE,"%04d %s %s Message from %s %s", socket, client.protocol, client_id, sender_info, str);
 							if(!is_spam) {
 								spamlog(&scfg, (char*)client.protocol, "IGNORED"
 									,str, host_name, host_ip, rcpt_addr, reverse_path);
 								is_spam=TRUE;
 							}
 						} else {
-							lprintf(LOG_DEBUG, "%04d %s Done searching SPAM database", socket, client.protocol);
+							lprintf(LOG_DEBUG, "%04d %s %s Done searching SPAM database", socket, client.protocol, client_id);
 							if(i!=SMB_ERR_NOT_FOUND)
-								lprintf(LOG_ERR,"%04d %s !ERROR %d (%s) opening SPAM database"
-									,socket, client.protocol, i, spam.last_error);
+								lprintf(LOG_ERR,"%04d %s %s !ERROR %d (%s) opening SPAM database"
+									,socket, client.protocol, client_id, i, spam.last_error);
 						}
-						
+
 						if(is_spam) {
 							size_t	n,total=0;
 							for(n=0;hashes[n]!=NULL;n++)
 								if(!(hashes[n]->flags&SMB_HASH_MARKED)) {
-									lprintf(LOG_INFO,"%04d %s Adding message %s (%s) to SPAM database"
-										,socket, client.protocol
+									lprintf(LOG_INFO,"%04d %s %s Adding message %s (%s) from %s to SPAM database"
+										,socket, client.protocol, client_id
 										,smb_hashsourcetype(hashes[n]->source)
 										,smb_hashsource(&msg,hashes[n]->source)
+										,sender_info
 										);
 									total++;
 								}
 							if(total) {
-								lprintf(LOG_DEBUG,"%04d %s Adding %lu message hashes to SPAM database", socket, client.protocol, (ulong)total);
+								lprintf(LOG_DEBUG,"%04d %s %s Adding %lu message hashes to SPAM database", socket, client.protocol, client_id, (ulong)total);
 								smb_addhashes(&spam, hashes, /* skip_marked: */TRUE);
 							}
 							if(i!=SMB_SUCCESS && !spam_bait_result && (dnsbl_recvhdr || dnsbl_result.s_addr))
@@ -3797,17 +3855,17 @@ static void smtp_thread(void* arg)
 
 						smb_freehashes(hashes);
 					} else
-						lprintf(LOG_ERR,"%04d %s smb_msghashes returned NULL", socket, client.protocol);
+						lprintf(LOG_ERR,"%04d %s %s !smb_msghashes returned NULL", socket, client.protocol, client_id);
 
 					if(is_spam || ((startup->options&MAIL_OPT_DNSBL_IGNORE) && (dnsbl_recvhdr || dnsbl_result.s_addr))) {
 						free(msgbuf);
 						if(is_spam)
-							lprintf(LOG_NOTICE,"%04d %s !IGNORED SPAM MESSAGE (%lu total)"
-								,socket, client.protocol, ++stats.msgs_ignored);
+							lprintf(LOG_NOTICE,"%04d %s %s !IGNORED SPAM MESSAGE from %s to <%s> (%lu total)"
+								,socket, client.protocol, client_id, sender_info, rcpt_addr, ++stats.msgs_ignored);
 						else {
 							SAFEPRINTF2(str,"Listed on %s as %s", dnsbl, inet_ntoa(dnsbl_result));
-							lprintf(LOG_NOTICE,"%04d %s !IGNORED MAIL from server: %s (%lu total)"
-								,socket, client.protocol, str, ++stats.msgs_ignored);
+							lprintf(LOG_NOTICE,"%04d %s %s !IGNORED MAIL from %s to <%s> from server: %s (%lu total)"
+								,socket, client.protocol, client_id, sender_info, rcpt_addr, str, ++stats.msgs_ignored);
 							spamlog(&scfg, (char*)client.protocol, "IGNORED"
 								,str, host_name, dnsbl_ip, rcpt_addr, reverse_path);
 						}
@@ -3818,8 +3876,14 @@ static void smtp_thread(void* arg)
 					}
 				}
 
-				lprintf(LOG_DEBUG,"%04d %s Saving message to: '%s'", socket, client.protocol, rcpt_name);
-
+				char rcpt_info[512];
+				if(compare_addrs(rcpt_name, rcpt_addr) == 0)
+					angle_bracket(rcpt_info, sizeof(rcpt_info), rcpt_addr);
+				else
+					safe_snprintf(rcpt_info, sizeof(rcpt_info), "'%s' %s", rcpt_name, angle_bracket(tmp, sizeof(tmp), rcpt_addr));
+				lprintf(LOG_DEBUG,"%04d %s %s Saving message data from %s to %s"
+					,socket, client.protocol, client_id, sender_info, rcpt_info);
+				pthread_mutex_lock(&savemsg_mutex);
 				/* E-mail */
 				smb.subnum=INVALID_SUB;
 				/* creates message data, but no header or index records (since msg.to==NULL) */
@@ -3830,20 +3894,22 @@ static void smtp_thread(void* arg)
 				free(msgbuf);
 				if(i!=SMB_SUCCESS) {
 					smb_close(&smb);
-					lprintf(LOG_CRIT,"%04d %s !ERROR %d (%s) saving message"
-						,socket, client.protocol,i,smb.last_error);
+					pthread_mutex_unlock(&savemsg_mutex);
+					lprintf(LOG_CRIT,"%04d %s %s !ERROR %d (%s) saving message from %s to %s"
+						,socket, client.protocol, client_id, i, smb.last_error, sender_info, rcpt_info);
 					sockprintf(socket,client.protocol,session, "452 ERROR %d (%s) saving message"
 						,i,smb.last_error);
 					continue;
 				}
 
-				lprintf(LOG_DEBUG,"%04d %s Saved message data to: '%s'", socket, client.protocol, rcpt_name);
+				lprintf(LOG_DEBUG,"%04d %s %s Saved message data from %s to %s"
+					,socket, client.protocol, client_id, sender_info, rcpt_info);
 
 				sec_list=iniReadSectionList(rcptlst,NULL);	/* Each section is a recipient */
 				for(rcpt_count=0; sec_list!=NULL
 					&& sec_list[rcpt_count]!=NULL 
 					&& (startup->max_recipients==0 || rcpt_count<startup->max_recipients); rcpt_count++) {
-				
+
 					section=sec_list[rcpt_count];
 
 					SAFECOPY(rcpt_to,iniReadString(rcptlst,section	,smb_hfieldtype(RECIPIENT),"unknown",value));
@@ -3853,16 +3919,20 @@ static void smtp_thread(void* arg)
 					SAFEPRINTF(str,"#%u",usernum);
 					SAFECOPY(rcpt_addr,iniReadString(rcptlst,section	,smb_hfieldtype(RECIPIENTNETADDR),str,value));
 					SAFECOPY(forward_path, iniReadString(rcptlst, section, smb_hfieldtype(SMTPFORWARDPATH), "", value));
+					if(compare_addrs(rcpt_to, rcpt_addr) == 0)
+						angle_bracket(rcpt_info, sizeof(rcpt_info), rcpt_addr);
+					else
+						safe_snprintf(rcpt_info, sizeof(rcpt_info), "'%s' %s", rcpt_to, angle_bracket(tmp, sizeof(tmp), rcpt_addr));
 
 					if(nettype==NET_NONE /* Local destination */ && usernum==0) {
-						lprintf(LOG_ERR,"%04d %s !can't deliver mail to user #0"
-							,socket, client.protocol);
+						lprintf(LOG_ERR,"%04d %s %s !Can't deliver mail from %s to user #0"
+							,socket, client.protocol, client_id, sender_info);
 						break;
 					}
 
 					if((i=smb_copymsgmem(&smb,&newmsg,&msg))!=SMB_SUCCESS) {
-						lprintf(LOG_ERR,"%04d %s !ERROR %d (%s) copying message"
-							,socket, client.protocol, i, smb.last_error);
+						lprintf(LOG_ERR,"%04d %s %s !ERROR %d (%s) copying message from %s"
+							,socket, client.protocol, client_id, i, smb.last_error, sender_info);
 						break;
 					}
 
@@ -3921,18 +3991,17 @@ static void smtp_thread(void* arg)
 						smb_hfield(&newmsg, RECIPIENTAGENT, sizeof(agent), &agent);
 
 					add_msg_ids(&scfg, &smb, &newmsg, /* remsg: */NULL);
+					lprintf(LOG_DEBUG,"%04d %s %s Adding message header from %s to %s"
+						,socket, client.protocol, client_id, sender_info, rcpt_info);
 					i=smb_addmsghdr(&smb,&newmsg,smb_storage_mode(&scfg, &smb));
 					smb_freemsgmem(&newmsg);
 					if(i!=SMB_SUCCESS) {
-						lprintf(LOG_ERR,"%04d %s !ERROR %d (%s) adding message header"
-							,socket, client.protocol, i, smb.last_error);
+						lprintf(LOG_ERR,"%04d %s %s !ERROR %d (%s) adding message header from %s to %s"
+							,socket, client.protocol, client_id, i, smb.last_error, sender_info, rcpt_info);
 						break;
 					}
-					sender_ext[0]=0;
-					if(msg.from_ext!=NULL)
-						SAFEPRINTF(sender_ext," #%s",msg.from_ext);
-					lprintf(LOG_INFO,"%04d %s Created message #%u from %s%s [%s] to %s [%s]"
-						,socket, client.protocol, newmsg.hdr.number, sender, sender_ext, smb_netaddrstr(&msg.from_net,tmp), rcpt_name, rcpt_addr);
+					lprintf(LOG_INFO,"%04d %s %s Added message header #%u from %s to %s"
+						,socket, client.protocol, client_id, newmsg.hdr.number, sender_info, rcpt_info);
 					if(relay_user.number!=0)
 						user_sent_email(&scfg, &relay_user, 1, usernum==1);
 
@@ -3987,6 +4056,7 @@ static void smtp_thread(void* arg)
 				smb_close_da(&smb);
 #endif
 				smb_close(&smb);
+				pthread_mutex_unlock(&savemsg_mutex);
 				continue;
 			}
 			if(buf[0]==0 && state==SMTP_STATE_DATA_HEADER) {	
@@ -4027,14 +4097,14 @@ static void smtp_thread(void* arg)
 					!(lines%startup->lines_per_yield))	
 					YIELD();
 				if((lines%100) == 0 && (msgtxt != NULL))
-					lprintf(LOG_DEBUG,"%04d %s received %lu lines (%lu bytes) of body text"
-						,socket, client.protocol, lines, ftell(msgtxt)-hdr_len);
+					lprintf(LOG_DEBUG,"%04d %s %s received %lu lines (%lu bytes) of body text"
+						,socket, client.protocol, client_id, lines, ftell(msgtxt)-hdr_len);
 				continue;
 			}
 			/* RFC822 Header parsing */
 			strip_char(buf, buf, '\r');	/* There should be no bare carriage returns in header fields */
 			if(startup->options&MAIL_OPT_DEBUG_RX_HEADER)
-				lprintf(LOG_DEBUG,"%04d %s %s",socket, client.protocol, buf);
+				lprintf(LOG_DEBUG,"%04d %s %s %s",socket, client.protocol, client_id, buf);
 
 			{
 				char field[32];
@@ -4046,7 +4116,7 @@ static void smtp_thread(void* arg)
 							,sender_addr,	sizeof(sender_addr)-1);
 					}
 					else if(stricmp(field,"CONTENT-TRANSFER-ENCODING")==0) {
-						lprintf(LOG_INFO,"%04d %s %s = %s", socket, client.protocol, field, p);
+						lprintf(LOG_INFO,"%04d %s %s %s = %s", socket, client.protocol, client_id, field, p);
 						if(stricmp(p,"base64")==0)
 							content_encoding=ENCODING_BASE64;
 						else if(stricmp(p,"quoted-printable")==0)
@@ -4068,7 +4138,7 @@ static void smtp_thread(void* arg)
 			continue;
 		}
 		strip_ctrl(buf, buf);
-		lprintf(LOG_DEBUG,"%04d %s RX: %s", socket, client.protocol, buf);
+		lprintf(LOG_DEBUG,"%04d %s %s RX: %s", socket, client.protocol, client_id, buf);
 		if(!strnicmp(buf,"HELO",4)) {
 			p=buf+4;
 			SKIP_WHITESPACE(p);
@@ -4103,33 +4173,35 @@ static void smtp_thread(void* arg)
 			subnum=INVALID_SUB;
 			continue;
 		}
-		ZERO_VAR(user_pass);
 		if((auth_login=(stricmp(buf,"AUTH LOGIN")==0))==TRUE 
 			|| strnicmp(buf,"AUTH PLAIN",10)==0) {
+			char user_pass[128] = "";
+			ZERO_VAR(relay_user);
+			listRemoveTaggedNode(&current_logins, socket, /* free_data */TRUE);
 			if(auth_login) {
 				sockprintf(socket,client.protocol,session,"334 VXNlcm5hbWU6");	/* Base64-encoded "Username:" */
 				if((rd=sockreadline(socket, client.protocol, session, buf, sizeof(buf)))<1) {
-					lprintf(LOG_WARNING,"%04d %s !missing AUTH LOGIN username argument", socket, client.protocol);
+					lprintf(LOG_WARNING,"%04d %s %s !Missing AUTH LOGIN username argument", socket, client.protocol, client_id);
 					badlogin(socket, session, client.protocol, badarg_rsp, NULL, NULL, host_name, &smtp.client_addr);
 					continue;
 				}
 				if(startup->options&MAIL_OPT_DEBUG_RX_RSP) 
-					lprintf(LOG_DEBUG,"%04d RX: %s",socket,buf);
-				if(b64_decode(user_name,sizeof(user_name),buf,rd)<1) {
-					lprintf(LOG_WARNING,"%04d %s !bad AUTH LOGIN username argument", socket, client.protocol);
+					lprintf(LOG_DEBUG,"%04d %s %s RX: %s", socket, client.protocol, client_id, buf);
+				if(b64_decode(user_name,sizeof(user_name),buf,rd)<1 || str_has_ctrl(user_name)) {
+					lprintf(LOG_WARNING,"%04d %s %s !Bad AUTH LOGIN username argument", socket, client.protocol, client_id);
 					badlogin(socket, session, client.protocol, badarg_rsp, NULL, NULL, host_name, &smtp.client_addr);
 					continue;
 				}
 				sockprintf(socket,client.protocol,session,"334 UGFzc3dvcmQ6");	/* Base64-encoded "Password:" */
 				if((rd=sockreadline(socket, client.protocol, session, buf, sizeof(buf)))<1) {
-					lprintf(LOG_WARNING,"%04d %s !missing AUTH LOGIN password argument", socket, client.protocol);
+					lprintf(LOG_WARNING,"%04d %s %s !Missing AUTH LOGIN password argument", socket, client.protocol, client_id);
 					badlogin(socket, session, client.protocol, badarg_rsp, user_name, NULL, host_name, &smtp.client_addr);
 					continue;
 				}
 				if(startup->options&MAIL_OPT_DEBUG_RX_RSP) 
-					lprintf(LOG_DEBUG,"%04d RX: %s",socket,buf);
-				if(b64_decode(user_pass,sizeof(user_pass),buf,rd)<1) {
-					lprintf(LOG_WARNING,"%04d %s !bad AUTH LOGIN password argument", socket, client.protocol);
+					lprintf(LOG_DEBUG,"%04d %s %s RX: %s", socket, client.protocol, client_id, buf);
+				if(b64_decode(user_pass,sizeof(user_pass),buf,rd)<1 || str_has_ctrl(user_pass)) {
+					lprintf(LOG_WARNING,"%04d %s %s !Bad AUTH LOGIN password argument", socket, client.protocol, client_id);
 					badlogin(socket, session, client.protocol, badarg_rsp, user_name, NULL, host_name, &smtp.client_addr);
 					continue;
 				}
@@ -4137,13 +4209,13 @@ static void smtp_thread(void* arg)
 				p=buf+10;
 				SKIP_WHITESPACE(p);
 				if(*p==0) {
-					lprintf(LOG_WARNING,"%04d %s !missing AUTH PLAIN argument", socket, client.protocol);
+					lprintf(LOG_WARNING,"%04d %s %s !Missing AUTH PLAIN argument", socket, client.protocol, client_id);
 					badlogin(socket, session, client.protocol, badarg_rsp, NULL, NULL, host_name, &smtp.client_addr);
 					continue;
 				}
 				ZERO_VAR(tmp);
-				if(b64_decode(tmp,sizeof(tmp),p,strlen(p))<1) {
-					lprintf(LOG_WARNING,"%04d %s !bad AUTH PLAIN argument", socket, client.protocol);
+				if(b64_decode(tmp,sizeof(tmp),p,strlen(p))<1 || str_has_ctrl(tmp)) {
+					lprintf(LOG_WARNING,"%04d %s %s !Bad AUTH PLAIN argument", socket, client.protocol, client_id);
 					badlogin(socket, session, client.protocol, badarg_rsp, NULL, NULL, host_name, &smtp.client_addr);
 					continue;
 				}
@@ -4151,7 +4223,7 @@ static void smtp_thread(void* arg)
 				while(*p) p++;	/* skip username */
 				p++;			/* skip NULL */
 				if(*p==0) {
-					lprintf(LOG_WARNING,"%04d %s !missing AUTH PLAIN user-id argument", socket, client.protocol);
+					lprintf(LOG_WARNING,"%04d %s %s !Missing AUTH PLAIN user-id argument", socket, client.protocol, client_id);
 					badlogin(socket, session, client.protocol, badarg_rsp, NULL, NULL, host_name, &smtp.client_addr);
 					continue;
 				}
@@ -4159,7 +4231,7 @@ static void smtp_thread(void* arg)
 				while(*p) p++;	/* skip user-id */
 				p++;			/* skip NULL */
 				if(*p==0) {
-					lprintf(LOG_WARNING,"%04d %s !missing AUTH PLAIN password argument", socket, client.protocol);
+					lprintf(LOG_WARNING,"%04d %s %s !Missing AUTH PLAIN password argument", socket, client.protocol, client_id);
 					badlogin(socket, session, client.protocol, badarg_rsp, user_name, NULL, host_name, &smtp.client_addr);
 					continue;
 				}
@@ -4168,51 +4240,57 @@ static void smtp_thread(void* arg)
 
 			if((relay_user.number=matchuser(&scfg,user_name,FALSE))==0) {
 				if(scfg.sys_misc&SM_ECHO_PW)
-					lprintf(LOG_WARNING,"%04d %s !UNKNOWN USER: '%s' (password: %s)"
-						,socket, client.protocol, user_name, user_pass);
+					lprintf(LOG_WARNING,"%04d %s %s !UNKNOWN USER: '%s' (password: %s)"
+						,socket, client.protocol, client_id, user_name, user_pass);
 				else
-					lprintf(LOG_WARNING,"%04d %s !UNKNOWN USER: '%s'"
-						,socket, client.protocol, user_name);
+					lprintf(LOG_WARNING,"%04d %s %s !UNKNOWN USER: '%s'"
+						,socket, client.protocol, client_id, user_name);
 				badlogin(socket, session, client.protocol, badauth_rsp, user_name, user_pass, host_name, &smtp.client_addr);
 				break;
 			}
 			if((i=getuserdat(&scfg, &relay_user))!=0) {
-				lprintf(LOG_ERR,"%04d %s !ERROR %d getting data on user (%s)"
-					,socket, client.protocol, i, user_name);
+				lprintf(LOG_ERR,"%04d %s %s !ERROR %d getting data on user (%s)"
+					,socket, client.protocol, client_id, i, user_name);
 				badlogin(socket, session, client.protocol, badauth_rsp, NULL, NULL, NULL, NULL);
 				break;
 			}
 			if(relay_user.misc&(DELETED|INACTIVE)) {
-				lprintf(LOG_WARNING,"%04d %s !DELETED or INACTIVE user #%u (%s)"
-					,socket, client.protocol, relay_user.number, user_name);
+				lprintf(LOG_WARNING,"%04d %s %s !DELETED or INACTIVE user #%u (%s)"
+					,socket, client.protocol, client_id, relay_user.number, user_name);
 				badlogin(socket, session, client.protocol, badauth_rsp, NULL, NULL, NULL, NULL);
 				break;
 			}
 			if(stricmp(user_pass,relay_user.pass)) {
 				if(scfg.sys_misc&SM_ECHO_PW)
-					lprintf(LOG_WARNING,"%04d %s !FAILED Password attempt for user %s: '%s' expected '%s'"
-						,socket, client.protocol, user_name, user_pass, relay_user.pass);
+					lprintf(LOG_WARNING,"%04d %s %s !FAILED Password attempt for user %s: '%s' expected '%s'"
+						,socket, client.protocol, client_id, user_name, user_pass, relay_user.pass);
 				else
-					lprintf(LOG_WARNING,"%04d %s !FAILED Password attempt for user %s"
-						,socket, client.protocol, user_name);
+					lprintf(LOG_WARNING,"%04d %s %s !FAILED Password attempt for user %s"
+						,socket, client.protocol, client_id, user_name);
 				badlogin(socket, session, client.protocol, badauth_rsp, user_name, user_pass, host_name, &smtp.client_addr);
 				break;
 			}
 
-			if(relay_user.pass[0])
+			if(relay_user.pass[0]) {
 				loginSuccess(startup->login_attempt_list, &smtp.client_addr);
+				listAddNodeData(&current_logins, client.addr, strlen(client.addr) + 1, socket, LAST_NODE);
+			}
 
 			/* Update client display */
 			client.user=relay_user.alias;
 			client.usernum = relay_user.number;
 			client_on(socket,&client,TRUE /* update */);
 
-			lprintf(LOG_INFO,"%04d %s %s authenticated using %s authentication"
-				,socket,client.protocol,relay_user.alias,auth_login ? "LOGIN" : "PLAIN");
+			lprintf(LOG_INFO,"%04d %s %s %s logged-in using %s authentication"
+				,socket,client.protocol, client_id, relay_user.alias, auth_login ? "LOGIN" : "PLAIN");
+			SAFEPRINTF(client_id, "<%s>", relay_user.alias);
 			sockprintf(socket,client.protocol,session,auth_ok);
 			continue;
 		}
 		if(!stricmp(buf,"AUTH CRAM-MD5")) {
+			ZERO_VAR(relay_user);
+			listRemoveTaggedNode(&current_logins, socket, /* free_data */TRUE);
+
 			safe_snprintf(challenge,sizeof(challenge),"<%x%x%lx%lx@%s>"
 				,rand(),socket,(ulong)time(NULL),(ulong)clock(),server_host_name());
 #if 0
@@ -4222,15 +4300,15 @@ static void smtp_thread(void* arg)
 			b64_encode(str,sizeof(str),challenge,0);
 			sockprintf(socket,client.protocol,session,"334 %s",str);
 			if((rd=sockreadline(socket, client.protocol, session, buf, sizeof(buf)))<1) {
-				lprintf(LOG_WARNING,"%04d %s !missing AUTH CRAM-MD5 response", socket, client.protocol);
+				lprintf(LOG_WARNING,"%04d %s %s !Missing AUTH CRAM-MD5 response", socket, client.protocol, client_id);
 				sockprintf(socket,client.protocol,session,badarg_rsp);
 				continue;
 			}
 			if(startup->options&MAIL_OPT_DEBUG_RX_RSP) 
-				lprintf(LOG_DEBUG,"%04d %s RX: %s",socket, client.protocol, buf);
+				lprintf(LOG_DEBUG,"%04d %s %s RX: %s",socket, client.protocol, client_id, buf);
 
-			if(b64_decode(response,sizeof(response),buf,rd)<1) {
-				lprintf(LOG_WARNING,"%04d %s !Bad AUTH CRAM-MD5 response", socket, client.protocol);
+			if(b64_decode(response,sizeof(response),buf,rd)<1 || str_has_ctrl(response)) {
+				lprintf(LOG_WARNING,"%04d %s %s !Bad AUTH CRAM-MD5 response", socket, client.protocol, client_id);
 				sockprintf(socket,client.protocol,session,badarg_rsp);
 				continue;
 			}
@@ -4244,20 +4322,20 @@ static void smtp_thread(void* arg)
 				p=response;
 			SAFECOPY(user_name,response);
 			if((relay_user.number=matchuser(&scfg,user_name,FALSE))==0) {
-				lprintf(LOG_WARNING,"%04d %s !UNKNOWN USER: '%s'"
-					,socket, client.protocol, user_name);
+				lprintf(LOG_WARNING,"%04d %s %s !UNKNOWN USER: '%s'"
+					,socket, client.protocol, client_id, user_name);
 				badlogin(socket, session, client.protocol, badauth_rsp, user_name, NULL, host_name, &smtp.client_addr);
 				break;
 			}
 			if((i=getuserdat(&scfg, &relay_user))!=0) {
-				lprintf(LOG_ERR,"%04d %s !ERROR %d getting data on user (%s)"
-					,socket, client.protocol, i, user_name);
+				lprintf(LOG_ERR,"%04d %s %s !ERROR %d getting data on user (%s)"
+					,socket, client.protocol, client_id, i, user_name);
 				badlogin(socket, session, client.protocol, badauth_rsp, NULL, NULL, NULL, NULL);
 				break;
 			}
 			if(relay_user.misc&(DELETED|INACTIVE)) {
-				lprintf(LOG_WARNING,"%04d %s !DELETED or INACTIVE user #%u (%s)"
-					,socket, client.protocol, relay_user.number, user_name);
+				lprintf(LOG_WARNING,"%04d %s %s !DELETED or INACTIVE user #%u (%s)"
+					,socket, client.protocol, client_id, relay_user.number, user_name);
 				badlogin(socket, session, client.protocol, badauth_rsp, NULL, NULL, NULL, NULL);
 				break;
 			}
@@ -4275,8 +4353,8 @@ static void smtp_thread(void* arg)
 			MD5_calc(digest,md5_data,sizeof(secret)+sizeof(digest));
 			MD5_hex((BYTE*)str,digest);
 			if(strcmp(p,str)) {
-				lprintf(LOG_WARNING,"%04d !SMTP %s FAILED CRAM-MD5 authentication"
-					,socket,relay_user.alias);
+				lprintf(LOG_WARNING,"%04d SMTP %s !%s FAILED CRAM-MD5 authentication"
+					,socket, client_id, relay_user.alias);
 #if 0
 				lprintf(LOG_DEBUG,"%04d !SMTP calc digest: %s"
 					,socket,str);
@@ -4287,16 +4365,19 @@ static void smtp_thread(void* arg)
 				break;
 			}
 
-			if(relay_user.pass[0])
+			if(relay_user.pass[0]) {
 				loginSuccess(startup->login_attempt_list, &smtp.client_addr);
+				listAddNodeData(&current_logins, client.addr, strlen(client.addr) + 1, socket, LAST_NODE);
+			}
 
 			/* Update client display */
 			client.user=relay_user.alias;
 			client.usernum = relay_user.number;
 			client_on(socket,&client,TRUE /* update */);
 
-			lprintf(LOG_INFO,"%04d %s %s authenticated using CRAM-MD5 authentication"
-				,socket, client.protocol,relay_user.alias);
+			lprintf(LOG_INFO,"%04d %s %s %s logged-in using CRAM-MD5 authentication"
+				,socket, client.protocol, client_id, relay_user.alias);
+			SAFEPRINTF(client_id, "<%s>", relay_user.alias);
 			sockprintf(socket,client.protocol,session,auth_ok);
 			continue;
 		}
@@ -4315,7 +4396,7 @@ static void smtp_thread(void* arg)
 		}
 		if(state<SMTP_STATE_HELO) {
 			/* RFC 821 4.1.1 "The first command in a session must be the HELO command." */
-			lprintf(LOG_WARNING,"%04d %s !MISSING 'HELO' command (Received: '%s')",socket, client.protocol, buf);
+			lprintf(LOG_WARNING,"%04d %s %s !MISSING 'HELO' command (Received: '%s')",socket, client.protocol, client_id, buf);
 			sockprintf(socket,client.protocol,session, badseq_rsp);
 			continue;
 		}
@@ -4336,9 +4417,9 @@ static void smtp_thread(void* arg)
 
 			/* reset recipient list */
 			if((rcptlst=freopen(rcptlst_fname,"w+",rcptlst))==NULL) {
-				lprintf(LOG_ERR,"%04d %s !ERROR %d re-opening %s"
-					,socket, client.protocol, errno, rcptlst_fname);
-				sockprintf(socket,client.protocol,session,sys_error);
+				lprintf(LOG_ERR,"%04d %s %s !ERROR %d re-opening %s"
+					,socket, client.protocol, client_id, errno, rcptlst_fname);
+				sockprintf(socket,client.protocol,session,smtp_error, "fopen error");
 				break;
 			}
 			rcpt_count=0;
@@ -4348,7 +4429,7 @@ static void smtp_thread(void* arg)
 
 			sockprintf(socket,client.protocol,session,ok_rsp);
 			badcmds=0;
-			lprintf(LOG_INFO,"%04d %s Session reset",socket, client.protocol);
+			lprintf(LOG_INFO,"%04d %s %s Session reset",socket, client.protocol, client_id);
 			continue;
 		}
 		if(!strnicmp(buf,"MAIL FROM:",10)
@@ -4370,8 +4451,8 @@ static void smtp_thread(void* arg)
 
 			/* If MAIL FROM address is in dnsbl_exempt.cfg, clear DNSBL results */
 			if(dnsbl_result.s_addr && email_addr_is_exempt(reverse_path)) {
-				lprintf(LOG_INFO,"%04d %s Ignoring DNSBL results for exempt sender: %s"
-					,socket, client.protocol,reverse_path);
+				lprintf(LOG_INFO,"%04d %s %s Ignoring DNSBL results for exempt sender: %s"
+					,socket, client.protocol, client_id, reverse_path);
 				dnsbl_result.s_addr=0;
 			}
 
@@ -4394,9 +4475,9 @@ static void smtp_thread(void* arg)
 
 			/* reset recipient list */
 			if((rcptlst=freopen(rcptlst_fname,"w+",rcptlst))==NULL) {
-				lprintf(LOG_ERR,"%04d %s !ERROR %d re-opening %s"
-					,socket, client.protocol, errno, rcptlst_fname);
-				sockprintf(socket,client.protocol,session,sys_error);
+				lprintf(LOG_ERR,"%04d %s %s !ERROR %d re-opening %s"
+					,socket, client.protocol, client_id, errno, rcptlst_fname);
+				sockprintf(socket,client.protocol,session,smtp_error, "fopen error");
 				break;
 			}
 			rcpt_count=0;
@@ -4421,7 +4502,7 @@ static void smtp_thread(void* arg)
 		if(!strnicmp(buf,"RCPT TO:",8)) {
 
 			if(state<SMTP_STATE_MAIL_FROM) {
-				lprintf(LOG_WARNING,"%04d %s !MISSING 'MAIL' command",socket, client.protocol);
+				lprintf(LOG_WARNING,"%04d %s %s !MISSING 'MAIL' command",socket, client.protocol, client_id);
 				sockprintf(socket,client.protocol,session, badseq_rsp);
 				continue;
 			}
@@ -4451,8 +4532,8 @@ static void smtp_thread(void* arg)
 			}
 
 			if(*p==0) {
-				lprintf(LOG_NOTICE,"%04d !SMTP NO RECIPIENT SPECIFIED"
-					,socket);
+				lprintf(LOG_NOTICE,"%04d %s %s !NO RECIPIENT SPECIFIED"
+					,socket, client.protocol, client_id);
 				sockprintf(socket,client.protocol,session, "500 No recipient specified");
 				continue;
 			}
@@ -4463,8 +4544,8 @@ static void smtp_thread(void* arg)
 			/* Check recipient counter */
 			if(startup->max_recipients) {
 				if(rcpt_count>=startup->max_recipients) {
-					lprintf(LOG_NOTICE,"%04d %s !MAXIMUM RECIPIENTS (%d) REACHED"
-						,socket, client.protocol, startup->max_recipients);
+					lprintf(LOG_NOTICE,"%04d %s %s !MAXIMUM RECIPIENTS (%d) REACHED"
+						,socket, client.protocol, client_id, startup->max_recipients);
 					SAFEPRINTF(tmp,"Maximum recipient count (%d)",startup->max_recipients);
 					spamlog(&scfg, (char*)client.protocol, "REFUSED", tmp
 						,host_name, host_ip, rcpt_addr, reverse_path);
@@ -4474,8 +4555,8 @@ static void smtp_thread(void* arg)
 				}
 				if(relay_user.number!=0 && !(relay_user.exempt&FLAG('M'))
 					&& rcpt_count+(waiting=getmail(&scfg,relay_user.number,/* sent: */TRUE, /* SPAM: */FALSE)) > startup->max_recipients) {
-					lprintf(LOG_NOTICE,"%04d %s !MAXIMUM PENDING SENT EMAILS (%lu) REACHED for User #%u (%s)"
-						,socket, client.protocol, waiting, relay_user.number, relay_user.alias);
+					lprintf(LOG_NOTICE,"%04d %s %s !MAXIMUM PENDING SENT EMAILS (%lu) REACHED for User #%u (%s)"
+						,socket, client.protocol, client_id, waiting, relay_user.number, relay_user.alias);
 					sockprintf(socket,client.protocol,session, "452 Too many pending emails sent");
 					stats.msgs_refused++;
 					continue;
@@ -4484,8 +4565,8 @@ static void smtp_thread(void* arg)
 
 			if(relay_user.number && (relay_user.etoday+rcpt_count) >= scfg.level_emailperday[relay_user.level]
 				&& !(relay_user.exempt&FLAG('M'))) {
-				lprintf(LOG_NOTICE,"%04d %s !EMAILS PER DAY LIMIT (%u) REACHED FOR USER #%u (%s)"
-					,socket, client.protocol, scfg.level_emailperday[relay_user.level], relay_user.number, relay_user.alias);
+				lprintf(LOG_NOTICE,"%04d %s %s !EMAILS PER DAY LIMIT (%u) REACHED FOR USER #%u (%s)"
+					,socket, client.protocol, client_id, scfg.level_emailperday[relay_user.level], relay_user.number, relay_user.alias);
 				SAFEPRINTF2(tmp,"Maximum emails per day (%u) for %s"
 					,scfg.level_emailperday[relay_user.level], relay_user.alias);
 				spamlog(&scfg, (char*)client.protocol, "REFUSED", tmp
@@ -4494,18 +4575,18 @@ static void smtp_thread(void* arg)
 				stats.msgs_refused++;
 				continue;
 			}
-				
+
 			/* Check for SPAM bait recipient */
 			if((spam_bait_result=findstr(rcpt_addr,spam_bait))==TRUE) {
 				char	reason[256];
 				SAFEPRINTF(reason,"SPAM BAIT (%s) taken", rcpt_addr);
-				lprintf(LOG_NOTICE,"%04d %s %s by: %s"
-					,socket, client.protocol, reason, reverse_path);
+				lprintf(LOG_NOTICE,"%04d %s %s %s by: %s"
+					,socket, client.protocol, client_id, reason, reverse_path);
 				if(relay_user.number==0) {
 					strcpy(tmp,"IGNORED");
 					if(dnsbl_result.s_addr==0						/* Don't double-filter */
 						&& !spam_block_exempt)	{ 
-						lprintf(LOG_NOTICE,"%04d !BLOCKING IP ADDRESS: %s in %s", socket, host_ip, spam_block);
+						lprintf(LOG_NOTICE,"%04d %s !BLOCKING IP ADDRESS: %s in %s", socket, client.protocol, client_id, spam_block);
 						filter_ip(&scfg, client.protocol, reason, host_name, host_ip, reverse_path, spam_block);
 						strcat(tmp," and BLOCKED");
 					}
@@ -4527,8 +4608,8 @@ static void smtp_thread(void* arg)
 			}
 
 			if(relay_user.number==0 && dnsbl_result.s_addr && startup->options&MAIL_OPT_DNSBL_BADUSER) {
-				lprintf(LOG_NOTICE,"%04d %s !REFUSED MAIL from blacklisted server (%lu total)"
-					,socket, client.protocol, ++stats.sessions_refused);
+				lprintf(LOG_NOTICE,"%04d %s %s !REFUSED MAIL from blacklisted server (%lu total)"
+					,socket, client.protocol, client_id, ++stats.sessions_refused);
 				SAFEPRINTF2(str,"Listed on %s as %s", dnsbl, inet_ntoa(dnsbl_result));
 				spamlog(&scfg, (char*)client.protocol, "REFUSED", str, host_name, host_ip, rcpt_addr, reverse_path);
 				sockprintf(socket,client.protocol,session
@@ -4547,8 +4628,8 @@ static void smtp_thread(void* arg)
 			/* Check for full address aliases */
 			p=alias(&scfg,p,alias_buf);
 			if(p==alias_buf) 
-				lprintf(LOG_DEBUG,"%04d %s ADDRESS ALIAS: %s (for %s)"
-					,socket, client.protocol,p,rcpt_addr);
+				lprintf(LOG_DEBUG,"%04d %s %s ADDRESS ALIAS: %s (for %s)"
+					,socket, client.protocol, client_id, p, rcpt_addr);
 
 			tp=strrchr(p,'@');
 			if(cmd==SMTP_CMD_MAIL && tp!=NULL) {
@@ -4578,8 +4659,8 @@ static void smtp_thread(void* arg)
 							faddr.net = net;
 							faddr.zone = zone;
 
-							lprintf(LOG_INFO,"%04d %s %s relaying to FidoNet address: %s (%s)"
-								,socket, client.protocol, relay_user.alias, tp+1, smb_faddrtoa(&faddr, NULL));
+							lprintf(LOG_INFO,"%04d %s %s %s relaying to FidoNet address: %s (%s)"
+								,socket, client.protocol, client_id, relay_user.alias, tp+1, smb_faddrtoa(&faddr, NULL));
 
 							fprintf(rcptlst,"[%u]\n",rcpt_count++);
 							fprintf(rcptlst,"%s=%s\n",smb_hfieldtype(RECIPIENT),rcpt_addr);
@@ -4621,8 +4702,8 @@ static void smtp_thread(void* arg)
 							|| relay_user.rest&(FLAG('G')|FLAG('M'))) &&
 						!findstr(host_name,relay_list) && 
 						!findstr(host_ip,relay_list)) {
-						lprintf(LOG_WARNING,"%04d %s !ILLEGAL RELAY ATTEMPT from %s [%s] to %s"
-							,socket, client.protocol, reverse_path, host_ip, p);
+						lprintf(LOG_WARNING,"%04d %s %s !ILLEGAL RELAY ATTEMPT from %s [%s] to %s"
+							,socket, client.protocol, client_id, reverse_path, host_ip, p);
 						SAFEPRINTF(tmp,"Relay attempt to: %s", p);
 						spamlog(&scfg, (char*)client.protocol, "REFUSED", tmp, host_name, host_ip, rcpt_addr, reverse_path);
 						if(startup->options&MAIL_OPT_ALLOW_RELAY)
@@ -4639,8 +4720,8 @@ static void smtp_thread(void* arg)
 					if(relay_user.number==0)
 						SAFECOPY(relay_user.alias,"Unknown User");
 
-					lprintf(LOG_INFO,"%04d %s %s relaying to external mail service: %s"
-						,socket, client.protocol, relay_user.alias, tp+1);
+					lprintf(LOG_INFO,"%04d %s %s %s relaying to external mail service: %s"
+						,socket, client.protocol, client_id, relay_user.alias, tp+1);
 
 					fprintf(rcptlst,"[%u]\n",rcpt_count++);
 					fprintf(rcptlst,"%s=%s\n",smb_hfieldtype(RECIPIENT),rcpt_addr);
@@ -4670,8 +4751,8 @@ static void smtp_thread(void* arg)
 
 			p=alias(&scfg,p,name_alias_buf);
 			if(p==name_alias_buf) 
-				lprintf(LOG_DEBUG,"%04d %s NAME ALIAS: %s (for %s)"
-					,socket, client.protocol,p,rcpt_addr);
+				lprintf(LOG_DEBUG,"%04d %s %s NAME ALIAS: %s (for %s)"
+					,socket, client.protocol, client_id, p, rcpt_addr);
 		
 			/* Check if message is to be processed by one or more external mail processors */
 			mailproc_match = INT_MAX;	// no match, by default
@@ -4699,8 +4780,8 @@ static void smtp_thread(void* arg)
 					if(!stricmp(p,scfg.sub[i]->code))
 						break;
 				if(i>=scfg.total_subs) {
-					lprintf(LOG_NOTICE,"%04d %s !UNKNOWN SUB-BOARD: %s", socket, client.protocol, p);
-					sockprintf(socket,client.protocol,session, "550 Unknown sub-board: %s", p);
+					lprintf(LOG_NOTICE,"%04d %s %s !UNKNOWN SUB-BOARD: %s", socket, client.protocol, client_id, p);
+					sockprintf(socket,client.protocol, session, "550 Unknown sub-board: %s", p);
 					continue;
 				}
 				subnum=i;
@@ -4719,8 +4800,8 @@ static void smtp_thread(void* arg)
 #if 0	/* should we fall-through to the sysop account? */
 				fprintf(rcptlst,"%s=%u\n",smb_hfieldtype(RECIPIENTEXT),1);
 #endif
-				lprintf(LOG_INFO,"%04d %s Routing mail for %s to External Mail Processor: %s"
-					,socket, client.protocol, rcpt_addr, mailproc_list[mailproc_match].name);
+				lprintf(LOG_INFO,"%04d %s %s Routing mail for %s to External Mail Processor: %s"
+					,socket, client.protocol, client_id, rcpt_addr, mailproc_list[mailproc_match].name);
 				sockprintf(socket,client.protocol,session,ok_rsp);
 				state=SMTP_STATE_RCPT_TO;
 				continue;
@@ -4738,8 +4819,8 @@ static void smtp_thread(void* arg)
 				}
 				if(i<scfg.total_qhubs) {	/* found matching QWKnet Hub */
 
-					lprintf(LOG_INFO,"%04d %s Routing mail for %s <%s> to QWKnet Hub: %s"
-						,socket, client.protocol, rcpt_addr, p, scfg.qhub[i]->id);
+					lprintf(LOG_INFO,"%04d %s %s Routing mail for %s <%s> to QWKnet Hub: %s"
+						,socket, client.protocol, client_id, rcpt_addr, p, scfg.qhub[i]->id);
 
 					fprintf(rcptlst,"[%u]\n",rcpt_count++);
 					fprintf(rcptlst,"%s=%s\n",smb_hfieldtype(RECIPIENT),rcpt_addr);
@@ -4754,7 +4835,7 @@ static void smtp_thread(void* arg)
 			}
 
 			if((p==alias_buf || p==name_alias_buf || startup->options&MAIL_OPT_ALLOW_RX_BY_NUMBER)
-				&& isdigit((uchar)*p)) {
+				&& IS_DIGIT(*p)) {
 				usernum=atoi(p);			/* RX by user number */
 				/* verify usernum */
 				username(&scfg,usernum,str);
@@ -4785,48 +4866,48 @@ static void smtp_thread(void* arg)
 			if(!usernum && startup->default_user[0]) {
 				usernum=matchuser(&scfg,startup->default_user,TRUE /* sysop_alias */);
 				if(usernum)
-					lprintf(LOG_INFO,"%04d %s Forwarding mail for UNKNOWN USER to default user-recipient: '%s' #%u"
-						,socket, client.protocol,startup->default_user,usernum);
+					lprintf(LOG_INFO,"%04d %s %s Forwarding mail for UNKNOWN USER to default user-recipient: '%s' #%u"
+						,socket, client.protocol, client_id,startup->default_user,usernum);
 				else
-					lprintf(LOG_WARNING,"%04d %s !UNKNOWN DEFAULT USER-RECIPIENT: '%s'"
-						,socket, client.protocol,startup->default_user);
+					lprintf(LOG_WARNING,"%04d %s %s !UNKNOWN DEFAULT USER-RECIPIENT: '%s'"
+						,socket, client.protocol, client_id, startup->default_user);
 			}
 
 			if(usernum==UINT_MAX) {
-				lprintf(LOG_INFO,"%04d %s Blocked tag: %s", socket, client.protocol, rcpt_to);
+				lprintf(LOG_INFO,"%04d %s %s Blocked tag: %s", socket, client.protocol, client_id, rcpt_to);
 				sockprintf(socket,client.protocol,session, "550 Unknown User: %s", rcpt_to);
 				continue;
 			}
 			if(!usernum) {
-				lprintf(LOG_WARNING,"%04d %s !UNKNOWN USER-RECIPIENT: '%s'", socket, client.protocol, rcpt_to);
+				lprintf(LOG_WARNING,"%04d %s %s !UNKNOWN USER-RECIPIENT: '%s'", socket, client.protocol, client_id, rcpt_to);
 				sockprintf(socket,client.protocol,session, "550 Unknown User: %s", rcpt_to);
 				continue;
 			}
 			user.number=usernum;
 			if((i=getuserdat(&scfg, &user))!=0) {
-				lprintf(LOG_ERR,"%04d %s !ERROR %d getting data on user-recipient #%u (%s)"
-					,socket, client.protocol, i, usernum, p);
+				lprintf(LOG_ERR,"%04d %s %s !ERROR %d getting data on user-recipient #%u (%s)"
+					,socket, client.protocol, client_id, i, usernum, p);
 				sockprintf(socket,client.protocol,session, "550 Unknown User: %s", rcpt_to);
 				continue;
 			}
 			if(user.misc&(DELETED|INACTIVE)) {
-				lprintf(LOG_WARNING,"%04d %s !DELETED or INACTIVE user-recipient #%u (%s)"
-					,socket, client.protocol, usernum, p);
+				lprintf(LOG_WARNING,"%04d %s %s !DELETED or INACTIVE user-recipient #%u (%s)"
+					,socket, client.protocol, client_id, usernum, p);
 				sockprintf(socket,client.protocol,session, "550 Unknown User: %s", rcpt_to);
 				continue;
 			}
 			if(cmd==SMTP_CMD_MAIL) {
 				if((user.rest&FLAG('M')) && relay_user.number==0) {
-					lprintf(LOG_NOTICE,"%04d %s !M-restricted user-recipient #%u (%s) cannot receive unauthenticated SMTP mail"
-						,socket, client.protocol, user.number, user.alias);
+					lprintf(LOG_NOTICE,"%04d %s %s !M-restricted user-recipient #%u (%s) cannot receive unauthenticated SMTP mail"
+						,socket, client.protocol, client_id, user.number, user.alias);
 					sockprintf(socket,client.protocol,session, "550 Closed mailbox: %s", rcpt_to);
 					stats.msgs_refused++;
 					continue;
 				}
 				if(startup->max_msgs_waiting && !(user.exempt&FLAG('W')) 
 					&& (waiting=getmail(&scfg, user.number, /* sent: */FALSE, /* spam: */FALSE)) > startup->max_msgs_waiting) {
-					lprintf(LOG_NOTICE,"%04d %s !User-recipient #%u (%s) mailbox (%lu msgs) exceeds the maximum (%u) msgs waiting"
-						,socket, client.protocol, user.number, user.alias, waiting, startup->max_msgs_waiting);
+					lprintf(LOG_NOTICE,"%04d %s %s !User-recipient #%u (%s) mailbox (%lu msgs) exceeds the maximum (%u) msgs waiting"
+						,socket, client.protocol, client_id, user.number, user.alias, waiting, startup->max_msgs_waiting);
 					sockprintf(socket,client.protocol,session, "450 Mailbox full: %s", rcpt_to);
 					stats.msgs_refused++;
 					continue;
@@ -4840,8 +4921,8 @@ static void smtp_thread(void* arg)
 						break;
 				}
 				if(i>=scfg.sys_nodes) {
-					lprintf(LOG_WARNING,"%04d %s !Attempt to send telegram to unavailable user-recipient #%u (%s)"
-						,socket, client.protocol, user.number, user.alias);
+					lprintf(LOG_WARNING,"%04d %s %s !Attempt to send telegram to unavailable user-recipient #%u (%s)"
+						,socket, client.protocol, client_id, user.number, user.alias);
 					sockprintf(socket,client.protocol,session,"450 User unavailable");
 					continue;
 				}
@@ -4862,8 +4943,8 @@ static void smtp_thread(void* arg)
 				&& (user.misc&NETMAIL || forward)
 				&& tp!=NULL && smb_netaddr_type(user.netmail)==NET_INTERNET 
 				&& !strstr(tp,scfg.sys_inetaddr)) {
-				lprintf(LOG_INFO,"%04d %s Forwarding to: %s"
-					,socket, client.protocol, user.netmail);
+				lprintf(LOG_INFO,"%04d %s %s Forwarding to: %s"
+					,socket, client.protocol, client_id, user.netmail);
 				fprintf(rcptlst,"%s=%u\n",smb_hfieldtype(RECIPIENTNETTYPE),NET_INTERNET);
 				fprintf(rcptlst,"%s=%s\n",smb_hfieldtype(RECIPIENTNETADDR),user.netmail);
 				sockprintf(socket,client.protocol,session,ok_rsp);	// used to be a 251 response, changed per RFC2821
@@ -4881,7 +4962,7 @@ static void smtp_thread(void* arg)
 		/* Message Data (header and body) */
 		if(!strnicmp(buf,"DATA",4)) {
 			if(state<SMTP_STATE_RCPT_TO) {
-				lprintf(LOG_WARNING,"%04d %s !MISSING 'RCPT TO' command", socket, client.protocol);
+				lprintf(LOG_WARNING,"%04d %s %s !MISSING 'RCPT TO' command", socket, client.protocol, client_id);
 				sockprintf(socket,client.protocol,session, badseq_rsp);
 				continue;
 			}
@@ -4890,8 +4971,8 @@ static void smtp_thread(void* arg)
 			}
 			remove(msgtxt_fname);
 			if((msgtxt=fopen(msgtxt_fname,"w+b"))==NULL) {
-				lprintf(LOG_ERR,"%04d %s !ERROR %d opening %s"
-					,socket, client.protocol, errno, msgtxt_fname);
+				lprintf(LOG_ERR,"%04d %s %s !ERROR %d opening %s"
+					,socket, client.protocol, client_id, errno, msgtxt_fname);
 				sockprintf(socket,client.protocol,session, insuf_stor);
 				continue;
 			}
@@ -4914,15 +4995,15 @@ static void smtp_thread(void* arg)
 				state=SMTP_STATE_DATA_BODY;	/* No RFC headers in Telegrams */
 			else
 				state=SMTP_STATE_DATA_HEADER;
-			lprintf(LOG_INFO,"%04d %s Receiving %s message from: %s to %s"
-				,socket, client.protocol, telegram ? "telegram":"mail", reverse_path, rcpt_addr);
+			lprintf(LOG_INFO,"%04d %s %s Receiving %s message from %s to <%s>"
+				,socket, client.protocol, client_id, telegram ? "telegram":"mail", reverse_path, rcpt_addr);
 			hdr_lines=0;
 			continue;
 		}
 		if(session == -1 && !stricmp(buf,"STARTTLS")) {
 			if (get_ssl_cert(&scfg, &estr, &level) == -1) {
 				if (estr) {
-					lprintf(level, "%04d %s !%s", socket, client.protocol, estr);
+					lprintf(level, "%04d %s %s !%s", socket, client.protocol, client_id, estr);
 					free_crypt_attrstr(estr);
 				}
 				sockprintf(socket, client.protocol, session, "454 TLS not available");
@@ -4944,7 +5025,7 @@ static void smtp_thread(void* arg)
 			if ((cstat=cryptSetAttribute(session, CRYPT_SESSINFO_PRIVATEKEY, scfg.tls_certificate)) != CRYPT_OK) {
 				unlock_ssl_cert();
 				GCES(cstat, "SMTPS", socket, session, "setting private key");
-				lprintf(LOG_ERR, "%04d !SMTP Unable to set private key", socket);
+				lprintf(LOG_ERR, "%04d SMTPS %s !Unable to set private key", socket, client_id);
 				cryptDestroySession(session);
 				session = -1;
 				sockprintf(socket, client.protocol, session, "454 TLS not available");
@@ -4979,9 +5060,9 @@ static void smtp_thread(void* arg)
 			continue;
 		}
 		sockprintf(socket,client.protocol,session,"500 Syntax error");
-		lprintf(LOG_WARNING,"%04d %s !UNSUPPORTED COMMAND: '%s'", socket, client.protocol, buf);
-		if(++badcmds>9) {
-			lprintf(LOG_WARNING,"%04d %s !TOO MANY INVALID COMMANDS (%lu)",socket, client.protocol, badcmds);
+		lprintf(LOG_WARNING,"%04d %s %s !UNSUPPORTED COMMAND: '%s'", socket, client.protocol, client_id, buf);
+		if(++badcmds > SMTP_MAX_BAD_CMDS) {
+			lprintf(LOG_WARNING,"%04d %s %s !TOO MANY INVALID COMMANDS (%lu)",socket, client.protocol, client_id, badcmds);
 			break;
 		}
 	}
@@ -5002,14 +5083,15 @@ static void smtp_thread(void* arg)
 
 	status(STATUS_WFC);
 
+	listRemoveTaggedNode(&current_logins, socket, /* free_data */TRUE);
 	protected_uint32_adjust(&active_clients, -1);
 	update_clients();
 	client_off(socket);
 
 	{
 		int32_t remain = thread_down();
-		lprintf(LOG_INFO,"%04d %s Session thread terminated (%u threads remain, %lu clients served)"
-			,socket, client.protocol, remain, ++stats.smtp_served);
+		lprintf(LOG_INFO,"%04d %s %s Session thread terminated (%u threads remain, %lu clients served)"
+			,socket, client.protocol, client_id, remain, ++stats.smtp_served);
 	}
 	free(mailproc_to_match);
 
@@ -5150,7 +5232,7 @@ static int remove_msg_intransit(smb_t* smb, smbmsg_t* msg)
 	msg->hdr.netattr&=~MSG_INTRANSIT;
 	i=smb_putmsghdr(smb,msg);
 	smb_unlockmsghdr(smb,msg);
-	
+
 	if(i!=0)
 		lprintf(LOG_ERR,"0000 SEND !ERROR %d (%s) writing message header #%u"
 			,i, smb->last_error, msg->idx.number);
@@ -5164,7 +5246,7 @@ void get_dns_server(char* dns_server, size_t len)
 	size_t		count;
 
 	sprintf(dns_server,"%.*s",(int)len-1,startup->dns_server);
-	if(!isalnum(dns_server[0])) {
+	if(!IS_ALPHANUMERIC(dns_server[0])) {
 		if((list=getNameServerList())!=NULL) {
 			if((count=strListCount(list))>0) {
 				sprintf(dns_server,"%.*s",(int)len,list[xp_random(count)]);
@@ -5225,9 +5307,7 @@ static SOCKET sendmail_negotiate(CRYPT_SESSION *session, smb_t *smb, smbmsg_t *m
 	ulong nb = 0;
 	int status;
 	char		buf[512];
-	char		err[1024];
-
-	strcpy(err,"UNKNOWN ERROR");
+	char		err[1024] = "UNKNOWN ERROR";
 
 	for (tls_retry = 0; tls_retry < 2; tls_retry++) {
 		if (!sendmail_open_socket(&sock, session))
@@ -5246,7 +5326,7 @@ static SOCKET sendmail_negotiate(CRYPT_SESSION *session, smb_t *smb, smbmsg_t *m
 			ip_addr=resolve_ip(server);
 			if(ip_addr==INADDR_NONE) {
 				SAFEPRINTF(err, "Error resolving hostname %s", server);
-				lprintf(LOG_WARNING,"%04d SEND !failure resolving hostname: %s", sock, server);
+				lprintf(LOG_WARNING,"%04d SEND !Failure resolving hostname: %s", sock, server);
 				continue;
 			}
 
@@ -5329,7 +5409,7 @@ static SOCKET sendmail_negotiate(CRYPT_SESSION *session, smb_t *smb, smbmsg_t *m
 					lprintf(LOG_DEBUG, "%04d SEND Starting TLS session", sock);
 					if(get_ssl_cert(&scfg, &estr, &level) == -1) {
 						if (estr) {
-							lprintf(level, "%04d !SEND/TLS %s", sock, estr);
+							lprintf(level, "%04d SEND/TLS !%s", sock, estr);
 							free_crypt_attrstr(estr);
 						}
 						continue;
@@ -5407,9 +5487,9 @@ static void sendmail_thread(void* arg)
 	char		err[1024];
 	char		buf[512];
 	char		str[128];
+	char		tmp[256];
 	char		resp[512];
 	char		toaddr[256];
-	char		fromext[128];
 	char		fromaddr[256];
 	char		challenge[256];
 	char		secret[64];
@@ -5539,11 +5619,29 @@ static void sendmail_thread(void* arg)
 				smb_unlockmsghdr(&smb,&msg);
 				continue;
 			}
+			if(msg.from_net.type==NET_INTERNET && msg.reverse_path!=NULL)
+				angle_bracket(fromaddr, sizeof(fromaddr), msg.reverse_path);
+			else 
+				angle_bracket(fromaddr, sizeof(fromaddr), usermailaddr(&scfg, str, msg.from));
+
+			char sender_info[512];
+			if(msg.from_ext != NULL)
+				SAFEPRINTF2(sender_info, "'%s' #%s", msg.from, msg.from_ext);
+			else if(strstr(fromaddr, msg.from) == fromaddr + 1)
+				SAFECOPY(sender_info, fromaddr);
+			else
+				SAFEPRINTF2(sender_info, "'%s' %s", msg.from, fromaddr);
+
+			char rcpt_info[512];
+			if(compare_addrs(msg.to, (char*)msg.to_net.addr) == 0)
+				angle_bracket(rcpt_info, sizeof(rcpt_info), msg.to);
+			else
+				safe_snprintf(rcpt_info, sizeof(rcpt_info), "'%s' %s", msg.to, angle_bracket(tmp, sizeof(tmp), (char*)msg.to_net.addr));
 
 			if(!(startup->options&MAIL_OPT_SEND_INTRANSIT) && msg.hdr.netattr&MSG_INTRANSIT) {
 				smb_unlockmsghdr(&smb,&msg);
 				lprintf(LOG_NOTICE,"0000 SEND Message #%u from %s to %s - in transit"
-					,msg.hdr.number, msg.from, (char*)msg.to_net.addr);
+					,msg.hdr.number, sender_info, rcpt_info);
 				continue;
 			}
 			msg.hdr.netattr|=MSG_INTRANSIT;	/* Prevent another sendmail thread from sending this msg */
@@ -5552,18 +5650,8 @@ static void sendmail_thread(void* arg)
 
 			active_sendmail=1, update_clients();
 
-			fromext[0]=0;
-			if(msg.from_ext)
-				SAFEPRINTF(fromext," #%s", msg.from_ext);
-			if(msg.from_net.type==NET_INTERNET && msg.reverse_path!=NULL)
-				SAFECOPY(fromaddr,msg.reverse_path);
-			else 
-				usermailaddr(&scfg,fromaddr,msg.from);
-			truncstr(fromaddr," ");
-
-			lprintf(LOG_INFO,"0000 SEND Message #%u (%u of %u) from %s%s %s to %s [%s]"
-				,msg.hdr.number, u+1, msgs, msg.from, fromext, fromaddr
-				,msg.to, (char*)msg.to_net.addr);
+			lprintf(LOG_INFO,"0000 SEND Message #%u (%u of %u) from %s to %s"
+				,msg.hdr.number, u+1, msgs, sender_info, rcpt_info);
 			SAFEPRINTF2(str,"Sending (%u of %u)", u+1, msgs);
 			status(str);
 #ifdef _WIN32
@@ -5745,10 +5833,7 @@ static void sendmail_thread(void* arg)
 			}
 
 			/* MAIL */
-			if(fromaddr[0]=='<')
-				sockprintf(sock,prot,session,"MAIL FROM: %s",fromaddr);
-			else
-				sockprintf(sock,prot,session,"MAIL FROM: <%s>",fromaddr);
+			sockprintf(sock,prot,session,"MAIL FROM:%s",fromaddr);
 			if(!sockgetrsp(sock,prot,session,"250", buf, sizeof(buf))) {
 				remove_msg_intransit(&smb,&msg);
 				SAFEPRINTF3(err,badrsp_err,server,buf,"250");
@@ -5769,10 +5854,7 @@ static void sendmail_thread(void* arg)
 					&& tp > p)
 					*tp=0;	/* Remove ":port" designation from envelope */
 			}
-			if(*toaddr == '<')
-				sockprintf(sock,prot,session,"RCPT TO: %s", toaddr);
-			else
-				sockprintf(sock,prot,session,"RCPT TO: <%s>", toaddr);
+			sockprintf(sock, prot, session, "RCPT TO:%s", angle_bracket(tmp, sizeof(tmp), toaddr));
 			if(!sockgetrsp(sock,prot,session,"25", buf, sizeof(buf))) {
 				remove_msg_intransit(&smb,&msg);
 				SAFEPRINTF3(err,badrsp_err,server,buf,"25*");
@@ -5802,9 +5884,8 @@ static void sendmail_thread(void* arg)
 					continue;
 				}
 			}
-			lprintf(LOG_INFO, "%04d %s Successfully sent message #%u (%lu bytes, %lu lines) from %s%s %s to %s [%s]"
-				,sock, prot, msg.hdr.number, bytes, lines, msg.from, fromext, fromaddr
-				,msg.to, toaddr);
+			lprintf(LOG_INFO, "%04d %s Successfully sent message #%u (%lu bytes, %lu lines) from %s to '%s' %s"
+				,sock, prot, msg.hdr.number, bytes, lines, sender_info, msg.to, toaddr);
 
 			/* Now lets mark this message for deletion without corrupting the index */
 			if((msg.hdr.netattr & MSG_KILLSENT) || msg.from_ext == NULL)
@@ -5823,7 +5904,7 @@ static void sendmail_thread(void* arg)
 			mail_close_socket(&sock, &session);
 
 			if(msg.from_agent==AGENT_PERSON && !(startup->options&MAIL_OPT_NO_AUTO_EXEMPT))
-				exempt_email_addr("SEND Auto-exempting",msg.from,fromext,fromaddr,toaddr);
+				exempt_email_addr("SEND Auto-exempting", sender_info, toaddr);
 		}
 		status(STATUS_WFC);
 		/* Free up resources here */
@@ -5887,8 +5968,15 @@ static void cleanup(int code)
 	mail_set=NULL;
 	terminated=TRUE;
 
+	while(savemsg_mutex_created && pthread_mutex_destroy(&savemsg_mutex)==EBUSY)
+		mswait(1);
+	savemsg_mutex_created = false;
+
 	update_clients();	/* active_clients is destroyed below */
 
+	listFree(&current_logins);
+	listFree(&current_connections);
+
 	if(protected_uint32_value(active_clients))
 		lprintf(LOG_WARNING,"!!!! Terminating with %d active clients", protected_uint32_value(active_clients));
 	else
@@ -5903,6 +5991,8 @@ static void cleanup(int code)
 	if(terminate_server || code) {
 		char str[1024];
 		sprintf(str,"%lu connections served", stats.connections_served);
+		if(stats.connections_exceeded)
+			sprintf(str+strlen(str),", %lu exceeded max", stats.connections_exceeded);
 		if(stats.connections_refused)
 			sprintf(str+strlen(str),", %lu refused", stats.connections_refused);
 		if(stats.connections_ignored)
@@ -5932,7 +6022,7 @@ const char* DLLCALL mail_ver(void)
 
 	DESCRIBE_COMPILER(compiler);
 
-	sscanf("$Revision: 1.734 $", "%*s %s", revision);
+	sscanf("$Revision: 1.735 $", "%*s %s", revision);
 
 	sprintf(ver,"%s %s%s  SMBLIB %s  "
 		"Compiled %s %s with %s"
@@ -5971,7 +6061,7 @@ void DLLCALL mail_server(void* arg)
 	smtp_t*			smtp;
 	FILE*			fp;
 	str_list_t		sec_list;
-	void			*cbdata;
+	char*			servprot = "N/A";
 	CRYPT_SESSION	session = -1;
 
 	mail_ver();
@@ -6012,6 +6102,8 @@ void DLLCALL mail_server(void* arg)
 
 	SetThreadName("sbbs/mailServer");
 	protected_uint32_init(&thread_count, 0);
+	listInit(&current_logins, LINK_LIST_MUTEX);
+	listInit(&current_connections, LINK_LIST_MUTEX);
 
 	do {
 		protected_uint32_init(&active_clients, 0);
@@ -6160,30 +6252,30 @@ void DLLCALL mail_server(void* arg)
 		}
 		terminated=FALSE;
 		if(!xpms_add_list(mail_set, PF_UNSPEC, SOCK_STREAM, 0, startup->interfaces
-			, startup->smtp_port, "SMTP Transfer Agent", mail_open_socket, startup->seteuid, "smtp"))
+			, startup->smtp_port, "SMTP Transfer Agent", mail_open_socket, startup->seteuid, (void*)servprot_smtp))
 			lprintf(LOG_INFO,"SMTP No extra interfaces listening");
 
 		if(startup->options&MAIL_OPT_USE_SUBMISSION_PORT) {
 			xpms_add_list(mail_set, PF_UNSPEC, SOCK_STREAM, 0, startup->interfaces
-				, startup->submission_port, "SMTP Submission Agent", mail_open_socket, startup->seteuid, "submission");
+				, startup->submission_port, "SMTP Submission Agent", mail_open_socket, startup->seteuid, (void*)servprot_submission);
 		}
 
 		if(startup->options&MAIL_OPT_TLS_SUBMISSION) {
 			xpms_add_list(mail_set, PF_UNSPEC, SOCK_STREAM, 0, startup->interfaces, startup->submissions_port
-				, "SMTPS Submission Agent", mail_open_socket, startup->seteuid, "submissions");
+				, "SMTPS Submission Agent", mail_open_socket, startup->seteuid, (void*)servprot_submissions);
 		}
 
 		if(startup->options&MAIL_OPT_ALLOW_POP3) {
 			/* open a socket and wait for a client */
 			if(!xpms_add_list(mail_set, PF_UNSPEC, SOCK_STREAM, 0, startup->pop3_interfaces, startup->pop3_port
-				, "POP3 Server", mail_open_socket, startup->seteuid, "pop3"))
+				, "POP3 Server", mail_open_socket, startup->seteuid, (void*)servprot_pop3))
 				lprintf(LOG_INFO,"POP3 No extra interfaces listening");
 		}
 
 		if(startup->options&MAIL_OPT_TLS_POP3) {
 			/* open a socket and wait for a client */
 			if(!xpms_add_list(mail_set, PF_UNSPEC, SOCK_STREAM, 0, startup->pop3_interfaces
-				, startup->pop3s_port, "POP3S Server", mail_open_socket, startup->seteuid, "pop3s"))
+				, startup->pop3s_port, "POP3S Server", mail_open_socket, startup->seteuid, (void*)servprot_pop3s))
 				lprintf(LOG_INFO,"POP3S No extra interfaces listening");
 		}
 
@@ -6209,6 +6301,9 @@ void DLLCALL mail_server(void* arg)
 			semfile_list_check(&initialized,shutdown_semfiles);
 		}
 
+		pthread_mutex_init(&savemsg_mutex, NULL);
+		savemsg_mutex_created = true;
+
 		/* signal caller that we've started up successfully */
 		if(startup->started!=NULL)
     		startup->started(startup->cbdata);
@@ -6242,13 +6337,30 @@ void DLLCALL mail_server(void* arg)
 
 			/* now wait for connection */
 			client_addr_len = sizeof(client_addr);
-			client_socket = xpms_accept(mail_set,&client_addr, &client_addr_len, startup->sem_chk_freq*1000, &cbdata);
+			client_socket = xpms_accept(mail_set,&client_addr, &client_addr_len, startup->sem_chk_freq*1000, (void**)&servprot);
 			if(client_socket != INVALID_SOCKET) {
+				bool is_smtp = (servprot != servprot_pop3 && servprot != servprot_pop3s);
 				if(startup->socket_open!=NULL)
 					startup->socket_open(startup->cbdata,TRUE);
 				stats.sockets++;
 
 				inet_addrtop(&client_addr, host_ip, sizeof(host_ip));
+
+				if(startup->max_concurrent_connections > 0) {
+					int ip_len = strlen(host_ip)+1;
+					int connections = listCountMatches(&current_connections, host_ip, ip_len);
+					int logins = listCountMatches(&current_logins, host_ip, ip_len);
+
+					if(connections - logins >= (int)startup->max_concurrent_connections
+						&& !is_host_exempt(&scfg, host_ip, /* host_name */NULL)) {
+						lprintf(LOG_NOTICE, "%04d %s [%s] !Maximum concurrent connections without login (%u) exceeded (%lu total)"
+ 							,client_socket, servprot, host_ip, startup->max_concurrent_connections, ++stats.connections_exceeded);
+						sockprintf(client_socket, servprot, session, is_smtp ? smtp_error : pop_error, "Maximum connections exceeded");
+						mail_close_socket(&client_socket, &session);
+						continue;
+					}
+				}
+
 				if(trashcan(&scfg,host_ip,"ip-silent")) {
 					mail_close_socket(&client_socket, &session);
 					stats.connections_ignored++;
@@ -6257,8 +6369,8 @@ void DLLCALL mail_server(void* arg)
 
 				if(protected_uint32_value(active_clients)>=startup->max_clients) {
 					lprintf(LOG_WARNING,"%04d %s !MAXIMUM CLIENTS (%u) reached, access denied (%lu total)"
-						,client_socket, (char *)cbdata, startup->max_clients, ++stats.connections_refused);
-					sockprintf(client_socket, cbdata, session,"-ERR Maximum active clients reached, please try again later.");
+						,client_socket, servprot, startup->max_clients, ++stats.connections_refused);
+					sockprintf(client_socket, servprot, session, is_smtp ? smtp_error : pop_error, "Maximum active clients reached");
 					mswait(3000);
 					mail_close_socket(&client_socket, &session);
 					continue;
@@ -6268,17 +6380,18 @@ void DLLCALL mail_server(void* arg)
 
 				if((i=ioctlsocket(client_socket, FIONBIO, &l))!=0) {
 					lprintf(LOG_CRIT,"%04d %s !ERROR %d (%d) disabling blocking on socket"
-						,client_socket, (char *)cbdata, i, ERROR_VALUE);
-					sockprintf(client_socket, cbdata, session,"-ERR System error, please try again later.");
+						,client_socket, servprot, i, ERROR_VALUE);
+					sockprintf(client_socket, servprot, session, is_smtp ? smtp_error : pop_error, "ioctlsocket error");
 					mswait(3000);
 					mail_close_socket(&client_socket, &session);
 					continue;
 				}
 
-				if(strcmp((char *)cbdata, "pop3") && strcmp((char *)cbdata, "pop3s")) { /* Not POP3 */
+				if(is_smtp) {
 					if((smtp=malloc(sizeof(smtp_t)))==NULL) {
 						lprintf(LOG_CRIT,"%04d SMTP !ERROR allocating %lu bytes of memory for smtp_t"
 							,client_socket, (ulong)sizeof(smtp_t));
+						sockprintf(client_socket, servprot, session, smtp_error, "malloc failure");
 						mail_close_socket(&client_socket, &session);
 						continue;
 					}
@@ -6286,14 +6399,14 @@ void DLLCALL mail_server(void* arg)
 					smtp->socket=client_socket;
 					memcpy(&smtp->client_addr, &client_addr, client_addr_len);
 					smtp->client_addr_len=client_addr_len;
-					smtp->tls_port = (strcmp((char *)cbdata, "submissions") == 0);
+					smtp->tls_port = (servprot == servprot_submissions);
 					protected_uint32_adjust(&thread_count,1);
 					_beginthread(smtp_thread, 0, smtp);
 					stats.connections_served++;
 				}
 				else {
 					if((pop3=malloc(sizeof(pop3_t)))==NULL) {
-						lprintf(LOG_CRIT,"%04d !POP3 ERROR allocating %lu bytes of memory for pop3_t"
+						lprintf(LOG_CRIT,"%04d POP3 !ERROR allocating %lu bytes of memory for pop3_t"
 							,client_socket,(ulong)sizeof(pop3_t));
 						sockprintf(client_socket, "POP3", session,"-ERR System error, please try again later.");
 						mswait(3000);
@@ -6304,7 +6417,7 @@ void DLLCALL mail_server(void* arg)
 					pop3->socket=client_socket;
 					memcpy(&pop3->client_addr, &client_addr, client_addr_len);
 					pop3->client_addr_len=client_addr_len;
-					pop3->tls_port = (strcmp((char *)cbdata, "pop3s") == 0);
+					pop3->tls_port = (servprot == servprot_pop3s);
 					protected_uint32_adjust(&thread_count,1);
 					_beginthread(pop3_thread, 0, pop3);
 					stats.connections_served++;
diff --git a/src/sbbs3/mailsrvr.h b/src/sbbs3/mailsrvr.h
index 8e23387f351285160e83cf9319f00109a2e3cf9d..95791bbd40636375d81e5abb1215a6f540ac8296 100644
--- a/src/sbbs3/mailsrvr.h
+++ b/src/sbbs3/mailsrvr.h
@@ -126,6 +126,7 @@ typedef struct {
 	/* Login Attempt parameters */
 	struct login_attempt_settings login_attempt;
 	link_list_t* login_attempt_list;
+	uint	max_concurrent_connections;
 
 } mail_startup_t;
 
diff --git a/src/sbbs3/main.cpp b/src/sbbs3/main.cpp
index 38dc48a68b7976c0bf01ccaddfa18cb39e0d6b6b..13866c396958c642fabab5be4f38924f0140c226 100644
--- a/src/sbbs3/main.cpp
+++ b/src/sbbs3/main.cpp
@@ -425,7 +425,7 @@ u_long resolve_ip(char *addr)
 		return((u_long)INADDR_NONE);
 
 	for(p=addr;*p;p++)
-		if(*p!='.' && !isdigit((uchar)*p))
+		if(*p!='.' && !IS_DIGIT(*p))
 			break;
 	if(!(*p))
 		return(inet_addr(addr));
@@ -4538,7 +4538,8 @@ void node_thread(void* arg)
 	if(sbbs->answer()) {
 
 		login_success = true;
-		listAddNodeData(&current_logins, sbbs->client.addr, strlen(sbbs->client.addr)+1, sbbs->cfg.node_num, LAST_NODE);
+		if(sbbs->useron.pass[0])
+			listAddNodeData(&current_logins, sbbs->client.addr, strlen(sbbs->client.addr)+1, sbbs->cfg.node_num, LAST_NODE);
 		if(sbbs->sys_status&SS_QWKLOGON) {
 			sbbs->getsmsg(sbbs->useron.number);
 			sbbs->qwk_sec();
@@ -4716,7 +4717,7 @@ bool sbbs_t::backup(const char* fname, int backup_level, bool rename)
 	if(!fexist(fname))
 		return false;
 
-	lprintf(LOG_DEBUG, "Backing-up %s (%lu bytes)", fname, flength(fname));
+	lprintf(LOG_DEBUG, "Backing-up %s (%lu bytes)", fname, (long)flength(fname));
 	return ::backup(fname, backup_level, rename) ? true : false;
 }
 
diff --git a/src/sbbs3/msgdate.c b/src/sbbs3/msgdate.c
index d5c564e3fa4e1f0921ee1d516173df5b96e10e6c..34a2028455d0d3c8b9718260e22679ad973328c2 100644
--- a/src/sbbs3/msgdate.c
+++ b/src/sbbs3/msgdate.c
@@ -86,10 +86,10 @@ when_t DLLCALL rfc822date(char* date)
 	memset(&when,0,sizeof(when));
 
 	while(*p && *p<=' ') p++;
-	while(*p && !isdigit(*p)) p++;
+	while(*p && !IS_DIGIT(*p)) p++;
 	/* DAY */
 	tm.tm_mday=atoi(p);
-	while(*p && isdigit(*p)) p++;
+	while(*p && IS_DIGIT(*p)) p++;
 	/* MONTH */
 	while(*p && *p<=' ') p++;
 	sprintf(month,"%3.3s",p);
@@ -125,23 +125,23 @@ when_t DLLCALL rfc822date(char* date)
 	else if(tm.tm_year>1900)
 		tm.tm_year-=1900;
 
-	while(*p && isdigit(*p)) p++;
+	while(*p && IS_DIGIT(*p)) p++;
 	/* HOUR */
 	while(*p && *p<=' ') p++;
 	tm.tm_hour=atoi(p);
-	while(*p && isdigit(*p)) p++;
+	while(*p && IS_DIGIT(*p)) p++;
 	/* MINUTE */
 	if(*p) p++;
 	tm.tm_min=atoi(p);
-	while(*p && isdigit(*p)) p++;
+	while(*p && IS_DIGIT(*p)) p++;
 	/* SECONDS */
 	if(*p) p++;
 	tm.tm_sec=atoi(p);
-	while(*p && isdigit(*p)) p++;
+	while(*p && IS_DIGIT(*p)) p++;
 	/* TIME ZONE */
 	while(*p && *p<=' ') p++;
 	if(*p) {
-		if(isdigit(*p) || *p=='-' || *p=='+') { /* [+|-]HHMM format */
+		if(IS_DIGIT(*p) || *p=='-' || *p=='+') { /* [+|-]HHMM format */
 			if(*p=='+') p++;
 			sprintf(str,"%.*s",*p=='-'? 3:2,p);
 			when.zone=atoi(str)*60;
diff --git a/src/sbbs3/netmail.cpp b/src/sbbs3/netmail.cpp
index 5e6f206c2c18d3f81bd46fb9d68bed507d565a52..eab9d211cddb2a309b1e3123f133de1841ad7a84 100644
--- a/src/sbbs3/netmail.cpp
+++ b/src/sbbs3/netmail.cpp
@@ -428,11 +428,11 @@ void sbbs_t::qwktonetmail(FILE *rep, char *block, char *into, uchar fromhub)
 
 
 	p=strrchr(to,'@');       /* Find '@' in name@addr */
-	if(p && !isdigit(*(p+1)) && !strchr(p,'.') && !strchr(p,':')) { /* QWKnet */
+	if(p && !IS_DIGIT(*(p+1)) && !strchr(p,'.') && !strchr(p,':')) { /* QWKnet */
 		qnet=1;
 		*p=0; 
 	}
-	else if(p==NULL || !isdigit(*(p+1)) || !cfg.total_faddrs) {
+	else if(p==NULL || !IS_DIGIT(*(p+1)) || !cfg.total_faddrs) {
 		if(cfg.inetmail_misc&NMAIL_ALLOW) {	/* Internet */
 			inet=1;
 		} else {
diff --git a/src/sbbs3/newuser.cpp b/src/sbbs3/newuser.cpp
index a06d1a91a1be0266893b27e1ce9e25a36c5e0cfd..f462e72fe84bdd4fcde64c2b7f75c062cc94bb3a 100644
--- a/src/sbbs3/newuser.cpp
+++ b/src/sbbs3/newuser.cpp
@@ -154,7 +154,7 @@ BOOL sbbs_t::newuser()
 
 		while(text[HitYourBackspaceKey][0] && !(useron.misc&(PETSCII|SWAP_DELETE)) && online) {
 			bputs(text[HitYourBackspaceKey]);
-			uchar key = getkey(K_NONE);
+			uchar key = getkey(K_CTRLKEYS);
 			bprintf(text[CharacterReceivedFmt], key, key);
 			if(key == '\b')
 				break;
@@ -179,7 +179,8 @@ BOOL sbbs_t::newuser()
 		}
 
 		if(useron.misc&ANSI) {
-			useron.rows=0;	/* Auto-rows */
+			useron.rows = TERM_ROWS_AUTO;
+			useron.cols = TERM_COLS_AUTO;
 			if(!(cfg.uq&UQ_COLORTERM) || useron.misc&(RIP|WIP|HTML) || yesno(text[ColorTerminalQ]))
 				useron.misc|=COLOR; 
 			else
@@ -382,7 +383,7 @@ BOOL sbbs_t::newuser()
 		c=0;
 		while(c < RAND_PASS_LEN) { 				/* Create random password */
 			useron.pass[c]=sbbs_random(43)+'0';
-			if(isalnum(useron.pass[c]))
+			if(IS_ALPHANUMERIC(useron.pass[c]))
 				c++; 
 		}
 		useron.pass[c]=0;
diff --git a/src/sbbs3/pack_qwk.cpp b/src/sbbs3/pack_qwk.cpp
index 110cbf3ebfe2abc5392ac6d6d4e0b8dd317252d8..1645895e470dc86b5cc22e256132a72331f295e9 100644
--- a/src/sbbs3/pack_qwk.cpp
+++ b/src/sbbs3/pack_qwk.cpp
@@ -712,7 +712,7 @@ bool sbbs_t::pack_qwk(char *packet, ulong *msgcnt, bool prepack)
 		for(i=0;i<(uint)g.gl_pathc;i++) { 			/* Copy BLT-*.* files */
 			fname=getfname(g.gl_pathv[i]);
 			padfname(fname,str);
-			if(isdigit(str[4]) && isdigit(str[9])) {
+			if(IS_DIGIT(str[4]) && IS_DIGIT(str[9])) {
 				SAFEPRINTF2(str,"%sQWK/%s",cfg.text_dir,fname);
 				SAFEPRINTF2(tmp2,"%s%s",cfg.temp_dir,fname);
 				mv(str,tmp2,/* copy: */TRUE); 
diff --git a/src/sbbs3/postmsg.cpp b/src/sbbs3/postmsg.cpp
index e54a2921f45db0adc84fb813e1b615a6bcebbd5e..20ef5cf5656ae46ddae37c0b87a3e4252c95130d 100644
--- a/src/sbbs3/postmsg.cpp
+++ b/src/sbbs3/postmsg.cpp
@@ -63,6 +63,16 @@ int msgbase_open(scfg_t* cfg, smb_t* smb, unsigned int subnum, int* storage, lon
 	return i;
 }
 
+static uchar* findsig(char* msgbuf)
+{
+	char* tail = strstr(msgbuf, "\n-- \r\n");
+	if(tail != NULL) {
+		*tail = '\0';
+		tail++;
+		truncsp(msgbuf);
+	}
+	return (uchar*)tail;
+}
 
 /****************************************************************************/
 /* Posts a message on sub-board number 'subnum'								*/
@@ -313,7 +323,7 @@ bool sbbs_t::postmsg(uint subnum, long wm_mode, smb_t* resmb, smbmsg_t* remsg)
 	if(tags[0])
 		smb_hfield_str(&msg, SMB_TAGS, tags);
 
-	i=smb_addmsg(&smb,&msg,storage,dupechk_hashes,xlat,(uchar*)msgbuf,NULL);
+	i=smb_addmsg(&smb,&msg,storage,dupechk_hashes,xlat,(uchar*)msgbuf, findsig(msgbuf));
 	free(msgbuf);
 
 	if(i==SMB_DUPE_MSG) {
@@ -402,7 +412,7 @@ extern "C" int DLLCALL msg_client_hfields(smbmsg_t* msg, client_t* client)
 	return SMB_SUCCESS;
 }
 
-/* Note: support MSG_BODY only, no tails or other data fields (dfields) */
+/* Note: finds signature delimiter automatically and (if applicable) separates msgbuf into body and tail */
 /* Adds/generates Message-IDs when needed */
 extern "C" int DLLCALL savemsg(scfg_t* cfg, smb_t* smb, smbmsg_t* msg, client_t* client, const char* server, char* msgbuf, smbmsg_t* remsg)
 {
@@ -484,7 +494,10 @@ extern "C" int DLLCALL savemsg(scfg_t* cfg, smb_t* smb, smbmsg_t* msg, client_t*
 		|| (msg->subj != NULL && !str_is_ascii(msg->subj) && utf8_str_is_valid(msg->subj)))
 		msg->hdr.auxattr |= MSG_HFIELDS_UTF8;
 
-	if((i=smb_addmsg(smb,msg,smb_storage_mode(cfg, smb),dupechk_hashes,xlat,(uchar*)msgbuf, /* tail: */NULL))==SMB_SUCCESS
+	msgbuf = strdup(msgbuf);
+	if(msgbuf == NULL)
+		return SMB_FAILURE;
+	if((i=smb_addmsg(smb,msg,smb_storage_mode(cfg, smb),dupechk_hashes,xlat,(uchar*)msgbuf, findsig(msgbuf)))==SMB_SUCCESS
 		&& msg->to!=NULL	/* no recipient means no header created at this stage */) {
 		if(smb->subnum == INVALID_SUB) {
 			if(msg->to_net.type == NET_FIDO && cfg->netmail_sem[0])
@@ -519,6 +532,7 @@ extern "C" int DLLCALL savemsg(scfg_t* cfg, smb_t* smb, smbmsg_t* msg, client_t*
 			}
 		}
 	}
+	free(msgbuf);
 	return(i);
 }
 
diff --git a/src/sbbs3/putmsg.cpp b/src/sbbs3/putmsg.cpp
index 6b8709908b5d1a25f5b42de0efa92d85395f8799..a18df8a2eb4fa57737fee43f29c75543d553b4fa 100644
--- a/src/sbbs3/putmsg.cpp
+++ b/src/sbbs3/putmsg.cpp
@@ -89,6 +89,7 @@ char sbbs_t::putmsgfrag(const char* buf, long* mode, long org_cols, JSObject* ob
 	char 	tmp2[256],tmp3[128];
 	char*	str=(char*)buf;
 	uchar	exatr=0;
+	char	mark = '\0';
 	int 	i;
 	ulong	l=0;
 	uint	lines_printed = 0;
@@ -158,11 +159,49 @@ char sbbs_t::putmsgfrag(const char* buf, long* mode, long org_cols, JSObject* ob
 				}
 				break;
 		}
+		if((*mode) & P_MARKUP) {
+			if(((mark == 0) && (str[l] == '*' || str[l] == '/' || str[l] == '_' || str[l] == '#')) || str[l] == mark) {
+				char* next= NULL;
+				if(mark == 0)
+					next = strchr(str + l + 1, str[l]);
+				char *blank = strstr(str + l + 1, "\n\r");
+				if(((l < 1 || IS_WHITESPACE(str[l - 1]) || IS_PUNCTUATION(str[l - 1]))
+						&& IS_ALPHANUMERIC(str[l + 1]) && mark == 0 && next != NULL	&& (*(next + 1) == '\0' || IS_WHITESPACE(*(next + 1)) || IS_PUNCTUATION(*(next+1)))
+						&& (blank == NULL || next < blank))
+					|| str[l] == mark) {
+					if(mark == 0)
+						mark = str[l];
+					else {
+						mark = 0;
+						if(!((*mode) & P_HIDEMARKS))
+							outchar(str[l]);
+					}
+					switch(str[l]) {
+						case '*':
+							attr(curatr ^ HIGH);
+							break;
+						case '/':
+							attr(curatr ^ BLINK);
+							break;							
+						case '_':
+							attr(curatr ^ (HIGH|BLINK));
+							break;
+						case '#':
+							attr(((curatr&0x0f) << 4) | ((curatr&0xf0) >> 4));
+							break;
+					}
+					if(mark != 0 && !((*mode) & P_HIDEMARKS))
+						outchar(str[l]);
+					l++;
+					continue;
+				}
+			}
+		}
 		if(str[l]==CTRL_A && str[l+1]!=0) {
 			if(str[l+1]=='"' && !(sys_status&SS_NEST_PF) && !((*mode)&P_NOATCODES)) {  /* Quote a file */
 				l+=2;
 				i=0;
-				while(i<(int)sizeof(tmp2)-1 && isprint((unsigned char)str[l]) && str[l]!='\\' && str[l]!='/')
+				while(i<(int)sizeof(tmp2)-1 && IS_PRINTABLE(str[l]) && str[l]!='\\' && str[l]!='/')
 					tmp2[i++]=str[l++];
 				if(i > 0) {
 					tmp2[i]=0;
@@ -195,7 +234,7 @@ char sbbs_t::putmsgfrag(const char* buf, long* mode, long org_cols, JSObject* ob
 		}
 		else if(!((*mode)&P_NOXATTRS)
 			&& (cfg.sys_misc&SM_PCBOARD) && str[l]=='@' && str[l+1]=='X'
-			&& isxdigit((unsigned char)str[l+2]) && isxdigit((unsigned char)str[l+3])) {
+			&& IS_HEXDIGIT(str[l+2]) && IS_HEXDIGIT(str[l+3])) {
 			sprintf(tmp2,"%.2s",str+l+2);
 			ulong val = ahtoul(tmp2);
 			// @X00 saves the current color and @XFF restores that saved color
@@ -216,15 +255,15 @@ char sbbs_t::putmsgfrag(const char* buf, long* mode, long org_cols, JSObject* ob
 		}
 		else if(!((*mode)&P_NOXATTRS)
 			&& (cfg.sys_misc&SM_WILDCAT) && str[l]=='@' && str[l+3]=='@'
-			&& isxdigit((unsigned char)str[l+1]) && isxdigit((unsigned char)str[l+2])) {
+			&& IS_HEXDIGIT(str[l+1]) && IS_HEXDIGIT(str[l+2])) {
 			sprintf(tmp2,"%.2s",str+l+1);
 			attr(ahtoul(tmp2));
 			// exatr=1;
 			l+=4;
 		}
 		else if(!((*mode)&P_NOXATTRS)
-			&& (cfg.sys_misc&SM_RENEGADE) && str[l]=='|' && isdigit((unsigned char)str[l+1])
-			&& isdigit((unsigned char)str[l+2]) && !(useron.misc&RIP)) {
+			&& (cfg.sys_misc&SM_RENEGADE) && str[l]=='|' && IS_DIGIT(str[l+1])
+			&& IS_DIGIT(str[l+2]) && !(useron.misc&RIP)) {
 			sprintf(tmp2,"%.2s",str+l+1);
 			i=atoi(tmp2);
 			if(i>=16) { 				/* setting background */
@@ -239,7 +278,7 @@ char sbbs_t::putmsgfrag(const char* buf, long* mode, long org_cols, JSObject* ob
 			l+=3;	/* Skip |xx */
 		}
 		else if(!((*mode)&P_NOXATTRS)
-			&& (cfg.sys_misc&SM_CELERITY) && str[l]=='|' && isalpha((unsigned char)str[l+1])
+			&& (cfg.sys_misc&SM_CELERITY) && str[l]=='|' && IS_ALPHA(str[l+1])
 			&& !(useron.misc&RIP)) {
 			switch(str[l+1]) {
 				case 'k':
@@ -298,7 +337,7 @@ char sbbs_t::putmsgfrag(const char* buf, long* mode, long org_cols, JSObject* ob
 			l+=2;	/* Skip |x */
 		}  /* Skip second digit if it exists */
 		else if(!((*mode)&P_NOXATTRS)
-			&& (cfg.sys_misc&SM_WWIV) && str[l]==CTRL_C && isdigit((unsigned char)str[l+1])) {
+			&& (cfg.sys_misc&SM_WWIV) && str[l]==CTRL_C && IS_DIGIT(str[l+1])) {
 			exatr=1;
 			switch(str[l+1]) {
 				default:
diff --git a/src/sbbs3/qwktomsg.cpp b/src/sbbs3/qwktomsg.cpp
index f8eedf254091428ecc1d98661255097950a21e49..7bf457d277ea0a2158addb0c68d04cafed3ccd20 100644
--- a/src/sbbs3/qwktomsg.cpp
+++ b/src/sbbs3/qwktomsg.cpp
@@ -366,10 +366,9 @@ bool sbbs_t::qwk_import_msg(FILE *qwk_fp, char *hdrblk, ulong blocks
 			if(!bodylen && !taillen)		/* Ignore blank lines at top of message */
 				continue;
 			if(!taillen && col==3 && bodylen>=3 && body[bodylen-3]=='-'
-				&& body[bodylen-2]=='-' && body[bodylen-1]=='-') {
+				&& body[bodylen-2]=='-' && (body[bodylen-1]=='-' || body[bodylen-1]==' ')) {
+				taillen = sprintf(tail, "--%c", body[bodylen-1]); /* DO NOT USE SAFECOPY */
 				bodylen-=3;
-				strcpy(tail,"---");	/* DO NOT USE SAFECOPY */
-				taillen=3; 
 			}
 			col=0;
 			if(taillen) {
diff --git a/src/sbbs3/readmsgs.cpp b/src/sbbs3/readmsgs.cpp
index 4fcf265ec2a1267c733f6deb354161f2d2768f9d..1cb29d9ec21a95c8027f83557aa57aa01d0a13ae 100644
--- a/src/sbbs3/readmsgs.cpp
+++ b/src/sbbs3/readmsgs.cpp
@@ -802,7 +802,7 @@ int sbbs_t::scanposts(uint subnum, long mode, const char *find)
 			domsg = true;
 			continue; 
 		}
-		if(thread_mode && (isalpha(l) || l=='?')) {
+		if(thread_mode && (IS_ALPHA(l) || l=='?')) {
 			thread_mode = false;
 			domsg = true;
 		}
diff --git a/src/sbbs3/readtext.c b/src/sbbs3/readtext.c
index 3454b53e2464334f9292ebe9696c9c66cb4dc47a..e92c1e962a3c5e0f5be27e4eac6bc81f8c43df3f 100644
--- a/src/sbbs3/readtext.c
+++ b/src/sbbs3/readtext.c
@@ -50,10 +50,10 @@ char *readtext(long *line,FILE *stream,long dflt)
 	for(i=1,j=0;i<k && j<sizeof(str)-1;j++) {
 		if(buf[i]=='\\')	{ /* escape */
 			i++;
-			if(isdigit(buf[i])) {
+			if(IS_DIGIT(buf[i])) {
 				str[j]=atoi(buf+i); 	/* decimal, NOT octal */
-				if(isdigit(buf[++i]))	/* skip up to 3 digits */
-					if(isdigit(buf[++i]))
+				if(IS_DIGIT(buf[++i]))	/* skip up to 3 digits */
+					if(IS_DIGIT(buf[++i]))
 						i++;
 				continue; 
 			}
diff --git a/src/sbbs3/release.bat b/src/sbbs3/release.bat
index c7e6716189e2166d00baee0c7d85829c92e05170..928d02cb898c6e0883ef9e15bc5e0b283ea832fa 100644
--- a/src/sbbs3/release.bat
+++ b/src/sbbs3/release.bat
@@ -1,2 +1,2 @@
 @echo off
-call build.bat "/p:Configuration=Release"
\ No newline at end of file
+call build.bat "/p:Configuration=Release" %*
\ No newline at end of file
diff --git a/src/sbbs3/sbbs.h b/src/sbbs3/sbbs.h
index b9ee228a36428cae4d79cd823db7920886746949..d5328abd56b2a806339df2e963128b5b9da0d8ea 100644
--- a/src/sbbs3/sbbs.h
+++ b/src/sbbs3/sbbs.h
@@ -784,6 +784,7 @@ public:
 	long	term_supports(long cmp_flags=0);
 	const char* term_type(long term_supports = -1);
 	const char* term_charset(long term_supports = -1);
+	bool	update_nodeterm(void);
 	int		backfill(const char* str, float pct, int full_attr, int empty_attr);
 	void	progress(const char* str, int count, int total, int interval=1);
 	bool	saveline(void);
@@ -893,7 +894,7 @@ public:
 	char*	parse_login(char*);
 
 	/* answer.cpp */
-	bool	answer();
+	bool	answer(void);
 
 	/* logon.ccp */
 	bool	logon(void);
@@ -1212,6 +1213,7 @@ extern "C" {
 	DLLEXPORT char 		ctrl_a_to_ascii_char(char code);
 	DLLEXPORT char *	truncstr(char* str, const char* set);
 	DLLEXPORT char *	ascii_str(uchar* str);
+	DLLEXPORT char *	condense_whitespace(char* str);
 	DLLEXPORT char		exascii_to_ascii_char(uchar ch);
 	DLLEXPORT BOOL		findstr(const char *insearch, const char *fname);
 	DLLEXPORT BOOL		findstr_in_string(const char* insearchof, char* string);
@@ -1220,6 +1222,7 @@ extern "C" {
 	DLLEXPORT BOOL		trashcan(scfg_t* cfg, const char *insearch, const char *name);
 	DLLEXPORT char *	trashcan_fname(scfg_t* cfg, const char *name, char* fname, size_t);
 	DLLEXPORT str_list_t trashcan_list(scfg_t* cfg, const char* name);
+	DLLEXPORT char *	strip_ansi(char* str);
 	DLLEXPORT char *	strip_exascii(const char *str, char* dest);
 	DLLEXPORT char *	strip_space(const char *str, char* dest);
 	DLLEXPORT char *	prep_file_desc(const char *str, char* dest);
diff --git a/src/sbbs3/sbbs_ini.c b/src/sbbs3/sbbs_ini.c
index e0bd769973b0bab5f24158b788e7499e169aae10..c6d8fd182e0e4493a3591ec13693de9065d5e63e 100644
--- a/src/sbbs3/sbbs_ini.c
+++ b/src/sbbs3/sbbs_ini.c
@@ -591,6 +591,7 @@ void sbbs_read_ini(
 		mail->bind_retry_count=iniGetInteger(list,section,strBindRetryCount,global->bind_retry_count);
 		mail->bind_retry_delay=iniGetInteger(list,section,strBindRetryDelay,global->bind_retry_delay);
 		mail->login_attempt = get_login_attempt_settings(list, section, global);
+		mail->max_concurrent_connections = iniGetInteger(list, section, "MaxConcurrentConnections", 0);
 	}
 
 	/***********************************************************************/
@@ -1042,6 +1043,8 @@ BOOL sbbs_write_ini(
 			break;
 		if(!iniSetInteger(lp,section,"ConnectTimeout",mail->connect_timeout,&style))
 			break;
+		if(!iniSetInteger(lp,section,"MaxConcurrentConnections",mail->max_concurrent_connections,&style))
+			break;
 
 		if(strcmp(mail->host_name,global->host_name)==0
             || (bbs != NULL && strcmp(bbs->host_name,cfg->sys_inetaddr)==0))
diff --git a/src/sbbs3/sbbscon.c b/src/sbbs3/sbbscon.c
index c603e9f9331dbaad4c37f82342695a75ad972700..205c69dff3798fc8693abc811bbd73fffc0914c6 100644
--- a/src/sbbs3/sbbscon.c
+++ b/src/sbbs3/sbbscon.c
@@ -1723,7 +1723,7 @@ int main(int argc, char** argv)
 						mail_startup.options=strtoul(arg,NULL,0);
 						break;
 					case 'S':	/* SMTP/SendMail */
-						if(isdigit(*arg)) {
+						if(IS_DIGIT(*arg)) {
 							mail_startup.smtp_port=atoi(arg);
 							break;
 						}
@@ -1737,7 +1737,7 @@ int main(int argc, char** argv)
 						}
 						break;
 					case 'P':	/* POP3 */
-						if(isdigit(*arg)) {
+						if(IS_DIGIT(*arg)) {
 							mail_startup.pop3_port=atoi(arg);
 							break;
 						}
diff --git a/src/sbbs3/sbbsdefs.h b/src/sbbs3/sbbsdefs.h
index 3790189074abbec58dbab0092c42041f25f8e8b7..2d475447d147b37094c6bcc69accc1f5d4fa0be1 100644
--- a/src/sbbs3/sbbsdefs.h
+++ b/src/sbbs3/sbbsdefs.h
@@ -530,7 +530,9 @@ typedef enum {						/* Values for xtrn_t.event				*/
 #define LEN_FDESC		58	/* File description 							*/
 #define LEN_FCDT		 9	/* 9 digits for file credit values				*/
 #define LEN_TITLE		70	/* Message title								*/
-#define LEN_MAIN_CMD	34	/* Storage in user.dat for custom commands		*/
+#define LEN_MAIN_CMD	28	/* Unused Storage in user.dat					*/
+#define LEN_COLS		3
+#define LEN_ROWS		3
 #define LEN_PASS		40
 #define MIN_PASS_LEN	 4
 #define RAND_PASS_LEN	 8
@@ -592,14 +594,16 @@ typedef enum {						/* Values for xtrn_t.event				*/
 #define U_FLAGS2	U_TL+2
 #define U_EXEMPT	U_FLAGS2+8
 #define U_REST		U_EXEMPT+8
-#define U_ROWS		U_REST+8+2 	/* Number of Rows on user's monitor */
-#define U_SEX		U_ROWS+2 		/* Sex, Del, ANSI, color etc.		*/
+#define U_OLDROWS	U_REST+8+2 	/* Number of Rows on user's monitor */
+#define U_SEX		U_OLDROWS+2 	/* Sex, Del, ANSI, color etc.		*/
 #define U_MISC		U_SEX+1 		/* Miscellaneous flags in 8byte hex */
 #define U_OLDXEDIT	U_MISC+8 		/* External editor (Version 1 method  */
 #define U_LEECH 	U_OLDXEDIT+2 	/* two hex digits - leech attempt count */
 #define U_CURSUB	U_LEECH+2  	/* Current sub (internal code)  */
 #define U_CURXTRN	U_CURSUB+16 /* Current xtrn (internal code) */
-#define U_MAIN_CMD	U_CURXTRN+8+2 	/* unused */
+#define U_ROWS		U_CURXTRN+8+2
+#define U_COLS		U_ROWS+LEN_ROWS
+#define U_MAIN_CMD	U_COLS+LEN_COLS	/* unused */
 #define U_PASS		U_MAIN_CMD+LEN_MAIN_CMD
 #define U_SCAN_CMD	U_PASS+LEN_PASS+2  				/* unused */
 #define U_IPADDR	U_SCAN_CMD+LEN_SCAN_CMD 		/* unused */
@@ -690,11 +694,13 @@ typedef enum {						/* Values for xtrn_t.event				*/
 #define TERM_KEY_PAGEUP	CTRL_P
 #define TERM_KEY_PAGEDN	CTRL_N
 
+#define TERM_COLS_AUTO		0
 #define TERM_COLS_MIN		40
-#define TERM_COLS_MAX		255
+#define TERM_COLS_MAX		999
 #define TERM_COLS_DEFAULT	80
+#define TERM_ROWS_AUTO		0
 #define TERM_ROWS_MIN		8	// Amstrad NC100 has an 8-line display
-#define TERM_ROWS_MAX		255
+#define TERM_ROWS_MAX		999
 #define TERM_ROWS_DEFAULT	24
 
 							/* Online status (online)						*/
@@ -761,6 +767,7 @@ typedef enum {						/* Values for xtrn_t.event				*/
 #define K_NOSPIN	(1L<<21)	/* Do not honor the user's spinning cursor	*/
 #define K_ANSI_CPR	(1L<<22)	/* Expect ANSI Cursor Position Report		*/
 #define K_TRIM		(1L<<23)	/* Trimmed white-space						*/
+#define K_CTRLKEYS	(1L<<24)	/* No control-key handling/eating in inkey()*/
 
 								/* Bits in 'mode' for putmsg and printfile  */
 #define P_NONE		0			/* No mode flags							*/
@@ -781,6 +788,8 @@ typedef enum {						/* Values for xtrn_t.event				*/
 #define P_UTF8		(1<<13)		/* Message is UTF-8							*/
 #define P_AUTO_UTF8	(1<<14)		/* Message may be UTF-8, auto-detect		*/
 #define P_NOXATTRS	(1<<15)		/* No "Extra Attribute Codes" supported		*/
+#define P_MARKUP	(1<<16)		/* Support StyleCodes/Rich/StructuredText	*/
+#define P_HIDEMARKS	(1<<17)		/* Hide the mark-up characters				*/
 
 								/* Bits in 'mode' for listfiles             */
 #define FL_ULTIME   (1<<0)		/* List files by upload time                */
@@ -1026,10 +1035,12 @@ typedef struct {						/* Users information */
 
 	uchar	level,						/* Security level */
 			sex,						/* Sex - M or F */
-			rows,               		/* Rows of text */
 			prot,						/* Default transfer protocol */
 			leech;						/* Leech attempt counter */
 
+	int		rows,               		/* Rows on terminal (0 = auto-detect) */
+			cols;						/* Columns on terminal (0 = auto-detect) */
+
 	ulong	misc,						/* Misc. bits - ANSI, Deleted etc. */
 			qwk,						/* QWK settings */
 			chat,						/* Chat defaults */
diff --git a/src/sbbs3/sbbsecho.c b/src/sbbs3/sbbsecho.c
index 165012b157f71edb018e82f778ad6b60843c8d4b..09f3d24afba10e14281eda524ff029095dde628f 100644
--- a/src/sbbs3/sbbsecho.c
+++ b/src/sbbs3/sbbsecho.c
@@ -849,7 +849,7 @@ int write_flofile(const char *infile, fidoaddr_t dest, bool bundle, bool use_out
 		infile++;
 
 #ifdef __unix__
-	if(isalpha(infile[0]) && infile[1] == ':')	// Ignore "C:" prefix
+	if(IS_ALPHA(infile[0]) && infile[1] == ':')	// Ignore "C:" prefix
 		infile += 2;
 #endif
 	SAFECOPY(attachment, infile);
@@ -2282,7 +2282,7 @@ char* process_areafix(fidoaddr_t addr, char* inbuf, const char* password, const
 		}
 	}
 
-	if(((tp=strstr(p,"---\r"))!=NULL || (tp=strstr(p,"--- "))!=NULL) &&
+	if(((tp=strstr(p,"---\r"))!=NULL || (tp=strstr(p,"--- "))!=NULL || (tp=strstr(p,"-- \r"))!=NULL) &&
 		(*(tp-1)=='\r' || *(tp-1)=='\n'))
 		*tp=0;
 
@@ -2317,7 +2317,7 @@ char* process_areafix(fidoaddr_t addr, char* inbuf, const char* password, const
 	add_area=strListInit();
 	del_area=strListInit();
 	for(l=0;l<m;l++) {
-		while(*(p+l) && isspace((uchar)*(p+l))) l++;
+		while(*(p+l) && IS_WHITESPACE(*(p+l))) l++;
 		while(*(p+l)==CTRL_A) {				/* Ignore kludge lines June-13-2004 */
 			while(*(p+l) && *(p+l)!='\r') l++;
 			continue;
@@ -3107,7 +3107,7 @@ time32_t fmsgtime(const char *str)
 	memset(&tm,0,sizeof(tm));
 	tm.tm_isdst=-1;	/* Do not adjust for DST */
 
-	if(isdigit((uchar)str[1])) {	/* Regular format: "01 Jan 86  02:34:56" */
+	if(IS_DIGIT(str[1])) {	/* Regular format: "01 Jan 86  02:34:56" */
 		tm.tm_mday=atoi(str);
 		sprintf(month,"%3.3s",str+3);
 		if(!stricmp(month,"jan"))
@@ -3503,7 +3503,8 @@ int fmsgtosmsg(char* fbuf, fmsghdr_t* hdr, uint user, uint subnum)
 		if(ch == '\n')
 			continue;
 		if(cr && (!strncmp(fbuf+l,"--- ",4)
-			|| !strncmp(fbuf+l,"---\r",4)))
+			|| !strncmp(fbuf+l,"---\r",4)
+			|| !strncmp(fbuf+l,"-- \r",4)))
 			done=1; 			/* tear line and down go into tail */
 		else if(cr && !strncmp(fbuf+l," * Origin: ",11) && subnum != INVALID_SUB) {
 			p=(char*)fbuf+l+11;
@@ -6207,7 +6208,7 @@ int main(int argc, char **argv)
 		else {
 			if(strchr(argv[i],'.')!=NULL && fexist(argv[i]))
 				SAFECOPY(cfg.cfgfile,argv[i]);
-			else if(isdigit((uchar)argv[i][0]))
+			else if(IS_DIGIT(argv[i][0]))
 				nodeaddr = atofaddr(argv[i]);
 			else
 				sub_code = argv[i];
@@ -6399,7 +6400,7 @@ int main(int argc, char **argv)
 			SKIP_WHITESPACE(p);		/* Skip white space */
 
 			while(*p && *p!=';') {
-				if(!isdigit(*p)) {
+				if(!IS_DIGIT(*p)) {
 					printf("\n");
 					lprintf(LOG_WARNING, "Invalid Area File line, expected link address(es) after echo-tag: '%s'", str);
 					break;
diff --git a/src/sbbs3/scfg/scfg.c b/src/sbbs3/scfg/scfg.c
index e6ca0748c3290e09d048e67303918d8cf3043910..9b43ad3bf3d3a7c14a08f78cae988f2a6c2c329c 100644
--- a/src/sbbs3/scfg/scfg.c
+++ b/src/sbbs3/scfg/scfg.c
@@ -233,7 +233,7 @@ int main(int argc, char **argv)
 					umask(strtoul(argv[i]+2,NULL,8));
 					break;
 				case 'G':
-					if(isalpha(argv[i][2]))
+					if(IS_ALPHA(argv[i][2]))
 						grpname = argv[i]+2;
 					else
 						grpnum = atoi(argv[i]+2);
diff --git a/src/sbbs3/scfg/scfgnet.c b/src/sbbs3/scfg/scfgnet.c
index eb3ac852c194c6450a033e6fbc0252a56dbdf207..d2305c2e6eb69a0f059d6999c6d9e2784da3507b 100644
--- a/src/sbbs3/scfg/scfgnet.c
+++ b/src/sbbs3/scfg/scfgnet.c
@@ -980,7 +980,7 @@ void qhub_edit(int num)
 				;
 				if(uifc.input(WIN_MID|WIN_SAV,0,0
 					,"Node to Perform Call-out",str,3,K_EDIT) > 0) {
-					if(isdigit(*str))
+					if(IS_DIGIT(*str))
 						cfg.qhub[num]->node=atoi(str);
 					else
 						cfg.qhub[num]->node = NODE_ANY;
diff --git a/src/sbbs3/scfg/scfgsub.c b/src/sbbs3/scfg/scfgsub.c
index 1df9769b76cabba36b501941ba53c0b1b8c3bebd..5e0cb7fae8db281f5ffca50da8a7eb977429b70d 100644
--- a/src/sbbs3/scfg/scfgsub.c
+++ b/src/sbbs3/scfg/scfgsub.c
@@ -560,6 +560,8 @@ void sub_cfg(uint grpnum)
 	#endif
 						sprintf(opt[n++],"%-27.27s%s","Compress Messages (LZH)"
 							,cfg.sub[i]->misc&SUB_LZH ? "Yes" : "No");
+						sprintf(opt[n++],"%-27.27s%s","Apply Markup Codes"
+							,cfg.sub[i]->pmode&P_MARKUP ? ((cfg.sub[i]->pmode&P_HIDEMARKS)  ? "Hide" : "Yes") : "No");
 						sprintf(opt[n++],"%-27.27s%s","Extra Attribute Codes"
 							,cfg.sub[i]->pmode&P_NOXATTRS ? "No" : "Yes");
 						sprintf(opt[n++],"%-27.27s%s","Word-wrap Messages"
@@ -581,7 +583,7 @@ void sub_cfg(uint grpnum)
 						if(n==-1)
 							break;
 						switch(n) {
-							case 0:
+							case __COUNTER__:
 								if(cfg.sub[i]->misc&SUB_PONLY)
 									n=2;
 								else 
@@ -620,7 +622,7 @@ void sub_cfg(uint grpnum)
 									cfg.sub[i]->misc|=(SUB_PRIV|SUB_PONLY); 
 								}
 								break;
-							case 1:
+							case __COUNTER__:
 								if(cfg.sub[i]->misc&SUB_AONLY)
 									n=2;
 								else 
@@ -659,7 +661,7 @@ void sub_cfg(uint grpnum)
 									cfg.sub[i]->misc|=(SUB_ANON|SUB_AONLY); 
 								}
 								break;
-							case 2:
+							case __COUNTER__:
 								n=(cfg.sub[i]->misc&SUB_NAME) ? 0:1;
 								uifc.helpbuf=
 									"`User Real Names in Posts on Sub-board:`\n"
@@ -682,7 +684,7 @@ void sub_cfg(uint grpnum)
 									cfg.sub[i]->misc&=~SUB_NAME; 
 								}
 								break;
-							case 3:
+							case __COUNTER__:
 								if(cfg.sub[i]->misc&SUB_EDITLAST)
 									n=2;
 								else
@@ -727,7 +729,7 @@ void sub_cfg(uint grpnum)
 									break;
 								}
 								break;
-							case 4:
+							case __COUNTER__:
 								if(cfg.sub[i]->misc&SUB_DELLAST)
 									n=2;
 								else 
@@ -768,7 +770,7 @@ void sub_cfg(uint grpnum)
 									cfg.sub[i]->misc|=(SUB_DEL|SUB_DELLAST); 
 								}
 								break;
-							case 5:
+							case __COUNTER__:
 								n=(cfg.sub[i]->misc&SUB_NSDEF) ? 0:1;
 								uifc.helpbuf=
 									"`Default On for New Scan:`\n"
@@ -790,7 +792,7 @@ void sub_cfg(uint grpnum)
 									cfg.sub[i]->misc&=~SUB_NSDEF; 
 								}
 								break;
-							case 6:
+							case __COUNTER__:
 								n=(cfg.sub[i]->misc&SUB_FORCED) ? 0:1;
 								uifc.helpbuf=
 									"`Forced On for New Scan:`\n"
@@ -813,7 +815,7 @@ void sub_cfg(uint grpnum)
 									cfg.sub[i]->misc&=~SUB_FORCED; 
 								}
 								break;
-							case 7:
+							case __COUNTER__:
 								n=(cfg.sub[i]->misc&SUB_SSDEF) ? 0:1;
 								uifc.helpbuf=
 									"`Default On for Your Scan:`\n"
@@ -835,7 +837,7 @@ void sub_cfg(uint grpnum)
 									cfg.sub[i]->misc&=~SUB_SSDEF; 
 								}
 								break;
-							case 8:
+							case __COUNTER__:
 								n=(cfg.sub[i]->misc&SUB_TOUSER) ? 0:1;
 								uifc.helpbuf=
 									"`Prompt for 'To' User on Public Posts:`\n"
@@ -858,7 +860,7 @@ void sub_cfg(uint grpnum)
 									cfg.sub[i]->misc&=~SUB_TOUSER; 
 								}
 								break;
-							case 9:
+							case __COUNTER__:
 								n=(cfg.sub[i]->misc&SUB_NOVOTING) ? 1:0;
 								uifc.helpbuf=
 									"`Allow Message Voting:`\n"
@@ -880,7 +882,7 @@ void sub_cfg(uint grpnum)
 									cfg.sub[i]->misc ^= SUB_NOVOTING; 
 								}
 								break;
-							case 10:
+							case __COUNTER__:
 								n=(cfg.sub[i]->misc&SUB_QUOTE) ? 0:1;
 								uifc.helpbuf=
 									"`Allow Message Quoting:`\n"
@@ -902,7 +904,7 @@ void sub_cfg(uint grpnum)
 									cfg.sub[i]->misc&=~SUB_QUOTE; 
 								}
 								break;
-							case 11:
+							case __COUNTER__:
 								n=(cfg.sub[i]->misc&SUB_MSGTAGS) ? 0:1;
 								uifc.helpbuf=
 									"`Allow Message Tagging:`\n"
@@ -924,7 +926,7 @@ void sub_cfg(uint grpnum)
 									cfg.sub[i]->misc&=~SUB_MSGTAGS; 
 								}
 								break;
-							case 12:
+							case __COUNTER__:
 								n=(cfg.sub[i]->misc&SUB_NOUSERSIG) ? 0:1;
 								uifc.helpbuf=
 									"Suppress User Signatures:\n"
@@ -946,7 +948,7 @@ void sub_cfg(uint grpnum)
 									cfg.sub[i]->misc&=~SUB_NOUSERSIG; 
 								}
 								break;
-							case 13:
+							case __COUNTER__:
 								n=(cfg.sub[i]->misc&SUB_SYSPERM) ? 0:1;
 								uifc.helpbuf=
 									"`Operator Messages Automatically Permanent:`\n"
@@ -1008,7 +1010,7 @@ void sub_cfg(uint grpnum)
 								}
 								break;
 	#endif
-							case 14:
+							case __COUNTER__:
 								n=(cfg.sub[i]->misc&SUB_LZH) ? 0:1;
 								uifc.helpbuf=
 									"`Compress Messages with LZH Encoding:`\n"
@@ -1037,7 +1039,44 @@ void sub_cfg(uint grpnum)
 									cfg.sub[i]->misc&=~SUB_LZH; 
 								}
 								break;
-							case 15:
+							case __COUNTER__:
+								n = (cfg.sub[i]->pmode&P_MARKUP) ? (cfg.sub[i]->pmode&P_HIDEMARKS ? 1 : 0) : 2;
+								uifc.helpbuf=
+									"`Interpret/Display Markup Codes in Messages:`\n"
+									"\n"
+									"Markup codes are called 'StyleCodes' in GoldEd, 'Rich Text' in SemPoint,\n"
+									"and 'Structured Text' in Mozilla/Thunderbird.\n"
+									"\n"
+									"`*Bold Text*`\n"
+									"/Italic Text/\n"
+									"~#Inverse Text#~\n"
+									"_Underlined Text_\n"
+									"\n"
+									"Markup character cannot be combined.\n"
+								;
+								strcpy(opt[0],"Yes");
+								strcpy(opt[1],"Yes and Hide the Markup Characters");
+								strcpy(opt[2],"No");
+								opt[3][0]=0;
+								n=uifc.list(WIN_SAV|WIN_MID,0,0,0,&n,0
+									,"Interpret/Display Markup Codes in Messages", opt);
+								if(n==-1)
+									break;
+								if(n == 0 && (cfg.sub[i]->pmode&(P_MARKUP|P_HIDEMARKS)) != P_MARKUP) {
+									uifc.changes = TRUE;
+									cfg.sub[i]->pmode |= P_MARKUP;
+									cfg.sub[i]->pmode &= ~P_HIDEMARKS;
+								}
+								else if(n == 1 && (cfg.sub[i]->pmode&(P_MARKUP|P_HIDEMARKS)) != (P_MARKUP|P_HIDEMARKS)) {
+									uifc.changes = TRUE;
+									cfg.sub[i]->pmode |= (P_MARKUP|P_HIDEMARKS);
+								}
+								else if(n == 2 && (cfg.sub[i]->pmode&(P_MARKUP|P_HIDEMARKS)) != 0) {
+									uifc.changes = TRUE;
+									cfg.sub[i]->pmode &= ~(P_MARKUP|P_HIDEMARKS);
+								}
+								break;
+							case __COUNTER__:
 								n=(cfg.sub[i]->pmode&P_NOXATTRS) ? 1:0;
 								uifc.helpbuf=
 									"`Extra Attribute Codes:`\n"
@@ -1059,7 +1098,7 @@ void sub_cfg(uint grpnum)
 									cfg.sub[i]->pmode ^= P_NOXATTRS;
 								}
 								break;
-							case 16:
+							case __COUNTER__:
 								n=(cfg.sub[i]->n_pmode&P_WORDWRAP) ? 1:0;
 								uifc.helpbuf=
 									"`Word-wrap Message Text:`\n"
@@ -1081,7 +1120,7 @@ void sub_cfg(uint grpnum)
 									cfg.sub[i]->n_pmode ^= P_WORDWRAP;
 								}
 								break;
-							case 17:
+							case __COUNTER__:
 								n=(cfg.sub[i]->pmode&P_AUTO_UTF8) ? 0:1;
 								uifc.helpbuf=
 									"`Automatically Detect UTF-8 Message Text:`\n"
@@ -1105,7 +1144,7 @@ void sub_cfg(uint grpnum)
 									cfg.sub[i]->pmode ^= P_AUTO_UTF8;
 								}
 								break;
-							case 18:
+							case __COUNTER__:
 								n=(cfg.sub[i]->misc&SUB_TEMPLATE) ? 0:1;
 								uifc.helpbuf=
 									"`Use this Sub-board as a Template for New Subs:`\n"
diff --git a/src/sbbs3/scfg/scfgxtrn.c b/src/sbbs3/scfg/scfgxtrn.c
index 98a3a31df30180cca2d5bee1b5ca7290c1468e08..3155544035ca03dff2784a5af4e8063a38429d56 100644
--- a/src/sbbs3/scfg/scfgxtrn.c
+++ b/src/sbbs3/scfg/scfgxtrn.c
@@ -623,7 +623,7 @@ void tevents_cfg()
 						SAFEPRINTF(str, "%u", cfg.event[i]->node);
 					if(uifc.input(WIN_MID|WIN_SAV,0,0,"Execution Node Number (or Any)"
 						,str,3,K_EDIT) > 0) {
-						if(isdigit(*str))
+						if(IS_DIGIT(*str))
 							cfg.event[i]->node=atoi(str);
 						else
 							cfg.event[i]->node = NODE_ANY;
@@ -643,7 +643,7 @@ void tevents_cfg()
 					for(p=str;*p;p++) {
 						if(atoi(p)) {
 							cfg.event[i]->months|=(1<<(atoi(p)-1));
-							while(*p && isdigit(*p))
+							while(*p && IS_DIGIT(*p))
 								p++;
 						} else {
 							for(j=0;j<12;j++)
@@ -672,7 +672,7 @@ void tevents_cfg()
 						if(!isdigit(*p))
 							continue;
 						cfg.event[i]->mdays|=(1<<atoi(p));
-						while(*p && isdigit(*p))
+						while(*p && IS_DIGIT(*p))
 							p++;
 					}
 					break;
diff --git a/src/sbbs3/services.c b/src/sbbs3/services.c
index 14aa6c479235a0cbb8b7ea81d62cd86ca4091bb4..cbb6372c9806b91ea21ec1e88eabc5f46afc8042 100644
--- a/src/sbbs3/services.c
+++ b/src/sbbs3/services.c
@@ -44,7 +44,6 @@
 #include <stdlib.h>			/* ltoa in GNU C lib */
 #include <stdarg.h>			/* va_list */
 #include <string.h>			/* strrchr */
-#include <ctype.h>			/* isdigit */
 #include <fcntl.h>			/* Open flags */
 #include <errno.h>			/* errno */
 
@@ -386,7 +385,7 @@ js_login(JSContext *cx, uintN argc, jsval *arglist)
 			putmsgptrs(&scfg, &client->user, client->subscan);
 	}
 
-	if(isdigit(*user))
+	if(IS_DIGIT(*user))
 		client->user.number=atoi(user);
 	else if(*user)
 		client->user.number=matchuser(&scfg,user,FALSE);
@@ -1575,7 +1574,7 @@ static service_t* read_services_ini(const char* services_ini, service_t* service
 		SAFECOPY(serv.cmd,iniGetString(list,sec_list[i],"Command","",cmd));
 
 		p=iniGetString(list,sec_list[i],"Port",serv.protocol,portstr);
-		if(isdigit(*p))
+		if(IS_DIGIT(*p))
 			serv.port=(ushort)strtol(p,NULL,0);
 		else {
 			struct servent* servent = getservbyname(p,serv.options&SERVICE_OPT_UDP ? "udp":"tcp");
diff --git a/src/sbbs3/sexyz.c b/src/sbbs3/sexyz.c
index dd9ff75f429ff4d3e8e31f4ecc23230587ed76b1..ee1c77a52da226f9ec9af51367ec1ea855a98737 100644
--- a/src/sbbs3/sexyz.c
+++ b/src/sbbs3/sexyz.c
@@ -1656,7 +1656,7 @@ int main(int argc, char **argv)
 
 	for(i=1;i<argc;i++) {
 
-		if(sock==INVALID_SOCKET && isdigit(argv[i][0])) {
+		if(sock==INVALID_SOCKET && IS_DIGIT(argv[i][0])) {
 			sock=atoi(argv[i]);
 			continue;
 		}
diff --git a/src/sbbs3/smbutil.c b/src/sbbs3/smbutil.c
index 3b17537b46f45bf79d34ec7b2ba7be9827e936e9..f651220e6bf503d400ab76a8e60715fe230d83ac 100644
--- a/src/sbbs3/smbutil.c
+++ b/src/sbbs3/smbutil.c
@@ -53,7 +53,6 @@ const char *mon[]={"Jan","Feb","Mar","Apr","May","Jun"
 #endif
 
 #if defined(_WIN32)
-	#include <ctype.h>	/* isdigit() */
 	#include <conio.h>	/* getch() */
 #endif
 
@@ -447,7 +446,7 @@ void config(void)
 	printf("Last Message  =%-6"PRIu32" New Value (CR=No Change): "
 		,smb.status.last_msg);
 	gets(str);
-	if(isdigit(str[0]))
+	if(IS_DIGIT(str[0]))
 		last_msg = atol(str);
 	printf("Header offset =%-5"PRIu32"  New value (CR=No Change): "
 		,smb.status.header_offset);
@@ -479,15 +478,15 @@ void config(void)
 	}
 	if(last_msg != 0)
 		smb.status.last_msg = last_msg;
-	if(isdigit(max_msgs[0]))
+	if(IS_DIGIT(max_msgs[0]))
 		smb.status.max_msgs=atol(max_msgs);
-	if(isdigit(max_crcs[0]))
+	if(IS_DIGIT(max_crcs[0]))
 		smb.status.max_crcs=atol(max_crcs);
-	if(isdigit(max_age[0]))
+	if(IS_DIGIT(max_age[0]))
 		smb.status.max_age=atoi(max_age);
-	if(isdigit(header_offset[0]))
+	if(IS_DIGIT(header_offset[0]))
 		smb.status.header_offset=atol(header_offset);
-	if(isdigit(attr[0]))
+	if(IS_DIGIT(attr[0]))
 		smb.status.attr=atoi(attr);
 	i=smb_putstatus(&smb);
 	smb_unlocksmbhdr(&smb);
@@ -1511,7 +1510,7 @@ short str2tzone(const char* str)
 	char tmp[32];
 	short zone;
 
-	if(isdigit(*str) || *str=='-' || *str=='+') { /* [+|-]HHMM format */
+	if(IS_DIGIT(*str) || *str=='-' || *str=='+') { /* [+|-]HHMM format */
 		if(*str=='+') str++;
 		sprintf(tmp,"%.*s",*str=='-'? 3:2,str);
 		zone=atoi(tmp)*60;
@@ -1626,7 +1625,7 @@ int main(int argc, char **argv)
 			argv[x][0]=='/' ||		/* for backwards compatibilty */
 #endif
 			argv[x][0]=='-') {
-			if(isdigit(argv[x][1])) {
+			if(IS_DIGIT(argv[x][1])) {
 				count=strtol(argv[x]+1,NULL,10);
 				continue;
 			}
diff --git a/src/sbbs3/str.cpp b/src/sbbs3/str.cpp
index c3732489c0e378403b4781b995bc5718967aa3da..d5286bf4b9552119d7be21dc89a92457d6429c99 100644
--- a/src/sbbs3/str.cpp
+++ b/src/sbbs3/str.cpp
@@ -255,15 +255,15 @@ void sbbs_t::sif(char *fname, char *answers, long len)
 				cr=1;
 				m++; 
 			}
-			if(isdigit((uchar)buf[m+1])) {
+			if(IS_DIGIT(buf[m+1])) {
 				max=buf[++m]&0xf;
-				if(isdigit((uchar)buf[m+1]))
+				if(IS_DIGIT(buf[m+1]))
 					max=max*10+(buf[++m]&0xf); 
 			}
-			if(buf[m+1]=='.' && isdigit((uchar)buf[m+2])) {
+			if(buf[m+1]=='.' && IS_DIGIT(buf[m+2])) {
 				m++;
 				min=buf[++m]&0xf;
-				if(isdigit((uchar)buf[m+1]))
+				if(IS_DIGIT(buf[m+1]))
 					min=min*10+(buf[++m]&0xf); 
 			}
 			if(buf[m+1]=='"') {
@@ -402,15 +402,15 @@ void sbbs_t::sof(char *fname, char *answers, long len)
 				cr=1;
 				m++; 
 			}
-			if(isdigit((uchar)buf[m+1])) {
+			if(IS_DIGIT(buf[m+1])) {
 				max=buf[++m]&0xf;
-				if(isdigit((uchar)buf[m+1]))
+				if(IS_DIGIT(buf[m+1]))
 					max=max*10+(buf[++m]&0xf); 
 			}
-			if(buf[m+1]=='.' && isdigit((uchar)buf[m+2])) {
+			if(buf[m+1]=='.' && IS_DIGIT(buf[m+2])) {
 				m++;
 				min=buf[++m]&0xf;
-				if(isdigit((uchar)buf[m+1]))
+				if(IS_DIGIT(buf[m+1]))
 					min=min*10+(buf[++m]&0xf); 
 			}
 			if(buf[m+1]=='"') {
@@ -542,9 +542,9 @@ size_t sbbs_t::gettmplt(char *strout, const char *templt, long mode)
 			}
 		}
 		else if(c<t) {
-			if(tmplt[c]=='N' && !isdigit((uchar)ch))
+			if(tmplt[c]=='N' && !IS_DIGIT(ch))
 				continue;
-			if(tmplt[c]=='A' && !isalpha((uchar)ch))
+			if(tmplt[c]=='A' && !IS_ALPHA(ch))
 				continue;
 			outchar(ch);
 			str[c++]=ch;
@@ -980,7 +980,7 @@ void sbbs_t::xfer_prot_menu(enum XFER_TYPE type)
 			continue;
 		if(type==XFER_BIDIR && cfg.prot[i]->bicmd[0]==0)
 			continue;
-		if(printed && (printed%2)==0)
+		if(printed && (cols < 80 || (printed%2)==0))
 			CRLF;
 		bprintf(text[TransferProtLstFmt],cfg.prot[i]->mnemonic,cfg.prot[i]->name);
 		printed++;
diff --git a/src/sbbs3/str_util.c b/src/sbbs3/str_util.c
index e8b3f1f65876da8e7226cf657a09525c9c3c494f..e64133be0ea701cc35a4c71e3e4d14708eb74948 100644
--- a/src/sbbs3/str_util.c
+++ b/src/sbbs3/str_util.c
@@ -89,6 +89,25 @@ char* strip_ctrl(const char *str, char* dest)
 	return dest;
 }
 
+char* strip_ansi(char* str)
+{
+	char* s = str;
+	char* d = str;
+	while(*s != '\0') {
+		if(*s == ESC && *(s + 1) == '[') {
+			s += 2;
+			while(*s != '\0' && (*s < '@' || *s > '~'))
+				s++;
+			if(*s != '\0') // Skip "final byte""
+				s++;
+		} else {
+			*(d++) = *(s++);
+		}
+	}
+	*d = '\0';
+	return str;
+}
+
 char* strip_exascii(const char *str, char* dest)
 {
 	int	i,j;
@@ -109,7 +128,7 @@ char* strip_space(const char *str, char* dest)
 	if(dest==NULL && (dest=strdup(str))==NULL)
 		return NULL;
 	for(i=j=0;str[i];i++)
-		if(!isspace((unsigned char)str[i]))
+		if(!IS_WHITESPACE(str[i]))
 			dest[j++]=str[i];
 	dest[j]=0;
 	return dest;
@@ -136,6 +155,7 @@ char* prep_file_desc(const char *str, char* dest)
 
 	if(dest==NULL && (dest=strdup(str))==NULL)
 		return NULL;
+	strip_ansi(dest);
 	for(i=j=0;str[i];i++)
 		if(str[i]==CTRL_A && str[i+1]!=0) {
 			i++;
@@ -147,7 +167,7 @@ char* prep_file_desc(const char *str, char* dest)
 		}
 		else if(j && str[i]<=' ' && dest[j-1]==' ')
 			continue;
-		else if(i && !isalnum((unsigned char)str[i]) && str[i]==str[i-1])
+		else if(i && !IS_ALPHANUMERIC(str[i]) && str[i]==str[i-1])
 			continue;
 		else if((uchar)str[i]>=' ')
 			dest[j++]=str[i];
@@ -496,7 +516,7 @@ uint hptoi(const char *str)
 
 	if(!str[1] || toupper(str[0])<='F')
 		return(ahtoul(str));
-	strcpy(tmp,str);
+	SAFECOPY(tmp,str);
 	tmp[0]='F';
 	i=ahtoul(tmp)+((toupper(str[0])-'F')*0x10);
 	return(i);
@@ -643,6 +663,25 @@ char* ascii_str(uchar* str)
 	return((char*)str);
 }
 
+/****************************************************************************/
+/* Condense consecutive white-space chars in a string to single spaces		*/
+/****************************************************************************/
+char* condense_whitespace(char* str)
+{
+	char* s = str;
+	char* d = str;
+	while(*s != '\0') {
+		if(IS_WHITESPACE(*s)) {
+			*(d++) = ' ';
+			SKIP_WHITESPACE(s);
+		} else {
+			*(d++) = *(s++);
+		}
+	}
+	*d = '\0';
+	return str;
+}
+
 uint32_t str_to_bits(uint32_t val, const char *str)
 {
 	/* op can be 0 for replace, + for add, or - for remove */
diff --git a/src/sbbs3/text.h b/src/sbbs3/text.h
index 8357736798594951d28dd93dffd128528130f5b1..0578967d3928e2c284ada20927740b0b16c097e7 100644
--- a/src/sbbs3/text.h
+++ b/src/sbbs3/text.h
@@ -853,6 +853,7 @@ enum {
 	,SpinningCursor7
 	,SpinningCursor8
 	,SpinningCursor9
+	,HowManyColumns
 
 	,TOTAL_TEXT
 };
diff --git a/src/sbbs3/text_defaults.c b/src/sbbs3/text_defaults.c
index c39e5bf8ac00c10fa6ebe7c9db90b3c6409d5e06..9261f22165e9060c2e1dfa88a8dd8b797021afb3 100644
--- a/src/sbbs3/text_defaults.c
+++ b/src/sbbs3/text_defaults.c
@@ -558,7 +558,7 @@ const char * const text_defaults[TOTAL_TEXT]={
 	,"\x01\x5f\x01\x62\x01\x68\x5b\x01\x63\x40\x43\x48\x45\x43\x4b\x4d\x41\x52\x4b\x40\x01\x62\x5d\x20\x01\x79\x45\x6e\x74\x65\x72\x20"
 		"\x79\x6f\x75\x72\x20\x76\x6f\x69\x63\x65\x20\x70\x68\x6f\x6e\x65\x20\x6e\x75\x6d\x62\x65\x72\x01\x5c\x3a\x20\x01\x77" // 344 EnterYourPhoneNumber
 	,"\x01\x5f\x01\x62\x01\x68\x5b\x01\x63\x40\x43\x48\x45\x43\x4b\x4d\x41\x52\x4b\x40\x01\x62\x5d\x20\x01\x79\x45\x6e\x74\x65\x72\x20"
-		"\x79\x6f\x75\x72\x20\x62\x69\x72\x74\x68\x64\x61\x79\x20\x28\x59\x59\x59\x59\x2f\x4d\x4d\x2f\x44\x44\x29\x01\x5c\x3a\x20\x01\x77"
+		"\x79\x6f\x75\x72\x20\x62\x69\x72\x74\x68\x64\x61\x79\x20\x28\x40\x42\x44\x41\x54\x45\x46\x4d\x54\x40\x29\x01\x5c\x3a\x20\x01\x77"
 		"" // 345 EnterYourBirthday
 	,"\x01\x5f\x01\x62\x01\x68\x5b\x01\x63\x40\x43\x48\x45\x43\x4b\x4d\x41\x52\x4b\x40\x01\x62\x5d\x20\x01\x79\x45\x6e\x74\x65\x72\x20"
 		"\x79\x6f\x75\x72\x20\x6c\x6f\x63\x61\x74\x69\x6f\x6e\x01\x5c\x20\x28\x65\x2e\x67\x2e\x20\x63\x69\x74\x79\x2c\x20\x73\x74\x61\x74"
@@ -692,8 +692,7 @@ const char * const text_defaults[TOTAL_TEXT]={
 	,"\x01\x6e\x01\x63\x52\x65\x61\x6c\x20\x4e\x61\x6d\x65\x20\x3a\x20\x01\x68\x25\x2d\x33\x30\x2e\x33\x30\x73\x20\x20\x01\x6e\x01\x63"
 		"\x50\x68\x6f\x6e\x65\x20\x6e\x75\x6d\x62\x65\x72\x20\x3a\x20\x01\x68\x25\x73\x0d\x0a" // 422 UeditRealNamePhone
 	,"\x01\x6e\x01\x63\x41\x64\x64\x72\x65\x73\x73\x20\x20\x20\x3a\x20\x01\x68\x25\x2d\x33\x30\x2e\x33\x30\x73\x20\x20\x01\x6e\x01\x63"
-		"\x41\x67\x65\x2f\x53\x65\x78\x2f\x42\x44\x61\x79\x20\x3a\x20\x01\x68\x25\x32\x64\x20\x25\x63\x20\x25\x2e\x30\x73\x25\x30\x34\x75"
-		"\x2f\x25\x30\x32\x75\x2f\x25\x30\x32\x75\x0d\x0a" // 423 UeditAddressBirthday
+		"\x41\x67\x65\x2f\x53\x65\x78\x2f\x42\x44\x61\x79\x20\x3a\x20\x01\x68\x25\x32\x64\x20\x25\x63\x20\x25\x73\x0d\x0a" // 423 UeditAddressBirthday
 	,"\x01\x6e\x01\x63\x4c\x6f\x63\x61\x74\x69\x6f\x6e\x20\x20\x3a\x20\x01\x68\x25\x2d\x33\x30\x2e\x33\x30\x73\x20\x20\x01\x6e\x01\x63"
 		"\x5a\x69\x70\x20\x43\x6f\x64\x65\x20\x20\x20\x20\x20\x3a\x20\x01\x68\x25\x73\x0d\x0a" // 424 UeditLocationZipcode
 	,"\x01\x6e\x01\x63\x4e\x6f\x74\x65\x20\x20\x20\x20\x20\x20\x3a\x20\x01\x68\x25\x2d\x33\x30\x2e\x33\x30\x73\x20\x20\x01\x6e\x01\x63"
@@ -780,8 +779,8 @@ const char * const text_defaults[TOTAL_TEXT]={
 		"\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x01\x6e\x01\x62\x01\x5c\x3a\x20\x01\x63\x25\x73\x0d\x0a" // 475 UserDefaultsTerminal
 	,"\x01\x6e\x01\x62\x5b\x01\x68\x01\x77\x45\x01\x6e\x01\x62\x5d\x20\x01\x68\x45\x78\x74\x65\x72\x6e\x61\x6c\x20\x45\x64\x69\x74\x6f"
 		"\x72\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x01\x6e\x01\x62\x01\x5c\x3a\x20\x01\x63\x25\x73\x0d\x0a" // 476 UserDefaultsXeditor
-	,"\x01\x6e\x01\x62\x5b\x01\x68\x01\x77\x4c\x01\x6e\x01\x62\x5d\x20\x01\x68\x53\x63\x72\x65\x65\x6e\x20\x4c\x65\x6e\x67\x74\x68\x20"
-		"\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x01\x6e\x01\x62\x01\x5c\x3a\x20\x01\x63\x25\x73\x20\x25\x73\x0d\x0a"
+	,"\x01\x6e\x01\x62\x5b\x01\x68\x01\x77\x4c\x01\x6e\x01\x62\x5d\x20\x01\x68\x54\x65\x72\x6d\x69\x6e\x61\x6c\x20\x44\x69\x6d\x65\x6e"
+		"\x73\x69\x6f\x6e\x73\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x01\x6e\x01\x62\x01\x5c\x3a\x20\x01\x63\x25\x73\x20\x25\x73\x0d\x0a"
 		"" // 477 UserDefaultsRows
 	,"\x01\x6e\x01\x62\x5b\x01\x68\x01\x77\x58\x01\x6e\x01\x62\x5d\x20\x01\x68\x45\x78\x70\x65\x72\x74\x20\x4d\x65\x6e\x75\x20\x4d\x6f"
 		"\x64\x65\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x01\x6e\x01\x62\x3a\x20\x01\x63\x25\x73\x0d\x0a" // 478 UserDefaultsMenuMode
@@ -820,9 +819,8 @@ const char * const text_defaults[TOTAL_TEXT]={
 	,"\x0d\x0a\x01\x6e\x01\x68\x01\x62\x57\x68\x69\x63\x68\x20\x6f\x72\x20\x5b\x01\x77\x51\x01\x62\x5d\x75\x69\x74\x3a\x20\x01\x63" // 494 UserDefaultsWhich
 	,"\x4f\x6e" // 495 On
 	,"\x4f\x66\x66" // 496 Off
-	,"\x0d\x0a\x01\x5f\x01\x62\x01\x68\x5b\x01\x63\x40\x43\x48\x45\x43\x4b\x4d\x41\x52\x4b\x40\x01\x62\x5d\x20\x01\x79\x48\x6f\x77\x20"
-		"\x6d\x61\x6e\x79\x20\x72\x6f\x77\x73\x20\x6f\x6e\x20\x79\x6f\x75\x72\x20\x6d\x6f\x6e\x69\x74\x6f\x72\x20\x5b\x01\x77\x41\x75\x74"
-		"\x6f\x20\x44\x65\x74\x65\x63\x74\x01\x79\x5d\x3a\x20" // 497 HowManyRows
+	,"\x01\x5f\x01\x62\x01\x68\x5b\x01\x63\x40\x43\x48\x45\x43\x4b\x4d\x41\x52\x4b\x40\x01\x62\x5d\x20\x01\x79\x54\x65\x72\x6d\x69\x6e"
+		"\x61\x6c\x20\x72\x6f\x77\x73\x20\x5b\x01\x77\x41\x75\x74\x6f\x01\x79\x5d\x3a\x20\x01\x6e" // 497 HowManyRows
 	,"\x0d\x0a\x01\x5f\x01\x79\x01\x68\x43\x75\x72\x72\x65\x6e\x74\x20\x50\x61\x73\x73\x77\x6f\x72\x64\x3a\x20\x01\x77" // 498 CurrentPassword
 	,"\x46\x6f\x72\x77\x61\x72\x64\x20\x70\x65\x72\x73\x6f\x6e\x61\x6c\x20\x65\x2d\x6d\x61\x69\x6c\x20\x74\x6f\x20\x6e\x65\x74\x77\x6f"
 		"\x72\x6b\x20\x6d\x61\x69\x6c\x20\x61\x64\x64\x72\x65\x73\x73" // 499 ForwardMailQ
@@ -1385,4 +1383,6 @@ const char * const text_defaults[TOTAL_TEXT]={
 	,"\xdc\xde\xdf\xdd" // 840 SpinningCursor7
 	,"\xdc\xdd\xdf\xde" // 841 SpinningCursor8
 	,"\xfa\xf9\xfe\xf9" // 842 SpinningCursor9
+	,"\x01\x5f\x01\x62\x01\x68\x5b\x01\x63\x40\x43\x48\x45\x43\x4b\x4d\x41\x52\x4b\x40\x01\x62\x5d\x20\x01\x79\x54\x65\x72\x6d\x69\x6e"
+		"\x61\x6c\x20\x63\x6f\x6c\x75\x6d\x6e\x73\x20\x5b\x01\x77\x41\x75\x74\x6f\x01\x79\x5d\x3a\x20\x01\x6e" // 843 HowManyColumns
 };
diff --git a/src/sbbs3/textgen.c b/src/sbbs3/textgen.c
index d8318d7b36a8a0165e330604c601e44897d710ad..476d0c60e124032a5f2f77d8ec30f7f8d4babc2b 100644
--- a/src/sbbs3/textgen.c
+++ b/src/sbbs3/textgen.c
@@ -27,7 +27,7 @@ char *readtext(FILE *stream, char **comment_ret)
 	}
 	comment[0]=0;
 	if(*(p+1)=='\\') {	/* merge multiple lines */
-		for(cp=p+2; *cp && isspace(*cp); cp++);
+		for(cp=p+2; *cp && IS_WHITESPACE(*cp); cp++);
 		truncsp(cp);
 		strcat(comment, cp);
 		while(strlen(buf)<2000) {
@@ -39,7 +39,7 @@ char *readtext(FILE *stream, char **comment_ret)
 			strcpy(p,p2+1);
 			p=strrchr(p,'"');
 			if(p && *(p+1)=='\\') {
-				for(cp=p+2; *cp && isspace(*cp); cp++);
+				for(cp=p+2; *cp && IS_WHITESPACE(*cp); cp++);
 				truncsp(cp);
 				strcat(comment, cp);
 				continue;
@@ -48,7 +48,7 @@ char *readtext(FILE *stream, char **comment_ret)
 		}
 	}
 	else {
-		for(cp=p+2; *cp && isspace(*cp); cp++);
+		for(cp=p+2; *cp && IS_WHITESPACE(*cp); cp++);
 		strcat(comment, cp);
 		truncsp(comment);
 	}
@@ -57,10 +57,10 @@ char *readtext(FILE *stream, char **comment_ret)
 	for(i=1,j=0;i<k;j++) {
 		if(buf[i]=='\\')	{ /* escape */
 			i++;
-			if(isdigit(buf[i])) {
+			if(IS_DIGIT(buf[i])) {
 				str[j]=atoi(buf+i); 	/* decimal, NOT octal */
-				if(isdigit(buf[++i]))	/* skip up to 3 digits */
-					if(isdigit(buf[++i]))
+				if(IS_DIGIT(buf[++i]))	/* skip up to 3 digits */
+					if(IS_DIGIT(buf[++i]))
 						i++;
 				continue; 
 			}
@@ -227,7 +227,7 @@ int main(int argc, char **argv)
 				fprintf(stderr,"Error creating C string! for %d\n", i+1);
 			}
 			lno=strtoul(comment, &macro, 10);
-			while(isspace(*macro))
+			while(IS_WHITESPACE(*macro))
 				macro++;
 			if((int)lno != i) {
 				fprintf(stderr,"Mismatch! %s has %ld... should be %d\n", comment, lno, i);
diff --git a/src/sbbs3/uedit/uedit.c b/src/sbbs3/uedit/uedit.c
index 09be4db916541cf32c88cea6cb2bbd646583a89f..6cf2fa35422d6c3efe35c202588414d1d6dfe13d 100644
--- a/src/sbbs3/uedit/uedit.c
+++ b/src/sbbs3/uedit/uedit.c
@@ -235,9 +235,9 @@ int edit_terminal(scfg_t *cfg, user_t *user)
 	char 	**opt;
 	char	str[256];
 
-	if((opt=(char **)alloca(sizeof(char *)*(10+1)))==NULL)
-		allocfail(sizeof(char *)*(10+1));
-	for(i=0;i<(10+1);i++)
+	if((opt=(char **)alloca(sizeof(char *)*(11+1)))==NULL)
+		allocfail(sizeof(char *)*(11+1));
+	for(i=0;i<11;i++)
 		if((opt[i]=(char *)alloca(MAX_OPLN))==NULL)
 			allocfail(MAX_OPLN);
 
@@ -254,9 +254,11 @@ int edit_terminal(scfg_t *cfg, user_t *user)
 		sprintf(opt[i++],"Pause            %s",user->misc & UPAUSE?"Yes":"No");
 		sprintf(opt[i++],"Hot Keys         %s",user->misc & COLDKEYS?"No":"Yes");
 		sprintf(opt[i++],"Spinning Cursor  %s",user->misc & SPIN?"Yes":"No");
+		sprintf(str,"%u",user->cols);
+		sprintf(opt[i++],"Screen Columns   %s",user->cols?str:"Auto");
 		sprintf(str,"%u",user->rows);
-		sprintf(opt[i++],"Number of Rows   %s",user->rows?str:"Auto");
-		opt[i][0]=0;
+		sprintf(opt[i++],"Screen Rows      %s",user->rows?str:"Auto");
+		opt[i] = NULL;
 		switch(uifc.list(WIN_MID|WIN_ACT|WIN_SAV,0,0,0,&j,0,"Terminal Settings",opt)) {
 			case -1:
 				return(0);
@@ -307,12 +309,21 @@ int edit_terminal(scfg_t *cfg, user_t *user)
 				putuserrec(cfg,user->number,U_MISC,8,ultoa(user->misc,str,16));
 				break;
 			case 9:
+				/* Columns */
+				SAFEPRINTF(str,"%u",user->cols);
+				uifc.input(WIN_MID|WIN_ACT|WIN_SAV,0,0, "Columns (0=auto-detect)", str, LEN_COLS, K_EDIT|K_NUMBER);
+				if(uifc.changes) {
+					user->cols=strtoul(str,NULL,10);
+					putuserrec(cfg,user->number,U_COLS,0,ultoa(user->cols,str,10));
+				}
+				break;
+			case 10:
 				/* Rows */
-				sprintf(str,"%u",user->rows);
-				uifc.input(WIN_MID|WIN_ACT|WIN_SAV,0,0,"Rows",str,2,K_EDIT|K_NUMBER);
+				SAFEPRINTF(str,"%u",user->rows);
+				uifc.input(WIN_MID|WIN_ACT|WIN_SAV,0,0, "Rows (0=auto-detect)", str, LEN_ROWS, K_EDIT|K_NUMBER);
 				if(uifc.changes) {
 					user->rows=strtoul(str,NULL,10);
-					putuserrec(cfg,user->number,U_ROWS,2,ultoa(user->rows,str,10));
+					putuserrec(cfg,user->number,U_ROWS,0,ultoa(user->rows,str,10));
 				}
 				break;
 		}
diff --git a/src/sbbs3/umonitor/umonitor.c b/src/sbbs3/umonitor/umonitor.c
index b89118e6e2b5beae8f04b89d130b5f0d9b607acf..305fb77b039debc9ee83ccf1cc0ae1468a9baa5a 100644
--- a/src/sbbs3/umonitor/umonitor.c
+++ b/src/sbbs3/umonitor/umonitor.c
@@ -643,57 +643,53 @@ int run_events(scfg_t *cfg)
 
 int recycle_servers(scfg_t *cfg)
 {
-	char str[1024];
+	char str[MAX_PATH + 1];
 	char **opt;
 	int i=0;
+	const int opt_count = 6;
 
-	if((opt=(char **)alloca(sizeof(char *)*(5+1)))==NULL)
-		allocfail(sizeof(char *)*(5+1));
-	for(i=0;i<(5+1);i++)
-		if((opt[i]=(char *)alloca(MAX_OPLN))==NULL)
-			allocfail(MAX_OPLN);
+	if((opt=(char **)alloca(sizeof(char *)*(opt_count+1)))==NULL)
+		allocfail(sizeof(char *)*(opt_count+1));
 
-	i=0;
-	strcpy(opt[i++],"FTP server");
-	strcpy(opt[i++],"Mail server");
-	strcpy(opt[i++],"Services");
-	strcpy(opt[i++],"Telnet server");
-	strcpy(opt[i++],"Web server");
-	opt[i][0]=0;
+	opt[i++] = "All Servers";
+	opt[i++] = "Terminal Server";
+	opt[i++] = "Mail Server";
+	opt[i++] = "FTP Server";
+	opt[i++] = "Web Server";
+	opt[i++] = "Services";
+	opt[i] = NULL;
 
 	uifc.helpbuf=	"`Recycle Servers\n"
 					"`---------------\n\n"
-					"To rerun a server, highlight it and press Enter.\n"
+					"To recycle a server, highlight it and press Enter.\n"
 					"This will reload the configuration files for selected\n"
 					"server.";
 
 	i=0;
 	while(1) {
+		const char* ext = "";
 		switch(uifc.list(WIN_MID|WIN_SAV,0,0,0,&i,0,"Recycle Servers",opt))  {
 			case -1:
 				return(0);
 				break;
-			case 0:
-				sprintf(str,"%sftpsrvr.rec",cfg->ctrl_dir);
-				ftouch(str);
-				break;
 			case 1:
-				sprintf(str,"%smailsrvr.rec",cfg->ctrl_dir);
-				ftouch(str);
+				ext = ".term";
 				break;
 			case 2:
-				sprintf(str,"%sservices.rec",cfg->ctrl_dir);
-				ftouch(str);
+				ext = ".mail";
 				break;
 			case 3:
-				sprintf(str,"%stelnet.rec",cfg->ctrl_dir);
-				ftouch(str);
+				ext = ".ftp";
 				break;
 			case 4:
-				sprintf(str,"%swebsrvr.rec",cfg->ctrl_dir);
-				ftouch(str);
+				ext = ".web";
+				break;
+			case 5:
+				ext = ".services";
 				break;
 		}
+		SAFEPRINTF2(str, "%srecycle%s", cfg->ctrl_dir, ext);
+		ftouch(str);
 	}
 	return(0);
 }
diff --git a/src/sbbs3/upload.cpp b/src/sbbs3/upload.cpp
index 4733ad1cabb11c9298b033bb6089e9c731805568..13e6c2a9f50023695e82f72b8cb3819aabf0dc08 100644
--- a/src/sbbs3/upload.cpp
+++ b/src/sbbs3/upload.cpp
@@ -174,7 +174,7 @@ bool sbbs_t::uploadfile(file_t *f)
 					strip_exascii(desc, desc);
 					prep_file_desc(desc, desc);
 					for(i=0;desc[i];i++)
-						if(isalnum(desc[i]))
+						if(IS_ALPHANUMERIC(desc[i]))
 							break;
 					sprintf(f->desc,"%.*s",LEN_FDESC,desc+i); 
 				}
@@ -412,7 +412,7 @@ bool sbbs_t::upload(uint dirnum)
 		SYNC;
 		bputs(text[RateThisFile]);
 		ch=getkey(K_ALPHA);
-		if(!isalpha(ch) || sys_status&SS_ABORT)
+		if(!IS_ALPHA(ch) || sys_status&SS_ABORT)
 			return(false);
 		CRLF;
 		sprintf(descbeg,text[Rated],toupper(ch)); 
diff --git a/src/sbbs3/userdat.c b/src/sbbs3/userdat.c
index 81d4de981f4acd9af6d00e90b0721a2242130eac..c90ad560b1d7657d437f8dfc86f286977bcf08d5 100644
--- a/src/sbbs3/userdat.c
+++ b/src/sbbs3/userdat.c
@@ -338,9 +338,15 @@ int parseuserdat(scfg_t* cfg, char *userdat, user_t *user)
 	getrec(userdat,U_FLAGS4,8,str); user->flags4=ahtoul(str);
 	getrec(userdat,U_EXEMPT,8,str); user->exempt=ahtoul(str);
 	getrec(userdat,U_REST,8,str); user->rest=ahtoul(str);
-	getrec(userdat,U_ROWS,2,str); user->rows=atoi(str);
-	if(user->rows && user->rows<10)
-		user->rows=10;
+	if(getrec(userdat,U_OLDROWS,2,str) <= 0) // Moved to new location
+		getrec(userdat, U_ROWS, LEN_ROWS, str);
+	user->rows = atoi(str);
+	if(user->rows && user->rows < TERM_ROWS_MIN)
+		user->rows = TERM_ROWS_MIN;
+	getrec(userdat, U_COLS, LEN_COLS, str);
+	user->cols = atoi(str);
+	if(user->cols && user->cols < TERM_COLS_MIN)
+		user->cols = TERM_COLS_MIN;
 	user->sex=userdat[U_SEX];
 	if(!user->sex)
 		user->sex=' ';	 /* fix for v1b04 that could save as 0 */
@@ -574,7 +580,8 @@ int putuserdat(scfg_t* cfg, user_t* user)
 	putrec(userdat,U_REST,8,ultoa(user->rest,str,16));
 	putrec(userdat,U_REST+8,2,crlf);
 
-	putrec(userdat,U_ROWS,2,ultoa(user->rows,str,10));
+	putrec(userdat, U_ROWS, LEN_ROWS, ultoa(user->rows,str,10));
+	putrec(userdat, U_COLS, LEN_COLS, ultoa(user->cols,str,10));
 	userdat[U_SEX]=user->sex;
 	userdat[U_PROT]=user->prot;
 	putrec(userdat,U_MISC,8,ultoa(user->misc,str,16));
@@ -738,7 +745,7 @@ int putusername(scfg_t* cfg, int number, char *name)
 
 int getbirthyear(const char* birth)
 {
-	if(isdigit(birth[2]))				// CCYYMMYY format
+	if(IS_DIGIT(birth[2]))				// CCYYMMYY format
 		return DECVAL(birth[0], 1000)
 				+ DECVAL(birth[1], 100)
 				+ DECVAL(birth[2], 10)
@@ -757,7 +764,7 @@ int getbirthyear(const char* birth)
 
 int getbirthmonth(scfg_t* cfg, const char* birth)
 {
-	if(isdigit(birth[5]))				// CCYYMMYY format
+	if(IS_DIGIT(birth[5]))				// CCYYMMYY format
 		return DECVAL(birth[4], 10)	+ DECVAL(birth[5], 1);
 	if(cfg->sys_misc & SM_EURODATE) {	// DD/MM/YY format
 		return DECVAL(birth[3], 10) + DECVAL(birth[4], 1);
@@ -768,7 +775,7 @@ int getbirthmonth(scfg_t* cfg, const char* birth)
 
 int getbirthday(scfg_t* cfg, const char* birth)
 {
-	if(isdigit(birth[5]))				// CCYYMMYY format
+	if(IS_DIGIT(birth[5]))				// CCYYMMYY format
 		return DECVAL(birth[6], 10)	+ DECVAL(birth[7], 1);
 	if(cfg->sys_misc & SM_EURODATE) {	// DD/MM/YY format
 		return DECVAL(birth[0], 10) + DECVAL(birth[1], 1);
@@ -2868,12 +2875,15 @@ int user_rec_len(int offset)
 		/* 3 char strings */
 		case U_TMPEXT:
 			return(3);
+		case U_ROWS:
+			return LEN_ROWS;
+		case U_COLS:
+			return LEN_COLS;
 
-		/* 2 digits integers (0-99) */
+		/* 2 digit integers (0-99 or 00-FF) */
 		case U_LEVEL:
 		case U_TL:
-		case U_ROWS:
-		case U_LEECH:	/* actually, 2 hex digits */
+		case U_LEECH:
 			return(2);
 
 		/* Single digits chars */
@@ -3118,7 +3128,7 @@ BOOL check_name(scfg_t* cfg, const char* name)
 		return FALSE;
 	if (   name[0] <= ' '			/* begins with white-space? */
 		|| name[len-1] <= ' '		/* ends with white-space */
-		|| !isalpha(name[0])
+		|| !IS_ALPHA(name[0])
 		|| !stricmp(name,cfg->sys_id)
 		|| strchr(name,0xff)
 		|| matchuser(cfg,name,TRUE /* sysop_alias */)
@@ -3253,7 +3263,7 @@ ulong loginFailure(link_list_t* list, const union xp_sockaddr* addr, const char*
 	if((node=login_attempted(list, addr)) != NULL) {
 		attempt=node->data;
 		/* Don't count consecutive duplicate attempts (same name and password): */
-		if((user!=NULL && strcmp(attempt->user,user)==0) && (pass==NULL || strcmp(attempt->pass,pass)==0))
+		if((user!=NULL && strcmp(attempt->user,user)==0) && (pass!=NULL && strcmp(attempt->pass,pass)==0))
 			attempt->dupes++;
 	}
 	SAFECOPY(attempt->prot,prot);
diff --git a/src/sbbs3/useredit.cpp b/src/sbbs3/useredit.cpp
index f4a8a237973e4d1aa71147afd932d784cec2cc0f..07f7176d0895558d3fd54c39765afe961614eec1 100644
--- a/src/sbbs3/useredit.cpp
+++ b/src/sbbs3/useredit.cpp
@@ -38,6 +38,7 @@
 /*******************************************************************/
 
 #include "sbbs.h"
+#include "petdefs.h"
 
 #define SEARCH_TXT 0
 #define SEARCH_ARS 1
@@ -292,7 +293,7 @@ void sbbs_t::useredit(int usernumber)
 						menu(str);
 						continue; 
 					}
-					if(isdigit(c)) {
+					if(IS_DIGIT(c)) {
 						i=c&0xf;
 						continue; 
 					}
@@ -336,13 +337,13 @@ void sbbs_t::useredit(int usernumber)
 							putuserrec(&cfg,user.number,U_FLAGS4,8
 								,ultoa(user.flags4,tmp,16));
 							break; 
-					} 
+					}
 				}
 				break;
 			case 'G':
 				bputs(text[GoToUser]);
 				if(getstr(str,LEN_ALIAS,K_UPPER|K_LINE)) {
-					if(isdigit(str[0])) {
+					if(IS_DIGIT(str[0])) {
 						i=atoi(str);
 						if(i>lastuser(&cfg))
 							break;
@@ -502,7 +503,7 @@ void sbbs_t::useredit(int usernumber)
 				ASYNC;
 				bputs(text[QuickValidatePrompt]);
 				c=getkey(0);
-				if(!isdigit(c))
+				if(!IS_DIGIT(c))
 					break;
 				i=c&0xf;
 				user.level=cfg.val_level[i];
@@ -597,7 +598,7 @@ void sbbs_t::useredit(int usernumber)
 					user.min+=l;
 				putuserrec(&cfg,user.number,U_MIN,10,ultoa(user.min,tmp,10));
 				break;
-			case '#': /* read new user questionaire */
+			case '#': /* read new user questionnaire */
 				SAFEPRINTF2(str,"%suser/%4.4u.dat", cfg.data_dir,user.number);
 				if(!cfg.new_sof[0] || !fexist(str))
 					break;
@@ -805,14 +806,17 @@ void sbbs_t::maindflts(user_t* user)
 	while(online) {
 		CLS;
 		getuserdat(&cfg,user);
-		if(user->rows)
-			rows=user->rows;
+		if(user->rows != TERM_ROWS_AUTO)
+			rows = user->rows;
+		if(user->cols != TERM_COLS_AUTO)
+			cols = user->cols;
 		bprintf(text[UserDefaultsHdr],user->alias,user->number);
+		if(user == &useron)
+			update_nodeterm();
 		long term = (user == &useron) ? term_supports() : user->misc;
 		if(term&PETSCII)
-			safe_snprintf(str,sizeof(str),"%sPETSCII %lu %s"
-							,user->misc&AUTOTERM ? text[TerminalAutoDetect]:nulstr
-							,cols, text[TerminalColumns]);
+			safe_snprintf(str,sizeof(str),"%sCBM/PETSCII"
+							,user->misc&AUTOTERM ? text[TerminalAutoDetect]:nulstr);
 		else
 			safe_snprintf(str,sizeof(str),"%s%s / %s %s%s%s"
 							,user->misc&AUTOTERM ? text[TerminalAutoDetect]:nulstr
@@ -823,12 +827,10 @@ void sbbs_t::maindflts(user_t* user)
 							,term&SWAP_DELETE ? "DEL=BS" : nulstr);
 		add_hotspot('T');
 		bprintf(text[UserDefaultsTerminal], truncsp(str));
-		if(user->rows)
-			ultoa(user->rows,tmp,10);
-		else
-			SAFEPRINTF2(tmp,"%s%ld", text[TerminalAutoDetect], rows);
+		safe_snprintf(str, sizeof(str), "%s%ld %s,", user->cols ? nulstr:text[TerminalAutoDetect], cols, text[TerminalColumns]);
+		safe_snprintf(tmp, sizeof(tmp), "%s%ld %s", user->rows ? nulstr:text[TerminalAutoDetect], rows, text[TerminalRows]);
 		add_hotspot('L');
-		bprintf(text[UserDefaultsRows], tmp, text[TerminalRows]);
+		bprintf(text[UserDefaultsRows], str, tmp);
 		if(cfg.total_shells>1) {
 			add_hotspot('K');
 			bprintf(text[UserDefaultsCommandSet]
@@ -975,9 +977,9 @@ void sbbs_t::maindflts(user_t* user)
 					else
 						user->misc&=~NO_EXASCII;
 					user->misc &= ~SWAP_DELETE;
-					while(text[HitYourBackspaceKey][0] && !(user->misc&SWAP_DELETE) && online) {
+					while(text[HitYourBackspaceKey][0] && !(user->misc&(PETSCII|SWAP_DELETE)) && online) {
 						bputs(text[HitYourBackspaceKey]);
-						uchar key = getkey(K_NONE);
+						uchar key = getkey(K_CTRLKEYS);
 						bprintf(text[CharacterReceivedFmt], key, key);
 						if(key == '\b')
 							break;
@@ -985,6 +987,12 @@ void sbbs_t::maindflts(user_t* user)
 							if(text[SwapDeleteKeyQ][0] == 0 || yesno(text[SwapDeleteKeyQ]))
 								user->misc |= SWAP_DELETE;
 						}
+						else if(key == PETSCII_DELETE) {
+							autoterm |= PETSCII;
+							user->misc |= PETSCII;
+							outcom(PETSCII_UPPERLOWER);
+							bputs(text[PetTerminalDetected]);
+						}
 						else
 							bprintf(text[InvalidBackspaceKeyFmt], key, key);
 					}
@@ -1030,13 +1038,21 @@ void sbbs_t::maindflts(user_t* user)
 					putuserrec(&cfg,user->number,U_TMPEXT,3,cfg.fcomp[i]->ext);
 				break;
 			case 'L':
+				bputs(text[HowManyColumns]);
+				if((i = getnum(TERM_COLS_MAX)) < 0)
+					break;
+				putuserrec(&cfg,user->number,U_COLS,0,ultoa(i,tmp,10));
+				if(user==&useron) {
+					useron.cols = i;
+					ansi_getlines();
+				}
 				bputs(text[HowManyRows]);
-				if((ch=(char)getnum(99))!=-1) {
-					putuserrec(&cfg,user->number,U_ROWS,2,ultoa(ch,tmp,10));
-					if(user==&useron) {
-						useron.rows=ch;
-						ansi_getlines();
-					}
+				if((i = getnum(TERM_ROWS_MAX)) < 0)
+					break;
+				putuserrec(&cfg,user->number,U_ROWS,0,ultoa(i,tmp,10));
+				if(user==&useron) {
+					useron.rows = i;
+					ansi_getlines();
 				}
 				break;
 			case 'P':
@@ -1175,7 +1191,7 @@ void sbbs_t::maindflts(user_t* user)
 			default:
 				clear_hotspots();
 				return; 
-		} 
+		}
 	}
 }
 
diff --git a/src/sbbs3/websrvr.c b/src/sbbs3/websrvr.c
index 3ae6bf3df5cf6a38e6df11f57afa658c52e84cc3..92062d890e9d8548836b1c385682a5074243e65d 100644
--- a/src/sbbs3/websrvr.c
+++ b/src/sbbs3/websrvr.c
@@ -1421,7 +1421,7 @@ static BOOL send_headers(http_session_t *session, const char *status, int chunke
 			if(session->req.range_start || session->req.range_end) {
 				switch(stat_code) {
 					case 206:	/* Partial reply */
-						safe_snprintf(header,sizeof(header),"%s: bytes %ld-%ld/%ld",get_header(HEAD_CONTENT_RANGE),session->req.range_start,session->req.range_end,stats.st_size);
+						safe_snprintf(header,sizeof(header),"%s: bytes %ld-%ld/%ld",get_header(HEAD_CONTENT_RANGE),session->req.range_start,session->req.range_end,(long)stats.st_size);
 						safecat(headers,header,MAX_HEADERS_SIZE);
 						break;
 					default:
@@ -2279,7 +2279,7 @@ static void unescape(char *p)
 
 	dst=p;
 	for(;*p;p++) {
-		if(*p=='%' && isxdigit((uchar)*(p+1)) && isxdigit((uchar)*(p+2))) {
+		if(*p=='%' && IS_HEXDIGIT(*(p+1)) && IS_HEXDIGIT(*(p+2))) {
 			sprintf(code,"%.2s",p+1);
 			*(dst++)=(char)strtol(code,NULL,16);
 			p+=2;
@@ -2528,7 +2528,7 @@ static char *get_token_value(char **p)
 		}
 	}
 end_of_text:
-	while(*pos==',' || isspace(*pos))
+	while(*pos==',' || IS_WHITESPACE(*pos))
 		pos++;
 	*out=0;
 	*p=pos;
@@ -2602,7 +2602,7 @@ static BOOL parse_headers(http_session_t * session)
 								session->req.auth.type=AUTHENTICATION_DIGEST;
 								/* Parse out values one at a time and store */
 								while(p != NULL && *p) {
-									while(isspace(*p))
+									while(IS_WHITESPACE(*p))
 										p++;
 									if(strnicmp(p,"username=",9)==0) {
 										p+=9;
@@ -2865,7 +2865,7 @@ static BOOL parse_js_headers(http_session_t * session)
 
 						p=value;
 						while((key=strtok_r(p,"=",&last))!=NULL) {
-							while(isspace(*key))
+							while(IS_WHITESPACE(*key))
 								key++;
 							p=NULL;
 							if((val=strtok_r(p,";\t\n\v\f\r ",&last))!=NULL) {	/* Whitespace */
@@ -2954,7 +2954,7 @@ static char * split_port_part(char *host)
 	char *ret = NULL;
 	char *p=strchr(host, 0)-1;
 
-	if (isdigit(*p)) {
+	if (IS_DIGIT(*p)) {
 		/*
 		 * If the first and last : are not the same, and it doesn't
 		 * start with '[', there's no port part.
@@ -2969,7 +2969,7 @@ static char * split_port_part(char *host)
 				ret = p+1;
 				break;
 			}
-			if (!isdigit(*p))
+			if (!IS_DIGIT(*p))
 				break;
 		}
 	}
@@ -5460,7 +5460,7 @@ js_login(JSContext *cx, uintN argc, jsval *arglist)
 
 	memset(&user,0,sizeof(user));
 
-	if(isdigit((uchar)*username))
+	if(IS_DIGIT(*username))
 		user.number=atoi(username);
 	else if(*username)
 		user.number=matchuser(&scfg,username,FALSE);
diff --git a/src/sbbs3/wordwrap.c b/src/sbbs3/wordwrap.c
index 8b6a4f7b90f633d6315b633a8335e8351e0228be..1885a7fbe72527899cd554657cbf543a4de771ee 100644
--- a/src/sbbs3/wordwrap.c
+++ b/src/sbbs3/wordwrap.c
@@ -214,7 +214,7 @@ static struct section_len get_ws_len(char *buf, int col)
 	for(ret.bytes=0; ; ret.bytes++) {
 		if (!buf[ret.bytes])
 			break;
-		if (!isspace((unsigned char)buf[ret.bytes]))
+		if (!IS_WHITESPACE(buf[ret.bytes]))
 			break;
 		if(buf[ret.bytes] == '\t') {
 			ret.len++;
@@ -246,7 +246,7 @@ static struct section_len get_word_len(char *buf, int maxlen, BOOL is_utf8)
 		len = 1;
 		if (!buf[ret.bytes])
 			break;
-		else if (isspace((unsigned char)buf[ret.bytes]))
+		else if (IS_WHITESPACE(buf[ret.bytes]))
 			break;
 		else if (buf[ret.bytes]==DEL)
 			continue;
diff --git a/src/sbbs3/writemsg.cpp b/src/sbbs3/writemsg.cpp
index 5a31310774affa0d81c638976b05cc7d1a237252..5c77a025671b1018ae5a7b46f92ee1273c38b814 100644
--- a/src/sbbs3/writemsg.cpp
+++ b/src/sbbs3/writemsg.cpp
@@ -422,7 +422,7 @@ bool sbbs_t::writemsg(const char *fname, const char *top, char *subj, long mode,
 					continue; 
 				}
 
-				if(!isdigit(quote[0]))
+				if(!IS_DIGIT(quote[0]))
 					break;
 				p=quote;
 				while(p) {
@@ -716,7 +716,7 @@ bool sbbs_t::writemsg(const char *fname, const char *top, char *subj, long mode,
 				while(!feof(sig)) {
 					if(!fgets(str,sizeof(str),sig))
 						break;
-					truncsp(str);
+					truncnl(str);
 					if(utf8) {
 						char buf[sizeof(str)*4];
 						cp437_to_utf8_str(str, buf, sizeof(buf) - 1, /* minval: */'\x02');
diff --git a/src/sbbs3/xtrn.cpp b/src/sbbs3/xtrn.cpp
index 9d0f49e983a8f9122d295369fd11648d18aff352..9c8b6dee3cd574deb3d865016d92b508396a6f6f 100644
--- a/src/sbbs3/xtrn.cpp
+++ b/src/sbbs3/xtrn.cpp
@@ -1673,8 +1673,12 @@ int sbbs_t::external(const char* cmdline, long mode, const char* startup_dir)
 					timeout.tv_sec=0;
 					timeout.tv_usec=1000;
 				}
-				if(i && !(mode&EX_NOLOG))
-					lprintf(LOG_NOTICE,"%.*s",i,buf);		/* lprintf mangles i? */
+				if(i > 0) {
+					buf[i] = '\0';
+					truncsp((char*)buf);
+					if(*buf)
+						lprintf(LOG_NOTICE, "%s", buf);
+				}
 
 				/* Eat stderr if mode is EX_BIN */
 				if(mode&EX_BIN)  {
@@ -1782,7 +1786,10 @@ int sbbs_t::external(const char* cmdline, long mode, const char* startup_dir)
 				if((rd=read(err_pipe[0],bp,1))>0)  {
 					i+=rd;
 					if(*bp=='\n') {
-						lprintf(LOG_NOTICE,"%.*s",i-1,buf);
+						buf[i] = '\0';
+						truncsp((char*)buf);
+						if(*buf)
+							lprintf(LOG_NOTICE, "%s", buf);
 						i=0;
 						bp=buf;
 					}
@@ -1792,8 +1799,12 @@ int sbbs_t::external(const char* cmdline, long mode, const char* startup_dir)
 				else
 					break;
 			}
-			if(i)
-				lprintf(LOG_NOTICE,"%.*s",i,buf);
+			if(i > 0) {
+				buf[i] = '\0';
+				truncsp((char*)buf);
+				if(*buf)
+					lprintf(LOG_NOTICE, "%s", buf);
+			}
 		}
 	}
 	if(!(mode&EX_OFFLINE)) {	/* !off-line execution */
@@ -1831,7 +1842,7 @@ const char* quoted_string(const char* str, char* buf, size_t maxlen)
 }
 
 #define QUOTED_STRING(ch, str, buf, maxlen) \
-	((isalpha(ch) && isupper(ch)) ? str : quoted_string(str,buf,maxlen))
+	((IS_ALPHA(ch) && IS_UPPERCASE(ch)) ? str : quoted_string(str,buf,maxlen))
 
 /*****************************************************************************/
 /* Returns command line generated from instr with %c replacements            */
@@ -1853,7 +1864,7 @@ char* sbbs_t::cmdstr(const char *instr, const char *fpath, const char *fspec, ch
             cmd[j]=0;
 			int avail = maxlen - j;
 			char ch=instr[i];
-			if(isalpha(ch))
+			if(IS_ALPHA(ch))
 				ch=toupper(ch);
             switch(ch) {
                 case 'A':   /* User alias */
@@ -1989,7 +2000,7 @@ char* sbbs_t::cmdstr(const char *instr, const char *fpath, const char *fspec, ch
 					strncat(cmd, ARCHITECTURE_DESC, avail);
 					break;
                 default:    /* unknown specification */
-                    if(isdigit(instr[i])) {
+                    if(IS_DIGIT(instr[i])) {
                         sprintf(str,"%0*d",instr[i]&0xf,useron.number);
                         strncat(cmd,str, avail); }
                     break; }
@@ -2022,7 +2033,7 @@ char* DLLCALL cmdstr(scfg_t* cfg, user_t* user, const char* instr, const char* f
             cmd[j]=0;
 			int avail = maxlen - j;
 			char ch=instr[i];
-			if(isalpha(ch))
+			if(IS_ALPHA(ch))
 				ch=toupper(ch);
             switch(ch) {
                 case 'A':   /* User alias */
@@ -2157,7 +2168,7 @@ char* DLLCALL cmdstr(scfg_t* cfg, user_t* user, const char* instr, const char* f
 					strncat(cmd, ARCHITECTURE_DESC, avail);
 					break;
                 default:    /* unknown specification */
-                    if(isdigit(instr[i]) && user!=NULL) {
+                    if(IS_DIGIT(instr[i]) && user!=NULL) {
                         sprintf(str,"%0*d",instr[i]&0xf,user->number);
                         strncat(cmd,str, avail);
 					}
diff --git a/src/sbbs3/xtrn_sec.cpp b/src/sbbs3/xtrn_sec.cpp
index 82c09f099dfae4a1fedc070fda2474dde949608a..d6654bcb95c6e47f5c2135571e4ab4ed279f189e 100644
--- a/src/sbbs3/xtrn_sec.cpp
+++ b/src/sbbs3/xtrn_sec.cpp
@@ -1246,7 +1246,7 @@ void sbbs_t::moduserdat(uint xtrnnum)
 			for(i=0;i<15;i++)			/* skip first 14 lines */
 				if(!fgets(str,128,stream))
 					break;
-			if(i==15 && isdigit(str[0])) {
+			if(i==15 && IS_DIGIT(str[0])) {
 				mod=atoi(str);
 				if(mod<SYSOP_LEVEL) {
 					useron.level=(char)mod;
@@ -1265,11 +1265,11 @@ void sbbs_t::moduserdat(uint xtrnnum)
 			for(;i<25;i++)
 				if(!fgets(str,128,stream))
 					break;
-			if(i==25 && isdigit(str[0]) && isdigit(str[1])
+			if(i==25 && IS_DIGIT(str[0]) && IS_DIGIT(str[1])
 				&& (str[2]=='/' || str[2]=='-') /* xx/xx/xx or xx-xx-xx */
-				&& isdigit(str[3]) && isdigit(str[4])
+				&& IS_DIGIT(str[3]) && IS_DIGIT(str[4])
 				&& (str[5]=='/' || str[5]=='-')
-				&& isdigit(str[6]) && isdigit(str[7])) { /* valid expire date */
+				&& IS_DIGIT(str[6]) && IS_DIGIT(str[7])) { /* valid expire date */
 				useron.expire=(ulong)dstrtounix(&cfg,str);
 				putuserrec(&cfg,useron.number,U_EXPIRE,8,ultoa((ulong)useron.expire,tmp,16)); 
 			}
@@ -1296,7 +1296,7 @@ void sbbs_t::moduserdat(uint xtrnnum)
 			for(;i<42;i++)
 				if(!fgets(str,128,stream))
 					break;
-			if(i==42 && isdigit(str[0])) {	/* Time Credits in Minutes */
+			if(i==42 && IS_DIGIT(str[0])) {	/* Time Credits in Minutes */
 				useron.min=atol(str);
 				putuserrec(&cfg,useron.number,U_MIN,10,ultoa(useron.min,tmp,10)); 
 			}
@@ -1351,7 +1351,7 @@ void sbbs_t::moduserdat(uint xtrnnum)
 		}
 		if(fgets(str,81,stream)) {		/* main level */
 			mod=atoi(str);
-			if(isdigit(str[0]) && mod<SYSOP_LEVEL) {
+			if(IS_DIGIT(str[0]) && mod<SYSOP_LEVEL) {
 				useron.level=(uchar)mod;
 				putuserrec(&cfg,useron.number,U_LEVEL,2,ultoa(useron.level,tmp,10)); 
 			} 
@@ -1647,6 +1647,11 @@ bool sbbs_t::exec_xtrn(uint xtrnnum)
 	thisnode.aux=0;
 	putnodedat(cfg.node_num,&thisnode);
 
+	if(cfg.xtrn[xtrnnum]->misc & XTRN_PAUSE)
+		pause();
+	else
+		lncntr = 0;
+
 	return(true);
 }
 
diff --git a/src/smblib/smballoc.c b/src/smblib/smballoc.c
index 1d51cff4a95ff8773375b992e931ad39646aa1bc..f1c00722ecaee9264839f325f0313f9a47312bdb 100644
--- a/src/smblib/smballoc.c
+++ b/src/smblib/smballoc.c
@@ -326,7 +326,7 @@ int SMBCALL smb_freemsghdr(smb_t* smb, ulong offset, ulong length)
 	}
 
 	if(fseek(smb->sha_fp, sha_offset, SEEK_SET)) {
-		safe_snprintf(smb->last_error,sizeof(smb->last_error),"%s seeking to %ld", __FUNCTION__, sha_offset);
+		safe_snprintf(smb->last_error,sizeof(smb->last_error),"%s seeking to %ld", __FUNCTION__, (long)sha_offset);
 		return(SMB_ERR_SEEK);
 	}
 	for(l=0;l<blocks;l++)
diff --git a/src/smblib/smbstr.c b/src/smblib/smbstr.c
index 47c7a02da5b38e578b4961f00c18fa2b71e05f91..d73662d411cc6a0b121ac39bdde2ce4c93d0d3f8 100644
--- a/src/smblib/smbstr.c
+++ b/src/smblib/smbstr.c
@@ -131,7 +131,7 @@ uint16_t SMBCALL smb_hfieldtypelookup(const char* str)
 {
 	uint16_t type;
 
-	if(isdigit(*str))
+	if(IS_DIGIT(*str))
 		return((uint16_t)strtol(str,NULL,0));
 
 	for(type=0;type<=UNUSED;type++)
@@ -395,13 +395,13 @@ enum smb_net_type SMBCALL smb_get_net_type_by_addr(const char* addr)
 	char* colon = strchr(p,':');
 	char* slash = strchr(p,'/');
 
-	if(at == NULL && isalpha(*p) && dot == NULL && colon == NULL)
+	if(at == NULL && IS_ALPHA(*p) && dot == NULL && colon == NULL)
 		return NET_QWK;
 
 	char last = 0;
 	for(tp = p; *tp != '\0'; tp++) {
 		last = *tp;
-		if(isdigit(*tp))
+		if(IS_DIGIT(*tp))
 			continue;
 		if(*tp == ':') {
 			if(tp != colon)
@@ -426,7 +426,7 @@ enum smb_net_type SMBCALL smb_get_net_type_by_addr(const char* addr)
 		}
 		break;
 	}
-	if(at == NULL && isdigit(*p) && *tp == '\0' && isdigit(last))
+	if(at == NULL && IS_DIGIT(*p) && *tp == '\0' && IS_DIGIT(last))
 		return NET_FIDO;
 	if(slash == NULL && (isalnum(*p) || p == colon))
 		return NET_INTERNET;
diff --git a/src/smblib/smbtxt.c b/src/smblib/smbtxt.c
index 84a4aab9d59b76c86b3e690e7ace56a769493e7c..1f1219c004bd16db6a68b9a64a4824be8a74ad04 100644
--- a/src/smblib/smbtxt.c
+++ b/src/smblib/smbtxt.c
@@ -234,7 +234,7 @@ char* qp_decode(char* buf)
 				break;
 			if(*p == '\n')
 				continue;
-			if(isxdigit(*p) && isxdigit(*(p+1))) {
+			if(IS_HEXDIGIT(*p) && IS_HEXDIGIT(*(p+1))) {
 				uchar ch = HEX_CHAR_TO_INT(*p) << 4;
 				p++;
 				ch |= HEX_CHAR_TO_INT(*p);
diff --git a/src/xpdev/dirwrap.c b/src/xpdev/dirwrap.c
index 2882ddaadf80f9809df2996a6ebe1922a82e8b50..49006c6a5a21dd05503b6fa4293938b0e03d7ba7 100644
--- a/src/xpdev/dirwrap.c
+++ b/src/xpdev/dirwrap.c
@@ -612,7 +612,7 @@ BOOL DLLCALL fexistcase(char *path)
 	SAFECOPY(fname,p);
 	*p=0;
 	for(i=0;fname[i];i++)  {
-		if(isalpha(fname[i]))
+		if(IS_ALPHA(fname[i]))
 			sprintf(tmp,"[%c%c]",toupper(fname[i]),tolower(fname[i]));
 		else
 			sprintf(tmp,"%c",fname[i]);
@@ -733,7 +733,7 @@ int removecase(const char *path)
 	p=getfname(inpath);
 	fname[0]=0;
 	for(i=0;p[i];i++)  {
-		if(isalpha(p[i]))
+		if(IS_ALPHA(p[i]))
 			sprintf(tmp,"[%c%c]",toupper(p[i]),tolower(p[i]));
 		else
 			sprintf(tmp,"%c",p[i]);
@@ -1099,7 +1099,7 @@ BOOL DLLCALL isfullpath(const char* filename)
 {
 	return(filename[0]=='/'
 #ifdef WIN32
-		|| filename[0]=='\\' || (isalpha(filename[0]) && filename[1]==':')
+		|| filename[0]=='\\' || (IS_ALPHA(filename[0]) && filename[1]==':')
 #endif
 		);
 }
diff --git a/src/xpdev/gen_defs.h b/src/xpdev/gen_defs.h
index c38856ac7518e23653aa6f64ca6e501abfac2bb7..68decddfa2bc1589fac5773d09c08e5a44835f04 100644
--- a/src/xpdev/gen_defs.h
+++ b/src/xpdev/gen_defs.h
@@ -416,33 +416,42 @@ typedef struct {
 #define SAFEPRINTF4(dst,fmt,a1,a2,a3,a4)	snprintf(dst,sizeof(dst),fmt,a1,a2,a3,a4), TERMINATE(dst)
 #endif
 
-/* Replace every occurance of c1 in str with c2, using p as a temporary char pointer */
+/* Replace every occurrence of c1 in str with c2, using p as a temporary char pointer */
 #define REPLACE_CHARS(str,c1,c2,p)      for((p)=(str);*(p);(p)++) if(*(p)==(c1)) *(p)=(c2);
 
 /* ASCIIZ char* parsing helper macros */
-#define SKIP_WHITESPACE(p)              while(*(p) && isspace((unsigned char)*(p)))             (p)++;
-#define FIND_WHITESPACE(p)              while(*(p) && !isspace((unsigned char)*(p)))            (p)++;
-#define SKIP_CHAR(p,c)                  while(*(p)==c)                                          (p)++;
-#define FIND_CHAR(p,c)                  while(*(p) && *(p)!=c)                                  (p)++;
-#define SKIP_CHARSET(p,s)               while(*(p) && strchr(s,*(p))!=NULL)                     (p)++;
-#define FIND_CHARSET(p,s)               while(*(p) && strchr(s,*(p))==NULL)                     (p)++;
+/* These (unsigned char) typecasts defeat MSVC debug assertion when passed a negative value */
+#define IS_WHITESPACE(c)				isspace((unsigned char)(c))
+#define IS_CONTROL(c)					iscntrl((unsigned char)(c))
+#define IS_ALPHA(c)						isalpha((unsigned char)(c))
+#define IS_ALPHANUMERIC(c)				isalnum((unsigned char)(c))
+#define IS_UPPERCASE(c)					isupper((unsigned char)(c))
+#define IS_LOWERCASE(c)					islower((unsigned char)(c))
+#define IS_PUNCTUATION(c)				ispunct((unsigned char)(c))
+#define IS_PRINTABLE(c)					isprint((unsigned char)(c))
+#define IS_DIGIT(c)						isdigit((unsigned char)(c))
+#define IS_HEXDIGIT(c)					isxdigit((unsigned char)(c))
+#define IS_OCTDIGIT(c)					((c) >= '0' && (c) <= '7')
+#define SKIP_WHITESPACE(p)              while(*(p) && IS_WHITESPACE(*(p)))        (p)++;
+#define FIND_WHITESPACE(p)              while(*(p) && !IS_WHITESPACE(*(p)))       (p)++;
+#define SKIP_CHAR(p,c)                  while(*(p)==c)                            (p)++;
+#define FIND_CHAR(p,c)                  while(*(p) && *(p)!=c)                    (p)++;
+#define SKIP_CHARSET(p,s)               while(*(p) && strchr(s,*(p))!=NULL)       (p)++;
+#define FIND_CHARSET(p,s)               while(*(p) && strchr(s,*(p))==NULL)       (p)++;
 #define SKIP_CRLF(p)					SKIP_CHARSET(p, "\r\n")
 #define FIND_CRLF(p)					FIND_CHARSET(p, "\r\n")
-#define SKIP_ALPHA(p)                   while(*(p) && isalpha((unsigned char)*(p)))             (p)++;
-#define FIND_ALPHA(p)                   while(*(p) && !isalpha((unsigned char)*(p)))            (p)++;
-#define SKIP_ALPHANUMERIC(p)            while(*(p) && isalnum((unsigned char)*(p)))             (p)++;
-#define FIND_ALPHANUMERIC(p)            while(*(p) && !isalnum((unsigned char)*(p)))            (p)++;
-#define SKIP_DIGIT(p)                   while(*(p) && isdigit((unsigned char)*(p)))             (p)++;
-#define FIND_DIGIT(p)                   while(*(p) && !isdigit((unsigned char)*(p)))            (p)++;
-#define SKIP_HEXDIGIT(p)                while(*(p) && isxdigit((unsigned char)*(p)))            (p)++;
-#define FIND_HEXDIGIT(p)                while(*(p) && !isxdigit((unsigned char)*(p)))           (p)++;
+#define SKIP_ALPHA(p)                   while(*(p) && IS_ALPHA(*(p)))             (p)++;
+#define FIND_ALPHA(p)                   while(*(p) && !IS_ALPHA(*(p)))            (p)++;
+#define SKIP_ALPHANUMERIC(p)            while(*(p) && IS_ALPHANUMERIC(*(p)))      (p)++;
+#define FIND_ALPHANUMERIC(p)            while(*(p) && !IS_ALPHANUMERIC(*(p)))     (p)++;
+#define SKIP_DIGIT(p)                   while(*(p) && IS_DIGIT(*(p)))             (p)++;
+#define FIND_DIGIT(p)                   while(*(p) && !IS_DIGIT(*(p)))            (p)++;
+#define SKIP_HEXDIGIT(p)                while(*(p) && IS_HEXDIGIT(*(p)))          (p)++;
+#define FIND_HEXDIGIT(p)                while(*(p) && !IS_HEXDIGIT(*(p)))         (p)++;
 
 #define HEX_CHAR_TO_INT(ch) 			(((ch)&0xf)+(((ch)>>6)&1)*9)
 #define DEC_CHAR_TO_INT(ch)				((ch)&0xf)
 #define OCT_CHAR_TO_INT(ch)				((ch)&0x7)
-#ifndef isodigit
-#define isodigit(ch)					((ch) >= '0' && (ch) <= '7')
-#endif
 
 /* Variable/buffer initialization (with zeros) */
 #define ZERO_VAR(var)                           memset(&(var),0,sizeof(var))
diff --git a/src/xpdev/genwrap.c b/src/xpdev/genwrap.c
index a9b0b31320ab4bb3c2d984f56874cd1cc979f577..5a72fee45c3f57c0f7f06b9ede47977157eee0ad 100644
--- a/src/xpdev/genwrap.c
+++ b/src/xpdev/genwrap.c
@@ -157,27 +157,27 @@ char DLLCALL c_unescape_char_ptr(const char* str, char** endptr)
 		int digits = 0;		// \x## for hexadecimal character literals (only 2 digits supported)
 		++str;
 		ch = 0;
-		while(digits < 2 && isxdigit(*str)) {
+		while(digits < 2 && IS_HEXDIGIT(*str)) {
 			ch *= 0x10;	
 			ch += HEX_CHAR_TO_INT(*str);
 			str++;
 			digits++;
 		}
 #ifdef C_UNESCAPE_OCTAL_SUPPORT
-	} else if(isodigit(*str)) {
+	} else if(IS_OCTDIGIT(*str)) {
 		int digits = 0;		// \### for octal character literals (only 3 digits supported)
 		ch = 0;
-		while(digits < 3 && isodigit(*str)) {
+		while(digits < 3 && IS_OCTDIGIT(*str)) {
 			ch *= 8;
 			ch += OCT_CHAR_TO_INT(*str);
 			str++;
 			digits++;
 		}
 #else
-	} else if(isdigit(*str)) {
-		int digits = 0;		// \### for decimal charater literals (only 3 digits supported)
+	} else if(IS_DIGIT(*str)) {
+		int digits = 0;		// \### for decimal character literals (only 3 digits supported)
 		ch = 0;
-		while(digits < 3 && isdigit(*str)) {
+		while(digits < 3 && IS_DIGIT(*str)) {
 			ch *= 10;
 			ch += DEC_CHAR_TO_INT(*str);
 			str++;
@@ -725,7 +725,7 @@ char* DLLCALL truncsp(char* str)
 
 	if(str!=NULL) {
 		i=len=strlen(str);
-		while(i && isspace((unsigned char)str[i-1]))
+		while(i && IS_WHITESPACE(str[i-1]))
 			i--;
 		if(i!=len)
 			str[i]=0;	/* truncate */
diff --git a/src/xpdev/ini_file.c b/src/xpdev/ini_file.c
index 074be46920fe0ee1a9347c13327ba1b484762daf..b096a82450e8092b456ebec83aa5069f54bbd335 100644
--- a/src/xpdev/ini_file.c
+++ b/src/xpdev/ini_file.c
@@ -1358,7 +1358,7 @@ static BOOL isTrue(const char* value)
 	char*	p;
 	BOOL	is_true;
 
-	if(!isalpha(*value))
+	if(!IS_ALPHA(*value))
 		return FALSE;
 
 	if((str=strdup(value)) == NULL)
@@ -1933,7 +1933,7 @@ static unsigned parseEnum(const char* value, str_list_t names, unsigned deflt)
 			return(i);
 
     i=strtoul(val, &endptr, 0);
-	if(*endptr != 0 && !isspace(*endptr))
+	if(*endptr != 0 && !IS_WHITESPACE(*endptr))
 		return deflt;
 	if(i>=count)
 		i=count-1;
diff --git a/src/xpdev/netwrap.c b/src/xpdev/netwrap.c
index b212c986b92b4cfd76262d79ce2f6ad7ca7a2a70..e4f13a637de09abc962618ab6945b551d7066837 100644
--- a/src/xpdev/netwrap.c
+++ b/src/xpdev/netwrap.c
@@ -40,7 +40,6 @@
 #include "netwrap.h"	/* verify prototypes */
 
 #include <stdlib.h>		/* malloc() */
-#include <ctype.h>		/* isspace() */
 
 #if defined(_WIN32)
 	#include <iphlpapi.h>	/* GetNetworkParams */
diff --git a/webv4/components/twit-button.xjs b/webv4/components/twit-button.xjs
index 94af80f7dbb662cf70d88451b7588163fe536a2a..d9ce9178d0f842d0d5f0206de624c4c0765c98fa 100644
--- a/webv4/components/twit-button.xjs
+++ b/webv4/components/twit-button.xjs
@@ -1,4 +1,4 @@
 <? var _bs = locale.get('button_block_sender', 'page_forum'); ?>
-<button id="bsb-%s" class="btn btn-default icon" aria-label="<? write(_bs); ?>" title="<? write(_bs); ?>" onclick="blockSender('%s', '%s', '%s')">
+<button id="bsb-%s" class="btn btn-default icon" aria-label="<? write(_bs); ?>" title="<? write(_bs); ?>" onclick="if (confirm('Permanently block this sender?')) { blockSender('%s', '%s', '%s') }">
     <span class="glyphicon glyphicon-ban-circle"></span>
 </button>
\ No newline at end of file
diff --git a/webv4/lib/pages.js b/webv4/lib/pages.js
index fc962ef983ea87ec6bd7de7c84adb73fd6eafa82..8ad898cb99ec08814a2cc0117fa5b29f1693bc93 100644
--- a/webv4/lib/pages.js
+++ b/webv4/lib/pages.js
@@ -70,7 +70,7 @@ function getCtrlLine(file) {
 			f.close();
 			break;
 		default:
-			ctrl = file_getname(file);
+			ctrl = pageName(file_getname(file));
 			break;
 	}
 
@@ -153,7 +153,7 @@ function _getPageList(dir) {
 }
 
 function pageName(p) {
-	return p.replace(/^\d*-/, '');
+	return p.replace(/^\d*-/, '').replace(/\..*$/, '');
 }
 
 function mergePageLists(stock, mods) {
diff --git a/webv4/pages/003-games.xjs b/webv4/pages/003-games.xjs
index 9f6ae7423737a8c2122bd22c84a48cb127478bc0..7b7604d8bb2b03cb992b44b613676ba42b0569e2 100644
--- a/webv4/pages/003-games.xjs
+++ b/webv4/pages/003-games.xjs
@@ -41,7 +41,7 @@
 	</div>
 </div>
 
-<?xjs write('<script id="fTelnetScript" src="//embed-v2.ftelnet.ca/js/ftelnet-loader.norip.noxfer.js?v=' + (new Date()).getTime() + '"></script>'); ?>
+<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)); ?>;
diff --git a/xtrn/bbsfinder.net/bbsfinder.js b/xtrn/bbsfinder.net/bbsfinder.js
deleted file mode 100644
index 6a84e8d1592634de9b280fb02777ce7bd560aa37..0000000000000000000000000000000000000000
--- a/xtrn/bbsfinder.net/bbsfinder.js
+++ /dev/null
@@ -1,60 +0,0 @@
-/*	bbsfinder.net updater for Synchronet BBS 3.15+
-	echicken -at- bbs.electronicchicken.com */
-
-load('sbbsdefs.js');
-load('event-timer.js');
-load('http.js');
-
-var delay = 240000;
-
-function loadSettings() {
-	
-	var f = new File(system.ctrl_dir + 'modopts.ini');
-	f.open('r');
-	if (!f.is_open) {
-		throw 'Unable to open ' + system.ctrl_dir + 'modopts.ini for reading.'
-	}
-	opts = f.iniGetObject('bbsfinder');
-	f.close();
-
-	if (!opts.hasOwnProperty('username') || !opts.hasOwnProperty('password')) {
-		throw 'BBSfinder account info could not be read from modopts.ini.'
-	}
-
-	opts.username = encodeURIComponent(opts.username);
-	opts.password = encodeURIComponent(opts.password);
-
-	return opts;
-
-}
-
-function update() {
-	try {
-		var opts = loadSettings();
-		var ret = (new HTTPRequest()).Get(
-			format(
-				'http://www.bbsfinder.net/update.asp?un=%s&pw=%s',
-				opts.username, opts.password
-			)
-		);
-	} catch (e) {
-		log(LOG_INFO, 'BBSfinder HTTP error: ' + e);
-	}
-	if(ret == 0) {
-		log(LOG_INFO, 'BBSfinder update succeded.');
-	} else {
-		log(LOG_INFO, 'BBSfinder update failed.');
-		log(LOG_DEBUG, ret);
-	}
-}
-
-update();
-
-if (argc > 0 && argv[0] === '-l') {
-	var timer = new Timer();
-	timer.addEvent(delay, true, update);
-	while (!js.terminated) {
-		timer.cycle();
-		mswait(1000);
-	}
-}
\ No newline at end of file
diff --git a/xtrn/bbsfinder.net/readme.txt b/xtrn/bbsfinder.net/readme.txt
deleted file mode 100644
index cb01859425184e9a4bba048d2e207d48e206ea20..0000000000000000000000000000000000000000
--- a/xtrn/bbsfinder.net/readme.txt
+++ /dev/null
@@ -1,77 +0,0 @@
-BBSfinder.net updater for Synchronet 3.15+
-echicken -at- bbs.electronicchicken.com
-
-Requirements:
--------------
-
-It's recommended that you grab the latest copies of the following files from
-the Synchronet CVS at cvs.synchro.net:
-
-exec/load/http.js
-exec/load/event-timer.js
-xtrn/bbsfinder.net/bbsfinder.js
-xtrn/bbsfinder.net/readme.txt
-
-
-Installation:
--------------
-
-I'm going to assume that your copy of bbsfinder.js is located at:
-
-/sbbs/xtrn/bbsfinder.net/bbsfinder.js
-
-Add the following section to your ctrl/modopts.ini file:
-
-[bbsfinder]
-username = <username>
-password = <password>
-
-
-Usage:
-------
-
-Method #1: Running as a timed event
-
-	Launch scfg (that's BBS->Configure from the Synchronet Control Panel in
-	Windows,) select "External Programs", then "Timed Events", and create a
-	new item as follows:
-	
-	Event Internal Code: BBSFINDR
-	
-	Edit the timed event that you've just created so that it looks like this:
-
-		BBSFINDR Timed Event
-		----------------------------------------------------------
-		�Internal Code                   BBSFINDR
-		�Start-up Directory              /sbbs/xtrn/bbsfinder.net
-		�Command Line                    ?bbsfinder.js
-		�Enabled                         Yes
-		�Execution Node                  1
-		�Execution Months                Any
-		�Execution Days of Month         Any
-		�Execution Days of Week          All
-		�Execution Frequency             360 times a day
-		�Requires Exclusive Execution    No
-		�Force Users Off-line For Event  No
-		�Native Executable               No
-		�Use Shell to Execute            No
-		�Background Execution            No
-		�Always Run After Init/Re-init   Yes
-
-	Hint: to set the "Execution frequency" to "360 times a day", select
-	"Execution frequency" and answer "No" at the "Execute at a specific time?"
-	prompt.  We want this event to run every four minutes, so (24*60)/4 = 360.
-	
-Method #2: Running from jsexec
-
-	From a command prompt, execute the following, substituting paths as
-	required:
-	
-	/sbbs/exec/jsexec /sbbs/xtrn/bbsfinder.net/bbsfinder.js -l
-	
-
-Support:
---------
-
-Post a message to 'echicken' in DOVENet's Synchronet Sysops echo, or find me
-in #synchronet on irc.synchro.net.
\ No newline at end of file
diff --git a/xtrn/bullshit/bullshit.js b/xtrn/bullshit/bullshit.js
index 2210c52d58c01e2814b4b01aee72ea9975d53128..9fc597358c4e0a32b5fb4f013bb6b530ce4288ea 100644
--- a/xtrn/bullshit/bullshit.js
+++ b/xtrn/bullshit/bullshit.js
@@ -271,10 +271,22 @@ function init() {
 }
 
 function main() {
-	const settings = lib.loadSettings(argv[0]);
+	var settings = lib.loadSettings(argv[0]);
+	
+	// if you set newOnly to logon, then on login time, it will treat it as newOnly=true and
+	// only show if new bulletins, but at all other times, treat it as newOnly=false
+	// (so always display them when called from external program menu context)
+	if ((settings.newOnly == "logon") && (bbs.node_action != NODE_LOGN)) {
+		settings.newOnly = false;
+	}
+	
 	const list = lib.loadList(settings);
-	if (!list.length && settings.newOnly) return;
-    const disp = initDisplay(settings, list);
+	
+	if (!list.length && settings.newOnly) {
+		return;
+	}
+	
+	const disp = initDisplay(settings, list);
 	displayList(list, disp.tree);
 	var ret;
 	var viewer;
diff --git a/xtrn/bullshit/readme.txt b/xtrn/bullshit/readme.txt
index c2e162850315df7e5a4c870ff7f6c64749deec63..6501301a192d9a48f1074cd94d6e1276443e1a34 100644
--- a/xtrn/bullshit/readme.txt
+++ b/xtrn/bullshit/readme.txt
@@ -90,6 +90,12 @@ Contents
 			is available to be displayed to the user. Bullshit will exit
 			silently.
 
+			If 'newOnly' is set to the string "logon", then only new bulletins
+			will be shown to the user at logon time (and will silently exit
+			if none are available), but all bulletins will be shown at other
+			times (for example, adding bullshit to the external programs menu)
+
+
 	In the 'colors' section:
 
 		-	'title' and 'text' are the colors used when viewing an item
diff --git a/xtrn/knk/knk.js b/xtrn/knk/knk.js
index 77498732117b2c55a7bed4848d0f2ec4b913d54b..30579383b8b56796fb323ef5ca6371648f64916a 100644
--- a/xtrn/knk/knk.js
+++ b/xtrn/knk/knk.js
@@ -1424,7 +1424,7 @@ function update_userfile(player, computer, won)
 	var lines=[];
 	var line;
 	var now=new Date();
-	var nowmonth=['January','February','March','April','May','June','July','August','September','Optober','November','December'][now.getMonth()];
+	var nowmonth=['January','February','March','April','May','June','July','August','September','October','November','December'][now.getMonth()];
 	var all_ud=[];
 
 	if(!Lock(f.name, dk.connection.node, true, 1))
diff --git a/xtrn/sdk/xsdk.c b/xtrn/sdk/xsdk.c
index 87c06ca157bf1c1a089b4d398a66fb3d25cea789..52549135666e1da830568f5b60f6f6c75c1b18a6 100644
--- a/xtrn/sdk/xsdk.c
+++ b/xtrn/sdk/xsdk.c
@@ -2247,7 +2247,7 @@ uint usernumber(char *username)
 char *ultoac(ulong l, char *string)
 {
 	char str[81];
-	char i,j,k;
+	int i,j,k;
 
 	sprintf(str,"%lu",l);
 	i=strlen(str)-1;
diff --git a/xtrn/sdk/xsdkdefs.h b/xtrn/sdk/xsdkdefs.h
index f7b91a2109a60a134590b4bdfba1a380c77b758e..6160313094fa3d14f0895eca479b13cd35c434e4 100644
--- a/xtrn/sdk/xsdkdefs.h
+++ b/xtrn/sdk/xsdkdefs.h
@@ -288,6 +288,44 @@ enum {								/* Node Action */
 
 #ifndef _SBBSDEFS_H
 #define KEY_BUFSIZE 256
+#endif
+
+#ifndef USE_XPDEV
+enum {
+	 CTRL_AT						// NUL
+	,CTRL_A							// SOH
+	,CTRL_B							// STX
+	,CTRL_C							// ETX
+	,CTRL_D							// EOT
+	,CTRL_E							// ENQ
+	,CTRL_F							// ACK
+	,CTRL_G							// BEL
+	,CTRL_H							// BS
+	,CTRL_I							// HT
+	,CTRL_J							// LF
+	,CTRL_K							// VT
+	,CTRL_L							// FF
+	,CTRL_M							// CR
+	,CTRL_N							// SO
+	,CTRL_O							// SI
+	,CTRL_P							// DLE
+	,CTRL_Q							// DC1
+	,CTRL_R							// DC2
+	,CTRL_S							// DC3
+	,CTRL_T							// DC4
+	,CTRL_U							// NAK
+	,CTRL_V							// SYN
+	,CTRL_W							// ETB
+	,CTRL_X							// CAN
+	,CTRL_Y							// EM
+	,CTRL_Z							// SUB
+	,CTRL_OPEN_BRACKET				// ESC
+	,CTRL_BACKSLASH					// FS
+	,CTRL_CLOSE_BRACKET				// GS
+	,CTRL_CARET						// RS
+	,CTRL_UNDERSCORE				// US
+	,CTRL_QUESTION_MARK	= 0x7f		// DEL
+};
 #endif
 
 									/* Special terminal key mappings */