diff --git a/DDMsgReader.js b/DDMsgReader.js
deleted file mode 100644
index 278b9fc7da67dd504f10294734976ecff8a7c0c6..0000000000000000000000000000000000000000
--- a/DDMsgReader.js
+++ /dev/null
@@ -1,24134 +0,0 @@
-/* This is a message reader/lister door for Synchronet.  Features include:
- * - Listing messages in the user's current message area with the ability to
- *   navigate forwards & backwards through the list (and for ANSI users, a
- *   lightbar interface will be used, or optionally can be set to use a more
- *   traditional interface for ANSI users)
- * - The user can select a message from the list to read and optionally reply to
- * - For ANSI users, reading messages is done with an enhanced user interface,
- *   with the ability to scroll up & down through the message, move to the next
- *   or previous message using the right & left arrow keys, display the message
- *   list to choose another message to read, etc.
- * - The ability to start up with the message list or reading messages in the
- *   user's current message area (AKA sub-board)
- * - Message searching
- *
- * Author: Eric Oulashin (AKA Nightfox)
- * BBS: Digital Distortion
- * BBS address: digitaldistortionbbs.com (or digdist.bbsindex.com)
- *
- * Date       Author            Description
- * 2014-09-13 Eric Oulashin     Started (based on my message lister script)
- * ... Comments trimmed ...
- * 2023-09-22 Eric Oulashin     Version 1.80 beta
- *                              Improved speed of new-to-you scans, and to an extent (hopefully) overall speed
- *                              Bug fix: Setting reverseListOrder to "ask" in the .cfg file works properly again.
- *                              Bug fix: When listing messages in reverse order, the selected menu index
- *                              (for lightbar mode) is now correct.
- *                              Bug fix: If the user is allowed to read deleted messages, then allow
- *                              the left & right arrow keys to to the next/previous message if it's deleted.
- *                              Small fixes for indexed scanning mode.
- *                              New: For personal email, unread emails will have an 'unread' message indicator
- *                              in the message list as a U between the message number and the 'from' name.
- *                              New user setting: "Quit from reader to message list": When enabled,
- *                              quitting from reader mode goes to the message list instead of exiting
- *                              out of DDMsgReader fully.
- *                              New user setting: Enter/selection from indexed mode menu shows message list
- *                              (instead of going into reader mode)
- *                              New user setting: List messages in reverse order
- * 2023-10-10 Eric Oulashin     Version 1.80
- *                              Releasing this version
- * 2023-10-11 Eric Oulashin     Version 1.81
- *                              Updated permission check functions (speed improvement)
- * 2023-10-18 Eric Oulashin     Version 1.82
- *                              Fix for # posts and missing dates in sub-board list when changing sub-board
- * 2023-10-25 Eric Oulashin     Version 1.83
- *                              Personal emails to the sysop received as "sysop" (or starting with "sysop")
- *                              are now correctly identified and marked as read when read
- * 2023-10-26 Eric Oulashin     Version 1.84
- *                              Fix in reader mode for refreshing the message area after
- *                              closing another window (necessary with recent changes to
- *                              substrWithAttrCodes())
- * 2023-11-01 Eric Oulashin     Version 1.85
- *                              Mark personal email as read if the user is just reading personal email
- * 2023-11-09 Eric Oulashin     Version 1.86
- *                              New feature: For indexed mode, when choosing a sub-board, the R key
- *                              can be used to mark all messages as read in the sub-board.
- *                              Fix: For continuous newscan or browse newscan (SCAN_BACK),
- *                              call the stock Synchronet behavior (DDMsgReader did this previously,
- *                              as DDMsgReader doesn't implement those yet).
- *                              Fix: In the message list, to-user alternate colors weren't being used
- *                              unless the message was read. The correct colors are used again.
- * 2023-11-09 Eric Oulashin     Version 1.87 Beta
- *                              Trying to speed things up by not getting vote headers all the time
- *                              when calling get_all_msg_headers(), unless the vote headers are needed
- *                              (when the sysop views who votes on a message when viewing tally info)
- *                              New: User setting to only show new messages in a newscan (defaults to true/enabled)
- *                              In the message list, there is now an additional space before the
- *                              'from' name, in case one of the status characters is a letter (this should
- *                              look better).
- *                              New: In lightbar mode, the indexed newscan menu can optionally 'snap' to
- *                              the next sub-board with new messages when showing/returning to the menu
- *                              Fix: When listing personal email, messages to the user were written
- *                              with the to-user color wuen unread. Now the regular colors are always
- *                              used (since all of a user's personal emails are 'to' them).
- *                              Fix: For indexed newscan, if there are no sub-boards selected for scan
- *                              in the user's newscan configuration, then output a message and return.
- *                              Otherwise, it would end up in an infinite loop.
- *                              Updated how user settings are loaded, to ensure that default user settings
- *                              from DDMsgReader.cfg actually get set properly in the user settings.
- * 2023-11-23 Eric Oulashin     Version 1.88
- *                              New user setting/configuration option to prompt the user whether or
- *                              not to delete a personal email after replying to it (defaults to false).
- *                              New: Displays whether a personal email has been replied to.
- *                              Fix: Now displaying message vote score in the default header again.
- *                              Fix: When viewing message headers (for the sysop), now correctly
- *                              shows the message attributes.
- * 2023-11-30 Eric Oulashin     Version 1.89
- *                              New: User option to toggle whether to display the email 'replied' indicator
- *                              (defaults to true).
- *                              Fix for setting colors for the key help lines so that the background
- *                              won't get un-done if the other help line colors have a N (normal) attribute.
- * 2023-12-02 Eric Oulashin     Version 1.90 Beta
-  *                             New: operator menu for read mode, with the option to add the author to the
- *                              twit list, etc.
- *                              Fix: When refreshing a rectangular area of a message, if it's a poll message,
- *                              the background color for the voted responses was used for the non-selected
- *                              responses.
- *                              Removed the setting useScrollingInterfaceForANSIMessages.
- * 2023-12-04 Eric Oulashin     Version 1.90
- *                              Releasing this version
- * 2023-12-12 Eric Oulashin     Version 1.90a
- *                              New configurable colors in the theme file for the indexed mode newscan menu:
- *                              indexMenuSeparatorLine (sub-board separator line) and indexMenuSeparatorText
- *                              (sub-board separator text)
- * 2023-12-15 Eric Oulashin     Version 1.90b
- *                              New configurable colors in the theme file for the indexed newscan menu
- *                              header text (indexMenuHeader), "NEW" indicator text (indexMenuNewIndicator),
- *                              and highlighted "NEW" indicator text (indexMenuNewIndicatorHighlight)
- * 2023-12-26 Eric Oulashin     Version 1.91
- *                              New sysop features while reading a message: Show message hex (with the X key)
- *                              and save message hex to a file (with Ctrl-X)
- * 2023-12-29 Eric Oulashin     Version 1.92
- *                              Indexed newscan: By default, if there are no new messages, it now shows
- *                              "No new messages." (578 QWKNoNewMessages from text.dat). There's a new user
- *                              setting to toggle whether to use the indexed newscan menu even if there are
- *                              no new messages. New configuration file option: displayIndexedModeMenuIfNoNewMessages,
- *                              which is a default for the user setting.
- * 2024-01-01 Eric Oulashin     Version 1.93
- *                              New user-toggleable behavior: Show indexed menu after reading all new messages.
- *                              Also, indexed reader mode (started with the -indexedMode command-line option)
- *                              now lists ALL sub-boards, rather than only sub-boards the user has enabled
- *                              for newscan. It also prompts the user to list sub-boards in the current group
- *                              or all.
- * 2024-01-04 Eric Oulashin     Version 1.93a Beta
- *                              Fix: For indexed read mode (not doing a newscan), when choosing a sub-board to
- *                              read, the correct (first unread) message is displayed. Also, the user's scan
- *                              pointer is also updated to the last_read pointer.
- * 2024-01-07 Eric Oulashin     Version 1.93a
- *                              New operator option for read mode: Add author email to email.can
- *                              Releasing this version
- */
-
-"use strict";
-
-
-/* Command-line arguments (in -arg=val format, or -arg format to enable an
-   option):
-   -search: A search type.  Available options:
-            keyword_search: Do a keyword search in message subject/body text (current message area)
-            from_name_search: 'From' name search (current message area)
-            to_name_search: 'To' name search (current message area)
-            to_user_search: To user search (current message area)
-            new_msg_scan: New message scan (prompt for current sub-board, current
-                          group, or all)
-            new_msg_scan_all: New message scan (all sub-boards)
-			new_msg_scan_cur_grp: New message scan (current message group only)
-            new_msg_scan_cur_sub: New message scan (current sub-board only).  This
-			                      can (optionally) be used with the -subBoard
-								  command-line parameter, which specifies an internal
-								  code for a sub-board, which may be different from
-								  the user's currently selected sub-board.
-			to_user_new_scan: Scan for new (unread) messages to the user (prompt
-			                  for current sub-board, current group, or all)
-			to_user_new_scan_all: Scan for new (unread) messages to the user
-			                      (all sub-boards)
-			to_user_new_scan_cur_grp: Scan for new (unread) messages to the user
-			                          (current group)
-            to_user_new_scan_cur_sub: Scan for new (unread) messages to the user
-			                          (current sub-board)
-			to_user_all_scan: Scan for all messages to the user (prompt for current
-			                  sub-board, current group, or all)
-            prompt: Prompt the user for one of several search/scan options to
-                    choose from
-        Note that if the -personalEmail option is specified (to read personal
-        email), the only valid search types are keyword_search and
-        from_name_search.
-   -suppressSearchTypeText: Disable the search type text that would appear
-                            above searches or scans (such as "New To You
-                            Message Scan", etc.)
-   -startMode: Startup mode.  Available options:
-               list (or lister): Message list mode
-               read (or reader): Message read mode
-   -configFilename: Specifies the name of the configuration file to use.
-                    Defaults to DDMsgReader.cfg.
-   -personalEmail: Read personal email to the user.  This is a true/false value.
-                   It doesn't need to explicitly have a =true or =false afterward;
-                   simply including -personalEmail will enable it.  If this is specified,
-				   the -chooseAreaFirst and -subBoard options will be ignored.
-   -personalEmailSent: Read personal email to the user.  This is a true/false
-                       value.  It doesn't need to explicitly have a =true or =false
-                       afterward; simply including -personalEmailSent will enable it.
-   -allPersonalEmail: Read all personal email (to/from all)
-   -userNum: Specify a user number (for the personal email options)
-   -chooseAreaFirst: Display the message area chooser before reading/listing
-                     messages.  This is a true/false value.  It doesn't need
-                     to explicitly have a =true or =false afterward; simply
-                     including -chooseAreaFirst will enable it.  If -personalEmail
-					 or -subBoard is specified, then this option won't have any
-                     effect.
-	-subBoard: The sub-board (internal code or number) to read, other than the user's
-               current sub-board. If this is specified, the -chooseAreaFirst option
-			   will be ignored.
-	-verboseLogging: Enable logging to the system log & node log.  Currently, there
-	                 isn't much that will be logged, but more log messages could be
-					 added in the future.
-*/
-
-// - For pageUp & pageDown, enable alternate keys:
-//  - When reading a message - scrollTextLines()
-//  - When listing messages
-//  - When listing message groups & sub-boards for sub-board selection
-// - For sub-board area search:
-//  - Enable searching in traditional interface
-//  - Update the keys in the lightbar help line and traditional interface
-
-// This script requires Synchronet version 3.18 or higher (for mouse hotspot support).
-// Exit if the Synchronet version is below the minimum.
-if (system.version_num < 31800)
-{
-	var message = "\x01n\x01h\x01y\x01i* Warning:\x01n\x01h\x01w Digital Distortion Message Reader "
-	             + "requires version \x01g3.18\x01w or\r\n"
-	             + "higher of Synchronet.  This BBS is using version \x01g" + system.version
-	             + "\x01w.  Please notify the sysop.";
-	console.crlf();
-	console.print(message);
-	console.crlf();
-	console.pause();
-	exit();
-}
-
-require("sbbsdefs.js", "K_UPPER");
-require("text.js", "Email"); // Text string definitions (referencing text.dat)
-require("utf8_cp437.js", "utf8_cp437");
-require("userdefs.js", "USER_UTF8");
-require("dd_lightbar_menu.js", "DDLightbarMenu");
-require("html2asc.js", 'html2asc');
-require("attr_conv.js", "convertAttrsToSyncPerSysCfg");
-require("graphic.js", 'Graphic');
-require("smbdefs.js", "SMB_POLL_ANSWER");
-load('822header.js');
-var ansiterm = require("ansiterm_lib.js", 'expand_ctrl_a');
-var hexdump = load('hexdump_lib.js');
-
-
-// Reader version information
-var READER_VERSION = "1.93a";
-var READER_DATE = "2024-01-07";
-
-// Keyboard key codes for displaying on the screen
-var UP_ARROW = ascii(24);
-var DOWN_ARROW = ascii(25);
-var LEFT_ARROW = ascii(17);
-var RIGHT_ARROW = ascii(16);
-// Ctrl keys for input
-var CTRL_A = "\x01";
-var CTRL_B = "\x02";
-var CTRL_C = "\x03";
-var CTRL_D = "\x04";
-var CTRL_E = "\x05";
-var CTRL_F = "\x06";
-var CTRL_G = "\x07";
-var BEEP = CTRL_G;
-var CTRL_H = "\x08";
-var BACKSPACE = CTRL_H;
-var CTRL_I = "\x09";
-var TAB = CTRL_I;
-var CTRL_J = "\x0a";
-var CTRL_K = "\x0b";
-var CTRL_L = "\x0c";
-var CTRL_M = "\x0d";
-var CTRL_N = "\x0e";
-var CTRL_O = "\x0f";
-var CTRL_P = "\x10";
-var CTRL_Q = "\x11";
-var XOFF = CTRL_Q;
-var CTRL_R = "\x12";
-var CTRL_S = "\x13";
-var XON = CTRL_S;
-var CTRL_T = "\x14";
-var CTRL_U = "\x15";
-var CTRL_V = "\x16";
-var KEY_INSERT = CTRL_V;
-var CTRL_W = "\x17";
-var CTRL_X = "\x18";
-var CTRL_Y = "\x19";
-var CTRL_Z = "\x1a";
-//var KEY_ESC = "\x1b";
-var KEY_ESC = ascii(27);
-var KEY_ENTER = CTRL_M;
-// PageUp & PageDown keys - Synchronet 3.17 as of about December 18, 2017
-// use CTRL-P and CTRL-N for PageUp and PageDown, respectively.  sbbsdefs.js
-// defines them as KEY_PAGEUP and KEY_PAGEDN; I've used slightly different names
-// in this script so that this script will work with Synchronet systems before
-// and after the update containing those key definitions.
-var KEY_PAGE_UP = CTRL_P;
-var KEY_PAGE_DOWN = CTRL_N;
-// Ensure KEY_PAGE_UP and KEY_PAGE_DOWN are set to what's defined in sbbs.js
-// for KEY_PAGEUP and KEY_PAGEDN in case they change
-if (typeof(KEY_PAGEUP) === "string")
-	KEY_PAGE_UP = KEY_PAGEUP;
-if (typeof(KEY_PAGEDN) === "string")
-	KEY_PAGE_DOWN = KEY_PAGEDN;
-	
-// These are defined in sbbsdefs.js:
-//var	  KEY_UP		='\x1e';	// ctrl-^ (up arrow)
-//var	  KEY_DOWN		='\x0a';	// ctrl-j (dn arrow)
-//var   KEY_RIGHT		='\x06';	// ctrl-f (rt arrow)
-//var	  KEY_LEFT		='\x1d';	// ctrl-] (lf arrow)
-//var	  KEY_HOME		='\x02';	// ctrl-b (home)
-//var   KEY_END       ='\x05';	// ctrl-e (end)
-//var   KEY_DEL       ='\x7f';    // (del)
-// These were added to sbbsdef.js around December 17, 2017:
-//var		KEY_PAGEUP	='\x10';	/* ctrl-p (Page Up)							*/
-//var		KEY_PAGEDN	='\x0e';	/* ctrl-n (Page Down)						*/
-
-// Characters for display
-// Box-drawing/border characters: Single-line
-var UPPER_LEFT_SINGLE = "\xDA";
-var HORIZONTAL_SINGLE = "\xC4";
-var UPPER_RIGHT_SINGLE = "\xBF";
-var VERTICAL_SINGLE = "\xB3";
-var LOWER_LEFT_SINGLE = "\xC0";
-var LOWER_RIGHT_SINGLE = "\xD9";
-var T_SINGLE = "\xC2";
-var LEFT_T_SINGLE = "\xC3";
-var RIGHT_T_SINGLE = "\xB4";
-var BOTTOM_T_SINGLE = "\xC1";
-var CROSS_SINGLE = "\xC5";
-// Box-drawing/border characters: Double-line
-var UPPER_LEFT_DOUBLE = "\xC9";
-var HORIZONTAL_DOUBLE = "\xCD";
-var UPPER_RIGHT_DOUBLE = "\xBB";
-var VERTICAL_DOUBLE = "\xBA";
-var LOWER_LEFT_DOUBLE = "\xC8";
-var LOWER_RIGHT_DOUBLE = "\xBC";
-var T_DOUBLE = "\xCB";
-var LEFT_T_DOUBLE = "\xCC";
-var RIGHT_T_DOUBLE = "\xB9";
-var BOTTOM_T_DOUBLE = "\xCA";
-var CROSS_DOUBLE = "\xCE";
-// Box-drawing/border characters: Vertical single-line with horizontal double-line
-var UPPER_LEFT_VSINGLE_HDOUBLE = "\xD5";
-var UPPER_RIGHT_VSINGLE_HDOUBLE = "\xB8";
-var LOWER_LEFT_VSINGLE_HDOUBLE = "\xD4";
-var LOWER_RIGHT_VSINGLE_HDOUBLE = "\xBE";
-// Other special characters
-var DOT_CHAR = "\xF9";
-var CHECK_CHAR = "\xFB";
-var THIN_RECTANGLE_LEFT = "\xDD";
-var THIN_RECTANGLE_RIGHT = "\xDE";
-
-var BLOCK1 = "\xB0"; // Dimmest block
-var BLOCK2 = "\xB1";
-var BLOCK3 = "\xB2";
-var BLOCK4 = "\xDB"; // Brightest block
-var MID_BLOCK = ascii(254);
-var TALL_UPPER_MID_BLOCK = "\xFE";
-var UPPER_CENTER_BLOCK = "\xDF";
-var LOWER_CENTER_BLOCK = "\xDC";
-
-
-const ERROR_PAUSE_WAIT_MS = 1500;
-
-// Reader mode definitions:
-const READER_MODE_LIST = 0;
-const READER_MODE_READ = 1;
-// Search types
-const SEARCH_NONE = -1;
-const SEARCH_KEYWORD = 2;
-const SEARCH_FROM_NAME = 3;
-const SEARCH_TO_NAME_CUR_MSG_AREA = 4;
-const SEARCH_TO_USER_CUR_MSG_AREA = 5;
-const SEARCH_MSG_NEWSCAN = 6;
-const SEARCH_MSG_NEWSCAN_CUR_SUB = 7;
-const SEARCH_MSG_NEWSCAN_CUR_GRP = 8;
-const SEARCH_MSG_NEWSCAN_ALL = 9;
-const SEARCH_TO_USER_NEW_SCAN = 10;
-const SEARCH_TO_USER_NEW_SCAN_CUR_SUB = 11;
-const SEARCH_TO_USER_NEW_SCAN_CUR_GRP = 12;
-const SEARCH_TO_USER_NEW_SCAN_ALL = 13;
-const SEARCH_ALL_TO_USER_SCAN = 14;
-
-// Message threading types
-const THREAD_BY_ID = 15;
-const THREAD_BY_TITLE = 16;
-const THREAD_BY_AUTHOR = 17;
-const THREAD_BY_TO_USER = 18;
-
-// Scan scopes
-const SCAN_SCOPE_SUB_BOARD = 0;
-const SCAN_SCOPE_GROUP = 1;
-const SCAN_SCOPE_ALL = 2;
-
-// Reader mode - Actions
-const ACTION_NONE = 19;
-const ACTION_GO_NEXT_MSG = 20;
-const ACTION_GO_PREVIOUS_MSG = 21;
-const ACTION_GO_SPECIFIC_MSG = 22;
-const ACTION_GO_FIRST_MSG = 23;
-const ACTION_GO_LAST_MSG = 24;
-const ACTION_DISPLAY_MSG_LIST = 25;
-const ACTION_CHG_MSG_AREA = 26;
-const ACTION_GO_PREV_MSG_AREA = 27;
-const ACTION_GO_NEXT_MSG_AREA = 28;
-const ACTION_QUIT = 29;
-// Actions for indexed Mode
-const INDEXED_MODE_SUBBOARD_MENU = 30;
-
-// Definitions for help line refresh parameters for error functions
-const REFRESH_MSG_AREA_CHG_LIGHTBAR_HELP_LINE = 0;
-
-// Message list sort types
-const MSG_LIST_SORT_DATETIME_RECEIVED = 0;
-const MSG_LIST_SORT_DATETIME_WRITTEN = 1;
-
-// Special value for passing to PopulateHdrsForCurrentSubBoard(): Start populating
-// message headers from the scan pointer.
-// This is a negative number so it won't get confuesd with a message header index.
-const POPULATE_MSG_HDRS_FROM_SCAN_PTR = -1;
-const POPULATE_NEWSCAN_FORCE_GET_ALL_HDRS = -2; // Get all message headers even for a newscan
-
-// Misc. defines
-var ERROR_WAIT_MS = 1500;
-var SEARCH_TIMEOUT_MS = 10000;
-
-// A regular expression to check whether a string is an email address
-var gEmailRegex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
-// A regular expression to check whether a string is a FidoNet email address
-var gFTNEmailRegex = /^.*@[0-9]+:[0-9]+\/[0-9]+$/;
-// An array of regular expressions for checking for ANSI codes (globally in a string & ignore case)
-var gANSIRegexes = [ new RegExp(ascii(27) + "\[[0-9]+[mM]", "gi"),
-                     new RegExp(ascii(27) + "\[[0-9]+(;[0-9]+)+[mM]", "gi"),
-                     new RegExp(ascii(27) + "\[[0-9]+[aAbBcCdD]", "gi"),
-                     new RegExp(ascii(27) + "\[[0-9]+;[0-9]+[hHfF]", "gi"),
-                     new RegExp(ascii(27) + "\[[sSuUkK]", "gi"),
-                     new RegExp(ascii(27) + "\[2[jJ]", "gi") ];
-
-// Determine the script's startup directory.
-// This code is a trick that was created by Deuce, suggested by Rob Swindell
-// as a way to detect which directory the script was executed in.  I've
-// shortened the code a little.
-var gStartupPath = '.';
-try { throw dig.dist(dist); } catch(e) { gStartupPath = e.fileName; }
-gStartupPath = backslash(gStartupPath.replace(/[\/\\][^\/\\]*$/,''));
-
-// See if we're running in Windows or not.  Until early 2015, the word_wrap()
-// function seemed to have a bug where the wrapping length in Linux was one
-// less than what it uses in Windows).  That seemed to be fixed in one of the
-// Synchronet 3.16 builds in early 2015.
-var gRunningInWindows = /^WIN/.test(system.platform.toUpperCase());
-
-// Temporary directory (in the logged-in user's node directory) to store
-// file attachments, etc.
-var gFileAttachDir = backslash(system.node_dir + "DDMsgReader_Attachments");
-// If the temporary attachments directory exists, then delete it (in case the last
-// user hung up while running this script, etc.)
-if (file_exists(gFileAttachDir))
-	deltree(gFileAttachDir);
-
-// See if the avatar support file is available, and load is if so
-var gAvatar = null;
-if (file_exists(backslash(system.exec_dir) + "load/avatar_lib.js"))
-	gAvatar = load({}, "avatar_lib.js");
-
-// User twitlist filename (and settings filename)
-var gUserTwitListFilename = backslash(system.data_dir + "user") + format("%04d", user.number) + ".DDMsgReader_twitlist";
-var gUserSettingsFilename = backslash(system.data_dir + "user") + format("%04d", user.number) + ".DDMsgReader_Settings";
-
-/////////////////////////////////////////////
-// Script execution code
-
-// Parse the command-line arguments
-
-var gCmdLineArgVals = parseArgs(argv);
-if (gCmdLineArgVals.exitNow)
-	exit(0);
-var gAllPersonalEmailOptSpecified = (gCmdLineArgVals.hasOwnProperty("allpersonalemail") && gCmdLineArgVals.allpersonalemail);
-// Check to see if the command-line argument for reading personal email is enabled
-var gListPersonalEmailCmdLineOpt = ((gCmdLineArgVals.hasOwnProperty("personalemail") && gCmdLineArgVals.personalemail) ||
-                                    (gCmdLineArgVals.hasOwnProperty("personalemailsent") && gCmdLineArgVals.personalemailsent) ||
-									gAllPersonalEmailOptSpecified);
-// If the command-line parameter "search" is specified as "prompt", then
-// prompt the user for the type of search to perform.
-var gDoDDMR = true; // If the user doesn't choose a search type, this will be set to false
-if (gCmdLineArgVals.hasOwnProperty("search") && (gCmdLineArgVals.search.toLowerCase() == "prompt"))
-{
-	console.attributes = "N";
-	console.crlf();
-	console.print("\x01cMessage search:");
-	console.crlf();
-	var allowedKeys = "";
-	if (!gListPersonalEmailCmdLineOpt)
-	{
-		allowedKeys = "ANKFTYUS";
-		console.print(" \x01g\x01hN\x01y = \x01n\x01cNew message scan");
-		console.crlf();
-		console.print(" \x01g\x01hK\x01y = \x01n\x01cKeyword");
-		console.crlf();
-		console.print(" \x01h\x01gF\x01y = \x01n\x01cFrom name");
-		console.crlf();
-		console.print(" \x01h\x01gT\x01y = \x01n\x01cTo name");
-		console.crlf();
-		console.print(" \x01h\x01gY\x01y = \x01n\x01cTo you");
-		console.crlf();
-		console.print(" \x01h\x01gU\x01y = \x01n\x01cUnread (new) messages to you");
-		console.crlf();
-		console.print(" \x01h\x01gS\x01y = \x01n\x01cScan for msgs to you");
-		console.crlf();
-	}
-	else
-	{
-		// Reading personal email - Allow fewer choices
-		allowedKeys = "KF";
-		console.print(" \x01g\x01hK\x01y = \x01n\x01cKeyword");
-		console.crlf();
-		console.print(" \x01h\x01gF\x01y = \x01n\x01cFrom name");
-		console.crlf();
-	}
-	console.print(" \x01h\x01gA\x01y = \x01n\x01cAbort");
-	console.crlf();
-	console.print("\x01n\x01cMake a selection\x01g\x01h: \x01c");
-	// TODO: Check to see if keyword & from name search work when reading
-	// personal email
-	switch (console.getkeys(allowedKeys))
-	{
-		case "N":
-			gCmdLineArgVals["search"] = "new_msg_scan";
-			break;
-		case "K":
-			gCmdLineArgVals["search"] = "keyword_search";
-			break;
-		case "F":
-			gCmdLineArgVals["search"] = "from_name_search";
-			break;
-		case "T":
-			gCmdLineArgVals["search"] = "to_name_search";
-			break;
-		case "Y":
-			gCmdLineArgVals["search"] = "to_user_search";
-			break;
-		case "U":
-			gCmdLineArgVals["search"] = "to_user_new_scan";
-			break;
-		case "S":
-			gCmdLineArgVals["search"] = "to_user_all_scan";
-			break;
-		case "A": // Abort
-		default:
-			gDoDDMR = false;
-			console.print("\x01n\x01h\x01y\x01iAborted\x01n");
-			console.crlf();
-			console.pause();
-			break;
-	}
-}
-
-if (gDoDDMR)
-{
-	// Write the user's default twitlist if it doesn't already exist
-	writeDefaultUserTwitListIfNotExist();
-
-	// When exiting this script, make sure to set the ctrl key pasthru back to what it was originally
-	js.on_exit("console.ctrlkey_passthru = " + console.ctrlkey_passthru);
-	// Set a control key pass-thru so we can capture certain control keys that we normally wouldn't be able to
-	var gOldCtrlKeyPassthru = console.ctrlkey_passthru; // Backup to be restored later
-	console.ctrlkey_passthru = "+AGKLOPQRTUVWXYZ_";
-
-	// Create an instance of the DigDistMsgReader class and use it to read/list the
-	// messages in the user's current sub-board.  Pass the parsed command-line
-	// argument values object to its constructor.
-	var readerSubCode = (gListPersonalEmailCmdLineOpt ? "mail" : bbs.cursub_code);
-	// If the -subBoard option was specified and the "read personal email" option was
-	// not specified, then use the sub-board specified by the -subBoard command-line
-	// option.
-	if (gCmdLineArgVals.hasOwnProperty("subboard") && !gListPersonalEmailCmdLineOpt)
-	{
-		// If the specified sub-board option is all digits, then treat it as the
-		// sub-board number.  Otherwise, treat it as an internal sub-board code.
-		if (/^[0-9]+$/.test(gCmdLineArgVals["subboard"]))
-			readerSubCode = getSubBoardCodeFromNum(Number(gCmdLineArgVals["subboard"]));
-		else
-			readerSubCode = gCmdLineArgVals["subboard"];
-	}
-	var msgReader = new DigDistMsgReader(readerSubCode, gCmdLineArgVals);
-	if (gCmdLineArgVals.indexedmode)
-	{
-		console.attributes = "N";
-		console.crlf();
-		console.mnemonics("Indexed read: ~Group: @GRP@, or ~@All@: ");
-		var scopeChar = console.getkeys("GA").toString();
-		if (typeof(scopeChar) === "string" && scopeChar != "")
-		{
-			var scanScope = SCAN_SCOPE_ALL;
-			if (scopeChar == "G")
-				scanScope = SCAN_SCOPE_GROUP;
-			else if (scopeChar == "G")
-				scanScope = SCAN_SCOPE_ALL;
-			msgReader.DoIndexedMode(scanScope, false);
-		}
-		else
-		{
-			console.putmsg(bbs.text(Aborted), P_SAVEATR);
-			console.pause();
-		}
-	}
-	else
-	{
-		// If the option to choose a message area first was enabled on the command-line
-		// (and neither the -subBoard nor the -personalEmail options were specified),
-		// then let the user choose a sub-board now.
-		if (gCmdLineArgVals.hasOwnProperty("chooseareafirst") && gCmdLineArgVals["chooseareafirst"] && !gCmdLineArgVals.hasOwnProperty("subboard") && !gListPersonalEmailCmdLineOpt)
-			msgReader.SelectMsgArea();
-		// Back up the user's current sub-board so that we can change back
-		// to it after searching is done, if a search is done.
-		var originalMsgGrpIdx = bbs.curgrp;
-		var originalSubBoardIdx = bbs.cursub;
-		var restoreOriginalSubCode = true;
-		// Based on the reader's start mode/search type, do the appropriate thing.
-		switch (msgReader.searchType)
-		{
-			case SEARCH_NONE:
-				restoreOriginalSubCode = false;
-				if (msgReader.subBoardCode != "mail")
-				{
-					console.attributes = "N";
-					console.crlf();
-					console.print("Loading " + subBoardGrpAndName(msgReader.subBoardCode) + "....");
-					console.line_counter = 0; // To prevent a pause before the message list comes up
-				}
-				msgReader.ReadOrListSubBoard();
-				break;
-			case SEARCH_KEYWORD:
-				var txtToSearch = (gCmdLineArgVals.hasOwnProperty("searchtext") ? gCmdLineArgVals.searchtext : null);
-				var subBoardCode = (gCmdLineArgVals.hasOwnProperty("subboard") ? gCmdLineArgVals.subboard : null);
-				msgReader.SearchMsgScan("keyword_search", txtToSearch, subBoardCode);
-				break;
-			case SEARCH_FROM_NAME:
-				msgReader.SearchMessages("from_name_search");
-				break;
-			case SEARCH_TO_NAME_CUR_MSG_AREA:
-				msgReader.SearchMessages("to_name_search");
-				break;
-			case SEARCH_TO_USER_CUR_MSG_AREA:
-				msgReader.SearchMessages("to_user_search");
-				break;
-			case SEARCH_MSG_NEWSCAN:
-				if (!gCmdLineArgVals.suppresssearchtypetext)
-				{
-					console.crlf();
-					console.putmsg(msgReader.text.newMsgScanText);
-				}
-				msgReader.MessageAreaScan(SCAN_CFG_NEW, SCAN_NEW);
-				break;
-			case SEARCH_MSG_NEWSCAN_CUR_SUB:
-				msgReader.MessageAreaScan(SCAN_CFG_NEW, SCAN_NEW, "S");
-				break;
-			case SEARCH_MSG_NEWSCAN_CUR_GRP:
-				msgReader.MessageAreaScan(SCAN_CFG_NEW, SCAN_NEW, "G");
-				break;
-			case SEARCH_MSG_NEWSCAN_ALL:
-				msgReader.MessageAreaScan(SCAN_CFG_NEW, SCAN_NEW, "A");
-				break;
-			case SEARCH_TO_USER_NEW_SCAN:
-				if (!gCmdLineArgVals.suppresssearchtypetext)
-				{
-					console.crlf();
-					console.putmsg(msgReader.text.newToYouMsgScanText);
-				}
-				msgReader.MessageAreaScan(SCAN_CFG_TOYOU/*SCAN_CFG_YONLY*/, SCAN_UNREAD);
-				break;
-			case SEARCH_TO_USER_NEW_SCAN_CUR_SUB:
-				msgReader.MessageAreaScan(SCAN_CFG_TOYOU/*SCAN_CFG_YONLY*/, SCAN_UNREAD, "S");
-				break;
-			case SEARCH_TO_USER_NEW_SCAN_CUR_GRP:
-				msgReader.MessageAreaScan(SCAN_CFG_TOYOU/*SCAN_CFG_YONLY*/, SCAN_UNREAD, "G");
-				break;
-			case SEARCH_TO_USER_NEW_SCAN_ALL:
-				msgReader.MessageAreaScan(SCAN_CFG_TOYOU/*SCAN_CFG_YONLY*/, SCAN_UNREAD, "A");
-				break;
-			case SEARCH_ALL_TO_USER_SCAN:
-				if (!gCmdLineArgVals.suppresssearchtypetext)
-				{
-					console.crlf();
-					console.putmsg(msgReader.text.allToYouMsgScanText);
-				}
-				msgReader.MessageAreaScan(SCAN_CFG_TOYOU, SCAN_TOYOU);
-				break;
-		}
-
-		// If we should restore the user's original message area, then do so.
-		if (restoreOriginalSubCode)
-		{
-			bbs.cursub = 0;
-			bbs.curgrp = originalMsgGrpIdx;
-			bbs.cursub = originalSubBoardIdx;
-		}
-	}
-
-	// Remove the temporary attachments & ANSI temp directories if they exists
-	deltree(gFileAttachDir);
-	deltree(backslash(system.node_dir + "DDMsgReaderANSIMsgTemp"));
-
-	// Before this script finishes, make sure the terminal attributes are set back
-	// to normal (in case there are any attributes left on, such as background,
-	// blink, etc.)
-	console.attributes = "N";
-}
-
-exit();
-// End of script execution.  Functions below:
-
-// Generates an internal enhanced reader header line for the 'To' user.
-//
-// Parameters:
-//  pColors: A JSON object containing the color settings read from the
-//           theme configuration file.  This function will use the
-//           'msgHdrToColor' or 'msgHdrToUserColor' property, depending
-//           on the pToReadingUser property.
-//  pToReadingUser: Boolean - Whether or not to generate the line with
-//                  the color/attribute for the reading user
-//
-// Return value: A string containing the internal enhanced reader header
-//               line specifying the 'to' user
-function genEnhHdrToUserLine(pColors, pToReadingUser)
-{
-	var toHdrLine = "\x01n\x01h\x01k" + VERTICAL_SINGLE + BLOCK1 + BLOCK2 + BLOCK3
-		          + "\x01gT\x01n\x01go  \x01h\x01c: " +
-		          (pToReadingUser ? pColors.msgHdrToUserColor : pColors.msgHdrToColor) +
-		          "@MSG_TO-L";
-	var numChars = console.screen_columns - 21;
-	for (var i = 0; i < numChars; ++i)
-		toHdrLine += "#";
-	toHdrLine += "@\x01k" + VERTICAL_SINGLE;
-	return toHdrLine;
-}
-
-///////////////////////////////////////////////////////////////////////////////////
-// DigDistMsgReader class stuff
-
-// DigDistMsgReader class constructor: Constructs a
-// DigDistMsgReader object, to be used for listing messages
-// in a message area.
-//
-// Parameters:
-//  pSubBoardCode: Optional - The Synchronet sub-board code, or "mail"
-//                 for personal email.
-//  pScriptArgs: Optional - An object containing key/value pairs representing
-//               the command-line arguments & values, as returned by parseArgs().
-function DigDistMsgReader(pSubBoardCode, pScriptArgs)
-{
-	// Set the methods for the object
-	this.setSubBoardCode = DigDistMsgReader_SetSubBoardCode;
-	this.RecalcMsgListWidthsAndFormatStrs = DigDistMsgReader_RecalcMsgListWidthsAndFormatStrs;
-	this.NumMessages = DigDistMsgReader_NumMessages;
-	this.SearchingAndResultObjsDefinedForCurSub = DigDistMsgReader_SearchingAndResultObjsDefinedForCurSub;
-	this.PopulateHdrsForCurrentSubBoard = DigDistMsgReader_PopulateHdrsForCurrentSubBoard;
-	this.FilterMsgHdrsIntoHdrsForCurrentSubBoard = DigDistMsgReader_FilterMsgHdrsIntoHdrsForCurrentSubBoard;
-	this.GetMsgIdx = DigDistMsgReader_GetMsgIdx;
-	this.RefreshSearchResultMsgHdr = DigDistMsgReader_RefreshSearchResultMsgHdr;   // Refreshes a message header in the search results
-	this.SearchMessages = DigDistMsgReader_SearchMessages; // Prompts the user for search text, then lists/reads messages, performing the search
-	this.SearchMsgScan = DigDistMsgReader_SearchMsgScan;
-	this.RefreshHdrInSubBoardHdrs = DigDistMsgReader_RefreshHdrInSubBoardHdrs;
-	this.RefreshHdrInSavedArrays = DigDistMsgReader_RefreshHdrInSavedArrays;
-	this.ReadMessages = DigDistMsgReader_ReadMessages;
-	this.DisplayEnhancedMsgReadHelpLine = DigDistMsgReader_DisplayEnhancedMsgReadHelpLine;
-	this.GoToPrevSubBoardForEnhReader = DigDistMsgReader_GoToPrevSubBoardForEnhReader;
-	this.GoToNextSubBoardForEnhReader = DigDistMsgReader_GoToNextSubBoardForEnhReader;
-	this.SetUpTraditionalMsgListVars = DigDistMsgReader_SetUpTraditionalMsgListVars;
-	this.SetUpLightbarMsgListVars = DigDistMsgReader_SetUpLightbarMsgListVars;
-	this.ListMessages = DigDistMsgReader_ListMessages;
-	this.ListMessages_Traditional = DigDistMsgReader_ListMessages_Traditional;
-	this.ListMessages_Lightbar = DigDistMsgReader_ListMessages_Lightbar;
-	this.CreateLightbarMsgListMenu = DigDistMsgReader_CreateLightbarMsgListMenu;
-	this.CreateLightbarMsgGrpMenu = DigDistMsgReader_CreateLightbarMsgGrpMenu;
-	this.CreateLightbarSubBoardMenu = DigDistMsgReader_CreateLightbarSubBoardMenu;
-	this.AdjustLightbarMsgListMenuIdxes = DigDistMsgReader_AdjustLightbarMsgListMenuIdxes;
-	this.ClearSearchData = DigDistMsgReader_ClearSearchData;
-	this.ReadOrListSubBoard = DigDistMsgReader_ReadOrListSubBoard;
-	this.PopulateHdrsIfSearch_DispErrorIfNoMsgs = DigDistMsgReader_PopulateHdrsIfSearch_DispErrorIfNoMsgs;
-	this.SearchTypePopulatesSearchResults = DigDistMsgReader_SearchTypePopulatesSearchResults;
-	this.SearchTypeRequiresSearchText = DigDistMsgReader_SearchTypeRequiresSearchText;
-	this.MessageAreaScan = DigDistMsgReader_MessageAreaScan;
-	this.PromptContinueOrReadMsg = DigDistMsgReader_PromptContinueOrReadMsg;
-	this.WriteMsgListScreenTopHeader = DigDistMsgReader_WriteMsgListScreenTopHeader;
-	this.ReadMessageEnhanced = DigDistMsgReader_ReadMessageEnhanced;
-	this.ReadMessageEnhanced_Scrollable = DigDistMsgReader_ReadMessageEnhanced_Scrollable;
-	this.ShowHdrOrKludgeLines_Scrollable = DigDistMsgReader_ShowHdrOrKludgeLines_Scrollable;
-	this.ShowVoteInfo_Scrollable = DigDistMsgReader_ShowVoteInfo_Scrollable;
-	this.ScrollableReaderNextReadableMessage = DigDistMsgReader_ScrollableReaderNextReadableMessage;
-	this.ScrollReaderDetermineClickCoordAction = DigDistMsgReader_ScrollReaderDetermineClickCoordAction;
-	this.ReadMessageEnhanced_Traditional = DigDistMsgReader_ReadMessageEnhanced_Traditional;
-	this.ShowReadModeOpMenuAndGetSelection = DigDistMsgReader_ShowReadModeOpMenuAndGetSelection;
-	this.CreateReadModeOpMenu = DigDistMsgReader_CreateReadModeOpMenu;
-	this.EnhReaderPrepLast2LinesForPrompt = DigDistMsgReader_EnhReaderPrepLast2LinesForPrompt;
-	this.LookForNextOrPriorNonDeletedMsg = DigDistMsgReader_LookForNextOrPriorNonDeletedMsg;
-	this.PrintMessageInfo = DigDistMsgReader_PrintMessageInfo;
-	this.ListScreenfulOfMessages = DigDistMsgReader_ListScreenfulOfMessages;
-	this.DisplayMsgListHelp = DigDistMsgReader_DisplayMsgListHelp;
-	this.DisplayTraditionalMsgListHelp = DigDistMsgReader_DisplayTraditionalMsgListHelp;
-	this.DisplayLightbarMsgListHelp = DigDistMsgReader_DisplayLightbarMsgListHelp;
-	this.DisplayMessageListNotesHelp = DigDistMsgReader_DisplayMessageListNotesHelp;
-	this.SetMsgListPauseTextAndLightbarHelpLine = DigDistMsgReader_SetMsgListPauseTextAndLightbarHelpLine;
-	this.SetEnhancedReaderHelpLine = DigDistMsgReader_SetEnhancedReaderHelpLine;
-	this.EditExistingMsg = DigDistMsgReader_EditExistingMsg;
-	this.EditExistingMessageOldWay = DigDistMsgReader_EditExistingMessageOldWay;
-	this.CanDelete = DigDistMsgReader_CanDelete;
-	this.CanDeleteLastMsg = DigDistMsgReader_CanDeleteLastMsg;
-	this.CanEdit = DigDistMsgReader_CanEdit;
-	this.CanQuote = DigDistMsgReader_CanQuote;
-	this.ReadConfigFile = DigDistMsgReader_ReadConfigFile;
-	this.ReadUserSettingsFile = DigDistMsgReader_ReadUserSettingsFile;
-	this.WriteUserSettingsFile = DigDistMsgReader_WriteUserSettingsFile;
-	// TODO: Is this.DisplaySyncMsgHeader even needed anymore?  Looks like it's not being called.
-	this.DisplaySyncMsgHeader = DigDistMsgReader_DisplaySyncMsgHeader;
-	this.GetMsgHdrFilenameFull = DigDistMsgReader_GetMsgHdrFilenameFull;
-	this.GetMsgHdrByIdx = DigDistMsgReader_GetMsgHdrByIdx;
-	this.GetMsgHdrByMsgNum = DigDistMsgReader_GetMsgHdrByMsgNum;
-	this.GetMsgHdrByAbsoluteNum = DigDistMsgReader_GetMsgHdrByAbsoluteNum;
-	this.AbsMsgNumToIdx = DigDistMsgReader_AbsMsgNumToIdx;
-	this.IdxToAbsMsgNum = DigDistMsgReader_IdxToAbsMsgNum;
-	this.NonDeletedMessagesExist = DigDistMsgReader_NonDeletedMessagesExist;
-	this.HighestMessageNum = DigDistMsgReader_HighestMessageNum;
-	this.IsValidMessageNum = DigDistMsgReader_IsValidMessageNum;
-	this.PromptForMsgNum = DigDistMsgReader_PromptForMsgNum;
-	this.ParseMsgAtCodes = DigDistMsgReader_ParseMsgAtCodes;
-	this.ReplaceMsgAtCodeFormatStr = DigDistMsgReader_ReplaceMsgAtCodeFormatStr;
-	this.FindNextReadableMsgIdx = DigDistMsgReader_FindNextReadableMsgIdx;
-	this.ChangeSubBoard = DigDistMsgReader_ChangeSubBoard;
-	this.EnhancedReaderChangeSubBoard = DigDistMsgReader_EnhancedReaderChangeSubBoard;
-	this.ReplyToMsg = DigDistMsgReader_ReplyToMsg;
-	this.DoPrivateReply = DigDistMsgReader_DoPrivateReply;
-	this.DisplayEnhancedReaderHelp = DigDistMsgReader_DisplayEnhancedReaderHelp;
-	this.DisplayEnhancedMsgHdr = DigDistMsgReader_DisplayEnhancedMsgHdr;
-	this.DisplayAreaChgHdr = DigDistMsgReader_DisplayAreaChgHdr;
-	this.DisplayEnhancedReaderWholeScrollbar = DigDistMsgReader_DisplayEnhancedReaderWholeScrollbar;
-	this.UpdateEnhancedReaderScrollbar = DigDistMsgReader_UpdateEnhancedReaderScrollbar;
-	this.MessageIsDeleted = DigDistMsgReader_MessageIsDeleted;
-	this.MessageIsLastFromUser = DigDistMsgReader_MessageIsLastFromUser;
-	this.DisplayEnhReaderError = DigDistMsgReader_DisplayEnhReaderError;
-	this.EnhReaderPromptYesNo = DigDistMsgReader_EnhReaderPromptYesNo;
-	this.PromptAndDeleteOrUndeleteMessage = DigDistMsgReader_PromptAndDeleteOrUndeleteMessage;
-	this.PromptAndDeleteOrUndeleteSelectedMessages = DigDistMsgReader_PromptAndDeleteOrUndeleteSelectedMessages;
-	this.GetExtdMsgHdrInfo = DigDistMsgReader_GetExtdMsgHdrInfo;
-	this.GetMsgHdrFieldListText = DigDistMsgReader_GetMsgHdrFieldListText;
-	this.GetMsgInfoForEnhancedReader = DigDistMsgReader_GetMsgInfoForEnhancedReader;
-	this.ShowMsgHex_Scrolling = DigDistMsgReader_ShowMsgHex_Scrolling;
-	this.GetMsgHexInfo = DigDistMsgReader_GetMsgHexInfo;
-	this.SaveMsgHexDumpToFile = DigDistMsgReader_SaveMsgHexDumpToFile;
-	this.GetLastReadMsgIdxAndNum = DigDistMsgReader_GetLastReadMsgIdxAndNum;
-	this.GetScanPtrMsgIdx = DigDistMsgReader_GetScanPtrMsgIdx;
-	this.RemoveFromSearchResults = DigDistMsgReader_RemoveFromSearchResults;
-	this.FindThreadNextOffset = DigDistMsgReader_FindThreadNextOffset;
-	this.FindThreadPrevOffset = DigDistMsgReader_FindThreadPrevOffset;
-	this.SaveMsgToFile = DigDistMsgReader_SaveMsgToFile;
-	this.ToggleSelectedMessage = DigDistMsgReader_ToggleSelectedMessage;
-	this.MessageIsSelected = DigDistMsgReader_MessageIsSelected;
-	this.AllSelectedMessagesCanBeDeleted = DigDistMsgReader_AllSelectedMessagesCanBeDeleted;
-	this.DeleteOrUndeleteSelectedMessages = DigDistMsgReader_DeleteOrUndeleteSelectedMessages;
-	this.NumSelectedMessages = DigDistMsgReader_NumSelectedMessages;
-	this.ForwardMessage = DigDistMsgReader_ForwardMessage;
-	this.VoteOnMessage = DigDistMsgReader_VoteOnMessage;
-	this.HasUserVotedOnMsg = DigDistMsgReader_HasUserVotedOnMsg;
-	this.GetUpvoteAndDownvoteInfo = DigDistMsgReader_GetUpvoteAndDownvoteInfo;
-	this.GetMsgBody = DigDistMsgReader_GetMsgBody;
-	this.RefreshMsgHdrInArrays = DigDistMsgReader_RefreshMsgHdrInArrays;
-	this.WriteLightbarKeyHelpErrorMsg = DigDistMsgReader_WriteLightbarKeyHelpErrorMsg;
-
-	// startMode specifies the mode for the reader to start in - List mode
-	// or reader mode, etc.  This is a setting that is read from the configuration
-	// file.  The configuration file can be either READER_MODE_READ or
-	// READER_MODE_LIST, but the optional "mode" parameter in the command-line
-	// arguments can specify another mode.
-	// Note: This isn't used for "indexed" reader mode (but maybe it should be in the future)
-	this.startMode = READER_MODE_LIST;
-
-	// hdrsForCurrentSubBoard is an array that will be populated with the
-	// message headers for the current sub-board.
-	this.hdrsForCurrentSubBoard = [];
-	// hdrsForCurrentSubBoardByMsgNum is an object that maps absolute message numbers
-	// to their index to hdrsForCurrentSubBoard
-	this.msgNumToIdxMap = {};
-
-	// msgSearchHdrs is an object containing message headers found via searching.
-	// It is indexed by internal message area code.  Each internal code index
-	// will specify an object containing the following properties:
-	//  indexed: A standard 0-based array containing message headers
-	this.msgSearchHdrs = {};
-	this.searchString = ""; // To be used for message searching
-	// this.searchType will specify the type of search:
-	//  SEARCH_NONE (-1): No search
-	//  SEARCH_KEYWORD: Keyword search in message subject & body
-	//  SEARCH_FROM_NAME: Search by 'from' name
-	//  SEARCH_TO_NAME_CUR_MSG_AREA: Search by 'to' name
-	//  SEARCH_TO_USER_CUR_MSG_AREA: Search by 'to' name, to the current user
-	//  SEARCH_MSG_NEWSCAN: New (unread) message scan (prompt the user for sub, group, or all)
-	//  SEARCH_MSG_NEWSCAN_CUR_SUB: New (unread) message scan (current sub-board)
-	//  SEARCH_MSG_NEWSCAN_CUR_GRP: New (unread) message scan (current message group)
-	//  SEARCH_MSG_NEWSCAN_ALL: New (unread) message scan (all message sub-boards)
-	//  SEARCH_TO_USER_NEW_SCAN: New (unread) messages to the current user (prompt the user for sub, group, or all)
-	//  SEARCH_TO_USER_NEW_SCAN_CUR_SUB: New (unread) messages to the current user (current sub-board)
-	//  SEARCH_TO_USER_NEW_SCAN_CUR_GRP: New (unread) messages to the current user (current group)
-	//  SEARCH_TO_USER_NEW_SCAN_ALL: New (unread) messages to the current user (all sub-board)
-	//  SEARCH_ALL_TO_USER_SCAN: All messages to the current user
-	this.searchType = SEARCH_NONE;
-	this.doingMsgScan = false; // Set to true in MessageAreaScan()
-	this.doingNewscan = false; // Whether or not we're doing a newscan
-
-	this.subBoardCode = bbs.cursub_code; // The message sub-board code
-	this.readingPersonalEmail = false;
-
-	// Whether or not we're in indexed reader mode
-	this.indexedMode = false;
-	// For indexed reader mode, whether or not to enable caching the message
-	// header lists for performance
-	this.enableIndexedModeMsgListCache = true;
-
-	// this.colors will be an array of colors to use in the message list
-	this.colors = getDefaultColors();
-	this.readingPersonalEmailFromUser = false;
-	if (typeof(pSubBoardCode) == "string")
-	{
-		var subCodeLowerCase = pSubBoardCode.toLowerCase();
-		if (subBoardCodeIsValid(subCodeLowerCase))
-		{
-			this.setSubBoardCode(subCodeLowerCase);
-			if (gCmdLineArgVals.hasOwnProperty("personalemailsent") && gCmdLineArgVals.personalemailsent)
-				this.readingPersonalEmailFromUser = true;
-		}
-	}
-
-	// This property controls whether or not the user will be prompted to
-	// continue listing messages after selecting a message to read.  Only for
-	// regular reading, not for newscans etc.
-	this.promptToContinueListingMessages = false;
-	// Whether or not to prompt the user to confirm to read a message
-	this.promptToReadMessage = false;
-	// For enhanced reader mode (reading only, not for newscan, etc.): Whether or
-	// not to ask the user whether to post on the sub-board in reader mode after
-	// reading the last message instead of prompting to go to the next sub-board.
-	// This is like the stock Synchronet behavior.
-	this.readingPostOnSubBoardInsteadOfGoToNext = false;
-
-	// String lengths for the columns to write
-	// Fixed field widths: Message number, date, and time
-	// TODO: It might be good to figure out the longest message number for a
-	// sub-board and set the message number length dynamically.  It would have
-	// to change whenever the user changes to a different sub-board, and the
-	// message list format string would have to change too.
-	//this.MSGNUM_LEN = 4;
-	this.MSGNUM_LEN = 5;
-	this.DATE_LEN = 10; // i.e., YYYY-MM-DD
-	this.TIME_LEN = 8;  // i.e., HH:MM:SS
-	// Variable field widths: From, to, and subject
-	this.FROM_LEN = ((console.screen_columns * (15/console.screen_columns)).toFixed(0)) - 1; // - 1 to make room for the space after the message indicator character
-	this.TO_LEN = ((console.screen_columns * (15/console.screen_columns)).toFixed(0)) - 1; // - 1 to account for the spaces around the status character
-	//var colsLeftForSubject = console.screen_columns-this.MSGNUM_LEN-this.DATE_LEN-this.TIME_LEN-this.FROM_LEN-this.TO_LEN-6; // 6 to account for the spaces
-	//this.SUBJ_LEN = (console.screen_columns * (colsLeftForSubject/console.screen_columns)).toFixed(0);
-	this.SUBJ_LEN = console.screen_columns-this.MSGNUM_LEN-this.DATE_LEN-this.TIME_LEN-this.FROM_LEN-this.TO_LEN-8; // 8 to account for the spaces
-	// For showing message scores in the message list
-	this.SCORE_LEN = 4;
-	// Whether or not to show message scores in the message list: Only if the terminal
-	// is at least 86 characters wide and if vote functions exist in the running build
-	// of Synchronet
-	this.showScoresInMsgList = ((console.screen_columns >= 86) && (typeof((new MsgBase("mail")).vote_msg) === "function"));
-	if (this.showScoresInMsgList)
-		this.SUBJ_LEN -= (this.SCORE_LEN + 1); // + 1 to account for a space
-
-	// Whether or not the user chose to read a message
-	this.readAMessage = false;
-	// Whether or not the user denied confirmation to read a message
-	this.deniedReadingMessage = false;
-
-	// msgListUseLightbarListInterface specifies whether or not to use the lightbar
-	// interface for the message list.  The lightbar interface will only be used if
-	// the user's terminal supports ANSI.
-	this.msgListUseLightbarListInterface = true;
-
-	// Whether or not to use the scrolling interface when reading a message
-	// (will only be used for ANSI terminals).
-	this.scrollingReaderInterface = true;
-
-	// displayBoardInfoInHeader specifies whether or not to display
-	// the message group and sub-board lines in the header at the
-	// top of the screen (an additional 2 lines).
-	this.displayBoardInfoInHeader = false;
-
-	// msgList_displayMessageDateImported specifies whether or not to use the
-	// message import date as the date displayed in the message list.  If false,
-	// the message written date will be displayed.
-	this.msgList_displayMessageDateImported = true;
-
-	// The number of spaces to use for tab characters - Used in the
-	// extended read mode
-	this.numTabSpaces = 3;
-
-	// this.text is an object containing text used for various prompts & functions.
-	this.text = {
-		scrollbarBGChar: BLOCK1,
-		scrollbarScrollBlockChar: BLOCK2,
-		goToPrevMsgAreaPromptText: "\x01n\x01c\x01hGo to the previous message area",
-		goToNextMsgAreaPromptText: "\x01n\x01c\x01hGo to the next message area",
-		newMsgScanText: "\x01c\x01hN\x01n\x01cew \x01hM\x01n\x01cessage \x01hS\x01n\x01ccan",
-		newToYouMsgScanText: "\x01c\x01hN\x01n\x01cew \x01hT\x01n\x01co \x01hY\x01n\x01cou \x01hM\x01n\x01cessage \x01hS\x01n\x01ccan",
-		allToYouMsgScanText: "\x01c\x01hA\x01n\x01cll \x01hM\x01n\x01cessages \x01hT\x01n\x01co \x01hY\x01n\x01cou \x01hS\x01n\x01ccan",
-		goToMsgNumPromptText: "\x01n\x01cGo to message # (or \x01hENTER\x01n\x01c to cancel)\x01g\x01h: \x01c",
-		msgScanAbortedText: "\x01n\x01h\x01cM\x01n\x01cessage scan \x01h\x01y\x01iaborted\x01n",
-		msgSearchAbortedText: "\x01n\x01h\x01cM\x01n\x01cessage search \x01h\x01y\x01iaborted\x01n",
-		deleteMsgNumPromptText: "\x01n\x01cNumber of the message to be deleted (or \x01hENTER\x01n\x01c to cancel)\x01g\x01h: \x01c",
-		editMsgNumPromptText: "\x01n\x01cNumber of the message to be edited (or \x01hENTER\x01n\x01c to cancel)\x01g\x01h: \x01c",
-		searchingSubBoardAbovePromptText: "\x01n\x01cSearching (current sub-board: \x01b\x01h%s\x01n\x01c)",
-		searchingSubBoardText: "\x01n\x01cSearching \x01h%s\x01n\x01c...",
-		scanningSubBoardText: "\x01n\x01cScanning \x01h%s\x01n\x01c...",
-		noMessagesInSubBoardText: "\x01n\x01h\x01bThere are no messages in the area \x01w%s\x01b.",
-		noSearchResultsInSubBoardText: "\x01n\x01h\x01bNo messages were found in \x01w%s\x01b with the given search criteria.",
-		msgScanCompleteText: "\x01n\x01h\x01cM\x01n\x01cessage scan complete\x01h\x01g.\x01n",
-		invalidMsgNumText: "\x01n\x01y\x01hInvalid message number: %d",
-		readMsgNumPromptText: "\x01n\x01g\x01h\x01i* \x01n\x01cRead message #: \x01h",
-		msgHasBeenDeletedText: "\x01n\x01h\x01g* \x01yMessage #\x01w%d \x01yhas been deleted.",
-		noHdrLinesForThisMsgText: "\x01n\x01h\x01yThere are no header lines for this message.",
-		noKludgeLinesForThisMsgText: "\x01n\x01h\x01yThere are no kludge lines for this message.",
-		searchingPersonalMailText: "\x01w\x01hSearching personal mail\x01n",
-		searchTextPromptText: "\x01cEnter the search text\x01g\x01h:\x01n\x01c ",
-		fromNamePromptText: "\x01cEnter the 'from' name to search for\x01g\x01h:\x01n\x01c ",
-		toNamePromptText: "\x01cEnter the 'to' name to search for\x01g\x01h:\x01n\x01c ",
-		abortedText: "\x01n\x01y\x01h\x01iAborted\x01n",
-		loadingPersonalMailText: "\x01n\x01cLoading %s...",
-		msgDelConfirmText: "\x01n\x01h\x01yDelete\x01n\x01c message #\x01h%d\x01n\x01c: Are you sure",
-		msgUndelConfirmText: "\x01n\x01h\x01yUndelete\x01n\x01c message #\x01h%d\x01n\x01c: Are you sure",
-		delSelectedMsgsConfirmText: "\x01n\x01h\x01yDelete selected messages: Are you sure",
-		undelSelectedMsgsConfirmText: "\x01n\x01h\x01yUndelete selected messages: Are you sure",
-		msgDeletedText: "\x01n\x01cMessage #\x01h%d\x01n\x01c has been marked for deletion.",
-		msgUndeletedText: "\x01n\x01cMessage #\x01h%d\x01n\x01c has been unmarked for deletion.",
-		selectedMsgsDeletedText: "\x01n\x01cSelected messages have been marked for deletion.",
-		selectedMsgsUndeletedText: "\x01n\x01cSelected messages have been unmarked for deletion.",
-		cannotDeleteMsgText_notYoursNotASysop: "\x01n\x01h\x01wCannot delete message #\x01y%d \x01wbecause it's not yours or you're not a sysop.",
-		cannotDeleteMsgText_notLastPostedMsg: "\x01n\x01h\x01g* \x01yCannot delete message #%d. You can only delete your last message in this area.\x01n",
-		cannotDeleteAllSelectedMsgsText: "\x01n\x01y\x01h* Cannot delete all selected messages",
-		msgEditConfirmText: "\x01n\x01cEdit message #\x01h%d\x01n\x01c: Are you sure",
-		noPersonalEmailText: "\x01n\x01cYou have no messages.",
-		postOnSubBoard: "\x01n\x01gPost on %s %s"
-	};
-
-
-	// These two variables keep track of whether we're doing a message scan that spans
-	// multiple sub-boards so that the enhanced reader function can enable use of
-	// the > key to go to the next sub-board.
-	this.doingMultiSubBoardScan = false;
-
-	// Whether or not to pause (with a message) after doing a new message scan
-	this.pauseAfterNewMsgScan = true;
-
-	// For the message area chooser header filename & maximum number of
-	// area chooser header lines to display
-	this.areaChooserHdrFilenameBase = "areaChgHeader";
-	this.areaChooserHdrMaxLines = 5;
-	
-	// Some key bindings for enhanced reader mode
-	this.enhReaderKeys = {
-		reply: "R",
-		privateReply: "I",
-		editMsg: "E",
-		showHelp: "?",
-		postMsg: "P",
-		nextMsg: KEY_RIGHT,
-		previousMsg: KEY_LEFT,
-		firstMsg: "F",
-		lastMsg: "L",
-		showKludgeLines: "K",
-		showHdrInfo: "H",
-		showMsgList: "M",
-		chgMsgArea: "C",
-		userEdit: "U",
-		quit: "Q",
-		prevMsgByTitle: "<",
-		nextMsgByTitle: ">",
-		prevMsgByAuthor: "{",
-		nextMsgByAuthor: "}",
-		prevMsgByToUser: "[",
-		nextMsgByToUser: "]",
-		prevMsgByThreadID: "(",
-		nextMsgByThreadID: ")",
-		prevSubBoard: "-",
-		nextSubBoard: "+",
-		downloadAttachments: CTRL_A,
-		saveToBBSMachine: CTRL_S,
-		deleteMessage: KEY_DEL,
-		selectMessage: " ",
-		batchDelete: CTRL_D,
-		forwardMsg: "O",
-		vote: "V",
-		showVotes: "T",
-		closePoll: "!",
-		bypassSubBoardInNewScan: "B",
-		userSettings: CTRL_U,
-		validateMsg: "A", // Only if the user is a sysop
-		quickValUser: CTRL_Q,
-		threadView: "*", // TODO: Implement this
-		operatorMenu: CTRL_O,
-		showMsgHex: "X",
-		hexDump: CTRL_X
-	};
-	//if (user.is_sysop)
-	//	this.enhReaderKeys.validateMsg = "A";
-	// Some key bindings for the message list (not necessarily all of them)
-	this.msgListKeys = {
-		deleteMessage: KEY_DEL,
-		undeleteMessage: "U",
-		batchDelete: CTRL_D,
-		editMsg: "E",
-		goToMsg: "G",
-		chgMsgArea: "C",
-		userSettings: CTRL_U,
-		quit: "Q",
-		showHelp: "?"
-	};
-	// Keys for the indexed mode menu
-	this.indexedModeMenuKeys = {
-		quit: "Q",
-		showMsgList: "M",
-		markAllRead: "R",
-		help: "?",
-		userSettings: CTRL_U,
-	};
-
-	// Message status characters for the message list
-	this.msgListStatusChars = {
-		selected: CHECK_CHAR,
-		unread: "U",
-		replied: "<",
-		attachments: "A",
-		deleted: "*"
-	};
-
-	// Whether or not to display avatars
-	this.displayAvatars = true;
-	this.rightJustifyAvatar = true;
-
-	// Message list sort option
-	this.msgListSort = MSG_LIST_SORT_DATETIME_RECEIVED;
-
-	// Whether or not to convert Y-style MCI attribute codes to Synchronet attribute codes
-	this.convertYStyleMCIAttrsToSync = false;
-
-	// Whether or not to prepend the subject for forwarded messages with "Fwd: "
-	this.prependFowardMsgSubject = true;
-
-	// An index of a quick-validation set for the sysop to apply to a user when reading their message, or
-	// an invalid index (such as -1) to show a menu of quick-validation values
-	this.quickUserValSetIndex = -1;
-
-	// For the sysop, whether to save all message headers when saving a message to the BBS
-	// PC. This could be a boolean (true/false) or the string "ask" to prompt every time
-	this.saveAllHdrsWhenSavingMsgToBBSPC = false;
-
-	this.cfgFilename = "DDMsgReader.cfg";
-	// Check the command-line arguments for a custom configuration file name
-	// before reading the configuration file.  Defaults to the current user
-	// number, but can be set by a loadable module command-line argument. Also,
-	// only allow changing it if the current user is a sysop.
-	this.personalMailUserNum = user.number;
-	var scriptArgsIsValid = (typeof(pScriptArgs) == "object");
-	if (scriptArgsIsValid)
-	{
-		if (pScriptArgs.hasOwnProperty("configfilename"))
-			this.cfgFilename = pScriptArgs.configfilename;
-		if (pScriptArgs.hasOwnProperty("usernum") && user.is_sysop)
-			this.personalMailUserNum = pScriptArgs.usernum;
-	}
-	this.userSettings = {
-		twitList: [],
-		// Whether or not to use the scrollbar in the enhanced message reader
-		useEnhReaderScrollbar: true,
-		// Whether or not to only show new messages when doing a newscan
-		newscanOnlyShowNewMsgs: true,
-		// Whether or not to use indexed mode for doing a newscan
-		useIndexedModeForNewscan: false,
-		// Whether or not the indexed mode sub-board menu should "snap" selection to sub-boards with new messages
-		// when the menu is shown
-		indexedModeMenuSnapToFirstWithNew: false,
-		// Whether to display the indexed mode newscan menu when there are no new messages
-		displayIndexedModeMenuIfNoNewMessages: true,
-		// Whether to show the indexed newscan menu after reading all new messages
-		showIndexedNewscanMenuAfterReadingAllNewMsgs: true,
-		// Whether or not to list messages in reverse order
-		listMessagesInReverse: false,
-		// Whether or not quitting from the reader goes to the message list (instead of exiting altogether)
-		quitFromReaderGoesToMsgList: false,
-		// Whether or not the enter key in the indexed newscan menu shows the message list (rather than going to reader mode)
-		enterFromIndexMenuShowsMsgList: false,
-		// When reading personal email, whether or not to propmt if the user wants to delete a message after replying to it
-		promptDelPersonalEmailAfterReply: false,
-		// Whether or not to display the 'replied' status character (for personal email)
-		displayMsgRepliedChar: true
-	};
-	// Read the settings from the config file (some settings could set user settings)
-	this.cfgFileSuccessfullyRead = false;
-	this.ReadConfigFile();
-	this.ReadUserSettingsFile(false);
-	// Set any other values specified by the command-line parameters
-	// Reader start mode - Read or list mode
-	if (scriptArgsIsValid)
-	{
-		if (pScriptArgs.hasOwnProperty("startmode"))
-		{
-			var readerStartMode = readerModeStrToVal(pScriptArgs["startmode"]);
-			if (readerStartMode != -1)
-				this.startMode = readerStartMode;
-		}
-		// Search mode
-		if (pScriptArgs.hasOwnProperty("search"))
-		{
-			var searchType = searchTypeStrToVal(pScriptArgs["search"]);
-			if (searchType != SEARCH_NONE)
-				this.searchType = searchType;
-		}
-	}
-	// Color value adjusting (must be done after reading the config file in case
-	// the color settings were changed from defaults)
-	// Message list highlight colors: For each (except for the background),
-	// prepend the normal attribute and append the background attribute to the end.
-	// This is to ensure that high attributes don't affect the rest of the line and
-	// the background attribute stays for the rest of the line.
-	this.colors.msgListMsgNumHighlightColor = "\x01n" + this.colors.msgListMsgNumHighlightColor + this.colors.msgListHighlightBkgColor;
-	this.colors.msgListFromHighlightColor = "\x01n" + this.colors.msgListFromHighlightColor + this.colors.msgListHighlightBkgColor;
-	this.colors.msgListToHighlightColor = "\x01n" + this.colors.msgListToHighlightColor + this.colors.msgListHighlightBkgColor;
-	this.colors.msgListSubjHighlightColor = "\x01n" + this.colors.msgListSubjHighlightColor + this.colors.msgListHighlightBkgColor;
-	this.colors.msgListDateHighlightColor = "\x01n" + this.colors.msgListDateHighlightColor + this.colors.msgListHighlightBkgColor;
-	this.colors.msgListTimeHighlightColor = "\x01n" + this.colors.msgListTimeHighlightColor + this.colors.msgListHighlightBkgColor;
-	// Similar for the area chooser lightbar highlight colors
-	this.colors.areaChooserMsgAreaNumHighlightColor = "\x01n" + this.colors.areaChooserMsgAreaNumHighlightColor + this.colors.areaChooserMsgAreaBkgHighlightColor;
-	this.colors.areaChooserMsgAreaDescHighlightColor = "\x01n" + this.colors.areaChooserMsgAreaDescHighlightColor + this.colors.areaChooserMsgAreaBkgHighlightColor;
-	this.colors.areaChooserMsgAreaDateHighlightColor = "\x01n" + this.colors.areaChooserMsgAreaDateHighlightColor + this.colors.areaChooserMsgAreaBkgHighlightColor;
-	this.colors.areaChooserMsgAreaTimeHighlightColor = "\x01n" + this.colors.areaChooserMsgAreaTimeHighlightColor + this.colors.areaChooserMsgAreaBkgHighlightColor;
-	this.colors.areaChooserMsgAreaNumItemsHighlightColor = "\x01n" + this.colors.areaChooserMsgAreaNumItemsHighlightColor + this.colors.areaChooserMsgAreaBkgHighlightColor;
-	// Similar for the enhanced reader help line colors
-	this.colors.enhReaderHelpLineGeneralColor = "\x01n" + this.colors.enhReaderHelpLineGeneralColor + this.colors.enhReaderHelpLineBkgColor;
-	this.colors.enhReaderHelpLineHotkeyColor = "\x01n" + this.colors.enhReaderHelpLineHotkeyColor + this.colors.enhReaderHelpLineBkgColor;
-	this.colors.enhReaderHelpLineParenColor = "\x01n" + this.colors.enhReaderHelpLineParenColor + this.colors.enhReaderHelpLineBkgColor;
-	// Similar for the lightbar message list help line colors
-	this.colors.lightbarMsgListHelpLineGeneralColor = "\x01n" + this.colors.lightbarMsgListHelpLineGeneralColor + this.colors.lightbarMsgListHelpLineBkgColor;
-	this.colors.lightbarMsgListHelpLineHotkeyColor = "\x01n" + this.colors.lightbarMsgListHelpLineHotkeyColor + this.colors.lightbarMsgListHelpLineBkgColor;
-	this.colors.lightbarMsgListHelpLineParenColor = "\x01n" + this.colors.lightbarMsgListHelpLineParenColor + this.colors.lightbarMsgListHelpLineBkgColor;
-	// Similar for the lightbar area chooser help line colors
-	this.colors.lightbarAreaChooserHelpLineGeneralColor = "\x01n" + this.colors.lightbarAreaChooserHelpLineGeneralColor + this.colors.lightbarAreaChooserHelpLineBkgColor;
-	this.colors.lightbarAreaChooserHelpLineHotkeyColor = "\x01n" + this.colors.lightbarAreaChooserHelpLineHotkeyColor + this.colors.lightbarAreaChooserHelpLineBkgColor;
-	this.colors.lightbarAreaChooserHelpLineParenColor = "\x01n" + this.colors.lightbarAreaChooserHelpLineParenColor + this.colors.lightbarAreaChooserHelpLineBkgColor;
-	// Prepend most of the text strings with the normal attribute (if they don't
-	// have it already) to make sure the correct colors are used.
-	for (var prop in this.text)
-	{
-		if ((prop != "scrollbarBGChar") && (prop != "scrollbarScrollBlockChar"))
-		{
-			if ((this.text[prop].length > 0) && (this.text[prop].charAt(0) != "\x01n"))
-				this.text[prop] = "\x01n" + this.text[prop];
-		}
-	}
-
-	// this.tabReplacementText will be the text that tabs will be replaced
-	// with in enhanced reader mode
-	//this.tabReplacementText = format("%" + this.numTabSpaces + "s", "");
-	this.tabReplacementText = format("%*s", this.numTabSpaces, "");
-
-	// Calculate the message list widths and format strings based on the current
-	// sub-board code and color settings.  Start with a message # field length
-	// of 4 characters.  This will be re-calculated later after message headers
-	// are loaded.
-	this.RecalcMsgListWidthsAndFormatStrs(4);
-
-	// If the user's terminal doesn't support ANSI, then append a newline to
-	// the end of the format string (we won't be able to move the cursor).
-	if (!canDoHighASCIIAndANSI())
-	{
-		this.sMsgInfoFormatStr += "\r\n";
-		this.sMsgInfoToUserFormatStr += "\r\n";
-		this.sMsgInfoFromUserFormatStr += "\r\n";
-		this.sMsgInfoFormatHighlightStr += "\r\n";
-	}
-	// Enhanced reader help line (will be set up in
-	// DigDistMsgReader_SetEnhancedReaderHelpLine())
-	this.enhReadHelpLine = "";
-
-	// Read the enhanced message header file and populate this.enhMsgHeaderLines,
-	// the header text for enhanced reader mode.  The enhanced reader header file
-	// name will start with 'enhMsgHeader', and there can be multiple versions for
-	// different terminal widths (i.e., msgHeader_80.ans for an 80-column console
-	// and msgHeader_132 for a 132-column console).
-	this.enhMsgHeaderLines = loadTextFileIntoArray("enhMsgHeader", 10);
-	// this.enhMsgHeaderLinesToReadingUser will be a copy of this.endMsgReaderLines
-	// but with the 'To' user line changed to highlight the name for messages to
-	// the logged-on reading user
-	this.enhMsgHeaderLinesToReadingUser = [];
-	// If the header file didn't exist, then populate the enhanced reader header
-	// array with default lines.
-	this.usingInternalEnhMsgHdr = (this.enhMsgHeaderLines.length == 0);
-	if (this.usingInternalEnhMsgHdr)
-	{
-		// Group name: 20% of console width
-		// Sub-board name: 34% of console width
-		var msgGrpNameLen = Math.floor(console.screen_columns * 0.2);
-		var subBoardNameLen = Math.floor(console.screen_columns * 0.34);
-		var hdrLine1 = "\x01n\x01h\x01c" + UPPER_LEFT_SINGLE + HORIZONTAL_SINGLE + "\x01n\x01c"
-		             + HORIZONTAL_SINGLE + " \x01h@GRP-L";
-		var numChars = msgGrpNameLen - 7;
-		for (var i = 0; i < numChars; ++i)
-			hdrLine1 += "#";
-		hdrLine1 += "@ @SUB-L";
-		numChars = subBoardNameLen - 7;
-		for (var i = 0; i < numChars; ++i)
-			hdrLine1 += "#";
-		hdrLine1 += "@\x01k";
-		numChars = console.screen_columns - console.strlen(hdrLine1) - 4;
-		for (var i = 0; i < numChars; ++i)
-			hdrLine1 += HORIZONTAL_SINGLE;
-		hdrLine1 += "\x01n\x01c" + HORIZONTAL_SINGLE + HORIZONTAL_SINGLE + "\x01h"
-		         + HORIZONTAL_SINGLE + UPPER_RIGHT_SINGLE;
-		this.enhMsgHeaderLines.push(hdrLine1);
-		this.enhMsgHeaderLinesToReadingUser.push(hdrLine1);
-		var hdrLine2 = "\x01n\x01c" + VERTICAL_SINGLE + "\x01h\x01k" + BLOCK1 + BLOCK2
-		             + BLOCK3 + "\x01gM\x01n\x01gsg#\x01h\x01c: " + this.colors.msgHdrMsgNumColor + "@MSG_NUM_AND_TOTAL-L";
-		numChars = console.screen_columns - 32;
-		for (var i = 0; i < numChars; ++i)
-			hdrLine2 += "#";
-		hdrLine2 += "@\x01n\x01c" + VERTICAL_SINGLE;
-		this.enhMsgHeaderLines.push(hdrLine2);
-		this.enhMsgHeaderLinesToReadingUser.push(hdrLine2);
-		var hdrLine3 = "\x01n\x01h\x01k" + VERTICAL_SINGLE + BLOCK1 + BLOCK2 + BLOCK3
-					 + "\x01gF\x01n\x01grom\x01h\x01c: " + this.colors.msgHdrFromColor + "@MSG_FROM_AND_FROM_NET-L";
-		numChars = console.screen_columns - 36;
-		for (var i = 0; i < numChars; ++i)
-			hdrLine3 += "#";
-		hdrLine3 += "@\x01k" + VERTICAL_SINGLE;
-		this.enhMsgHeaderLines.push(hdrLine3);
-		this.enhMsgHeaderLinesToReadingUser.push(hdrLine3);
-		this.enhMsgHeaderLines.push(genEnhHdrToUserLine(this.colors, false));
-		this.enhMsgHeaderLinesToReadingUser.push(genEnhHdrToUserLine(this.colors, true));
-		var hdrLine5 = "\x01n\x01h\x01k" + VERTICAL_SINGLE + BLOCK1 + BLOCK2 + BLOCK3
-		             + "\x01gS\x01n\x01gubj\x01h\x01c: " + this.colors.msgHdrSubjColor + "@MSG_SUBJECT-L";
-		numChars = console.screen_columns - 26;
-		for (var i = 0; i < numChars; ++i)
-			hdrLine5 += "#";
-		hdrLine5 += "@\x01k" + VERTICAL_SINGLE;
-		this.enhMsgHeaderLines.push(hdrLine5);
-		this.enhMsgHeaderLinesToReadingUser.push(hdrLine5);
-		var hdrLine6 = "\x01n\x01c" + VERTICAL_SINGLE + "\x01h\x01k" + BLOCK1 + BLOCK2 + BLOCK3
-		             + "\x01gD\x01n\x01gate\x01h\x01c: " + this.colors.msgHdrDateColor + "@MSG_DATE-L";
-		//numChars = console.screen_columns - 67;
-		//Wed, 08 Mar 2023 19:06:37
-		numChars = 26 - 11;
-		for (var i = 0; i < numChars; ++i)
-			hdrLine6 += "#";
-		//hdrLine6 += "@\x01n\x01c" + VERTICAL_SINGLE;
-		hdrLine6 += "@ @MSG_TIMEZONE@\x01n";
-		numChars = console.screen_columns - 42;
-		hdrLine6 += format("%*s", numChars, ""); // More correct than format("%" + numChars + "s", "");
-		hdrLine6 += "\x01n\x01c" + VERTICAL_SINGLE;
-		this.enhMsgHeaderLines.push(hdrLine6);
-		this.enhMsgHeaderLinesToReadingUser.push(hdrLine6);
-		var hdrLine7 = "\x01n\x01h\x01c" + BOTTOM_T_SINGLE + HORIZONTAL_SINGLE + "\x01n\x01c"
-		             + HORIZONTAL_SINGLE + HORIZONTAL_SINGLE + "\x01h\x01k";
-		numChars = console.screen_columns - 8;
-		for (var i = 0; i < numChars; ++i)
-			hdrLine7 += HORIZONTAL_SINGLE;
-		hdrLine7 += "\x01n\x01c" + HORIZONTAL_SINGLE + HORIZONTAL_SINGLE + "\x01h"
-		         + HORIZONTAL_SINGLE + BOTTOM_T_SINGLE;
-		this.enhMsgHeaderLines.push(hdrLine7);
-		this.enhMsgHeaderLinesToReadingUser.push(hdrLine7);
-	}
-	else
-	{
-		// We loaded the enhanced message header lines from a custom file.
-		// Copy from this.enhMsgHeaderLines to this.enhMsgHeaderLinesToReadingUser
-		// but change any 'To:' line to highlight the 'to' username.
-		this.enhMsgHeaderLinesToReadingUser = this.enhMsgHeaderLines.slice();
-		// Go through the header lines and ensure the 'To' line has a different
-		// color
-		for (var lineIdx = 0; lineIdx < this.enhMsgHeaderLinesToReadingUser.length; ++lineIdx)
-			this.enhMsgHeaderLinesToReadingUser[lineIdx] = syncAttrCodesToANSI(strWithToUserColor(this.enhMsgHeaderLinesToReadingUser[lineIdx], this.colors.msgHdrToUserColor));
-	}
-	// Save the enhanced reader header width.  This will be the length of the longest
-	// line in the header.
-	this.enhMsgHeaderWidth = 0;
-	if (this.enhMsgHeaderLines.length > 0)
-	{
-		var lineLen = 0;
-		for (var i = 0; i < this.enhMsgHeaderLines.length; ++i)
-		{
-			lineLen = console.strlen(this.enhMsgHeaderLines[i]);
-			if (lineLen > this.enhMsgHeaderWidth)
-				this.enhMsgHeaderWidth = lineLen;
-		}
-	}
-
-	// Message display area information
-	this.msgAreaTop = this.enhMsgHeaderLines.length + 1;
-	this.msgAreaBottom = console.screen_rows-1;  // The last line of the message area
-	// msgAreaLeft and msgAreaRight are the rightmost and leftmost columns of the
-	// message area, respectively.  These are 1-based.  1 is subtracted from
-	// msgAreaRight to leave room for the scrollbar in enhanced reader mode.
-	this.msgAreaLeft = 1;
-	this.msgAreaRight = console.screen_columns - 1;
-	this.msgAreaWidth = this.msgAreaRight - this.msgAreaLeft + 1;
-	this.msgAreaHeight = this.msgAreaBottom - this.msgAreaTop + 1;
-
-	//////////////////////////////////////////////
-	// Things related to changing to a different message group & sub-board
-
-	// In the message area lists (for changing to another message area), the
-	// date & time of the last-imported message will be shown.
-	// msgAreaList_lastImportedMsg_showImportTime is a boolean to specify
-	// whether or not to use the import time for the last-imported message.
-	// If false, the message written time will be used.
-	this.msgAreaList_lastImportedMsg_showImportTime = true;
-
-	// These variables store the lengths of the various columns displayed in
-	// the message group/sub-board lists.
-	// Sub-board info field lengths
-	this.areaNumLen = 4;
-	this.numItemsLen = 4;
-	this.dateLen = 10; // i.e., YYYY-MM-DD
-	this.timeLen = 8;  // i.e., HH:MM:SS
-	// Sub-board name length - This should be 47 for an 80-column display.
-	this.subBoardNameLen = console.screen_columns - this.areaNumLen - this.numItemsLen - this.dateLen - this.timeLen - 7;
-	// Message group description length (67 chars on an 80-column screen)
-	this.msgGrpDescLen = console.screen_columns - this.areaNumLen - this.numItemsLen - 5;
-
-	// Some methods for choosing the message area
-	this.WriteChgMsgAreaKeysHelpLine = DigDistMsgReader_WriteLightbarChgMsgAreaKeysHelpLine;
-	this.WriteGrpListHdrLine1 = DigDistMsgReader_WriteGrpListTopHdrLine1;
-	this.WriteSubBrdListHdrLine = DigDistMsgReader_WriteSubBrdListHdrLine;
-	this.SelectMsgArea = DigDistMsgReader_SelectMsgArea;
-	this.SelectMsgArea_Lightbar = DigDistMsgReader_SelectMsgArea_Lightbar;
-	this.SelectMsgArea_Traditional = DigDistMsgReader_SelectMsgArea_Traditional;
-	this.ListMsgGrps = DigDistMsgReader_ListMsgGrps_Traditional;
-	this.ListSubBoardsInMsgGroup = DigDistMsgReader_ListSubBoardsInMsgGroup_Traditional;
-	// Lightbar-specific methods
-	this.WriteMsgGroupLine = DigDistMsgReader_writeMsgGroupLine;
-	this.UpdateMsgAreaPageNumInHeader = DigDistMsgReader_updateMsgAreaPageNumInHeader;
-	this.GetMsgSubBoardLine = DigDistMsgReader_GetMsgSubBrdLine;
-	// Choose Message Area help screen
-	this.ShowChooseMsgAreaHelpScreen = DigDistMsgReader_showChooseMsgAreaHelpScreen;
-	// Method to build the sub-board printf information for a message
-	// group
-	this.BuildSubBoardPrintfInfoForGrp = DigDistMsgReader_BuildSubBoardPrintfInfoForGrp;
-	// Methods for calculating a page number for a message list item
-	this.CalcTraditionalMsgListTopIdx = DigDistMsgReader_CalcTraditionalMsgListTopIdx;
-	this.CalcLightbarMsgListTopIdx = DigDistMsgReader_CalcLightbarMsgListTopIdx;
-	this.CalcMsgListScreenIdxVarsFromMsgNum = DigDistMsgReader_CalcMsgListScreenIdxVarsFromMsgNum;
-	// A method for validating a user's choice of message area
-	this.ValidateMsgAreaChoice = DigDistMsgReader_ValidateMsgAreaChoice;
-	this.ValidateMsg = DigDistMsgReader_ValidateMsg;
-	this.GetGroupNameAndDesc = DigDistMsgReader_GetGroupNameAndDesc;
-	this.DoUserSettings_Scrollable = DigDistMsgReader_DoUserSettings_Scrollable;
-	this.DoUserSettings_Traditional = DigDistMsgReader_DoUserSettings_Traditional;
-	this.RefreshMsgAreaRectangle = DigDistMsgReader_RefreshMsgAreaRectangle;
-	this.MsgHdrFromOrToInUserTwitlist = DigDistMsgReader_MsgHdrFromOrToInUserTwitlist;
-	// For indexed mode
-	this.DoIndexedMode = DigDistMsgReader_DoIndexedMode;
-	this.IndexedModeChooseSubBoard = DigDistMsgReader_IndexedModeChooseSubBoard;
-	this.CreateLightbarIndexedModeMenu = DigDistMsgReader_CreateLightbarIndexedModeMenu;
-	this.GetIndexedModeSubBoardMenuItemTextAndInfo = DigDistMsgReader_GetIndexedModeSubBoardMenuItemTextAndInfo;
-	this.MakeIndexedModeHelpLine = DigDistMsgReader_MakeIndexedModeHelpLine;
-	this.ShowIndexedListHelp = DigDistMsgReader_ShowIndexedListHelp;
-
-	// printf strings for message group/sub-board lists
-	// Message group information (printf strings)
-	this.msgGrpListPrintfStr = "\x01n " + this.colors.areaChooserMsgAreaNumColor + "%" + this.areaNumLen
-	                         + "d " + this.colors.areaChooserMsgAreaDescColor + "%-"
-	                         + this.msgGrpDescLen + "s " + this.colors.areaChooserMsgAreaNumItemsColor
-	                         + "%" + this.numItemsLen + "d";
-	this.msgGrpListHilightPrintfStr = "\x01n" + this.colors.areaChooserMsgAreaBkgHighlightColor + " "
-	                                + "\x01n" + this.colors.areaChooserMsgAreaBkgHighlightColor
-	                                + this.colors.areaChooserMsgAreaNumHighlightColor + "%" + this.areaNumLen
-	                                + "d \x01n" + this.colors.areaChooserMsgAreaBkgHighlightColor
-	                                + this.colors.areaChooserMsgAreaDescHighlightColor + "%-"
-	                                + this.msgGrpDescLen + "s \x01n" + this.colors.areaChooserMsgAreaBkgHighlightColor
-	                                + this.colors.areaChooserMsgAreaNumItemsHighlightColor + "%" + this.numItemsLen
-	                                + "d";
-	// Message group list header (printf string)
-	this.msgGrpListHdrPrintfStr = this.colors.areaChooserMsgAreaHeaderColor + "%6s %-"
-	                            + +(this.msgGrpDescLen-8) + "s %-12s";
-	// Sub-board information header (printf string)
-	this.subBoardListHdrPrintfStr = this.colors.areaChooserMsgAreaHeaderColor + " %5s %-"
-	                              + +(this.subBoardNameLen-3) + "s %-7s %-19s";
-	// Lightbar area chooser help line text
-	// For PageUp, normally I'd think KEY_PAGEUP should work, but that triggers sending a telegram instead.  \x1b[V seems to work though.
-	this.lightbarAreaChooserHelpLine = "\x01n"
-	                          + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "@CLEAR_HOT@@`" + UP_ARROW + "`" + KEY_UP + "@"
-	                          + this.colors.lightbarAreaChooserHelpLineGeneralColor + ", "
-	                          + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "@`" + DOWN_ARROW + "`" + KEY_DOWN + "@"
-	                          + this.colors.lightbarAreaChooserHelpLineGeneralColor + ", "
-	                          + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "@`HOME`" + KEY_HOME + "@"
-	                          + this.colors.lightbarAreaChooserHelpLineGeneralColor + ", "
-	                          + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "@`END`" + KEY_END + "@"
-	                          + this.colors.lightbarAreaChooserHelpLineGeneralColor + ", "
-	                          + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "#"
-	                          + this.colors.lightbarAreaChooserHelpLineGeneralColor + ", "
-	                          + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "@`PgUp`" + "\x1b[V" + "@"
-	                          + this.colors.lightbarAreaChooserHelpLineGeneralColor + "/"
-	                          + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "@`Dn`" + KEY_PAGEDN + "@"
-	                          + this.colors.lightbarAreaChooserHelpLineGeneralColor + ", "
-	                          + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "@`F`F@"
-	                          + this.colors.lightbarAreaChooserHelpLineParenColor + ")"
-	                          + this.colors.lightbarAreaChooserHelpLineGeneralColor + "irst pg, "
-	                          + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "@`L`L@"
-	                          + this.colors.lightbarAreaChooserHelpLineParenColor + ")"
-	                          + this.colors.lightbarAreaChooserHelpLineGeneralColor + "ast pg, "
-	                          + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "@`CTRL-F`" + CTRL_F + "@"
-	                          + this.colors.lightbarAreaChooserHelpLineGeneralColor + ", "
-	                          + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "@`/`/@"
-	                          + this.colors.lightbarAreaChooserHelpLineGeneralColor + ", "
-	                          + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "@`N`N@"
-	                          + this.colors.lightbarAreaChooserHelpLineGeneralColor + ", "
-	                          + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "@`Q`Q@"
-	                          + this.colors.lightbarAreaChooserHelpLineParenColor + ")"
-	                          + this.colors.lightbarAreaChooserHelpLineGeneralColor + "uit, "
-	                          + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "@`?`?@";
-	var lbAreaChooserHelpLineLen = 72;
-	// Pad the lightbar key help text on either side to center it on the screen
-	// (but leave off the last character to avoid screen drawing issues)
-	var padLen = console.screen_columns - lbAreaChooserHelpLineLen - 1;
-	var leftPadLen = Math.floor(padLen/2);
-	var rightPadLen = padLen - leftPadLen;
-	this.lightbarAreaChooserHelpLine = this.colors.lightbarAreaChooserHelpLineGeneralColor
-	                                 + format("%*s", leftPadLen, "")
-	                                 + this.lightbarAreaChooserHelpLine
-	                                 + this.colors.lightbarAreaChooserHelpLineGeneralColor
-	                                 + format("%" + rightPadLen + "s", "") + "\x01n";
-
-	// this.subBoardListPrintfInfo will be an array of printf strings
-	// for the sub-boards in the message groups.  The index is the
-	// message group index.  The sub-board printf information is created
-	// on the fly the first time the user lists sub-boards for a message
-	// group.
-	this.subBoardListPrintfInfo = [];
-
-	// Variables to save the top message index for the traditional & lightbar
-	// message lists.  Initialize them to -1 to mean the message list hasn't been
-	// displayed yet - In that case, the lister will use the user's last
-	// read pointer.
-	this.tradListTopMsgIdx = -1;
-	this.tradMsgListNumLines = console.screen_rows-3;
-	if (this.displayBoardInfoInHeader)
-		this.tradMsgListNumLines -= 2;
-	this.lightbarListTopMsgIdx = -1;
-	this.lightbarMsgListNumLines = console.screen_rows-2;
-	this.lightbarMsgListStartScreenRow = 2; // The first line number on the screen for the message list
-	// If we will be displaying the message group and sub-board in the
-	// header at the top of the screen (an additional 2 lines), then
-	// update this.lightbarMsgListNumLines and this.lightbarMsgListStartScreenRow to
-	// account for this.
-	if (this.displayBoardInfoInHeader)
-	{
-		this.lightbarMsgListNumLines -= 2;
-		this.lightbarMsgListStartScreenRow += 2;
-	}
-	// The selected message index for the lightbar message list (initially -1, will
-	// be set in the lightbar list method)
-	this.lightbarListSelectedMsgIdx = -1;
-	// The selected message cursor position for the lightbar message list (initially
-	// null, will be set in the lightbar list message)
-	this.lightbarListCurPos = null;
-
-	// selectedMessages will be an object (indexed by sub-board internal code)
-	// containing objects that contain message indexes (as properties) for the
-	// sub-boards.  Messages can be selected by the user for doing things such
-	// as a batch delete, etc.
-	this.selectedMessages = {};
-
-	// areaChangeHdrLines is an array of text lines to use as a header to display
-	// above the message area changer lists.
-	this.areaChangeHdrLines = loadTextFileIntoArray(this.areaChooserHdrFilenameBase, this.areaChooserHdrMaxLines);
-
-	// pausePromptText is the text that will be used for some of the pause
-	// prompts.  It's loaded from text.dat, but in case that text contains
-	// "@EXEC:" (to execute a script), this script will default to a "press
-	// a key" message.
-	this.pausePromptText = bbs.text(Pause);
-	if (this.pausePromptText.toUpperCase().indexOf("@EXEC:") > -1)
-		this.pausePromptText = "\x01n\x01c[ Press a key ] ";
-
-	// Menu option values to use for the operator menu for reader mode
-	this.readerOpMenuOptValues = {
-		validateMsg: 0,
-		editAuthorUserAccount: 1,
-		showHdrLines: 2,
-		showKludgeLines: 3,
-		showTallyStats: 4,
-		showMsgHex: 5,
-		saveMsgHexToFile: 6,
-		quickValUser: 7,
-		addAuthorToTwitList: 8,
-		addAuthorEmailToEmailFilter: 9
-	};
-
-	// For indexed mode, whether to set the indexed mode menu item index to 1 more when showing
-	// the indexed mode menu again (i.e., when the user wants to go to the next sub-board)
-	this.indexedModeSetIdxMnuIdxOneMore = false;
-}
-
-// For the DigDistMsgReader class: Sets the subBoardCode property and also
-// sets the readingPersonalEmail property, a boolean for whether or not
-// personal email is being read (whether the sub-board code is "mail")
-//
-// Parameters:
-//  pSubCode: The sub-board code to set in the object
-function DigDistMsgReader_SetSubBoardCode(pSubCode)
-{
-	this.subBoardCode = pSubCode;
-	this.readingPersonalEmail = (this.subBoardCode.toLowerCase() == "mail");
-}
-
-// For the DigDistMsgReader class: Populates the hdrsForCurrentSubBoard
-// array with message headers from the current sub-board.  Filters out
-// messages that are deleted, unvalidated, private, and voting messages.
-//
-// Parameters:
-//  pStartIdx: Optional - The index of the first message to retrieve. Only used if pEndIdx is also valid.
-//             This can also be the value POPULATE_MSG_HDRS_FROM_SCAN_PTR, to signify that the headers
-//             should be populated starting from the scan pointer for the current sub-board.
-//  pEndIdx: Optional - One past the index of the last message to retrieve. Only used if pStartIdx is also valid.
-function DigDistMsgReader_PopulateHdrsForCurrentSubBoard(pStartIdx, pEndIdx)
-{
-	if (this.subBoardCode == "mail")
-	{
-		this.hdrsForCurrentSubBoard = [];
-		this.msgNumToIdxMap = {};
-		return;
-	}
-
-	var tmpHdrs = null;
-	var msgbase = new MsgBase(this.subBoardCode);
-	if (msgbase.open())
-	{
-		// First get all headers in a temporary array, then filter them into
-		// this.hdrsForCurrentSubBoard.
-		var startIdxIsNumber = (typeof(pStartIdx) === "number");
-		// If doing a newscan but we want to get all headers anyway, then do it.
-		if (this.doingNewscan && startIdxIsNumber && pStartIdx == POPULATE_NEWSCAN_FORCE_GET_ALL_HDRS)
-			tmpHdrs = msgbase.get_all_msg_headers(true, false); // Include votes, don't expand fields
-		// If doing a newscan and the setting to only show new messages for a newscan is enabled, then
-		// only get messages from the user's scan pointer
-		else if (this.doingNewscan && (this.userSettings.newscanOnlyShowNewMsgs || (startIdxIsNumber && pStartIdx == POPULATE_MSG_HDRS_FROM_SCAN_PTR)))
-		{
-			
-			{
-				// Populate the list of headers starting at the scan pointer.
-				var startMsgIdx = absMsgNumToIdxWithMsgbaseObj(msgbase, msg_area.sub[this.subBoardCode].scan_ptr);
-				var endMsgIdx = msgbase.total_msgs;
-				tmpHdrs = {};
-				for (var i = startMsgIdx+1; i < endMsgIdx; ++i)
-				{
-					//var msgHdr = msgbase.get_msg_header(true, i, false, false); // Don't expand fields, don't include votes
-					// TODO: I think we should be able to call get_msg_header() and get valid vote information,
-					// but that doesn't seem to be the case:
-					var msgHdr = msgbase.get_msg_header(true, i, false, true); // Don't expand fields, include votes
-					if (msgHdr != null)
-						tmpHdrs[msgHdr.number] = msgHdr;
-				}
-			}
-		}
-		// If pStartIdx & pEndIdx are valid, then use those
-		else if (startIdxIsNumber && pStartIdx >= 0 && typeof(pEndIdx) === "number" && pEndIdx <= msgbase.total_msgs)
-		{
-			tmpHdrs = {};
-			for (var i = pStartIdx; i < pEndIdx; ++i)
-			{
-				// Get message header by index
-				var msgHdr = msgbase.get_msg_header(true, i, false, true); // Don't expand fields, include vutes
-				//var msgHdr = msgbase.get_msg_header(true, i, false, false); // Don't expand fields, don't include votes
-				if (msgHdr != null)
-					tmpHdrs[msgHdr.number] = msgHdr;
-			}
-		}
-		else
-		{
-			// Get all message headers
-			// Note: get_all_msg_headers() was added in Synchronet 3.16.  DDMsgReader requires a minimum
-			// of 3.18, so we're okay to use it.
-			tmpHdrs = msgbase.get_all_msg_headers(true, false); // Include votes, don't expand fields
-		}
-		msgbase.close();
-	}
-
-	// Filter the headers into this.hdrsForCurrentSubBoard
-	this.FilterMsgHdrsIntoHdrsForCurrentSubBoard(tmpHdrs, true);
-}
-
-// For the DigDistMsgReader class: Takes an array of message headers in the current
-// sub-board and filters them into this.hdrsForCurrentSubBoard and
-// this.msgNumToIdxMap based on which messages are readable to the
-// user.
-//
-// Parameters:
-//  pMsgHdrs: An array/object of message header objects
-//  pClearFirst: Boolean - Whether or not to empty this.hdrsForCurrentSubBoard
-//               and this.msgNumToIdxMap first.
-function DigDistMsgReader_FilterMsgHdrsIntoHdrsForCurrentSubBoard(pMsgHdrs, pClearFirst)
-{
-	if (pClearFirst)
-	{
-		this.hdrsForCurrentSubBoard = [];
-		this.msgNumToIdxMap = {};
-	}
-	if (pMsgHdrs == null)
-		return;
-
-	var idx = (this.hdrsForCurrentSubBoard.length > 0 ? this.hdrsForCurrentSubBoard.length - 1 : 0);
-	for (var prop in pMsgHdrs)
-	{
-		// Only add the message header if the message is readable to the user
-		// and the from & to name isn't in the user's personal twitlist.
-		if (isReadableMsgHdr(pMsgHdrs[prop], this.subBoardCode) && !this.MsgHdrFromOrToInUserTwitlist(pMsgHdrs[prop]))
-		{
-			this.hdrsForCurrentSubBoard.push(pMsgHdrs[prop]);
-			this.msgNumToIdxMap[pMsgHdrs[prop].number] = idx++;
-		}
-	}
-
-	// If the sort type is date/time written, then sort the message header
-	// array as such
-	if (this.msgListSort == MSG_LIST_SORT_DATETIME_WRITTEN)
-	{
-		this.hdrsForCurrentSubBoard.sort(sortMessageHdrsByDateTime);
-		// Re-populate this.msgNumToIdxMap to match the order of this.hdrsForCurrentSubBoard
-		this.msgNumToIdxMap = {};
-		for (var idx = 0; idx < this.hdrsForCurrentSubBoard.length; ++idx)
-			this.msgNumToIdxMap[this.hdrsForCurrentSubBoard[idx].number] = idx;
-	}
-}
-
-// For the DigDistMsgReader class: Gets the message offset (index) for a message, given
-// a message header.  Returns -1 on failure.  The returned index is for the object's
-// message header array(s), if populated, in the priority of search headers, then
-// hdrsForCurrentSubBoard.  If neither of those are populated, the offset of the header
-// in the messagebase will be returned.
-//
-// Parameters:
-//  pHdrOrMsgNum: Can either be a message header object or a message number.
-//
-// Return value: The message index (or offset in the messagebase)
-function DigDistMsgReader_GetMsgIdx(pHdrOrMsgNum)
-{
-	var msgNum = -1;
-	if (typeof(pHdrOrMsgNum) == "object")
-		msgNum = pHdrOrMsgNum.number;
-	else if (typeof(pHdrOrMsgNum) == "number")
-		msgNum = pHdrOrMsgNum;
-	else
-		return -1;
-
-	if (typeof(msgNum) != "number")
-		return -1;
-
-	var msgIdx = 0;
-	if (this.msgSearchHdrs.hasOwnProperty(this.subBoardCode) &&
-	    (this.msgSearchHdrs[this.subBoardCode].indexed.length > 0))
-	{
-		for (var i = 0; i < this.msgSearchHdrs[this.subBoardCode].indexed.length; ++i)
-		{
-			if (this.msgSearchHdrs[this.subBoardCode].indexed[i].number == msgNum)
-			{
-				msgIdx = i;
-				break;
-			}
-		}
-	}
-	else if (this.hdrsForCurrentSubBoard.length > 0)
-	{
-		if (this.msgNumToIdxMap.hasOwnProperty(msgNum))
-			msgIdx = this.msgNumToIdxMap[msgNum];
-		else
-		{
-			msgIdx = absMsgNumToIdx(this.subBoardCode, msgNum);
-			if (msgIdx != -1)
-				this.msgNumToIdxMap[msgNum] = msgIdx;
-		}
-	}
-	else
-		msgIdx = absMsgNumToIdx(this.subBoardCode, msgNum);
-	return msgIdx;
-}
-
-// For the DigDistMsgReader class: Refreshes a message header in the message header
-// arrays in this.msgSearchHdrs.
-//
-// Parameters:
-//  pMsgIndex: The index (0-based) of the message header
-//  pAttrib: Optional - An attribute to apply.  If this is is not specified,
-//           then the message header will be retrieved from the message base.
-//  pApply: Optional boolean - Whether or not to apply the attribute or remove it. Defaults to true.
-//  pSubBoardCode: Optional - An internal sub-board code.  If not specified, then
-//                 this method will default to this.subBoardCode.
-//  pWriteHdrToMsgbase: Optional boolean - Whether or not to also save the message to the messagebase.  Defaults to true.
-function DigDistMsgReader_RefreshSearchResultMsgHdr(pMsgIndex, pAttrib, pApply, pSubBoardCode, pWriteHdrToMsgbase)
-{
-	if (typeof(pMsgIndex) != "number")
-		return;
-
-	var applyAttr = (typeof(pApply) === "boolean" ? pApply : true);
-	var subCode = (typeof(pSubBoardCode) === "string" ? pSubBoardCode : this.subBoardCode);
-	var writeHdrToMsgbase = (typeof(pWriteHdrToMsgbase) === "boolean" ? pWriteHdrToMsgbase : true);
-	if (this.msgSearchHdrs.hasOwnProperty(subCode))
-	{
-		var msgNum = pMsgIndex + 1;
-		if (typeof(pAttrib) != "undefined")
-		{
-			if (this.msgSearchHdrs[this.subBoardCode].indexed.hasOwnProperty(pMsgIndex))
-			{
-				if (applyAttr)
-					this.msgSearchHdrs[this.subBoardCode].indexed[pMsgIndex].attr = this.msgSearchHdrs[this.subBoardCode].indexed[pMsgIndex].attr | pAttrib;
-				else
-					this.msgSearchHdrs[this.subBoardCode].indexed[pMsgIndex].attr = this.msgSearchHdrs[this.subBoardCode].indexed[pMsgIndex].attr ^ pAttrib;
-				if (writeHdrToMsgbase)
-				{
-					// Using applyAttrsInMsgHdrInMessagbase(), which loads the header without
-					// expanded fields and saves the attributes with that header.
-					var msgNumFromHdr = this.msgSearchHdrs[this.subBoardCode].indexed[pMsgIndex].number;
-					applyAttrsInMsgHdrInMessagbase(pSubBoardCode, msgNumFromHdr, this.msgSearchHdrs[this.subBoardCode].indexed[pMsgIndex].attr);
-				}
-			}
-		}
-		else
-		{
-			var msgHeader = this.GetMsgHdrByIdx(pMsgIndex);
-			if (msgHeader != null && this.msgSearchHdrs[this.subBoardCode].indexed.hasOwnProperty(pMsgIndex))
-			{
-				this.msgSearchHdrs[this.subBoardCode].indexed[pMsgIndex] = msgHeader;
-				if (writeHdrToMsgbase)
-				{
-					var msgbase = new MsgBase(subCode);
-					if (msgbase.open())
-					{
-						msgbase.put_msg_header(true, msgHeader.offset, msgHeader);
-						msgbase.close();
-					}
-				}
-			}
-		}
-	}
-}
-
-// For the DigDistMsgReader class: Refreshes a message header in the message header
-// array for the current sub-board.
-//
-// Parameters:
-//  pMsgIndex: The index (0-based) of the message header
-//  pAttrib: Optional - An attribute to apply.  If this is is not specified,
-//           then the message header will be retrieved from the message base.
-//  pApply: Optional boolean - Whether or not to apply the attribute or remove it. Defaults to true.
-function DigDistMsgReader_RefreshHdrInSubBoardHdrs(pMsgIndex, pAttrib, pApply)
-{
-	if (typeof(pMsgIndex) != "number")
-		return;
-
-	if ((pMsgIndex >= 0) && (pMsgIndex < this.hdrsForCurrentSubBoard.length))
-	{
-		var applyAttr = (typeof(pApply) === "boolean" ? pApply : true);
-		if (applyAttr)
-			this.hdrsForCurrentSubBoard[pMsgIndex].attr = this.hdrsForCurrentSubBoard[pMsgIndex].attr | pAttrib;
-		else
-			this.hdrsForCurrentSubBoard[pMsgIndex].attr = this.hdrsForCurrentSubBoard[pMsgIndex].attr ^ pAttrib;
-	}
-}
-
-// For the DigDistMsgReader class: Refreshes a message header in the saved message
-// header arrays.
-//
-// Parameters:
-//  pMsgIndex: The index (0-based) of the message header
-//  pAttrib: Optional - An attribute to apply.  If this is is not specified,
-//           then the message header will be retrieved from the message base.
-//  pApply: Optional boolean - Whether or not to apply the attribute or remove it. Defaults to true.
-//  pSubBoardCode: Optional - An internal sub-board code.  If not specified, then
-//                 this method will default to this.subBoardCode.
-//  pWriteHdrToMsgbase: Optional boolean - Whether or not to also save the message to the messagebase.  Defaults to true.
-function DigDistMsgReader_RefreshHdrInSavedArrays(pMsgIndex, pAttrib, pApply, pSubBoardCode, pWriteHdrToMsgbase)
-{
-	var applyAttr = (typeof(pApply) === "boolean" ? pApply : true);
-	this.RefreshSearchResultMsgHdr(pMsgIndex, pAttrib, applyAttr, pSubBoardCode, pWriteHdrToMsgbase);
-	this.RefreshHdrInSubBoardHdrs(pMsgIndex, pAttrib, applyAttr);
-}
-
-// For the DigDistMsgReader class: Inputs search text from the user, then reads/lists
-// messages, which will perform the search.
-//
-// Paramters:
-//  pSearchModeStr: A string to specify the lister mode to use - This can
-//                  be one of the search modes to specify how to search:
-//                  "keyword_search": Search the message subjects & bodies by keyword
-//                  "from_name_search": Search messages by from name
-//                  "to_name_search": Search messages by to name
-//                  "to_user_search": Search messages by to name, to the logged-in user
-//  pSubBoardCode: Optional - The Synchronet sub-board code, or "mail"
-//                 for personal email.  Or, this can be the a boolean false for scan
-//                 search mode to scan through sub-boards while searching each of them.
-//  pScanScopeChar: Optional string with a character specifying "A" to scan all sub-boards,
-//                  "G" for the current message group, or "S" for the user's current sub-board.
-//                  If this is not specified, the current sub-board will be used.
-//  pTxtToSearch: Optional - Text to search for (if specified, this won't prompt the user for search text)
-//  pSkipSubBoardScanCfgCheck: Optional boolean - Whether or not to skip the sub-board scan config check for
-//                                                  each sub-board.  Defaults to false.
-function DigDistMsgReader_SearchMessages(pSearchModeStr, pSubBoardCode, pScanScopeChar, pTxtToSearch, pSkipSubBoardScanCfgCheck)
-{
-	var searchTextProvided = (typeof(pTxtToSearch) === "string" && pTxtToSearch != "");
-	var skipSubBoardScanCfgCheck = (typeof(pSkipSubBoardScanCfgCheck) === "boolean" ? pSkipSubBoardScanCfgCheck : false);
-	// Convert the search mode string to an integer representing the search
-	// mode.  If we get back -1, that means the search mode string was invalid.
-	// If that's the case, simply list messages.  Otherwise, do the search.
-	this.searchType = searchTypeStrToVal(pSearchModeStr);
-	if (this.searchType == SEARCH_NONE) // No search; search mode string was invalid
-	{
-		// Clear the search information and read/list messages.
-		this.ClearSearchData();
-		this.ReadOrListSubBoard(pSubBoardCode);
-	}
-	else
-	{
-		// The search mode string was valid, so go ahead and search.
-		console.attributes = "N";
-		console.crlf();
-		var subCode = "";
-		if (typeof(pScanScopeChar) !== "string")
-		{
-			subCode = (typeof(pSubBoardCode) === "string" ? pSubBoardCode : this.subBoardCode);
-			if (subCode == "mail")
-			{
-				//console.print("\x01n" + replaceAtCodesInStr(this.text.searchingPersonalMailText));
-				console.putmsg("\x01n" + this.text.searchingPersonalMailText);
-			}
-			else
-			{
-				var formattedText = format(this.text.searchingSubBoardAbovePromptText, subBoardGrpAndName(bbs.cursub_code));
-				//console.print("\x01n" + replaceAtCodesInStr(formattedText) + "\x01n");
-				console.putmsg("\x01n" + formattedText + "\x01n");
-			}
-			console.crlf();
-		}
-		// Output the prompt text to the user (for modes where a prompt is needed)
-		switch (this.searchType)
-		{
-			case SEARCH_KEYWORD:
-				if (!searchTextProvided)
-				{
-					//console.print("\x01n" + replaceAtCodesInStr(this.text.searchTextPromptText));
-					console.putmsg("\x01n" + this.text.searchTextPromptText);
-				}
-				else
-					console.print("\x01n\x01gSearching for: \x01c" + pTxtToSearch + "\x01n\r\n");
-				break;
-			case SEARCH_FROM_NAME:
-				if (!searchTextProvided)
-				{
-					//console.print("\x01n" + replaceAtCodesInStr(this.text.fromNamePromptText));
-					console.putmsg("\x01n" + this.text.fromNamePromptText);
-				}
-				else
-					console.print("\x01n\x01gSearching for: \x01c" + pTxtToSearch + "\x01n\r\n");
-				break;
-			case SEARCH_TO_NAME_CUR_MSG_AREA:
-				if (!searchTextProvided)
-				{
-					//console.print("\x01n" + replaceAtCodesInStr(this.text.toNamePromptText));
-					console.putmsg("\x01n" + this.text.toNamePromptText);
-				}
-				else
-					console.print("\x01n\x01gSearching for: \x01c" + pTxtToSearch + "\x01n\r\n");
-				break;
-			case SEARCH_TO_USER_CUR_MSG_AREA:
-				// Note: No prompt needed for this - Will search for the user's name/handle
-				console.line_counter = 0; // To prevent a pause before the message list comes up
-				break;
-			default:
-				break;
-		}
-		//var promptUserForText = this.SearchTypePopulatesSearchResults();
-		var promptUserForText = this.SearchTypeRequiresSearchText();
-		// Get the search text from the user
-		if (promptUserForText)
-		{
-			if (searchTextProvided)
-				this.searchString = pTxtToSearch;
-			else
-				this.searchString = console.getstr(512, K_UPPER);
-		}
-		// If the user was prompted for search text but no search text was entered,
-		// then show an abort message and don't do anything.  Otherwise, go ahead
-		// and list/read messages.
-		if (promptUserForText && (this.searchString.length == 0))
-		{
-			this.ClearSearchData();
-			//console.print("\x01n" + replaceAtCodesInStr(this.text.abortedText));
-			//console.crlf();
-			console.putmsg("\x01n" + this.text.abortedText);
-			console.crlf();
-			console.pause();
-			return;
-		}
-		else
-		{
-			// If pScanScopeChar is a string, then do scan search mode.  Otherwise,
-			// scan/search the current sub-board.
-			if (typeof(pScanScopeChar) === "string" && (pScanScopeChar === "S" || pScanScopeChar === "G" || pScanScopeChar === "A"))
-			{
-				var subBoardCodeBackup = this.subBoardCode;
-				var subBoardsToScan = getSubBoardsToScanArray(pScanScopeChar);
-				this.doingMsgScan = true;
-				var continueScan = true;
-				this.doingMultiSubBoardScan = (subBoardsToScan.length > 1);
-				// If the sub-board's access requirements allows the user to read it
-				// and it's enabled in the user's message scan configuration, then go
-				// ahead with this sub-board.
-				// Note: Used to use this to determine whether the user could access the
-				// sub-board:
-				//user.compare_ars(msg_area.grp_list[grpIndex].sub_list[subIndex].ars)
-				// Now using the can_read property.
-				for (var subCodeIdx = 0; (subCodeIdx < subBoardsToScan.length) && continueScan && !console.aborted; ++subCodeIdx)
-				{
-					// Pause for a short moment to avoid causing CPU usage goign to 99%
-					mswait(10);
-
-					subCode = subBoardsToScan[subCodeIdx];
-					if (skipSubBoardScanCfgCheck || (msg_area.sub[subCode].can_read && ((msg_area.sub[subCode].scan_cfg & SCAN_CFG_NEW) == SCAN_CFG_NEW)))
-					{
-						// Force garbage collection to ensure enough memory is available to continue
-						js.gc(true);
-						// Set the console line counter to 0 to prevent screen pausing
-						// when the "Searching ..." and "No messages were found" text is
-						// displayed repeatedly
-						console.line_counter = 0;
-						// let the user read the sub-board (and toggle betweeen reading and
-						// listing)
-						var readOrListRetObj = this.ReadOrListSubBoard(subCode, null, true, true, false, true, READER_MODE_READ);
-						console.attributes = "N";
-						console.crlf();
-						//if (this.SearchTypePopulatesSearchResults())
-						//	console.print("\x01n\r\nSearching...");
-						console.line_counter = 0;
-						if (readOrListRetObj.stoppedReading)
-							break;
-					}
-				}
-				this.subBoardCode = subBoardCodeBackup;
-				if (console.aborted)
-				{
-					console.print("\x01n" + replaceAtCodesInStr(this.text.msgSearchAbortedText) + "\x01n");
-					console.crlf();
-					console.aborted = false; // So that the console.pause() a couple lines down will indeed pause
-				}
-				console.pause();
-			}
-			else
-				this.ReadOrListSubBoard(subCode);
-			// Clear the search data so that subsequent listing or reading sessions
-			// don't repeat the same search
-			this.ClearSearchData();
-		}
-	}
-}
-
-// For the DigDistMsgReader class: Performs a message search scan through sub-boards.
-// Prompts the user for Sub-board/Group/All, then inputs search text from the user, then
-// reads/lists messages through the sub-boards, performing the search in each sub-board.
-//
-// Paramters:
-//  pSearchModeStr: A string to specify the lister mode to use - This can
-//                  be one of the search modes to specify how to search:
-//                  "keyword_search": Search the message subjects & bodies by keyword
-//                  "from_name_search": Search messages by from name
-//                  "to_name_search": Search messages by to name
-//                  "to_user_search": Search messages by to name, to the logged-in user
-//  pTxtToSearch: Optional - Text to search for (if specified, this won't prompt the user for search text)
-//  pSubCode: Optional - An internal code of a sub-board if scanning just one sub-board
-function DigDistMsgReader_SearchMsgScan(pSearchModeStr, pTxtToSearch, pSubCode)
-{
-	if (typeof(pSearchModeStr) !== "string" || pSearchModeStr.length == 0)
-		return;
-
-	// If the given sub-board code is valid, then use that and scan only in that
-	// sub-board.  Otherwise, prompt the user for sub-board, group, or all, then
-	// call SearchMessages to do the search.
-	var scanScopeChar = "";
-	var previousSubBoardCode = null;
-	if (typeof(pSubCode) === "string" && subBoardCodeIsValid(pSubCode))
-	{
-		var previousSubBoardCode = this.subBoardCode;
-		this.subBoardCode = pSubCode;
-		scanScopeChar = "S";
-	}
-	else
-	{
-		console.mnemonics(bbs.text(SubGroupOrAll));
-		scanScopeChar = console.getkeys("SGAC").toString();
-	}
-	if (pSubCode == "mail") // Searching personal email
-		this.SearchMessages(pSearchModeStr, pSubCode, null, pTxtToSearch, true); // Skip/ignore scan config checks
-	else if (scanScopeChar.length > 0)
-		this.SearchMessages(pSearchModeStr, null, scanScopeChar, pTxtToSearch, true); // Skip/ignore scan config checks
-	else
-	{
-		console.crlf();
-		//console.print(replaceAtCodesInStr(this.text.msgScanAbortedText));
-		//console.crlf();
-		//console.putmsg(this.text.msgScanAbortedText);
-		console.crlf();
-		console.pause();
-	}
-	// Restore this.subBoardCode if necessary
-	if (typeof(previousSubBoardCode) === "string")
-		this.subBoardCode = previousSubBoardCode;
-}
-
-// This function clears the search data from the object.
-function DigDistMsgReader_ClearSearchData()
-{
-   this.searchType = SEARCH_NONE;
-   this.searchString == "";
-   if (this.msgSearchHdrs != null)
-   {
-		for (var subCode in this.msgSearchHdrs)
-		{
-			delete this.msgSearchHdrs[subCode].indexed;
-			delete this.msgSearchHdrs[subCode];
-		}
-		this.msgSearchHdrs = {};
-   }
-}
-
-// For the DigDistMsgReader class: Performs message reading/listing.
-// Depending on the value of this.startMode, starts in either reader
-// mode or lister mode.  Uses an input loop to let the user switch
-// between the two modes.
-//
-// Parameters:
-//  pSubBoardCode: Optional - The internal code of a sub-board to read.
-//                 If not specified, the internal sub-board code specified
-//                 when creating the object will be used.
-//  pStartingMsgOffset: Optional - The offset of a message to start at
-//  pAllowChgArea: Optional boolean - Whether or not to allow changing the
-//                 message area
-//  pReturnOnNextAreaNav: Optional boolean - Whether or not this method should
-//                        return when it would move to the next message area due
-//                        navigation from the user (i.e., with the right arrow key)
-//  pPauseOnNoMsgSrchResults: Optional boolean - Whether or not to pause when
-//                            a message search doesn't find any search results
-//                            in the current sub-board.  Defaults to true.
-//  pPromptToGoNextIfNoResults: Optional boolean - Whether or not to prompt the user
-//                         to go onto the next/previous sub-board if there are no
-//                         search results in the current sub-board.  Defaults to true.
-//  pInitialModeOverride: Optional (numeric) to override the initial mode in this
-//                        function (READER_MODE_READ or READER_MODE_LIST).  If not
-//                        specified, defaults to this.startMode.
-//  pOutputSubBoardPopulationMsgs: Optional (boolean): Whether or not to output sub-board
-//                                 population messages
-//
-// Return value: An object with the following properties:
-//               stoppedReading: Boolean - Whether or not the user stopped reading.
-//                               This can also be true if there is an error.
-function DigDistMsgReader_ReadOrListSubBoard(pSubBoardCode, pStartingMsgOffset,
-                                             pAllowChgArea, pReturnOnNextAreaNav,
-                                             pPauseOnNoMsgSrchResults,
-                                             pPromptToGoNextIfNoResults,
-                                             pInitialModeOverride, pOutputSubBoardPopulationMsgs)
-{
-	var retObj = {
-		stoppedReading: false
-	};
-
-	// Set the sub-board code if applicable
-	var previousSubBoardCode = this.subBoardCode;
-	if (typeof(pSubBoardCode) === "string")
-	{
-		if (subBoardCodeIsValid(pSubBoardCode))
-			this.setSubBoardCode(pSubBoardCode);
-		else
-		{
-			console.print("\x01n\x01h\x01yWarning: \x01wThe Message Reader connot continue because an invalid");
-			console.crlf();
-			console.print("sub-board code was specified (" + pSubBoardCode + "). Please notify the sysop.");
-			console.crlf();
-			console.pause();
-			retObj.stoppedReading = true;
-			return retObj;
-		}
-	}
-
-	// If the user doesn't have permission to read the current sub-board, then
-	// don't allow the user to read it.
-	if (this.subBoardCode != "mail")
-	{
-		if (!msg_area.sub[this.subBoardCode].can_read)
-		{
-			var errorMsg = format(bbs.text(CantReadSub), msg_area.sub[this.subBoardCode].grp_name, msg_area.sub[this.subBoardCode].name);
-			console.print("\x01n" + errorMsg);
-			console.pause();
-			retObj.stoppedReading = true;
-			return retObj;
-		}
-	}
-
-	// Populate this.msgSearchHdrs for the current sub-board if there is a search
-	// specified.  If there are no messages to read in the current sub-board, then
-	// just return.
-	var pauseOnNoSearchResults = (typeof(pPauseOnNoMsgSrchResults) == "boolean" ? pPauseOnNoMsgSrchResults : true);
-	var outputPopulateHdrsMsgs = (typeof(pOutputSubBoardPopulationMsgs) === "boolean" ? pOutputSubBoardPopulationMsgs : true);
-	if (!this.PopulateHdrsIfSearch_DispErrorIfNoMsgs(true, outputPopulateHdrsMsgs, pauseOnNoSearchResults))
-	{
-		retObj.stoppedReading = false;
-		return retObj;
-	}
-	// If not searching, then populate the array of all readable headers for the
-	// current sub-board.
-	if (!this.SearchingAndResultObjsDefinedForCurSub())
-		this.PopulateHdrsForCurrentSubBoard();
-
-	// Check the pAllowChgArea parameter.  If it's a boolean, then use it.  If
-	// not, then check to see if we're reading personal mail - If not, then allow
-	// the user to change to a different message area.
-	var allowChgMsgArea = true;
-	if (typeof(pAllowChgArea) == "boolean")
-		allowChgMsgArea = pAllowChgArea;
-	else
-		allowChgMsgArea = (this.subBoardCode != "mail");
-	// If reading personal email and messages haven't been collected (searched)
-	// yet, then do so now.
-	if (this.readingPersonalEmail && (!this.msgSearchHdrs.hasOwnProperty(this.subBoardCode)))
-		this.msgSearchHdrs[this.subBoardCode] = searchMsgbase(this.subBoardCode, this.searchType, this.searchString, this.readingPersonalEmailFromUser);
-
-	// Determine whether to start in list or reader mode, depending
-	// on the value of this.startMode.
-	var readerMode = this.startMode;
-	// If an initial mode override was specified and is valid, then use it.
-	if (typeof(pInitialModeOverride) === "number" && (pInitialModeOverride == READER_MODE_READ || pInitialModeOverride == READER_MODE_LIST))
-		readerMode = pInitialModeOverride;
-	// User input loop
-	var selectedMessageOffset = 0;
-	if (typeof(pStartingMsgOffset) == "number")
-		selectedMessageOffset = pStartingMsgOffset;
-	else if (this.SearchingAndResultObjsDefinedForCurSub())
-	{
-		// If reading personal mail, start at the first unread message index
-		// (or the last message, if all messages have been read)
-		if (this.readingPersonalEmail)
-		{
-			selectedMessageOffset = this.GetLastReadMsgIdxAndNum(false).lastReadMsgIdx; // Used to be true
-			if ((selectedMessageOffset > -1) && (selectedMessageOffset < this.NumMessages() - 1))
-				++selectedMessageOffset;
-		}
-		else
-			selectedMessageOffset = 0;
-	}
-	else if (this.hdrsForCurrentSubBoard.length > 0)
-	{
-		selectedMessageOffset = this.GetMsgIdx(GetScanPtrOrLastMsgNum(this.subBoardCode));
-		if (selectedMessageOffset < 0)
-			selectedMessageOffset = 0;
-		else if (selectedMessageOffset >= this.hdrsForCurrentSubBoard.length)
-			selectedMessageOffset = this.hdrsForCurrentSubBoard.length - 1;
-	}
-	else
-		selectedMessageOffset = -1;
-	var otherRetObj = null;
-	var continueOn = true;
-	while (continueOn)
-	{
-		switch (readerMode)
-		{
-			case READER_MODE_READ:
-				// Call the ReadMessages method - DOn't change the sub-board,
-				// and pass the selected index of the message to read.  If that
-				// index is -1, the ReadMessages method will use the user's
-				// last-read message index.
-				otherRetObj = this.ReadMessages(null, selectedMessageOffset, true, allowChgMsgArea,
-				                                pReturnOnNextAreaNav, pPromptToGoNextIfNoResults);
-				// If the user wants to quit or if there was an error, then stop
-				// the input loop.
-				if (otherRetObj.stoppedReading)
-				{
-					retObj.stoppedReading = true;
-					continueOn = false;
-				}
-				// If we're set to return on navigation to the next message area and
-				// the user's last keypress was the right arrow key or next action
-				// was to go to the next message area, then don't continue the input
-				// loop, and also say that the user didn't stop reading.
-				else if (pReturnOnNextAreaNav &&
-				         ((otherRetObj.lastUserInput == KEY_RIGHT) || (otherRetObj.lastUserInput == KEY_ENTER) || (otherRetObj.lastAction == ACTION_GO_NEXT_MSG_AREA)))
-				{
-					retObj.stoppedReading = false;
-					continueOn = false;
-				}
-				else if (otherRetObj.messageListReturn)
-					readerMode = READER_MODE_LIST;
-				break;
-			case READER_MODE_LIST:
-				// Note: Doing the message list is also handled in this.ReadMessages().
-				// This code is here in case the reader is configured to start up
-				// in list mode first.
-				// List messages
-				otherRetObj = this.ListMessages(null, pAllowChgArea);
-				// If the user wants to quit, set continueOn to false to get out
-				// of the loop.  Otherwise, set the selected message offset to
-				// what the user chose from the list.
-				if (otherRetObj.lastUserInput == "Q")
-				{
-					retObj.stoppedReading = true;
-					continueOn = false;
-				}
-				else
-				{
-					selectedMessageOffset = otherRetObj.selectedMsgOffset;
-					readerMode = READER_MODE_READ;
-				}
-				break;
-			default:
-				break;
-		}
-	}
-
-	console.clear("\x01n");
-
-	return retObj;
-}
-// Helper for DigDistMsgReader_ReadOrListSubBoard(): Populates this.msgSearchHdrs
-// if an applicable search type is specified; also, if there are no messages in
-// the current sub-board, outputs an error to the user.
-//
-// Parameters:
-//  pCloseMsgbaseAndSetNullIfNoMsgs: Optional boolean - Whether or not to close the message
-//                         base if there are no messages.  Defaults to true.
-//  pOutputMessages: Boolean - Whether or not to output messages to the screen.
-//                   Defaults to true.
-//  pPauseOnNoMsgError: Optional boolean - Whether or not to pause for a keypress
-//                      after displaying the "no messages" error.  Defaults to true.
-//
-// Return value: Boolean - Whether or not there are messages to read in the current
-//               sub-board
-function DigDistMsgReader_PopulateHdrsIfSearch_DispErrorIfNoMsgs(pCloseMsgbaseAndSetNullIfNoMsgs,
-                                                                 pOutputMessages, pPauseOnNoMsgError)
-{
-	var thereAreMessagesToRead = true;
-
-	var outputMessages = (typeof(pOutputMessages) == "boolean" ? pOutputMessages : true);
-
-	// If a search is is specified that would populate the search results, then
-	// perform the message search for the current sub-board.
-	if (this.SearchTypePopulatesSearchResults())
-	{
-		if (!this.msgSearchHdrs.hasOwnProperty(this.subBoardCode))
-		{
-			// TODO: In case new messages were posted in this sub-board, it might help
-			// to check the current number of messages vs. the previous number of messages
-			// and search the new messages if there are more.
-			if (outputMessages)
-			{
-				console.crlf();
-				var formattedText = "";
-				if (this.readingPersonalEmail)
-					formattedText = format(this.text.loadingPersonalMailText, subBoardGrpAndName(this.subBoardCode));
-				else
-					formattedText = format(this.text.searchingSubBoardText, subBoardGrpAndName(this.subBoardCode));
-				formattedText = replaceAtCodesAndRemoveCRLFs(formattedText);
-				console.print("\x01n" + formattedText + "\x01n");
-			}
-			var readingMailUserNum = user.is_sysop ? this.personalMailUserNum : user.number;
-			this.msgSearchHdrs[this.subBoardCode] = searchMsgbase(this.subBoardCode, this.searchType, this.searchString, this.readingPersonalEmailFromUser, null, null, readingMailUserNum);
-		}
-	}
-	else
-	{
-		// There is no search is specified, so clear the search results for the
-		// current sub-board to help ensure that there are messages to read.
-		if (this.msgSearchHdrs.hasOwnProperty(this.subBoardCode))
-		{
-			delete this.msgSearchHdrs[this.subBoardCode].indexed;
-			delete this.msgSearchHdrs[this.subBoardCode];
-		}
-	}
-
-	// If there are no messages to display in the current sub-board, then set the
-	// return value and let the user know (if outputMessages is true).
-	if (this.NumMessages() == 0)
-	{
-		thereAreMessagesToRead = false;
-		if (outputMessages)
-		{
-			console.attributes = "N";
-			console.crlf();
-			if (this.readingPersonalEmail)
-			{
-				//console.print(replaceAtCodesInStr(this.text.noPersonalEmailText));
-				if (this.searchType == SEARCH_NONE)
-					console.putmsg(this.text.noPersonalEmailText);
-				else
-					printf("\x01n" + this.text.noSearchResultsInSubBoardText, "Personal E-Mail");
-			}
-			else
-			{
-				var formattedText = "";
-				if (this.msgSearchHdrs.hasOwnProperty(this.subBoardCode))
-					formattedText = format(this.text.noSearchResultsInSubBoardText, subBoardGrpAndName(this.subBoardCode));
-				else
-					formattedText = format(this.text.noMessagesInSubBoardText, subBoardGrpAndName(this.subBoardCode));
-				//console.putmsg(formattedText); // Doesn't seem to be word-wrapping
-				formattedText = replaceAtCodesInStr(formattedText);
-				formattedText = word_wrap(formattedText, console.screen_columns-1, formattedText.length, false).replace(/\r|\n/g, "\r\n");
-				console.print(formattedText);
-			}
-			console.crlf();
-			var pauseOnNoMsgsError = (typeof(pPauseOnNoMsgError) == "boolean" ? pPauseOnNoMsgError : true);
-			if (pauseOnNoMsgsError)
-				console.pause();
-		}
-	}
-
-	return thereAreMessagesToRead;
-}
-
-// For the DigDistMsgReader class: Returns whether the search type is a type
-// that would result in the search results structure being populated.  Search
-// types where that wouldn't happen are SEARCH_NONE (no search) and any of the
-// message scan search types.
-function DigDistMsgReader_SearchTypePopulatesSearchResults()
-{
-	return (this.readingPersonalEmail || searchTypePopulatesSearchResults(this.searchType));
-}
-
-// For the DigDistMsgReader class: Returns whether the search type is a type
-// that requires search text.  Search types that require search text are the
-// keyword search, from name search, and to name search.  Search types that
-// don't require search text are SEARCH_NONE (no search) & the message scan search
-// types.
-function DigDistMsgReader_SearchTypeRequiresSearchText()
-{
-	return searchTypeRequiresSearchText(this.searchType);
-}
-
-// Returns whether a search type value would populate search results.
-//
-// Parameters:
-//  pSearchType: A search type integer value
-//
-// Return value: Boolean - Whether or not the search type would populate search
-//               results
-function searchTypePopulatesSearchResults(pSearchType)
-{
-	return ((pSearchType == SEARCH_KEYWORD) ||
-	        (pSearchType == SEARCH_FROM_NAME) ||
-	        (pSearchType == SEARCH_TO_NAME_CUR_MSG_AREA) ||
-	        (pSearchType == SEARCH_TO_USER_CUR_MSG_AREA) ||
-	        (pSearchType == SEARCH_TO_USER_NEW_SCAN) ||
-			(pSearchType == SEARCH_TO_USER_NEW_SCAN_CUR_SUB) ||
-	        (pSearchType == SEARCH_TO_USER_NEW_SCAN_CUR_GRP) ||
-	        (pSearchType == SEARCH_TO_USER_NEW_SCAN_ALL) ||
-	        (pSearchType == SEARCH_ALL_TO_USER_SCAN));
-}
-
-// Returns whether a search type value requires search text.
-//
-// Parameters:
-//  pSearchType: A search type integer value
-//
-// Return value: Boolean - Whether or not the search type requires search text
-function searchTypeRequiresSearchText(pSearchType)
-{
-	return ((pSearchType == SEARCH_KEYWORD) ||
-	         (pSearchType == SEARCH_FROM_NAME) ||
-	         (pSearchType == SEARCH_TO_NAME_CUR_MSG_AREA));
-}
-
-// For the DigDistMsgReader class: Scans the message area(s) for new messages,
-// unread messages to the user, or all messages to the user.
-//
-// Parameters:
-//  pScanCfgOpt: The scan configuration option to check for in the sub-boards
-//               (from sbbsdefs.js). Supported values are SCAN_CFG_NEW (new
-//               message scan), and SCAN_CFG_TOYOU (messages to the user)
-//  pScanMode: The scan mode (from sbbsdefs.js).  Supported values are SCAN_NEW
-//             (new message scan), SCAN_TOYOU (scan for all messages to the
-//             user), and SCAN_UNREAD (scan for new messages to the user).
-//  pScanScopeChar: Optional - A character (as a string) representing the scan
-//                  scope: "S" for sub-board, "G" for group, or "A" for all.
-//                  If this is not specified, the user will be prompted for the
-//                  scan scope.
-//  pOutputMessages: Optional boolean: Whether or not to output scan status messages. Defaults to true.
-function DigDistMsgReader_MessageAreaScan(pScanCfgOpt, pScanMode, pScanScopeChar, pOutputMessages)
-{
-	var scanScopeChar = "";
-	if ((typeof(pScanScopeChar) == "string") && /^[SGA]$/.test(pScanScopeChar))
-		scanScopeChar = pScanScopeChar;
-	else
-	{
-		// Prompt the user to scan in the current sub-board, the current message group,
-		// or all.  Default to all.
-		console.attributes = "N";
-		console.mnemonics(bbs.text(SubGroupOrAll));
-		scanScopeChar = console.getkeys("SGAC").toString();
-		// If the user just pressed Enter without choosing anything, then abort and return.
-		if (scanScopeChar.length == 0)
-		{
-			console.crlf();
-			//console.print(replaceAtCodesInStr(this.text.msgScanAbortedText));
-			console.putmsg(this.text.msgScanAbortedText);
-			console.crlf();
-			console.pause();
-			return;
-		}
-	}
-	
-	var outputMessages = (typeof(pOutputMessages) === "boolean" ? pOutputMessages : true);
-
-	// Do some logging if verbose logging is enabled
-	if (gCmdLineArgVals.verboselogging)
-	{
-		var logMessage = "Doing a message area scan (";
-		if (pScanCfgOpt == SCAN_CFG_NEW)
-		{
-			// The only valid value for pScanMode in this case is SCAN_NEW, so no
-			// need to check pScanMode to append more to the log message.
-			logMessage += "new";
-		}
-		else if (pScanCfgOpt == SCAN_CFG_TOYOU)
-		{
-			// Valid values for pScanMode in this case are SCAN_UNREAD and SCAN_TOYOU.
-			if (pScanMode == SCAN_UNREAD)
-				logMessage += "unread messages to the user";
-			else if (pScanMode == SCAN_TOYOU)
-				logMessage += "all messages to the user";
-		}
-		if (scanScopeChar == "A") // All sub-boards
-			logMessage += ", all sub-boards";
-		else if (scanScopeChar == "G") // Current message group
-		{
-			logMessage += ", current message group (" +
-			              msg_area.grp_list[bbs.curgrp].description + ")";
-		}
-		else if (scanScopeChar == "S") // Current sub-board
-		{
-			logMessage += ", current sub-board (" +
-			              msg_area.grp_list[bbs.curgrp].description + " - " +
-						  msg_area.grp_list[bbs.curgrp].sub_list[bbs.cursub].description + ")";
-		}
-		logMessage += ")";
-		writeToSysAndNodeLog(logMessage);
-	}
-
-	this.doingNewscan = pScanMode === SCAN_NEW;
-
-	// If doing a newscan of all sub-boards, and the user has their setting for indexed mode
-	// for newscan enabled, then do that and return instead of the traditional newscan.
-	if (pScanCfgOpt === SCAN_CFG_NEW && pScanMode === SCAN_NEW && this.userSettings.useIndexedModeForNewscan)
-	{
-		var scanScope = SCAN_SCOPE_ALL;
-		if (scanScopeChar === "S") scanScope = SCAN_SCOPE_SUB_BOARD;
-		else if (scanScopeChar === "G") scanScope = SCAN_SCOPE_GROUP;
-		msgReader.DoIndexedMode(scanScope, true);
-		this.doingNewscan = false;
-		return;
-	}
-
-	// Save the original search type, sub-board code, searched message headers,
-	// etc. to be restored later
-	var originalSearchType = this.searchType;
-	var originalSubBoardCode = this.subBoardCode;
-	var originalBBSCurGrp = bbs.curgrp;
-	var originalBBSCurSub = bbs.cursub;
-	var originalMsgSrchHdrs = this.msgSearchHdrs;
-
-	// Make sure there is no search data
-	this.ClearSearchData();
-
-	// Create an array of internal codes of sub-boards to scan
-	var subBoardsToScan = getSubBoardsToScanArray(scanScopeChar);
-	// Scan through the sub-boards
-	this.doingMsgScan = true;
-	var continueNewScan = true;
-	var userAborted = false;
-	this.doingMultiSubBoardScan = (subBoardsToScan.length > 1);
-	for (var subCodeIdx = 0; (subCodeIdx < subBoardsToScan.length) && continueNewScan && !console.aborted; ++subCodeIdx)
-	{
-		// Force garbage collection to ensure enough memory is available to continue
-		js.gc(true);
-		// Set the console line counter to 0 to prevent screen pausing
-		// when the "Searching ..." and "No messages were found" text is
-		// displayed repeatedly
-		console.line_counter = 0;
-		// If the sub-board's access requirements allows the user to read it
-		// and it's enabled in the user's message scan configuration, then go
-		// ahead with this sub-board.
-		// Note: Used to use this to determine whether the user could access the
-		// sub-board:
-		//user.compare_ars(msg_area.grp_list[grpIndex].sub_list[subIndex].ars)
-		// Now using the can_read property.
-		this.setSubBoardCode(subBoardsToScan[subCodeIdx]); // Needs to be set before getting the last read/scan pointer index
-		if (msg_area.sub[this.subBoardCode].can_read && ((msg_area.sub[this.subBoardCode].scan_cfg & pScanCfgOpt) == pScanCfgOpt))
-		{
-			// If we are to output status messages, then do so and 
-			if (outputMessages)
-			{
-				// If we're running a new message scan, then output which sub-board that is currently being scanned
-				if (pScanMode == SCAN_NEW || pScanMode == SCAN_BACK || pScanMode == SCAN_UNREAD)
-				{
-					var statusText = format(this.text.scanningSubBoardText, subBoardGrpAndName(this.subBoardCode));
-					console.print("\x01n" + replaceAtCodesAndRemoveCRLFs(statusText) + "\x01n");
-					console.crlf();
-					console.line_counter = 0; // Prevent pausing for screen output and when displaying a message
-				}
-			}
-
-			var grpIndex = msg_area.sub[this.subBoardCode].grp_index;
-			var subIndex = msg_area.sub[this.subBoardCode].index;
-			// Sub-board description: msg_area.grp_list[grpIndex].sub_list[subIndex].description
-			// Open the sub-board and check for unread messages.  If there are any, then let
-			// the user read the messages in the sub-board.
-			//var msgbase = new MsgBasesubBoardsToScan[subCodeIdx]);
-			var msgbase = new MsgBase(this.subBoardCode);
-			if (msgbase.open())
-			{
-				//this.setSubBoardCode(subBoardsToScan[subCodeIdx]); // Needs to be set before getting the last read/scan pointer index
-
-				// TODO: This commented-out section of code may be redundant and no longer necessary.
-				// Perhaps this should be refactored?  GetScanPtrMsgIdx() and FindNextReadableMsgIdx()
-				// may be relying on the fact that we poulate an array of cached message headers,
-				// filtered where deleted & unreadable ones are removed, so scan_ptr and last_read may
-				// not be quite accurate when we're using our cached header arrays.
-				/*
-				// If the current sub-board contains only deleted messages,
-				// or if the user has already read the last message in this
-				// sub-board, then skip it.
-				var scanPtrMsgIdx = this.GetScanPtrMsgIdx();
-				var readableMsgsExist = (this.FindNextReadableMsgIdx(scanPtrMsgIdx-1, true) > -1);
-				var userHasReadLastMessage = false;
-				if (this.subBoardCode != "mail")
-				{
-					// What if newest_message_header.number is invalid  (e.g. NaN or 0xffffffff or >
-					// msgbase.last_msg)?
-					if (this.hdrsForCurrentSubBoard.length > 0)
-					{
-						if ((msg_area.sub[this.subBoardCode].last_read == this.hdrsForCurrentSubBoard[this.hdrsForCurrentSubBoard.length-1].number) ||
-						    (scanPtrMsgIdx == this.hdrsForCurrentSubBoard.length-1))
-						{
-							userHasReadLastMessage = true;
-						}
-					}
-				}
-				if (!readableMsgsExist || userHasReadLastMessage)
-				{
-					if (msgbase != null)
-						msgbase.close();
-					continue;
-				}
-				*/
-
-				var scanPtrMsgIdx = this.GetScanPtrMsgIdx();
-				// In the switch cases below, bbs.curgrp and bbs.cursub are
-				// temporarily changed the user's sub-board to the current
-				// sub-board so that certain @-codes (such as @GRP-L@, etc.)
-				// are displayed by Synchronet correctly.
-
-				// We might want the starting message index to be different
-				// depending on the scan mode.
-				switch (pScanMode)
-				{
-					case SCAN_NEW:
-						// Make sure the sub-board has some messages.  Let the user read it if
-						// the scan pointer index is -1 (one unread message) or if it points to
-						// a message within the number of messages in the sub-board.
-						var totalNumMsgs = msgbase.total_msgs;
-						// If the user's scan_ptr for the sub-board isn't the 'last message' special value,
-						// then check the user's scan_ptr against the message number of the last readable
-						// message (i.e., the last message in the sub-board could be a vote header, which
-						// isn't readable)
-						var scanPtrBeforeLastReadableMsg = false;
-						if (!subBoardScanPtrIsLatestMsgSpecialVal(this.subBoardCode))
-						{
-							var lastReadableMsgHdr = getLastReadableMsgHdrInMsgbase(msgbase, this.subBoardCode);
-							if (lastReadableMsgHdr != null)
-								scanPtrBeforeLastReadableMsg = (msg_area.sub[this.subBoardCode].scan_ptr < lastReadableMsgHdr.number);
-							else
-								scanPtrBeforeLastReadableMsg = (msg_area.sub[this.subBoardCode].scan_ptr < msgbase.last_msg);
-						}
-						//if ((totalNumMsgs > 0) && ((scanPtrMsgIdx == -1) || (scanPtrMsgIdx < totalNumMsgs-1)))
-						if (totalNumMsgs > 0 && scanPtrBeforeLastReadableMsg)
-						{
-							bbs.curgrp = grpIndex;
-							bbs.cursub = subIndex;
-							// For a newscan, start at index 0 if the user wants to only show new messages
-							// during a newscan; otherwise, start at the scan pointer message (the sub-board
-							// will have to be populated with all messages)
-							var startMsgIdx = 0;
-							if (!this.userSettings.newscanOnlyShowNewMsgs)
-							{
-								// Start at the scan pointer
-								startMsgIdx = scanPtrMsgIdx;
-								// If the message has already been read, then start at the next message
-								var tmpMsgHdr = this.GetMsgHdrByIdx(startMsgIdx);
-								if ((tmpMsgHdr != null) && (msg_area.sub[this.subBoardCode].last_read == tmpMsgHdr.number) && (startMsgIdx < this.NumMessages() - 1))
-									++startMsgIdx;
-							}
-							// Allow the user to read messages in this sub-board.  Don't allow
-							// the user to change to a different message area, don't pause
-							// when there's no search results in a sub-board, and return
-							// instead of going to the next sub-board via navigation.
-							var readRetObj = this.ReadOrListSubBoard(null, startMsgIdx, false, true, false, null, null, false);
-							// If the user stopped reading & decided to quit, then exit the
-							// message scan loops.
-							if (readRetObj.stoppedReading)
-							{
-								continueNewScan = false;
-								userAborted = true;
-							}
-						}
-						break;
-					case SCAN_TOYOU: // All messages to the user
-						bbs.curgrp = grpIndex;
-						bbs.cursub = subIndex;
-						// Search for messages to the user in the current sub-board
-						// and let the user read the sub-board if messages are
-						// found.  Don't allow the user to change to a different
-						// message area, don't pause when there's no search results
-						// in a sub-board, and return instead of going to the next
-						// sub-board via navigation.
-						this.searchType = SEARCH_TO_USER_CUR_MSG_AREA;
-						var readRetObj = this.ReadOrListSubBoard(null, 0, false, true, false, null, null, false);
-						// If the user stopped reading & decided to quit, then exit the
-						// message scan loops.
-						if (readRetObj.stoppedReading)
-						{
-							continueNewScan = false;
-							userAborted = true;
-						}
-						break;
-					case SCAN_UNREAD: // New (unread) messages to the user
-						bbs.curgrp = grpIndex;
-						bbs.cursub = subIndex;
-						// Search for messages to the user in the current sub-board
-						// and let the user read the sub-board if messages are
-						// found.  Don't allow the user to change to a different
-						// message area, don't pause when there's no search results
-						// in a sub-board, and return instead of going to the next
-						// sub-board via navigation.
-						this.searchType = SEARCH_TO_USER_NEW_SCAN;
-						var anyUnreadToUser = anyUnreadMsgsToUserWithMsgbase(msgbase, this.subBoardCode);
-						if (anyUnreadToUser)
-						{
-							var readRetObj = this.ReadOrListSubBoard(null, 0, false, true, false, null, null, false);
-							// If the user stopped reading & decided to quit, then exit the
-							// message scan loops.
-							if (readRetObj.stoppedReading)
-							{
-								continueNewScan = false;
-								userAborted = true;
-							}
-						}
-						break;
-					default:
-						break;
-				}
-
-				if (msgbase != null)
-					msgbase.close();
-			}
-		}
-		// Briefly wait, to prevent the CPU from reaching 99% usage
-		mswait(10);
-	}
-	this.doingMultiSubBoardScan = false;
-
-
-	// Restore the original sub-board code, searched message headers, etc.
-	this.searchType = originalSearchType;
-	this.setSubBoardCode(originalSubBoardCode);
-	this.msgSearchHdrs = originalMsgSrchHdrs;
-	bbs.curgrp = originalBBSCurGrp;
-	bbs.cursub = originalBBSCurSub;
-	if ((msgbase != null) && msgbase.is_open)
-		msgbase.close();
-	this.doingMultiSubBoardScan = false;
-	this.doingMsgScan = false;
-
-	if (this.pauseAfterNewMsgScan)
-	{
-		console.crlf();
-		if (userAborted || console.aborted)
-		{
-			console.print("\x01n" + replaceAtCodesInStr(this.text.msgScanAbortedText) + "\x01n");
-			if (console.aborted)
-				console.aborted = false; // So that the console.pause() several lines down will indeed pause
-		}
-		else
-			console.print("\x01n" + replaceAtCodesInStr(this.text.msgScanCompleteText) + "\x01n");
-		console.crlf();
-		console.pause();
-	}
-
-	this.doingNewscan = false;
-}
-
-// For the DigDistMsgReader class: Performs the message reading activity.
-//
-// Parameters:
-//  pSubBoardCode: Optional - The internal code of a sub-board to read.
-//                 If not specified, the internal sub-board code specified
-//                 when creating the object will be used.
-//  pStartingMsgOffset: Optional - The offset of a message to start at
-//  pReturnOnMessageList: Optional boolean - Whether or not to quit when the
-//                      user wants to list messages (used when this method
-//                      is called from ReadOrListSubBoard()).
-//  pAllowChgArea: Optional boolean - Whether or not to allow changing the
-//                 message area
-//  pReturnOnNextAreaNav: Optional boolean - Whether or not this method should
-//                        return when it would move to the next message area due
-//                        navigation from the user (i.e., with the right arrow
-//                        key or with < (go to previous message area) or > (go
-//                        to next message area))
-//  pPromptToGoToNextAreaIfNoSearchResults: Optional boolean - Whether or not to
-//                          prompt the user to go to the next/previous sub-board
-//                          when there are no search results
-//
-// Return value: An object that has the following properties:
-//               lastUserInput: The user's last keypress/input
-//               lastAction: The last action chosen by the user based on their
-//                           last keypress, etc.
-//               stoppedReading: Boolean - Whether reading has stopped
-//                               (due to user quitting, error, or otherwise)
-//               messageListReturn: Boolean - Whether this method is returning for
-//                                  the caller to display the message list.  This
-//                                  will only be true when the pReturnOnMessageList
-//                                  parameter is true and the user wants to list
-//                                  messages.
-function DigDistMsgReader_ReadMessages(pSubBoardCode, pStartingMsgOffset, pReturnOnMessageList,
-                                       pAllowChgArea, pReturnOnNextAreaNav, pPromptToGoToNextAreaIfNoSearchResults)
-{
-	var retObj = {
-		lastUserInput: "",
-		lastAction: ACTION_NONE,
-		stoppedReading: false,
-		messageListReturn: false
-	};
-
-	// If the passed-in sub-board code was different than what was set in the object before,
-	// then open the new message sub-board.
-	var previousSubBoardCode = this.subBoardCode;
-	if (typeof(pSubBoardCode) == "string")
-	{
-		if (subBoardCodeIsValid(pSubBoardCode))
-			this.setSubBoardCode(pSubBoardCode);
-		else
-		{
-			console.print("\x01n\x01h\x01yWarning: \x01wThe Message Reader connot continue because an invalid");
-			console.crlf();
-			console.print("sub-board code was specified (" + pSubBoardCode + "). Please notify the sysop.");
-			console.crlf();
-			console.pause();
-			retObj.stoppedReading = true;
-			return retObj;
-		}
-	}
-	if (this.subBoardCode.length == 0)
-	{
-		console.print("\x01n\x01h\x01yWarning: \x01wThe Message Reader connot continue because no message");
-		console.crlf();
-		console.print("sub-board was specified. Please notify the sysop.");
-		console.crlf();
-		console.pause();
-		retObj.stoppedReading = true;
-		return retObj;
-	}
-
-	// If the user doesn't have permission to read the current sub-board, then
-	// don't allow the user to read it.
-	if (this.subBoardCode != "mail")
-	{
-		if (!msg_area.sub[this.subBoardCode].can_read)
-		{
-			var errorMsg = format(bbs.text(CantReadSub), msg_area.sub[this.subBoardCode].grp_name, msg_area.sub[this.subBoardCode].name);
-			console.print("\x01n" + errorMsg);
-			console.pause();
-			retObj.stoppedReading = true;
-			return retObj;
-		}
-	}
-
-	var msgbase = new MsgBase(this.subBoardCode);
-	// If the message base was not opened, then output an error and return.
-	if (!msgbase.open())
-	{
-		console.attributes = "N";
-		console.crlf();
-		console.print("\x01h\x01y* \x01wUnable to open message sub-board:");
-		console.crlf();
-		console.print(subBoardGrpAndName(this.subBoardCode));
-		console.crlf();
-		console.pause();
-		retObj.stoppedReading = true;
-		return retObj;
-	}
-
-	// If there are no messages to display in the current sub-board, then let the
-	// user know and exit.
-	var numOfMessages = this.NumMessages(msgbase);
-	msgbase.close();
-	if (numOfMessages == 0)
-	{
-		console.clear("\x01n");
-		console.center("\x01n\x01h\x01yThere are no messages to display.");
-		console.crlf();
-		console.pause();
-		retObj.stoppedReading = true;
-		return retObj;
-	}
-
-	// Check the pAllowChgArea parameter.  If it's a boolean, then use it.  If
-	// not, then check to see if we're reading personal mail - If not, then allow
-	// the user to change to a different message area.
-	var allowChgMsgArea = true;
-	if (typeof(pAllowChgArea) == "boolean")
-		allowChgMsgArea = pAllowChgArea;
-	else
-		allowChgMsgArea = (this.subBoardCode != "mail");
-	// If reading personal email and messages haven't been collected (searched)
-	// yet, then do so now.
-	if (this.readingPersonalEmail && (!this.msgSearchHdrs.hasOwnProperty(this.subBoardCode)))
-		this.msgSearchHdrs[this.subBoardCode] = searchMsgbase(this.subBoardCode, this.searchType, this.searchString, this.readingPersonalEmailFromUser);
-
-	// Determine the index of the message to start at.  This will be
-	// pStartingMsgOffset if pStartingMsgOffset is valid, or the index
-	// of the user's last-read message in this sub-board.
-	var msgIndex = 0;
-	if ((typeof(pStartingMsgOffset) == "number") && (pStartingMsgOffset >= 0) && (pStartingMsgOffset < this.NumMessages()))
-		msgIndex = pStartingMsgOffset;
-	else if (this.SearchingAndResultObjsDefinedForCurSub())
-		msgIndex = 0;
-	else
-	{
-		msgIndex = this.GetLastReadMsgIdxAndNum().lastReadMsgIdx;
-		if (msgIndex == -1)
-			msgIndex = 0;
-		else if (msgIndex >= numOfMessages)
-			msgIndex = numOfMessages - 1;
-	}
-
-	// If the current message index is for a message that has been
-	// deleted and the user is not able to read deleted messages, then find the next non-deleted message.
-	var testMsgHdr = this.GetMsgHdrByIdx(msgIndex);
-	// TODO: Should this really allow reading messages that are marked for deletion?
-	if ((testMsgHdr == null) || (((testMsgHdr.attr & MSG_DELETE) == MSG_DELETE) && !canViewDeletedMsgs()))
-	{
-		// First try going forward
-		var readableMsgIdx = this.FindNextReadableMsgIdx(msgIndex, true);
-		// If a non-deleted message was not found, then try going backward.
-		if (readableMsgIdx == -1)
-			readableMsgIdx = this.FindNextReadableMsgIdx(msgIndex, false);
-		// If a non-deleted message was found, then set msgIndex to it.
-		// Otherwise, tell the user there are no messages in this sub-board
-		// and return.
-		if (readableMsgIdx > -1)
-			msgIndex = readableMsgIdx;
-		else
-		{
-			console.clear("\x01n");
-			console.center("\x01h\x01yThere are no messages to display.");
-			console.crlf();
-			console.pause();
-			retObj.stoppedReading = true;
-			return retObj;
-		}
-	}
-
-	// Construct the hotkey help line (needs to be done after the message
-	// base is open so that the delete & edit keys can be added correctly).
-	this.SetEnhancedReaderHelpLine();
-
-	// Get the screen ready for reading messages - First, clear the screen.
-	console.clear("\x01n");
-	// Display the help line at the bottom of the screen
-	if (this.scrollingReaderInterface && console.term_supports(USER_ANSI))
-		this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
-	// Input loop
-	var msgHdr = null;
-	var dateTimeStr = null;
-	var screenY = 1; // For screen updates requiring more than one line
-	var continueOn = true;
-	var readMsgRetObj = null;
-	// previousNextAction will store the next action from the previous iteration.
-	// It is useful for some checks, such as when the current message is deleted,
-	// we'll want to see if the user wanted to go to the previous message/area
-	// for navigation purposes.
-	var previousNextAction = ACTION_NONE;
-	while (continueOn && (msgIndex >= 0) && (msgIndex < this.NumMessages()))
-	{
-		// Display the message with the enhanced read method
-		readMsgRetObj = this.ReadMessageEnhanced(msgIndex, allowChgMsgArea);
-		retObj.lastUserInput = readMsgRetObj.lastKeypress;
-		retObj.lastAction = readMsgRetObj.nextAction;
-		// If we should refresh the enhanced reader help line on the screen (and
-		// the returned message offset is valid and the user's terminal supports ANSI),
-		// then refresh the help line.
-		if (readMsgRetObj.refreshEnhancedRdrHelpLine && readMsgRetObj.offsetValid && console.term_supports(USER_ANSI))
-			this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
-		// If the returned message offset is invalid, then quit.
-		if (!readMsgRetObj.offsetValid)
-		{
-			continueOn = false;
-			retObj.stoppedReading = true;
-			break;
-		}
-		// If the message is not readable to the user, then go to the
-		// next/previous message
-		else if (readMsgRetObj.msgNotReadable)
-		{
-			// If the user's next action in the last iteration was to go to the
-			// previous message, then go backwards; otherwise, go forward.
-			if (previousNextAction == ACTION_GO_PREVIOUS_MSG)
-				msgIndex = this.FindNextReadableMsgIdx(msgIndex, false);
-			else
-				msgIndex = this.FindNextReadableMsgIdx(msgIndex, true);
-			continueOn = ((msgIndex >= 0) && (msgIndex < this.NumMessages()));
-		}
-		else if (readMsgRetObj.nextAction == ACTION_QUIT) // Quit
-		{
-			// Quit
-			continueOn = false;
-			retObj.stoppedReading = true;
-			break;
-		}
-		else if (readMsgRetObj.lastKeypress == "R")
-		{
-			// Replying to the message is handled in ReadMessageEnhanced().
-			// The help line at the bottom of the screen needs to be redrawn though,
-			// for ANSI users.
-			if (this.scrollingReaderInterface && console.term_supports(USER_ANSI))
-				this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
-		}
-		else if (readMsgRetObj.nextAction == ACTION_GO_PREVIOUS_MSG) // Go to previous message/area
-		{
-			// TODO: There is some opportunity for screen redraw optimization - If
-			// already at the first readable sub-board, this would redraw the
-			// screen unnecessarily.  Similar for the right arrow key too.
-
-			// The newMsgOffset value will be 0 or more if a prior non-deleted
-			// message was found.  If it's -1, then allow going to the previous
-			// message sub-board/group.
-			if (readMsgRetObj.newMsgOffset > -1)
-				msgIndex = readMsgRetObj.newMsgOffset;
-			else
-			{
-				// The user is at the beginning of the current sub-board.
-				if (allowChgMsgArea)
-				{
-					if (this.SearchTypePopulatesSearchResults())
-						console.print("\x01n\r\nLoading messages...");
-					var goToPrevRetval = this.GoToPrevSubBoardForEnhReader(allowChgMsgArea, pPromptToGoToNextAreaIfNoSearchResults);
-					retObj.stoppedReading = goToPrevRetval.shouldStopReading;
-					// If we're going to stop reading, then 
-					if (retObj.stoppedReading)
-						msgIndex = 0;
-					else if (goToPrevRetval.changedMsgArea)
-						msgIndex = goToPrevRetval.msgIndex;
-				}
-
-				// If the caller wants this method to return instead of going to the next
-				// sub-board with messages, then do so.
-				if (pReturnOnNextAreaNav)
-					return retObj;
-			}
-		}
-		// Go to next message action - This can happen with the right arrow key or
-		// if the user deletes the message in the ReadMessageEnhanced() method.
-		else if (readMsgRetObj.nextAction == ACTION_GO_NEXT_MSG)
-		{
-			// The newMsgOffset value will be 0 or more if a later non-deleted
-			// message was found.  If it's -1, then allow going to the next
-			// message sub-board/group.
-			if (readMsgRetObj.newMsgOffset > -1)
-				msgIndex = readMsgRetObj.newMsgOffset;
-			else
-			{
-				// The user is at the end of the current sub-board.
-				if (allowChgMsgArea && !pReturnOnNextAreaNav)
-				{
-					if (this.SearchTypePopulatesSearchResults())
-						console.print("\x01n\r\nLoading messages...");
-					var goToNextRetval = this.GoToNextSubBoardForEnhReader(allowChgMsgArea, pPromptToGoToNextAreaIfNoSearchResults);
-					retObj.stoppedReading = goToNextRetval.shouldStopReading;
-					// If we're going to stop reading, then 
-					if (retObj.stoppedReading)
-						msgIndex = 0;
-					else if (goToNextRetval.changedMsgArea)
-						msgIndex = goToNextRetval.msgIndex;
-				}
-				// If the caller wants this method to return instead of going to the next
-				// sub-board with messages, then do so.
-				if (pReturnOnNextAreaNav)
-					return retObj;
-			}
-		}
-		else if (readMsgRetObj.nextAction == ACTION_GO_FIRST_MSG) // Go to the first message
-		{
-			// Go to the first message that's not marked as deleted.  This passes -1 as the
-			// starting message index because FindNextReadableMsgIdx() will increment it
-			// before searching in order to find the "next" message.
-			msgIndex = this.FindNextReadableMsgIdx(-1, true);
-		}
-		else if (readMsgRetObj.nextAction == ACTION_GO_LAST_MSG) // Go to the last message
-		{
-			// Go to the last message that's not marked as deleted
-			msgIndex = this.FindNextReadableMsgIdx(this.NumMessages(), false);
-		}
-		else if (readMsgRetObj.nextAction == ACTION_CHG_MSG_AREA) // Change message area, if allowed
-		{
-			if (allowChgMsgArea)
-			{
-				// Change message sub-board.  If a different sub-board was
-				// chosen, then change some variables to use the new
-				// chosen sub-board.
-				var oldSubBoardCode = this.subBoardCode;
-				this.SelectMsgArea();
-				if (this.subBoardCode != oldSubBoardCode)
-					this.PopulateHdrsForCurrentSubBoard();
-				var chgSubBoardRetObj = this.EnhancedReaderChangeSubBoard(bbs.cursub_code);
-				if (chgSubBoardRetObj.succeeded)
-				{
-					// Set the message index, etc.
-					// If there are search results, then set msgIndex to the first
-					// message.  Otherwise (if there is no search specified), then
-					// set the message index to the user's last read message.
-					if (this.SearchingAndResultObjsDefinedForCurSub())
-						msgIndex = 0;
-					else
-						msgIndex = chgSubBoardRetObj.lastReadMsgIdx;
-					// If the current message index is for a message that has been
-					// deleted and the user is not able to read deleted messages, then find the next non-deleted message.
-					testMsgHdr = this.GetMsgHdrByIdx(msgIndex);
-					// TODO: Should this really allow reading deleted messages?
-					if ((testMsgHdr == null) || (((testMsgHdr.attr & MSG_DELETE) == MSG_DELETE) && !canViewDeletedMsgs()))
-					{
-						// First try going forward
-						var readableMsgIdx = this.FindNextReadableMsgIdx(msgIndex, true);
-						// If a non-deleted message was not found, then try going backward.
-						if (readableMsgIdx == -1)
-							readableMsgIdx = this.FindNextReadableMsgIdx(msgIndex, false);
-						// If a non-deleted message was found, then set msgIndex to it.
-						// Otherwise, return.
-						// Note: If there are no messages in the chosen sub-board at all,
-						// then the error would have already been shown.
-						if (readableMsgIdx > -1)
-							msgIndex = readableMsgIdx;
-						else
-						{
-							if (this.NumMessages() != 0)
-							{
-								// There are messages, but none that are not deleted.
-								console.clear("\x01n");
-								console.center("\x01h\x01yThere are no messages to display.");
-								console.crlf();
-								console.pause();
-							}
-							retObj.stoppedReading = true;
-							return retObj;
-						}
-					}
-					// Set the hotkey help line again, since the new sub-board might have
-					// different settings for whether messages can be edited or deleted,
-					// then refresh it on the screen.
-					var oldHotkeyHelpLine = this.enhReadHelpLine;
-					this.SetEnhancedReaderHelpLine();
-					if ((oldHotkeyHelpLine != this.enhReadHelpLine) && this.scrollingReaderInterface && console.term_supports(USER_ANSI))
-						this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
-				}
-				else
-				{
-					retObj.stoppedReading = false;
-					return retObj;
-				}
-			}
-		}
-		else if (readMsgRetObj.nextAction == ACTION_GO_PREV_MSG_AREA) // Go to the previous message area
-		{
-			// The user is at the beginning of the current sub-board.
-			if (allowChgMsgArea)
-			{
-				var goToPrevRetval = this.GoToPrevSubBoardForEnhReader(allowChgMsgArea, pPromptToGoToNextAreaIfNoSearchResults);
-				retObj.stoppedReading = goToPrevRetval.shouldStopReading;
-				if (retObj.stoppedReading)
-					msgIndex = 0;
-				else if (goToPrevRetval.changedMsgArea)
-					msgIndex = goToPrevRetval.msgIndex;
-			}
-			// If the caller wants this method to return instead of going to the next
-			// sub-board with messages, then do so.
-			if (pReturnOnNextAreaNav)
-				return retObj;
-		}
-		else if (readMsgRetObj.nextAction == ACTION_GO_NEXT_MSG_AREA) // Go to the next message area
-		{
-			if (allowChgMsgArea && !pReturnOnNextAreaNav)
-			{
-				var goToNextRetval = this.GoToNextSubBoardForEnhReader(allowChgMsgArea, pPromptToGoToNextAreaIfNoSearchResults);
-				retObj.stoppedReading = goToNextRetval.shouldStopReading;
-				if (retObj.stoppedReading)
-					msgIndex = 0;
-				else if (goToNextRetval.changedMsgArea)
-					msgIndex = goToNextRetval.msgIndex;
-			}
-			// If the caller wants this method to return instead of going to the next
-			// sub-board with messages, then do so.
-			if (pReturnOnNextAreaNav)
-				return retObj;
-		}
-		else if (readMsgRetObj.nextAction == ACTION_DISPLAY_MSG_LIST) // Display message list
-		{
-			// If we need to return to the caller for this, then do so.
-			if (pReturnOnMessageList)
-			{
-				retObj.messageListReturn = true;
-				return retObj;
-			}
-			else
-			{
-				// List messages
-				var listRetObj = this.ListMessages(null, pAllowChgArea);
-				// If the user wants to quit, then stop the input loop.
-				if (listRetObj.lastUserInput == "Q")
-				{
-					continueOn = false;
-					retObj.stoppedReading = true;
-				}
-				// If the user chose a different message, then set the message index
-				else if ((listRetObj.selectedMsgOffset > -1) && (listRetObj.selectedMsgOffset < this.NumMessages()))
-					msgIndex = listRetObj.selectedMsgOffset;
-			}
-		}
-		// Go to specific message & new message offset is valid: Read the new
-		// message
-		else if ((readMsgRetObj.nextAction == ACTION_GO_SPECIFIC_MSG) && (readMsgRetObj.newMsgOffset > -1))
-			msgIndex = readMsgRetObj.newMsgOffset;
-
-		// Save this iteration's next action for the "previous" next action for the next iteration
-		previousNextAction = readMsgRetObj.nextAction;
-	}
-
-	return retObj;
-}
-
-// For the DigDistMsgReader class: Performs the message listing, given a
-// sub-board code.
-//
-// Paramters:
-//  pSubBoardCode: Optional - The internal sub-board code, or "mail"
-//                 for personal email.
-//  pAllowChgSubBoard: Optional - A boolean to specify whether or not to allow
-//                     changing to another sub-board.  Defaults to true.
-// Return value: An object containing the following properties:
-//               lastUserInput: The user's last keypress/input
-//               selectedMsgOffset: The index of the message selected to read,
-//                                  if one was selected.  If none was selected,
-//                                  this will be -1.
-function DigDistMsgReader_ListMessages(pSubBoardCode, pAllowChgSubBoard)
-{
-	var retObj = {
-		lastUserInput: "",
-		selectedMsgOffset: -1
-	};
-
-	// If the passed-in sub-board code was different than what was set in the object before,
-	// then open the new message sub-board.
-	var previousSubBoardCode = this.subBoardCode;
-	if (typeof(pSubBoardCode) == "string")
-	{
-		if (subBoardCodeIsValid(pSubBoardCode))
-			this.setSubBoardCode(pSubBoardCode);
-		else
-		{
-			console.print("\x01n\x01h\x01yWarning: \x01wThe Message Reader connot continue because an invalid");
-			console.crlf();
-			console.print("sub-board code was specified (" + pSubBoardCode + "). Please notify the sysop.");
-			console.crlf();
-			console.pause();
-			return retObj;
-		}
-	}
-	if (this.subBoardCode.length == 0)
-	{
-		console.print("\x01n\x01h\x01yWarning: \x01wThe Message Reader connot continue because no message\r\n");
-		console.print("sub-board was specified. Please notify the sysop.\r\n\x01p");
-		return retObj;
-	}
-
-	// If the user doesn't have permission to read the current sub-board, then
-	// don't allow the user to read it.
-	if (this.subBoardCode != "mail")
-	{
-		if (!msg_area.sub[this.subBoardCode].can_read)
-		{
-			var errorMsg = format(bbs.text(CantReadSub), msg_area.sub[this.subBoardCode].grp_name, msg_area.sub[this.subBoardCode].name);
-			console.print("\x01n" + errorMsg);
-			console.pause();
-			return retObj;
-		}
-	}
-
-	// If there are no messages to display in the current sub-board, then let the
-	// user know and exit.
-	if (this.NumMessages() == 0)
-	{
-		console.clear("\x01n");
-		console.center("\x01n\x01h\x01yThere are no messages to display.\r\n\x01p");
-		return retObj;
-	}
-
-	// Construct the traditional UI pause text and the line of help text for lightbar
-	// mode.  This adds the delete and edit keys if the user is allowed to delete & edit
-	// messages.
-	this.SetMsgListPauseTextAndLightbarHelpLine();
-
-	// List the messages using the lightbar or traditional interface, depending on
-	// what this.msgListUseLightbarListInterface is set to.  The lightbar interface requires ANSI.
-	if (this.msgListUseLightbarListInterface && canDoHighASCIIAndANSI())
-		retObj = this.ListMessages_Lightbar(pAllowChgSubBoard);
-	else
-		retObj = this.ListMessages_Traditional(pAllowChgSubBoard);
-	return retObj;
-}
-// For the DigDistMsgReader class: Performs the message listing, given a
-// sub-board code.  This version uses a traditional user interface, prompting
-// the user at the end of each page to continue, quit, or read a message.
-//
-// Parameters:
-//  pAllowChgSubBoard: Optional - A boolean to specify whether or not to allow
-//                     changing to another sub-board.  Defaults to true.
-//
-// Return value: An object containing the following properties:
-//               lastUserInput: The user's last keypress/input
-//               selectedMsgOffset: The index of the message selected to read,
-//                                  if one was selected.  If none was selected,
-//                                  this will be -1.
-function DigDistMsgReader_ListMessages_Traditional(pAllowChgSubBoard)
-{
-	var retObj = {
-		lastUserInput: "",
-		selectedMsgOffset: -1
-	};
-
-	// If the user doesn't have permission to read the current sub-board, then
-	// don't allow the user to read it.
-	if (this.subBoardCode != "mail")
-	{
-		if (!msg_area.sub[this.subBoardCode].can_read)
-		{
-			var errorMsg = format(bbs.text(CantReadSub), msg_area.sub[this.subBoardCode].grp_name, msg_area.sub[this.subBoardCode].name);
-			console.print("\x01n" + errorMsg);
-			console.pause();
-			return retObj;
-		}
-	}
-
-	// Reset this.readAMessage and deniedReadingmessage to false, in case the
-	// message listing has previously ended with them set to true.
-	this.readAMessage = false;
-	this.deniedReadingMessage = false;
-
-	var msgbase = new MsgBase(this.subBoardCode);
-	if (!msgbase.open())
-	{
-		console.center("\x01n\x01h\x01yError: \x01wUnable to open the sub-board.\r\n\x01p");
-		return retObj;
-	}
-
-	var allowChgSubBoard = (typeof(pAllowChgSubBoard) == "boolean" ? pAllowChgSubBoard : true);
-
-	// this.tradMsgListNumLines stores the maximum number of lines to write.  It's the number
-	// of rows on the user's screen - 3 to make room for the header line
-	// at the top, the question line at the bottom, and 1 extra line at
-	// the bottom of the screen so that displaying carriage returns
-	// doesn't mess up the position of the header lines at the top.
-	this.tradMsgListNumLines = console.screen_rows-3;
-	var nListStartLine = 2; // The first line number on the screen for the message list
-	// If we will be displaying the message group and sub-board in the
-	// header at the top of the screen (an additional 2 lines), then
-	// update this.tradMsgListNumLines and nListStartLine to account for this.
-	if (this.displayBoardInfoInHeader)
-	{
-		this.tradMsgListNumLines -= 2;
-		nListStartLine += 2;
-	}
-
-	// If the user's terminal doesn't support ANSI, then re-calculate
-	// this.tradMsgListNumLines - we won't be keeping the headers at the top of the
-	// screen.
-	if (!canDoHighASCIIAndANSI()) // Could also be !console.term_supports(USER_ANSI)
-		this.tradMsgListNumLines = console.screen_rows - 2;
-
-	this.RecalcMsgListWidthsAndFormatStrs();
-
-	// Clear the screen and write the header at the top
-	console.clear("\x01n");
-	this.WriteMsgListScreenTopHeader();
-
-	// If this.tradListTopMsgIdx hasn't been set yet, then get the index of the user's
-	// last read message and figure out which page it's on and set the top message
-	// index accordingly.
-	if (this.tradListTopMsgIdx == -1)
-		this.SetUpTraditionalMsgListVars();
-	// Write the message list
-	var continueOn = true;
-	var retvalObj = null;
-	var curpos = null; // Current character position
-	var lastScreen = false;
-	while (continueOn)
-	{
-		// Go to the top and write the current page of message information,
-		// then update curpos.
-		console.gotoxy(1, nListStartLine);
-		lastScreen = this.ListScreenfulOfMessages(this.tradListTopMsgIdx, this.tradMsgListNumLines);
-		curpos = console.getxy();
-		clearToEOS(curpos.y);
-		console.gotoxy(curpos);
-		// Prompt the user whether or not to continue or to read a message
-		// (by message number).
-		if (this.userSettings.listMessagesInReverse)
-			retvalObj = this.PromptContinueOrReadMsg((this.tradListTopMsgIdx == this.NumMessages()-1), lastScreen, allowChgSubBoard);
-		else
-			retvalObj = this.PromptContinueOrReadMsg((this.tradListTopMsgIdx == 0), lastScreen, allowChgSubBoard);
-		retObj.lastUserInput = retvalObj.userInput;
-		retObj.selectedMsgOffset = retvalObj.selectedMsgOffset;
-
-		continueOn = retvalObj.continueOn;
-		// TODO: Update this to use PageUp & PageDown keys for paging?  It would
-		// require updating PromptContinueOrReadMsg(), which would be non-trivial
-		// because that method uses console.getkeys() with a list of allowed keys
-		// and a message number limit.
-		if (continueOn)
-		{
-			// If the user chose to go to the previous page of listings,
-			// then subtract the appropriate number of messages from
-			// this.tradListTopMsgIdx in order to do so.
-			if (retvalObj.userInput == "P")
-			{
-				if (this.userSettings.listMessagesInReverse)
-				{
-					this.tradListTopMsgIdx += this.tradMsgListNumLines;
-					// If we go past the beginning, then we need to reset
-					// msgNum so we'll be at the beginning of the list.
-					var totalNumMessages = this.NumMessages();
-					if (this.tradListTopMsgIdx >= totalNumMessages)
-						this.tradListTopMsgIdx = totalNumMessages - 1;
-				}
-				else
-				{
-					this.tradListTopMsgIdx -= this.tradMsgListNumLines;
-					// If we go past the beginning, then we need to reset
-					// msgNum so we'll be at the beginning of the list.
-					if (this.tradListTopMsgIdx < 0)
-						this.tradListTopMsgIdx = 0;
-				}
-			}
-			// If the user chose to go to the next page, update
-			// this.tradListTopMsgIdx appropriately.
-			else if (retvalObj.userInput == "N")
-			{
-				if (this.userSettings.listMessagesInReverse)
-					this.tradListTopMsgIdx -= this.tradMsgListNumLines;
-				else
-					this.tradListTopMsgIdx += this.tradMsgListNumLines;
-			}
-			// First page
-			else if (retvalObj.userInput == "F")
-			{
-				if (this.userSettings.listMessagesInReverse)
-					this.tradListTopMsgIdx = this.NumMessages() - 1;
-				else
-					this.tradListTopMsgIdx = 0;
-			}
-			// Last page
-			else if (retvalObj.userInput == "L")
-			{
-				if (this.userSettings.listMessagesInReverse)
-				{
-					this.tradListTopMsgIdx = (this.NumMessages() % this.tradMsgListNumLines) - 1;
-					// If this.tradListTopMsgIdx is now invalid (below 0), then adjust it
-					// to properly display the last page of messages.
-					if (this.tradListTopMsgIdx < 0)
-						this.tradListTopMsgIdx = this.tradMsgListNumLines - 1;
-				}
-				else
-				{
-					var totalNumMessages = this.NumMessages();
-					this.tradListTopMsgIdx = totalNumMessages - (totalNumMessages % this.tradMsgListNumLines);
-					if (this.tradListTopMsgIdx >= totalNumMessages)
-						this.tradListTopMsgIdx = totalNumMessages - this.tradMsgListNumLines;
-				}
-			}
-			// D: Delete a message
-			else if (retvalObj.userInput == "D" || retvalObj.userInput == this.msgListKeys.deleteMessage || retvalObj.userInput == '\x7f' || retvalObj.userInput == '\x08')
-			{
-				if (retvalObj.userInput == '\x08')
-					console.crlf();
-				if (this.CanDelete() || this.CanDeleteLastMsg())
-				{
-					var msgNum = this.PromptForMsgNum({ x: curpos.x, y: curpos.y+1 }, replaceAtCodesInStr(this.text.deleteMsgNumPromptText), false, ERROR_PAUSE_WAIT_MS, false);
-					// If the user enters a valid message number, then call the
-					// DeleteMessage() method, which will prompt the user for
-					// confirmation and delete the message if confirmed.
-					if (msgNum > 0)
-						this.PromptAndDeleteOrUndeleteMessage(msgNum-1, null, true);
-
-					// Refresh the top header on the screen for continuing to list
-					// messages.
-					console.clear("\x01n");
-					this.WriteMsgListScreenTopHeader();
-				}
-			}
-			// E: Edit a message
-			else if (retvalObj.userInput == this.msgListKeys.editMsg) // "E"
-			{
-				if (this.CanEdit())
-				{
-					var msgNum = this.PromptForMsgNum({ x: curpos.x, y: curpos.y+1 }, replaceAtCodesInStr(this.text.editMsgNumPromptText), false, ERROR_PAUSE_WAIT_MS, false);
-					// If the user entered a valid message number, then let the
-					// user edit the message.
-					if (msgNum > 0)
-					{
-						// See if the current message header has our "isBogus" property and it's true.
-						// Only let the user edit the message if it's not a bogus message header.
-						// The message header could have the "isBogus" property, for instance, if
-						// it's a vote message (introduced in Synchronet 3.17).
-						var tmpMsgHdr = this.GetMsgHdrByIdx(msgNum-1);
-						if (tmpMsgHdr != null)
-							var returnObj = this.EditExistingMsg(msgNum-1);
-						else
-						{
-							console.print("\x01n\r\n\x01h\x01yThat message isn't editable.\x01n");
-							console.crlf();
-							console.pause();
-						}
-					}
-
-					// Refresh the top header on the screen for continuing to list
-					// messages.
-					console.clear("\x01n");
-					this.WriteMsgListScreenTopHeader();
-				}
-			}
-			// G: Go to a specific message by # (place that message on the top)
-			else if (retvalObj.userInput == this.msgListKeys.goToMsg)
-			{
-				var msgNum = this.PromptForMsgNum(curpos, "\x01n" + replaceAtCodesInStr(this.text.goToMsgNumPromptText), false, ERROR_PAUSE_WAIT_MS, false);
-				if (msgNum > 0)
-					this.tradListTopMsgIdx = msgNum - 1;
-
-				// Refresh the top header on the screen for continuing to list
-				// messages.
-				console.clear("\x01n");
-				this.WriteMsgListScreenTopHeader();
-			}
-			// ?: Display help
-			else if (retvalObj.userInput == "?")
-			{
-				console.clear("\x01n");
-				this.DisplayMsgListHelp(!this.readingPersonalEmail && allowChgSubBoard, true);
-				console.clear("\x01n");
-				this.WriteMsgListScreenTopHeader();
-			}
-			// C: Change to another message area (sub-board)
-			else if (retvalObj.userInput == this.msgListKeys.chgMsgArea) // "C"
-			{
-				if (allowChgSubBoard && (this.subBoardCode != "mail"))
-				{
-					// Store the current sub-board code so we can see if it changed
-					var oldSubCode = bbs.cursub_code;
-					// Let the user choose another message area.  If they chose
-					// a different message area, then set up the message base
-					// object accordingly.
-					this.SelectMsgArea();
-					if (bbs.cursub_code != oldSubCode)
-					{
-						var chgSubRetval = this.ChangeSubBoard(bbs.cursub_code);
-						continueOn = chgSubRetval.succeeded;
-					}
-					// Update the traditional list variables and refresh the screen
-					if (continueOn)
-					{
-						this.SetUpTraditionalMsgListVars();
-						console.clear("\x01n");
-						this.WriteMsgListScreenTopHeader();
-					}
-				}
-			}
-			// S: Select message(s)
-			else if (retvalObj.userInput == "S")
-			{
-				// Input the message number list from the user
-				console.print("\x01n\x01cNumber(s) of message(s) to select, (\x01hA\x01n\x01c=All, \x01hN\x01n\x01c=None, \x01hENTER\x01n\x01c=cancel)\x01g\x01h: \x01c");
-				var userNumberList = console.getstr(128, K_UPPER);
-				// If the user entered A or N, then select/un-select all messages.
-				// Otherwise, select only the messages that the user entered.
-				if ((userNumberList == "A") || (userNumberList == "N"))
-				{
-					var messageSelectToggle = (userNumberList == "A");
-					var totalNumMessages = this.NumMessages();
-					for (var msgIdx = 0; msgIdx < totalNumMessages; ++msgIdx)
-						this.ToggleSelectedMessage(this.subBoardCode, msgIdx, messageSelectToggle);
-				}
-				else
-				{
-					if (userNumberList.length > 0)
-					{
-						var numArray = parseNumberList(userNumberList);
-						for (var numIdx = 0; numIdx < numArray.length; ++numIdx)
-							this.ToggleSelectedMessage(this.subBoardCode, numArray[numIdx]-1);
-					}
-				}
-				// Refresh the top header on the screen for continuing to list
-				// messages.
-				console.clear("\x01n");
-				this.WriteMsgListScreenTopHeader();
-			}
-			// Ctrl-D: Batch delete (for selected messages)
-			else if (retvalObj.userInput == this.msgListKeys.batchDelete) // CTRL_D
-			{
-				console.attributes = "N";
-				console.crlf();
-				if (this.NumSelectedMessages() > 0)
-				{
-					// The PromptAndDeleteOrUndeleteSelectedMessages() method will prompt the user for confirmation
-					// to delete the message and then delete it if confirmed.
-					this.PromptAndDeleteOrUndeleteSelectedMessages(null, true);
-
-					// In case all messages were deleted, if the user can't view deleted messages,
-					// show an appropriate message and don't continue listing messages.
-					//if (this.NumMessages() == 0)
-					if (!this.NonDeletedMessagesExist() && !canViewDeletedMsgs())
-					{
-						continueOn = false;
-						// Note: The following doesn't seem to be necessary, since
-						// the ReadOrListSubBoard() method will show a message saying
-						// there are no messages to read and then will quit out.
-						
-						//msgbase.close();
-						//msgbase = null;
-						//console.clear("\x01n");
-						//console.center("\x01n\x01h\x01yThere are no messages to display.");
-						//console.crlf();
-						//console.pause();
-						
-					}
-					else
-					{
-						// There are still messages to list, so refresh the top
-						// header on the screen for continuing to list messages.
-						console.clear("\x01n");
-						this.WriteMsgListScreenTopHeader();
-					}
-				}
-				else
-				{
-					// There are no selected messages
-					console.print("\x01n\x01h\x01yThere are no selected messages.");
-					mswait(ERROR_PAUSE_WAIT_MS);
-					// Refresh the top header on the screen for continuing to list messages.
-					console.clear("\x01n");
-					this.WriteMsgListScreenTopHeader();
-				}
-			}
-			// User settings
-			else if (retvalObj.userInput == this.msgListKeys.userSettings)
-			{
-				/*
-				var continueOn = true;
-				var retvalObj = null;
-				var curpos = null; // Current character position
-				var lastScreen = false;
-
-				this.RecalcMsgListWidthsAndFormatStrs();
-				if (this.tradListTopMsgIdx == -1)
-					this.SetUpTraditionalMsgListVars();
-				this.WriteMsgListScreenTopHeader();
-				*/
-				var userSettingsRetObj = this.DoUserSettings_Traditional();
-				retvalObj.userInput = "";
-				//drawMenu = userSettingsRetObj.needWholeScreenRefresh;
-				// In case the user changed their twitlist, re-filter the messages for this sub-board
-				if (userSettingsRetObj.userTwitListChanged)
-				{
-					console.gotoxy(1, console.screen_rows);
-					console.crlf();
-					console.print("\x01nTwitlist changed; re-filtering..");
-					var tmpMsgbase = new MsgBase(this.subBoardCode);
-					if (tmpMsgbase.open())
-					{
-						var tmpAllMsgHdrs = tmpMsgbase.get_all_msg_headers(true);
-						tmpMsgbase.close();
-						this.FilterMsgHdrsIntoHdrsForCurrentSubBoard(tmpAllMsgHdrs, true);
-					}
-					else
-						console.print("\x01y\x01hFailed to open the messagbase!\x01\r\n\x01p");
-					this.RecalcMsgListWidthsAndFormatStrs();
-					if (this.tradListTopMsgIdx == -1)
-						this.SetUpTraditionalMsgListVars();
-					// If there are still messages in this sub-board, and the message offset is beyond the last
-					// message, then adjust the top message index as necessary.
-					if (this.hdrsForCurrentSubBoard.length > 0)
-					{
-						if (this.tradListTopMsgIdx > this.hdrsForCurrentSubBoard.length)
-							this.tradListTopMsgIdx = this.hdrsForCurrentSubBoard.length - this.tradMsgListNumLines;
-					}
-					else
-					{
-						continueOn = false;
-						retObj.selectedMsgOffset = -1;
-					}
-				}
-				if (userSettingsRetObj.needWholeScreenRefresh)
-					this.WriteMsgListScreenTopHeader();
-			}
-			else
-			{
-				// If a message has been selected, exit out of this input loop
-				// so we can return from this method - The calling method will
-				// call the enhanced reader method.
-				if (retObj.selectedMsgOffset >= 0)
-					continueOn = false;
-			}
-		}
-	}
-
-	msgbase.close();
-
-	return retObj;
-}
-// For the DigDistMsgReader class: Performs the message listing, given a
-// sub-board code.  This verison uses a lightbar interface for message
-// navigation.
-//
-// Parameters:
-//  pAllowChgSubBoard: Optional - A boolean to specify whether or not to allow
-//                     changing to another sub-board.  Defaults to true.
-//
-// Return value: An object containing the following properties:
-//               lastUserInput: The user's last keypress/input
-//               selectedMsgOffset: The index of the message selected to read,
-//                                  if one was selected.  If none was selected,
-//                                  this will be -1.
-function DigDistMsgReader_ListMessages_Lightbar(pAllowChgSubBoard)
-{
-	var retObj = {
-		lastUserInput: "",
-		selectedMsgOffset: -1
-	};
-
-	// If the user doesn't have permission to read the current sub-board, then
-	// don't allow the user to read it.
-	if (this.subBoardCode != "mail")
-	{
-		if (!msg_area.sub[this.subBoardCode].can_read)
-		{
-			var errorMsg = format(bbs.text(CantReadSub), msg_area.sub[this.subBoardCode].grp_name, msg_area.sub[this.subBoardCode].name);
-			console.print("\x01n" + errorMsg);
-			console.pause();
-			return retObj;
-		}
-	}
-
-	// This method is only supported if the user's terminal supports
-	// ANSI.
-	if (!canDoHighASCIIAndANSI()) // Could also be !console.term_supports(USER_ANSI)
-	{
-		console.print("\r\n\x01h\x01ySorry, an ANSI terminal is required for this operation.\x01n\x01w\r\n");
-		console.pause();
-		return retObj;
-	}
-
-	// Reset this.readAMessage and deniedReadingMessage to false, in case the
-	// message listing has previously ended with them set to true.
-	this.readAMessage = false;
-	this.deniedReadingMessage = false;
-
-	this.RecalcMsgListWidthsAndFormatStrs();
-
-	var allowChgSubBoard = (typeof(pAllowChgSubBoard) == "boolean" ? pAllowChgSubBoard : true);
-
-	// This function will be used for displaying the help line at
-	// the bottom of the screen.
-	function DisplayHelpLine(pHelpLineText)
-	{
-		console.gotoxy(1, console.screen_rows);
-		// Mouse: console.print replaced with console.putmsg for mouse click hotspots
-		//console.print(pHelpLineText);
-		console.putmsg(pHelpLineText); // console.putmsg() can process @-codes, which we use for mouse click tracking
-		console.cleartoeol("\x01n");
-	}
-
-	// Clear the screen and write the header at the top
-	console.clear("\x01n");
-	this.WriteMsgListScreenTopHeader();
-	DisplayHelpLine(this.msgListLightbarModeHelpLine);
-
-	// If the lightbar message list index & cursor position variables haven't been
-	// set yet, then set them.
-	if ((this.lightbarListTopMsgIdx == -1) || (this.lightbarListSelectedMsgIdx == -1) ||
-	    (this.lightbarListCurPos == null))
-	{
-		this.SetUpLightbarMsgListVars();
-	}
-
-	// Create a DDLightbarMenu for the message list and list messages
-	// and let the user choose one
-	var msgListMenu = this.CreateLightbarMsgListMenu();
-	// numMessages & numMessagesPerPage are used with msgListMenu.CalcPageForItemAndSetTopItemIdx() when
-	// going to a specific message. They're optional, but it can be faster to get them just once instead of
-	// every time.
-	var numMessages = msgListMenu.NumItems();
-	var numMessagesPerPage = msgListMenu.GetNumItemsPerPage();
-	var msgHeader = null;
-	var drawMenu = true;
-	var continueOn = true;
-	while (continueOn)
-	{
-		var userChoice = msgListMenu.GetVal(drawMenu);
-		drawMenu = true;
-		var lastUserInputUpper = (typeof(msgListMenu.lastUserInput) == "string" ? msgListMenu.lastUserInput.toUpperCase() : msgListMenu.lastUserInput);
-		// If the user's last input is null, then something bad/weird must have
-		// happened, so don't continue the input loop.
-		if (lastUserInputUpper == null)
-		{
-			continueOn = false;
-			break;
-		}
-		this.lightbarListSelectedMsgIdx = msgListMenu.selectedItemIdx;
-		// If userChoice is a number, then it will be a message number for a message to read
-		if (typeof(userChoice) == "number")
-		{
-			// The user choice a message to read
-			this.lightbarListSelectedMsgIdx = msgListMenu.selectedItemIdx;
-			msgHeader = this.GetMsgHdrByIdx(this.lightbarListSelectedMsgIdx, this.showScoresInMsgList);
-			// TODO: Is this commented-out code necessary?  In indexed newscan mode, when reading a
-			// message, then switching to the list, then selecting a message to read, this is re-printing
-			// the message info line from the message list and it's appearing over the help line at the
-			// bottom of the screen.
-			//if (msgHeader != null)
-			//	this.PrintMessageInfo(msgHeader, true, this.lightbarListSelectedMsgIdx+1);
-			console.gotoxy(this.lightbarListCurPos); // Make sure the cursor is still in the right place
-			if (msgHeader != null)
-			{
-				// Allow the user to read the current message.
-				var readMsg = true;
-				if (this.promptToReadMessage)
-				{
-					// Confirm with the user whether to read the message.
-					var sReadMsgConfirmText = this.colors.readMsgConfirmColor
-											+ "Read message "
-											+ this.colors.readMsgConfirmNumberColor
-											+ +(this.GetMsgIdx(msgHeader.number) + 1)
-											+ this.colors.readMsgConfirmColor
-											+ ": Are you sure";
-					console.gotoxy(1, console.screen_rows);
-					console.attributes = "N";
-					console.clearline();
-					readMsg = console.yesno(sReadMsgConfirmText);
-				}
-				if (readMsg)
-				{
-					// If there is a search specified and the search result objects are
-					// set up for the current sub-board, then the selected message offset
-					// should be the search result array index.  Otherwise (if not
-					// searching), the message offset should be the actual message offset
-					// in the message base.
-					if (this.SearchingAndResultObjsDefinedForCurSub())
-						retObj.selectedMsgOffset = this.lightbarListSelectedMsgIdx;
-					else
-					{
-						//retObj.selectedMsgOffset = msgHeader.offset;
-						retObj.selectedMsgOffset = this.GetMsgIdx(msgHeader.number);
-						if (retObj.selectedMsgOffset < 0)
-							retObj.selectedMsgOffset = 0;
-					}
-					// Return from here so that the calling function can switch into
-					// reader mode.
-					continueOn = false;
-					return retObj;
-				}
-				else
-					this.deniedReadingMessage = true;
-
-				// Ask the user if  they want to continue reading messages
-				if (this.promptToContinueListingMessages)
-					continueOn = console.yesno(this.colors["afterReadMsg_ListMorePromptColor"] + "Continue listing messages");
-				// If the user chose to continue reading messages, then refresh
-				// the screen.  Even if the user chooses not to read the message,
-				// the screen needs to be re-drawn so it appears properly.
-				if (continueOn)
-				{
-					this.WriteMsgListScreenTopHeader();
-					DisplayHelpLine(this.msgListLightbarModeHelpLine);
-				}
-			}
-		}
-		// If userChoice is not a number, then it should be null in this case,
-		// and the user would have pressed one of the additional quit keys set
-		// up for the menu.  So look at the menu's lastUserInput and do the
-		// appropriate thing.
-		else if ((lastUserInputUpper == this.msgListKeys.quit) || (lastUserInputUpper == KEY_ESC)) // Quit
-		{
-			continueOn = false;
-			retObj.lastUserInput = "Q"; // So the reader will quit out
-		}
-		// Numeric digit: The start of a number of a message to read
-		else if (lastUserInputUpper.match(/[0-9]/))
-		{
-			// Put the user's input back in the input buffer to
-			// be used for getting the rest of the message number.
-			console.ungetstr(lastUserInputUpper);
-			// Move the cursor to the bottom of the screen and
-			// prompt the user for the message number.
-			console.gotoxy(1, console.screen_rows);
-			var userInput = this.PromptForMsgNum({ x: 1, y: console.screen_rows }, replaceAtCodesInStr(this.text.readMsgNumPromptText), true, ERROR_PAUSE_WAIT_MS, false);
-			if (userInput > 0)
-			{
-				// See if the current message header has our "isBogus" property and it's true.
-				// Only let the user read the message if it's not a bogus message header.
-				// The message header could have the "isBogus" property, for instance, if
-				// it's a vote message (introduced in Synchronet 3.17).
-				//GetMsgHdrByIdx(pMsgIdx, pExpandFields)
-				var tmpMsgHdr = this.GetMsgHdrByIdx(+(userInput-1), false);
-				if (tmpMsgHdr != null)
-				{
-					// Confirm with the user whether to read the message
-					var readMsg = true;
-					if (this.promptToReadMessage)
-					{
-						var sReadMsgConfirmText = this.colors.readMsgConfirmColor
-												+ "Read message "
-												+ this.colors.readMsgConfirmNumberColor
-												+ userInput + this.colors.readMsgConfirmColor
-												+ ": Are you sure";
-												readMsg = console.yesno(sReadMsgConfirmText);
-					}
-					if (readMsg)
-					{
-						// Update the message list screen variables
-						this.CalcMsgListScreenIdxVarsFromMsgNum(+userInput);
-						retObj.selectedMsgOffset = userInput - 1;
-						// Return from here so that the calling function can switch
-						// into reader mode.
-						return retObj;
-					}
-					else
-						this.deniedReadingMessage = true;
-
-					// Prompt the user whether or not to continue listing
-					// messages.
-					if (this.promptToContinueListingMessages)
-						continueOn = console.yesno(this.colors.afterReadMsg_ListMorePromptColor + "Continue listing messages");
-				}
-				else
-					writeWithPause(1, console.screen_rows, "\x01n\x01h\x01yThat's not a readable message.", ERROR_PAUSE_WAIT_MS, "\x01n", true);
-			}
-
-			// If the user chose to continue listing messages, then re-draw
-			// the screen.
-			if (continueOn)
-			{
-				this.WriteMsgListScreenTopHeader();
-				DisplayHelpLine(this.msgListLightbarModeHelpLine);
-			}
-		}
-		// DEL key: Delete a message
-		else if (lastUserInputUpper == this.msgListKeys.deleteMessage || lastUserInputUpper == '\x7f' || lastUserInputUpper == '\x08')
-		{
-			if (this.CanDelete() || this.CanDeleteLastMsg())
-			{
-				console.gotoxy(1, console.screen_rows);
-				console.attributes = "N";
-				console.clearline();
-
-				// The PromptAndDeleteOrUndeleteMessage() method will prompt the user for confirmation
-				// to delete the message and then delete it if confirmed.
-				this.PromptAndDeleteOrUndeleteMessage(this.lightbarListSelectedMsgIdx, { x: 1, y: console.screen_rows}, true);
-
-				// In case all messages were deleted, if the user can't view deleted messages,
-				// show an appropriate message and don't continue listing messages.
-				//if (this.NumMessages() == 0)
-				if (!this.NonDeletedMessagesExist() && !canViewDeletedMsgs())
-					continueOn = false;
-				else
-				{
-					// There are still some messages to show, so refresh the screen.
-					// Refresh the header & help line.
-					this.WriteMsgListScreenTopHeader();
-					DisplayHelpLine(this.msgListLightbarModeHelpLine);
-				}
-			}
-		}
-		// E: Edit a message
-		else if (lastUserInputUpper == this.msgListKeys.editMsg)
-		{
-			if (this.CanEdit())
-			{
-				// See if the current message header has our "isBogus" property and it's true.
-				// Only let the user edit the message if it's not a bogus message header.
-				// The message header could have the "isBogus" property, for instance, if
-				// it's a vote message (introduced in Synchronet 3.17).
-				var tmpMsgHdr = this.GetMsgHdrByIdx(this.lightbarListSelectedMsgIdx, false);
-				if (tmpMsgHdr != null)
-				{
-					// Ask the user if they really want to edit the message
-					console.gotoxy(1, console.screen_rows);
-					console.attributes = "N";
-					console.clearline();
-					// Let the user edit the message
-					//var returnObj = this.EditExistingMsg(tmpMsgHdr.offset);
-					var returnObj = this.EditExistingMsg(this.lightbarListSelectedMsgIdx);
-					// Refresh the header & help line
-					this.WriteMsgListScreenTopHeader();
-					DisplayHelpLine(this.msgListLightbarModeHelpLine);
-				}
-			}
-			else
-				drawMenu = false; // No need to re-draw the menu
-		}
-		// G: Go to a specific message by # (highlight or place that message on the top, or in the page if can't put it on top)
-		else if (lastUserInputUpper == this.msgListKeys.goToMsg)
-		{
-			// Move the cursor to the bottom of the screen and
-			// prompt the user for a message number.
-			console.gotoxy(1, console.screen_rows);
-			var userMsgNum = this.PromptForMsgNum({ x: 1, y: console.screen_rows }, "\n" + replaceAtCodesInStr(this.text.goToMsgNumPromptText), true, ERROR_PAUSE_WAIT_MS, false);
-			if (userMsgNum > 0)
-			{
-				// Make sure the message number is for a valid message (i.e., it
-				// could be an invalid message number if there is a search, where
-				// not all message numbers are consecutive).
-				if (this.GetMsgHdrByMsgNum(userMsgNum) != null)
-				{
-					// If the message is on the current page, then just go to and
-					// highlight it.  Otherwise, set the user's selected message on the
-					// top of the page.  We also have to make sure that this.lightbarListCurPos.y and
-					// originalCurpos.y are set correctly.  Also, account for search
-					// results if there are any (we'll need to have the correct array
-					// index for the search results).
-					var chosenMsgIndex = userMsgNum - 1;
-					msgListMenu.selectedItemIdx = chosenMsgIndex;
-					if ((chosenMsgIndex < msgListMenu.NumItems()) && (chosenMsgIndex >= this.lightbarListTopMsgIdx))
-					{
-						this.lightbarListSelectedMsgIdx = chosenMsgIndex;
-						msgListMenu.selectedItemIdx = this.lightbarListSelectedMsgIdx;
-						msgListMenu.CalcPageForItemAndSetTopItemIdx(numMessagesPerPage, numMessages);
-					}
-					else
-					{
-						this.lightbarListTopMsgIdx = this.lightbarListSelectedMsgIdx = chosenMsgIndex;
-						msgListMenu.topItemIdx = this.lightbarListTopMsgIdx;
-					}
-				}
-				else
-				{
-					// The user entered an invalid message number
-					console.print("\x01n" + replaceAtCodesInStr(format(this.text.invalidMsgNumText, userMsgNum)) + "\x01n");
-					console.inkey(K_NONE, ERROR_PAUSE_WAIT_MS);
-				}
-			}
-
-			// Refresh the header & help lines
-			this.WriteMsgListScreenTopHeader();
-			DisplayHelpLine(this.msgListLightbarModeHelpLine);
-		}
-		// C: Change to another message area (sub-board)
-		else if (lastUserInputUpper == this.msgListKeys.chgMsgArea)
-		{
-			if (allowChgSubBoard && (this.subBoardCode != "mail"))
-			{
-				// Store the current sub-board code so we can see if it changed
-				var oldSubCode = bbs.cursub_code;
-				// Let the user choose another message area.  If they chose
-				// a different message area, then set up the message base
-				// object accordingly.
-				this.SelectMsgArea();
-				if (bbs.cursub_code != oldSubCode)
-				{
-					var chgSubRetval = this.ChangeSubBoard(bbs.cursub_code);
-					continueOn = chgSubRetval.succeeded;
-					if (chgSubRetval.succeeded)
-					{
-						console.attributes = "N";
-						console.gotoxy(1, console.screen_rows);
-						console.cleartoeol("\x01n");
-						console.gotoxy(1, console.screen_rows);
-						console.print("Loading...");
-						this.PopulateHdrsForCurrentSubBoard();
-						this.SetUpLightbarMsgListVars();
-					}
-				}
-				// Update the lightbar list variables and refresh the header & help lines
-				if (continueOn)
-				{
-					console.clear("\x01n");
-					// Adjust the menu indexes to ensure they're correct for the current sub-board
-					this.AdjustLightbarMsgListMenuIdxes(msgListMenu);
-					this.WriteMsgListScreenTopHeader();
-					DisplayHelpLine(this.msgListLightbarModeHelpLine);
-				}
-			}
-			else
-				drawMenu = false; // No need to re-draw the menu
-		}
-		else if (lastUserInputUpper == this.msgListKeys.showHelp) // Show help
-		{
-			console.clear("\x01n");
-			this.DisplayMsgListHelp(!this.readingPersonalEmail && allowChgSubBoard, true);
-			// Re-draw the message list header & help line before
-			// the menu is re-drawn
-			this.WriteMsgListScreenTopHeader();
-			DisplayHelpLine(this.msgListLightbarModeHelpLine);
-		}
-		// Spacebar: Select a message for batch operations (such as batch
-		// delete, etc.)
-		else if (lastUserInputUpper == " ")
-		{
-			this.ToggleSelectedMessage(this.subBoardCode, this.lightbarListSelectedMsgIdx);
-			// Have the menu draw only the check character column in the
-			// next iteration
-			msgListMenu.nextDrawOnlyItemSubstr = { start: this.MSGNUM_LEN, end: this.MSGNUM_LEN+1 };
-		}
-		// Ctrl-A: Select/de-select all messages
-		else if (lastUserInputUpper == CTRL_A)
-		{
-			console.gotoxy(1, console.screen_rows);
-			console.attributes = "N";
-			console.clearline();
-			console.gotoxy(1, console.screen_rows);
-
-			// Prompt the user to select All, None (un-select all), or Cancel
-			console.print("\x01n\x01gSelect \x01c(\x01hA\x01n\x01c)\x01gll, \x01c(\x01hN\x01n\x01c)\x01gone, or \x01c(\x01hC\x01n\x01c)\x01gancel: \x01h\x01g");
-			var userChoice = getAllowedKeyWithMode("ANC", K_UPPER | K_NOCRLF);
-			if ((userChoice == "A") || (userChoice == "N"))
-			{
-				// Toggle all the messages
-				var messageSelectToggle = (userChoice == "A");
-				var totalNumMessages = this.NumMessages();
-				var messageIndex = 0;
-				for (messageIndex = 0; messageIndex < totalNumMessages; ++messageIndex)
-					this.ToggleSelectedMessage(this.subBoardCode, messageIndex, messageSelectToggle);
-				// Have the menu draw only the check character column in the
-				// next iteration
-				msgListMenu.nextDrawOnlyItemSubstr = { start: this.MSGNUM_LEN, end: this.MSGNUM_LEN+1 };
-			}
-			else
-				drawMenu = false; // No need to re-draw the menu
-
-			// Refresh the help line
-			DisplayHelpLine(this.msgListLightbarModeHelpLine);
-		}
-		// Ctrl-D: Batch delete (for selected messages)
-		else if (lastUserInputUpper == this.msgListKeys.batchDelete) // CTRL_D
-		{
-			if (this.CanDelete() || this.CanDeleteLastMsg())
-			{
-				if (this.NumSelectedMessages() > 0)
-				{
-					console.gotoxy(1, console.screen_rows);
-					console.attributes = "N";
-					console.clearline();
-
-					// The PromptAndDeleteOrUndeleteSelectedMessages() method will prompt the user for confirmation
-					// to delete the message and then delete it if confirmed.
-					var delSuccessful = this.PromptAndDeleteOrUndeleteSelectedMessages({ x: 1, y: console.screen_rows}, true);
-					// If successfully deleted, have the menu draw only the check character column in the
-					// next iteration
-					if (delSuccessful)
-					{
-						this.selectedMessages = {};
-						drawMenu = true;
-					}
-
-					// In case all messages were deleted, if the user can't view deleted messages,
-					// show an appropriate message and don't continue listing messages.
-					//if (this.NumMessages() == 0)
-					if (!this.NonDeletedMessagesExist() && !canViewDeletedMsgs())
-						continueOn = false;
-					else
-					{
-						// There are still messages to list, so refresh the header & help lines
-						this.WriteMsgListScreenTopHeader();
-						DisplayHelpLine(this.msgListLightbarModeHelpLine);
-					}
-				}
-				else
-				{
-					// There are no selected messages
-					writeWithPause(1, console.screen_rows, "\x01n\x01h\x01yThere are no selected messages.", ERROR_PAUSE_WAIT_MS, "\x01n", true);
-					// Refresh the help line
-					DisplayHelpLine(this.msgListLightbarModeHelpLine);
-				}
-			}
-		}
-		// U: Undelete message(s)
-		else if (lastUserInputUpper == this.msgListKeys.undeleteMessage)
-		{
-			if (this.CanDelete() || this.CanDeleteLastMsg())
-			{
-				console.gotoxy(1, console.screen_rows);
-				console.attributes = "N";
-				console.clearline();
-				if (this.NumSelectedMessages() > 0)
-				{
-					// Multi-message undelete
-					this.PromptAndDeleteOrUndeleteSelectedMessages({ x: 1, y: console.screen_rows}, false);
-				}
-				else
-				{
-					// Single-message undelete
-					this.PromptAndDeleteOrUndeleteMessage(this.lightbarListSelectedMsgIdx, { x: 1, y: console.screen_rows}, false);
-				}
-
-				// Refresh the header & help line.
-				this.WriteMsgListScreenTopHeader();
-				DisplayHelpLine(this.msgListLightbarModeHelpLine);
-			}
-		}
-		else if (lastUserInputUpper == this.msgListKeys.userSettings)
-		{
-			var userSettingsRetObj = this.DoUserSettings_Scrollable(function(pReader) { DisplayHelpLine(pReader.msgListLightbarModeHelpLine); });
-			lastUserInputUpper = "";
-			drawMenu = userSettingsRetObj.needWholeScreenRefresh;
-			// In case the user changed their twitlist, re-filter the messages for this sub-board
-			if (userSettingsRetObj.userTwitListChanged)
-			{
-				console.gotoxy(1, console.screen_rows);
-				console.crlf();
-				console.print("\x01nTwitlist changed; re-filtering..");
-				var tmpMsgbase = new MsgBase(this.subBoardCode);
-				if (tmpMsgbase.open())
-				{
-					var tmpAllMsgHdrs = tmpMsgbase.get_all_msg_headers(true);
-					tmpMsgbase.close();
-					this.FilterMsgHdrsIntoHdrsForCurrentSubBoard(tmpAllMsgHdrs, true);
-				}
-				else
-					console.print("\x01y\x01hFailed to open the messagbase!\x01\r\n\x01p");
-				this.SetUpLightbarMsgListVars();
-				msgListMenu = this.CreateLightbarMsgListMenu();
-				drawMenu = true;
-			}
-			if (userSettingsRetObj.needWholeScreenRefresh)
-			{
-				this.WriteMsgListScreenTopHeader();
-				DisplayHelpLine(this.msgListLightbarModeHelpLine);
-			}
-			else
-				msgListMenu.DrawPartialAbs(userSettingsRetObj.optionBoxTopLeftX, userSettingsRetObj.optionBoxTopLeftY, userSettingsRetObj.optionBoxWidth, userSettingsRetObj.optionBoxHeight);
-		}
-		// S: Sorting options
-		// TODO
-		else if (lastUserInputUpper == "S")
-		{
-			// Refresh the help line
-			DisplayHelpLine(this.msgListLightbarModeHelpLine);
-		}
-	}
-	this.lightbarListSelectedMsgIdx = msgListMenu.selectedItemIdx;
-	this.lightbarListTopMsgIdx = msgListMenu.topItemIdx;
-
-	return retObj;
-}
-// For the DigDistMsgLister class: Creates & returns a DDLightbarMenu for
-// performing the lightbar message list.
-function DigDistMsgReader_CreateLightbarMsgListMenu()
-{
-	// Start & end indexes for the various items in each message list row
-	var msgListIdxes = {
-		msgNumStart: 0,
-		msgNumEnd: this.MSGNUM_LEN,
-		selectMarkStart: this.MSGNUM_LEN,
-		selectMarkEnd: this.MSGNUM_LEN+2,
-	};
-	msgListIdxes.fromNameStart = this.MSGNUM_LEN + 2;
-	msgListIdxes.fromNameEnd = msgListIdxes.fromNameStart + +this.FROM_LEN + 1;
-	msgListIdxes.toNameStart = msgListIdxes.fromNameEnd;
-	msgListIdxes.toNameEnd = msgListIdxes.toNameStart + +this.TO_LEN + 1;
-	msgListIdxes.subjStart = msgListIdxes.toNameEnd;
-	msgListIdxes.subjEnd = msgListIdxes.subjStart + +this.SUBJ_LEN + 1;
-	if (this.showScoresInMsgList)
-	{
-		msgListIdxes.scoreStart = msgListIdxes.subjEnd;
-		msgListIdxes.scoreEnd = msgListIdxes.scoreStart + +this.SCORE_LEN + 1;
-		msgListIdxes.dateStart = msgListIdxes.scoreEnd;
-	}
-	else
-		msgListIdxes.dateStart = msgListIdxes.subjEnd;
-	msgListIdxes.dateEnd = msgListIdxes.dateStart + +this.DATE_LEN + 1;
-	msgListIdxes.timeStart = msgListIdxes.dateEnd;
-	msgListIdxes.timeEnd = console.screen_columns - 1; // msgListIdxes.timeStart + +this.TIME_LEN + 1;
-	var msgListMenuHeight = console.screen_rows - this.lightbarMsgListStartScreenRow;
-	var msgListMenu = new DDLightbarMenu(1, this.lightbarMsgListStartScreenRow, console.screen_columns, msgListMenuHeight);
-	msgListMenu.scrollbarEnabled = true;
-	msgListMenu.borderEnabled = false;
-	msgListMenu.SetColors({
-		itemColor: [{start: msgListIdxes.msgNumStart, end: msgListIdxes.msgNumEnd, attrs: this.colors.msgListMsgNumColor},
-		            {start: msgListIdxes.selectMarkStart, end: msgListIdxes.selectMarkEnd, attrs: this.colors.selectedMsgMarkColor},
-		            {start: msgListIdxes.fromNameStart, end: msgListIdxes.fromNameEnd, attrs: this.colors.msgListFromColor},
-		            {start: msgListIdxes.toNameStart, end: msgListIdxes.toNameEnd, attrs: this.colors.msgListToColor},
-		            {start: msgListIdxes.subjStart, end: msgListIdxes.subjEnd, attrs: this.colors.msgListSubjectColor},
-		            {start: msgListIdxes.dateStart, end: msgListIdxes.dateEnd, attrs: this.colors.msgListDateColor},
-		            {start: msgListIdxes.timeStart, end: msgListIdxes.timeEnd, attrs: this.colors.msgListTimeColor}],
-		altItemColor: [{start: msgListIdxes.msgNumStart, end: msgListIdxes.msgNumEnd, attrs: this.colors.msgListToUserMsgNumColor},
-		               {start: msgListIdxes.selectMarkStart, end: msgListIdxes.selectMarkEnd, attrs: this.colors.selectedMsgMarkColor},
-		               {start: msgListIdxes.fromNameStart, end: msgListIdxes.fromNameEnd, attrs: this.colors.msgListToUserFromColor},
-		               {start: msgListIdxes.toNameStart, end: msgListIdxes.toNameEnd, attrs: this.colors.msgListToUserToColor},
-		               {start: msgListIdxes.subjStart, end: msgListIdxes.subjEnd, attrs: this.colors.msgListToUserSubjectColor},
-		               {start: msgListIdxes.dateStart, end: msgListIdxes.dateEnd, attrs: this.colors.msgListToUserDateColor},
-		               {start: msgListIdxes.timeStart, end: msgListIdxes.timeEnd, attrs: this.colors.msgListToUserTimeColor}],
-		selectedItemColor: [{start: msgListIdxes.msgNumStart, end: msgListIdxes.msgNumEnd, attrs: this.colors.msgListMsgNumHighlightColor},
-		                    {start: msgListIdxes.selectMarkStart, end: msgListIdxes.selectMarkEnd, attrs: this.colors.selectedMsgMarkColor + this.colors.msgListHighlightBkgColor},
-		                    {start: msgListIdxes.fromNameStart, end: msgListIdxes.fromNameEnd, attrs: this.colors.msgListFromHighlightColor},
-		                    {start: msgListIdxes.toNameStart, end: msgListIdxes.toNameEnd, attrs: this.colors.msgListToHighlightColor},
-		                    {start: msgListIdxes.subjStart, end: msgListIdxes.subjEnd, attrs: this.colors.msgListSubjHighlightColor},
-		                    {start: msgListIdxes.dateStart, end: msgListIdxes.dateEnd, attrs: this.colors.msgListDateHighlightColor},
-		                    {start: msgListIdxes.timeStart, end: msgListIdxes.timeEnd, attrs: this.colors.msgListTimeHighlightColor}],
-		altSelectedItemColor: [{start: msgListIdxes.msgNumStart, end: msgListIdxes.msgNumEnd, attrs: this.colors.msgListMsgNumHighlightColor},
-		                       {start: msgListIdxes.selectMarkStart, end: msgListIdxes.selectMarkEnd, attrs: this.colors.selectedMsgMarkColor + this.colors.msgListHighlightBkgColor},
-		                       {start: msgListIdxes.fromNameStart, end: msgListIdxes.fromNameEnd, attrs: this.colors.msgListFromHighlightColor},
-		                       {start: msgListIdxes.toNameStart, end: msgListIdxes.toNameEnd, attrs: this.colors.msgListToHighlightColor},
-		                       {start: msgListIdxes.subjStart, end: msgListIdxes.subjEnd, attrs: this.colors.msgListSubjHighlightColor},
-		                       {start: msgListIdxes.dateStart, end: msgListIdxes.dateEnd, attrs: this.colors.msgListDateHighlightColor},
-		                       {start: msgListIdxes.timeStart, end: msgListIdxes.timeEnd, attrs: this.colors.msgListTimeHighlightColor}]
-	});
-	// If we are to show message vote scores in the list (i.e., if the
-	// user's terminal is wide enough), then splice in color specifiers
-	// for the score column.
-	if (this.showScoresInMsgList)
-	{
-		msgListMenu.colors.itemColor.splice(5, 0, {start: msgListIdxes.scoreStart, end: msgListIdxes.scoreEnd, attrs: this.colors.msgListScoreColor});
-		msgListMenu.colors.altItemColor.splice(5, 0, {start: msgListIdxes.scoreStart, end: msgListIdxes.scoreEnd, attrs: this.colors.msgListToUserScoreColor});
-		msgListMenu.colors.selectedItemColor.splice(5, 0, {start: msgListIdxes.scoreStart, end: msgListIdxes.scoreEnd, attrs: this.colors.msgListScoreHighlightColor + this.colors.msgListHighlightBkgColor});
-		msgListMenu.colors.altSelectedItemColor.splice(5, 0, {start: msgListIdxes.scoreStart, end: msgListIdxes.scoreEnd, attrs: this.colors.msgListScoreHighlightColor + this.colors.msgListHighlightBkgColor});
-	}
-
-	msgListMenu.multiSelect = false;
-	msgListMenu.ampersandHotkeysInItems = false;
-	msgListMenu.wrapNavigation = false;
-
-	// Add additional keypresses for quitting the menu's input loop so we can
-	// respond to these keys
-	// Ctrl-A: Select all messages
-	var additionalQuitKeys = "EeqQgGcCsS ?0123456789" + CTRL_A + this.msgListKeys.batchDelete + this.msgListKeys.userSettings;
-	if (this.CanDelete() || this.CanDeleteLastMsg())
-	{
-		additionalQuitKeys += this.msgListKeys.deleteMessage + this.msgListKeys.undeleteMessage.toLowerCase() + this.msgListKeys.undeleteMessage.toUpperCase();
-		additionalQuitKeys += '\x7f'; // Ensure DEL is in there
-		additionalQuitKeys += '\x08'; // Ensure BACKSPACE is in there (can be an alternate for delete)
-	}
-	if (this.CanEdit())
-		additionalQuitKeys += this.msgListKeys.editMsg;
-	msgListMenu.AddAdditionalQuitKeys(additionalQuitKeys);
-
-	// Add additional keypresses for PageUp, PageDown, HOME (first page), and END (last page)
-	msgListMenu.AddAdditionalPageUpKeys("Pp"); // Previous page
-	msgListMenu.AddAdditionalPageDownKeys("Nn"); // Next page
-	msgListMenu.AddAdditionalFirstPageKeys("Ff"); // First page
-	msgListMenu.AddAdditionalLastPageKeys("Ll"); // Last page
-
-	// Change the menu's NumItems() and GetItem() function to reference
-	// the message list in this object rather than add the menu items
-	// to the menu
-	msgListMenu.msgListIdxes = msgListIdxes;
-	msgListMenu.msgReader = this; // Add this object to the menu object
-	msgListMenu.NumItems = function() {
-		return this.msgReader.NumMessages();
-	};
-	msgListMenu.readingPersonalEmail = this.subBoardCode.toLowerCase() == "mail";
-	msgListMenu.GetItem = function(pItemIndex) {
-		var menuItemObj = this.MakeItemWithRetval(-1);
-		var itemIdx = (this.msgReader.userSettings.listMessagesInReverse ? this.msgReader.NumMessages() - pItemIndex - 1 : pItemIndex);
-		// In order to get vote score information (displayed if the user's terminal is wide
-		// enough), the 2nd parameter to GetMsgHdrByIdx() should be true.
-		var msgHdr = this.msgReader.GetMsgHdrByIdx(itemIdx, this.msgReader.showScoresInMsgList);
-		if (msgHdr != null)
-		{
-			// When setting the item text, call PrintMessageInfo with true as
-			// the last parameter to return the string instead
-			menuItemObj.text = strip_ctrl(this.msgReader.PrintMessageInfo(msgHdr, false, itemIdx+1, true));
-			menuItemObj.retval = msgHdr.number;
-			var msgIsToUser = userHandleAliasNameMatch(msgHdr.to);
-			var msgIsFromUser = userHandleAliasNameMatch(msgHdr.from);
-			var readingPersonalEmail = (this.msgReader.subBoardCode == "mail");
-			if (!readingPersonalEmail)
-				menuItemObj.useAltColors = msgIsToUser;
-			// For any indicator character next to the message, prioritize deleted, then selected,
-			// then unread, then attachments.
-			// First, to ensure the correct status character color is used, copy the menu item colors
-			// in any of these cases; then change the attributes for the 2nd color (selected).
-			// Having the separate printf strings for regular, to-user, and from-user are a bit
-			// bit pointless now that coloring & alternate coloring is done via DDLightbarMenu
-			if (this.msgReader.MessageIsSelected(this.msgReader.subBoardCode, pItemIndex) || Boolean(msgHdr.attr & MSG_DELETE) || !Boolean(msgHdr.attr & MSG_READ) || Boolean(msgHdr.attr & MSG_REPLIED) || msgHdrHasAttachmentFlag(msgHdr))
-			{
-				menuItemObj.itemColor = [];
-				var colorSet = this.colors.itemColor;
-				var selectedColorSet = this.colors.selectedItemColor;
-				if (!readingPersonalEmail)
-				{
-					if (msgIsToUser)
-					{
-						colorSet = this.colors.altItemColor;
-						selectedColorSet = this.colors.altSelectedItemColor;
-					}
-					else if (msgIsFromUser)
-					{
-						colorSet = [{start: this.msgListIdxes.msgNumStart, end: this.msgListIdxes.msgNumEnd, attrs: this.msgReader.colors.msgListFromUserMsgNumColor},
-									{start: this.msgListIdxes.selectMarkStart, end: this.msgListIdxes.selectMarkEnd, attrs: this.msgReader.colors.selectedMsgMarkColor},
-									{start: this.msgListIdxes.fromNameStart, end: this.msgListIdxes.fromNameEnd, attrs: this.msgReader.colors.msgListFromUserFromColor},
-									{start: this.msgListIdxes.toNameStart, end: this.msgListIdxes.toNameEnd, attrs: this.msgReader.colors.msgListFromUserToColor},
-									{start: this.msgListIdxes.subjStart, end: this.msgListIdxes.subjEnd, attrs: this.msgReader.colors.msgListFromUserSubjectColor},
-									{start: this.msgListIdxes.dateStart, end: this.msgListIdxes.dateEnd, attrs: this.msgReader.colors.msgListFromUserDateColor},
-									{start: this.msgListIdxes.timeStart, end: this.msgListIdxes.timeEnd, attrs: this.msgReader.colors.msgListFromUserTimeColor}];
-						selectedColorSet = this.colors.altSelectedItemColor;
-					}
-				}
-				for (var i = 0; i < colorSet.length; ++i)
-					menuItemObj.itemColor.push(colorSet[i]);
-				menuItemObj.itemSelectedColor = [];
-				for (var i = 0; i < selectedColorSet.length; ++i)
-					menuItemObj.itemSelectedColor.push(selectedColorSet[i]);
-			}
-			// Change the color
-			// Deleted
-			if (Boolean(msgHdr.attr & MSG_DELETE))
-			{
-				if (menuItemObj.itemColor.length >= 2)
-					menuItemObj.itemColor[1].attrs = "\x01r\x01h\x01i";
-				if (menuItemObj.itemSelectedColor.length >= 2)
-					menuItemObj.itemSelectedColor[1].attrs = "\x01r\x01h\x01i" + this.msgReader.colors.msgListHighlightBkgColor;
-			}
-			// Selected, unread, replied, or has attachments
-			else if (this.msgReader.MessageIsSelected(this.msgReader.subBoardCode, pItemIndex) || !Boolean(msgHdr.attr & MSG_READ) || Boolean(msgHdr.attr & MSG_REPLIED) || msgHdrHasAttachmentFlag(msgHdr))
-			{
-				if (menuItemObj.itemColor.length >= 2)
-					menuItemObj.itemColor[1].attrs = "\x01n" + this.msgReader.colors.selectedMsgMarkColor;
-				if (menuItemObj.itemSelectedColor.length >= 2)
-					menuItemObj.itemSelectedColor[1].attrs = "\x01n" + this.msgReader.colors.selectedMsgMarkColor + this.msgReader.colors.msgListHighlightBkgColor;
-			}
-		}
-		return menuItemObj;
-	};
-
-	// Adjust the menu indexes to ensure they're correct for the current sub-board
-	this.AdjustLightbarMsgListMenuIdxes(msgListMenu);
-
-	return msgListMenu;
-}
-// For the DigDistMsgLister class: Creates a DDLightbarMenu object for the user to choose
-// a message group.
-//
-// Return value: A DDLightbarMenu object set up to let the user choose a message group
-function DigDistMsgReader_CreateLightbarMsgGrpMenu()
-{
-	// Start & end indexes for the various items in each mssage group list row
-	// Selected mark, group#, description, # sub-boards
-	var msgGrpListIdxes = {
-		markCharStart: 0,
-		markCharEnd: 1,
-		grpNumStart: 1,
-		grpNumEnd: 2 + (+this.areaNumLen)
-	};
-	msgGrpListIdxes.descStart = msgGrpListIdxes.grpNumEnd;
-	msgGrpListIdxes.descEnd = msgGrpListIdxes.descStart + +this.msgGrpDescLen;
-	msgGrpListIdxes.numItemsStart = msgGrpListIdxes.descEnd;
-	msgGrpListIdxes.numItemsEnd = msgGrpListIdxes.numItemsStart + +this.numItemsLen;
-	// Set numItemsEnd to -1 to let the whole rest of the lines be colored
-	msgGrpListIdxes.numItemsEnd = -1;
-	var listStartRow = this.areaChangeHdrLines.length + 2;
-	var msgGrpMenuHeight = console.screen_rows - listStartRow;
-	var msgGrpMenu = new DDLightbarMenu(1, listStartRow, console.screen_columns, msgGrpMenuHeight);
-	msgGrpMenu.scrollbarEnabled = true;
-	msgGrpMenu.borderEnabled = false;
-	msgGrpMenu.SetColors({
-		itemColor: [{start: msgGrpListIdxes.markCharStart, end: msgGrpListIdxes.markCharEnd, attrs: this.colors.areaChooserMsgAreaMarkColor},
-		            {start: msgGrpListIdxes.grpNumStart, end: msgGrpListIdxes.grpNumEnd, attrs: this.colors.areaChooserMsgAreaNumColor},
-		            {start: msgGrpListIdxes.descStart, end: msgGrpListIdxes.descEnd, attrs: this.colors.areaChooserMsgAreaDescColor},
-		            {start: msgGrpListIdxes.numItemsStart, end: msgGrpListIdxes.numItemsEnd, attrs: this.colors.areaChooserMsgAreaNumItemsColor}],
-		selectedItemColor: [{start: msgGrpListIdxes.markCharStart, end: msgGrpListIdxes.markCharEnd, attrs: this.colors.areaChooserMsgAreaMarkColor + this.colors.areaChooserMsgAreaBkgHighlightColor},
-		                    {start: msgGrpListIdxes.grpNumStart, end: msgGrpListIdxes.grpNumEnd, attrs: this.colors.areaChooserMsgAreaNumHighlightColor},
-		                    {start: msgGrpListIdxes.descStart, end: msgGrpListIdxes.descEnd, attrs: this.colors.areaChooserMsgAreaDescHighlightColor},
-		                    {start: msgGrpListIdxes.numItemsStart, end: msgGrpListIdxes.numItemsEnd, attrs: this.colors.areaChooserMsgAreaNumItemsHighlightColor}]
-	});
-
-	msgGrpMenu.multiSelect = false;
-	msgGrpMenu.ampersandHotkeysInItems = false;
-	msgGrpMenu.wrapNavigation = false;
-
-	// Add additional keypresses for quitting the menu's input loop so we can
-	// respond to these keys
-	msgGrpMenu.AddAdditionalQuitKeys("nNqQ ?0123456789/" + CTRL_F);
-
-	// Change the menu's NumItems() and GetItem() function to reference
-	// the message list in this object rather than add the menu items
-	// to the menu
-	msgGrpMenu.msgReader = this; // Add this object to the menu object
-	msgGrpMenu.NumItems = function() {
-		return msg_area.grp_list.length;
-	};
-	msgGrpMenu.GetItem = function(pGrpIndex) {
-		var menuItemObj = this.MakeItemWithRetval(-1);
-		if ((pGrpIndex >= 0) && (pGrpIndex < msg_area.grp_list.length))
-		{
-			menuItemObj.text = format(((typeof(bbs.curgrp) == "number") && (pGrpIndex == msg_area.sub[this.msgReader.subBoardCode].grp_index)) ? "*" : " ");
-			menuItemObj.text += format(this.msgReader.msgGrpListPrintfStr, +(pGrpIndex+1),
-			                           msg_area.grp_list[pGrpIndex].description.substr(0, this.msgReader.msgGrpDescLen),
-			                           msg_area.grp_list[pGrpIndex].sub_list.length);
-			menuItemObj.text = strip_ctrl(menuItemObj.text);
-			menuItemObj.retval = pGrpIndex;
-		}
-
-		return menuItemObj;
-	};
-
-	// Set the currently selected item to the current group
-	msgGrpMenu.SetSelectedItemIdx(msg_area.sub[this.subBoardCode].grp_index);
-	/*
-	msgGrpMenu.selectedItemIdx = msg_area.sub[this.subBoardCode].grp_index;
-	if (msgGrpMenu.selectedItemIdx >= msgGrpMenu.topItemIdx+msgGrpMenu.GetNumItemsPerPage())
-		msgGrpMenu.topItemIdx = msgGrpMenu.selectedItemIdx - msgGrpMenu.GetNumItemsPerPage() + 1;
-	*/
-
-	return msgGrpMenu;
-}
-// For the DigDistMsgLister class: Creates a DDLightbarMenu object for the user to choose
-// a sub-board within a message group.
-//
-// Parameters:
-//  pGrpIdx: The index of the group to list sub-boards for
-//
-// Return value: A DDLightbarMenu object set up to let the user choose a sub-board within the
-//               given message group
-function DigDistMsgReader_CreateLightbarSubBoardMenu(pGrpIdx)
-{
-	// Start & end indexes for the various items in each sub-board list row
-	// Selected mark, group#, description, # sub-boards
-	var subBrdListIdxes = {
-		markCharStart: 0,
-		markCharEnd: 1,
-		subNumStart: 1,
-		subNumEnd: 2 + (+this.areaNumLen)
-	};
-	subBrdListIdxes.descStart = subBrdListIdxes.subNumEnd;
-	subBrdListIdxes.descEnd = subBrdListIdxes.descStart + +(this.subBoardListPrintfInfo[pGrpIdx].nameLen) + 1;
-	subBrdListIdxes.numItemsStart = subBrdListIdxes.descEnd;
-	subBrdListIdxes.numItemsEnd = subBrdListIdxes.numItemsStart + +(this.subBoardListPrintfInfo[pGrpIdx].numMsgsLen) + 1;
-	subBrdListIdxes.dateStart = subBrdListIdxes.numItemsEnd;
-	subBrdListIdxes.dateEnd = subBrdListIdxes.dateStart + +this.dateLen + 1;
-	subBrdListIdxes.timeStart = subBrdListIdxes.dateEnd;
-	// Set timeEnd to -1 to let the whole rest of the lines be colored
-	subBrdListIdxes.timeEnd = -1;
-	var listStartRow = this.areaChangeHdrLines.length + 3;
-	var subBrdMenuHeight = console.screen_rows - listStartRow;
-	var subBoardMenu = new DDLightbarMenu(1, listStartRow, console.screen_columns, subBrdMenuHeight);
-	subBoardMenu.scrollbarEnabled = true;
-	subBoardMenu.borderEnabled = false;
-	subBoardMenu.SetColors({
-		itemColor: [{start: subBrdListIdxes.markCharStart, end: subBrdListIdxes.markCharEnd, attrs: this.colors.areaChooserMsgAreaMarkColor},
-		            {start: subBrdListIdxes.subNumStart, end: subBrdListIdxes.subNumEnd, attrs: this.colors.areaChooserMsgAreaNumColor},
-		            {start: subBrdListIdxes.descStart, end: subBrdListIdxes.descEnd, attrs: this.colors.areaChooserMsgAreaDescColor},
-		            {start: subBrdListIdxes.numItemsStart, end: subBrdListIdxes.numItemsEnd, attrs: this.colors.areaChooserMsgAreaNumItemsColor},
-		            {start: subBrdListIdxes.dateStart, end: subBrdListIdxes.dateEnd, attrs: this.colors.areaChooserMsgAreaLatestDateColor},
-		            {start: subBrdListIdxes.timeStart, end: subBrdListIdxes.timeEnd, attrs: this.colors.areaChooserMsgAreaLatestTimeColor}],
-		selectedItemColor: [{start: subBrdListIdxes.markCharStart, end: subBrdListIdxes.markCharEnd, attrs: this.colors.areaChooserMsgAreaMarkColor + this.colors.areaChooserMsgAreaBkgHighlightColor},
-		                    {start: subBrdListIdxes.subNumStart, end: subBrdListIdxes.subNumEnd, attrs: this.colors.areaChooserMsgAreaNumHighlightColor},
-		                    {start: subBrdListIdxes.descStart, end: subBrdListIdxes.descEnd, attrs: this.colors.areaChooserMsgAreaDescHighlightColor},
-		                    {start: subBrdListIdxes.numItemsStart, end: subBrdListIdxes.numItemsEnd, attrs: this.colors.areaChooserMsgAreaNumItemsHighlightColor},
-		                    {start: subBrdListIdxes.dateStart, end: subBrdListIdxes.dateEnd, attrs: this.colors.areaChooserMsgAreaDateHighlightColor},
-		                    {start: subBrdListIdxes.timeStart, end: subBrdListIdxes.timeEnd, attrs: this.colors.areaChooserMsgAreaTimeHighlightColor}]
-	});
-
-	subBoardMenu.multiSelect = false;
-	subBoardMenu.ampersandHotkeysInItems = false;
-	subBoardMenu.wrapNavigation = false;
-
-	// Add additional keypresses for quitting the menu's input loop so we can
-	// respond to these keys
-	subBoardMenu.AddAdditionalQuitKeys("nNqQ ?0123456789/" + CTRL_F);
-
-	// Add the sub-board items to the menu
-	for (var subIdx = 0; subIdx < msg_area.grp_list[pGrpIdx].sub_list.length; ++subIdx)
-	{
-		var itemText = this.GetMsgSubBoardLine(pGrpIdx, subIdx, false);
-		subBoardMenu.Add(strip_ctrl(itemText), subIdx);
-	}
-	// Alternately, we could change the menu's NumItems() and GetItem():
-	/*
-	// Change the menu's NumItems() and GetItem() function to reference
-	// the message list in this object rather than add the menu items
-	// to the menu
-	subBoardMenu.msgReader = this; // Add this object to the menu object
-	subBoardMenu.grpIdx = pGrpIdx;
-	subBoardMenu.NumItems = function() {
-		return msg_area.grp_list[pGrpIdx].sub_list.length;
-	};
-	subBoardMenu.GetItem = function(pSubIdx) {
-		var menuItemObj = this.MakeItemWithRetval(-1);
-		if ((pSubIdx >= 0) && (pSubIdx < msg_area.grp_list[this.grpIdx].sub_list.length))
-		{
-			//var highlight = (msg_area.grp_list[this.grpIdx].sub_list[pSubIdx].code.toUpperCase() == this.msgReader.subBoardCode.toUpperCase());
-			menuItemObj.text = this.msgReader.GetMsgSubBoardLine(this.grpIdx, pSubIdx, false);
-			menuItemObj.text = strip_ctrl(menuItemObj.text);
-			menuItemObj.retval = pSubIdx;
-		}
-
-		return menuItemObj;
-	};
-	*/
-
-	// Set the currently selected item to the current group
-	if (msg_area.sub[this.subBoardCode].grp_index == pGrpIdx)
-	{
-		subBoardMenu.SetSelectedItemIdx(msg_area.sub[this.subBoardCode].index);
-		/*
-		subBoardMenu.selectedItemIdx = msg_area.sub[this.subBoardCode].index;
-		if (subBoardMenu.selectedItemIdx >= subBoardMenu.topItemIdx+subBoardMenu.GetNumItemsPerPage())
-			subBoardMenu.topItemIdx = subBoardMenu.selectedItemIdx - subBoardMenu.GetNumItemsPerPage() + 1;
-		*/
-	}
-	else
-	{
-		subBoardMenu.SetSelectedItemIdx(0);
-		/*
-		subBoardMenu.selectedItemIdx = 0;
-		subBoardMenu.topItemIdx = 0;
-		*/
-	}
-
-	return subBoardMenu;
-}
-// For the DigDistMsgLister class: Adjusts lightbar menu indexes for a message list menu
-function DigDistMsgReader_AdjustLightbarMsgListMenuIdxes(pMsgListMenu)
-{
-	pMsgListMenu.SetSelectedItemIdx(this.lightbarListSelectedMsgIdx);
-	/*
-	pMsgListMenu.selectedItemIdx = this.lightbarListSelectedMsgIdx;
-	pMsgListMenu.topItemIdx = this.lightbarListTopMsgIdx;
-	*/
-
-	// In the DDLightbarMenu class, the top index on the last page should
-	// allow for displaying a full page of items.  So if
-	// this.lightbarListTopMsgIdx is beyond the top index for the last
-	// page in the menu object, then adjust this.lightbarListTopMsgIdx.
-	var menuTopItemIdxOnLastPage = pMsgListMenu.GetTopItemIdxOfLastPage();
-	if (pMsgListMenu.topItemIdx > menuTopItemIdxOnLastPage)
-	{
-		pMsgListMenu.topItemIdx = menuTopItemIdxOnLastPage;
-		this.lightbarListTopMsgIdx = menuTopItemIdxOnLastPage;
-	}
-	// TODO: Ensure this.lightbarListTopMsgIdx is always correct for the last page
-}
-// For the DigDistMsgListerClass: Prints a line of information about
-// a message.
-//
-// Parameters:
-//  pMsgHeader: The message header object, returned by MsgBase.get_msg_header().
-//  pHighlight: Optional boolean - Whether or not to highlight the line (true) or
-//              use the standard colors (false).
-//  pMsgNum: Optional - A number to use for the message instead of the number/offset
-//           in the message header
-//  pReturnStrInstead: Optional boolean - Whether or not to return a formatted string
-//                     instead of printing to the console.  Defaults to false.
-function DigDistMsgReader_PrintMessageInfo(pMsgHeader, pHighlight, pMsgNum, pReturnStrInstead)
-{
-	// pMsgHeader must be a valid object.
-	if (typeof(pMsgHeader) == "undefined")
-		return;
-	if (pMsgHeader == null)
-		return;
-
-	var highlight = false;
-	if (typeof(pHighlight) == "boolean")
-		highlight = pHighlight;
-
-	// Get the message's import date & time as strings.  If
-	// this.msgList_displayMessageDateImported is true, use the message import date.
-	// Otherwise, use the message written date.
-	var sDate;
-	var sTime;
-	if (this.msgList_displayMessageDateImported)
-	{
-		sDate = strftime("%Y-%m-%d", pMsgHeader.when_imported_time);
-		sTime = strftime("%H:%M:%S", pMsgHeader.when_imported_time);
-	}
-	else
-	{
-		//sDate = strftime("%Y-%m-%d", pMsgHeader.when_written_time);
-		//sTime = strftime("%H:%M:%S", pMsgHeader.when_written_time);
-		var msgWrittenLocalTime = msgWrittenTimeToLocalBBSTime(pMsgHeader);
-		if (msgWrittenLocalTime != -1)
-		{
-			sDate = strftime("%Y-%m-%d", msgWrittenLocalTime);
-			sTime = strftime("%H:%M:%S", msgWrittenLocalTime);
-		}
-		else
-		{
-			sDate = strftime("%Y-%m-%d", pMsgHeader.when_written_time);
-			sTime = strftime("%H:%M:%S", pMsgHeader.when_written_time);
-		}
-	}
-
-	//var msgNum = (typeof(pMsgNum) == "number" ? pMsgNum : pMsgHeader.offset+1);
-	var msgNum = (typeof(pMsgNum) == "number" ? pMsgNum : this.GetMsgIdx(pMsgHeader.number)+1);
-	if (msgNum == 0) // In case GetMsgIdx() returns -1 for failure
-		msgNum = 1;
-
-	// Determine if the message has been deleted.
-	var msgDeleted = ((pMsgHeader.attr & MSG_DELETE) == MSG_DELETE);
-
-	// msgIndicatorChar will contain (possibly) a character to display after
-	// the message number to indicate whether it has been deleted, selected,
-	// etc.  If not, then it will just be a space.
-	var msgIndicatorChar = " ";
-
-	// Get the message score value
-	var msgVoteInfo = getMsgUpDownvotesAndScore(pMsgHeader);
-	// Ensure the score number can fit within 4 digits
-	if (msgVoteInfo.voteScore > 9999)
-		msgVoteInfo.voteScore = 9999;
-	else if (msgVoteInfo.voteScore < -999)
-		msgVoteInfo.voteScore = -999;
-
-	// Generate the string with the message header information.
-	var msgHdrStr = "";
-	// Note: The message header has the following fields:
-	// 'number': The message number
-	// 'offset': The message offset
-	// 'to': Who the message is directed to (string)
-	// 'from' Who wrote the message (string)
-	// 'subject': The message subject (string)
-	// 'date': The date - Full text (string)
-	// To access one of these, use brackets; i.e., msgHeader['to']
-	if (highlight)
-	{
-		// For any indicator character next to the message, prioritize selected, then deleted,
-		// then unread, then attachments
-		if (this.MessageIsSelected(this.subBoardCode, msgNum-1))
-			msgIndicatorChar = "\x01n" + this.colors.selectedMsgMarkColor + this.colors.msgListHighlightBkgColor + this.msgListStatusChars.selected + "\x01n";
-		else if (msgDeleted)
-			msgIndicatorChar = "\x01n\x01r\x01h\x01i" + this.colors.msgListHighlightBkgColor + this.msgListStatusChars.deleted + "\x01n";
-		else if (this.readingPersonalEmail && !Boolean(pMsgHeader.attr & MSG_READ))
-			msgIndicatorChar = "\x01n" + this.colors.selectedMsgMarkColor + this.colors.msgListHighlightBkgColor + this.msgListStatusChars.unread + "\x01n";
-		else if (this.readingPersonalEmail && Boolean(pMsgHeader.attr & MSG_REPLIED))
-		{
-			if (this.userSettings.displayMsgRepliedChar)
-				msgIndicatorChar = "\x01n" + this.colors.selectedMsgMarkColor + this.colors.msgListHighlightBkgColor + this.msgListStatusChars.replied + "\x01n";
-		}
-		else if (msgHdrHasAttachmentFlag(pMsgHeader))
-			msgIndicatorChar = "\x01n" + this.colors.selectedMsgMarkColor + this.colors.msgListHighlightBkgColor + this.msgListStatusChars.attachments + "\x01n";
-		var fromName = pMsgHeader.from;
-		// If the message was posted anonymously and the logged-in user is
-		// not the sysop, then show "Anonymous" for the 'from' name.
-		if (!user.is_sysop && ((pMsgHeader.attr & MSG_ANONYMOUS) == MSG_ANONYMOUS))
-			fromName = "Anonymous";
-		if (this.showScoresInMsgList)
-		{
-			msgHdrStr += format(this.sMsgInfoFormatHighlightStr, msgNum, msgIndicatorChar,
-			       fromName.substr(0, this.FROM_LEN),
-			       pMsgHeader.to.substr(0, this.TO_LEN),
-			       pMsgHeader.subject.substr(0, this.SUBJ_LEN),
-			       msgVoteInfo.voteScore, sDate, sTime);
-		}
-		else
-		{
-			msgHdrStr += format(this.sMsgInfoFormatHighlightStr, msgNum, msgIndicatorChar,
-			       fromName.substr(0, this.FROM_LEN),
-			       pMsgHeader.to.substr(0, this.TO_LEN),
-			       pMsgHeader.subject.substr(0, this.SUBJ_LEN),
-			       sDate, sTime);
-		}
-	}
-	else
-	{
-		// For any indicator character next to the message, prioritize selected, then deleted,
-		// then unread, then attachments
-		if (this.MessageIsSelected(this.subBoardCode, msgNum-1))
-			msgIndicatorChar = "\x01n" +  this.colors.selectedMsgMarkColor + this.msgListStatusChars.selected + "\x01n";
-		else if (msgDeleted)
-			msgIndicatorChar = "\x01n\x01r\x01h\x01i" + this.msgListStatusChars.deleted + "\x01n";
-		else if (this.readingPersonalEmail && !Boolean(pMsgHeader.attr & MSG_READ))
-			msgIndicatorChar = "\x01n" + this.colors.selectedMsgMarkColor + this.msgListStatusChars.unread + "\x01n";
-		else if (this.readingPersonalEmail && Boolean(pMsgHeader.attr & MSG_REPLIED))
-		{
-			if (this.userSettings.displayMsgRepliedChar)
-				msgIndicatorChar = "\x01n" + this.colors.selectedMsgMarkColor + this.msgListStatusChars.replied + "\x01n";
-		}
-		else if (msgHdrHasAttachmentFlag(pMsgHeader))
-			msgIndicatorChar = "\x01n" + this.colors.selectedMsgMarkColor + this.msgListStatusChars.attachments + "\x01n";
-
-		// Determine whether to use the normal, "to-user", or "from-user" format string.
-		// The differences are the colors.  Then, output the message information line.
-		var msgToUser = userHandleAliasNameMatch(pMsgHeader.to);
-		var fromNameUpper = pMsgHeader.from.toUpperCase();
-		var msgIsFromUser = ((fromNameUpper == user.alias.toUpperCase()) || (fromNameUpper == user.name.toUpperCase()) || (fromNameUpper == user.handle.toUpperCase()));
-		var formatStr = ""; // Format string for printing the message information
-		if (this.readingPersonalEmail)
-			formatStr = this.sMsgInfoFormatStr;
-		else
-			formatStr = (msgToUser ? this.sMsgInfoToUserFormatStr : (msgIsFromUser ? this.sMsgInfoFromUserFormatStr : this.sMsgInfoFormatStr));
-		var fromName = pMsgHeader.from;
-		// If the message was posted anonymously and the logged-in user is
-		// not the sysop, then show "Anonymous" for the 'from' name.
-		if (!user.is_sysop && ((pMsgHeader.attr & MSG_ANONYMOUS) == MSG_ANONYMOUS))
-			fromName = "Anonymous";
-		if (this.showScoresInMsgList)
-		{
-			msgHdrStr += format(formatStr, msgNum, msgIndicatorChar, fromName.substr(0, this.FROM_LEN),
-			       pMsgHeader.to.substr(0, this.TO_LEN), pMsgHeader.subject.substr(0, this.SUBJ_LEN),
-			       msgVoteInfo.voteScore, sDate, sTime);
-		}
-		else
-		{
-			msgHdrStr += format(formatStr, msgNum, msgIndicatorChar, fromName.substr(0, this.FROM_LEN),
-			       pMsgHeader.to.substr(0, this.TO_LEN), pMsgHeader.subject.substr(0, this.SUBJ_LEN),
-			       sDate, sTime);
-		}
-	}
-
-	var returnStrInstead = (typeof(pReturnStrInstead) == "boolean" ? pReturnStrInstead : false);
-	if (!returnStrInstead)
-	{
-		console.print(msgHdrStr);
-		console.cleartoeol("\x01n"); // To clear away any extra text that may have been entered by the user
-	}
-	return msgHdrStr;
-}
-// For the traditional interface of DigDistMsgListerClass: Prompts the user to
-// continue or read a message (by number).
-//
-// Parameters:
-//  pStart: Whether or not we're on the first page (true or false)
-//  pEnd: Whether or not we're at the last page (true or false)
-//  pAllowChgSubBoard: Optional - A boolean to specify whether or not to allow
-//                     changing to another sub-board.  Defaults to true.
-//
-// Return value: An object with the following properties:
-//               continueOn: Boolean, whether or not the user wants to continue
-//                           listing the messages
-//               userInput: The user's input
-//               selectedMsgOffset: The offset of the message selected to read,
-//                                  if one was selected.  If a message was not
-//                                  selected, this will be -1.
-function DigDistMsgReader_PromptContinueOrReadMsg(pStart, pEnd, pAllowChgSubBoard)
-{
-	// Create the return object and set some initial default values
-	var retObj = {
-		continueOn: true,
-		userInput: "",
-		selectedMsgOffset: -1
-	};
-
-	var allowChgSubBoard = (typeof(pAllowChgSubBoard) == "boolean" ? pAllowChgSubBoard : true);
-
-	var continueOn = true;
-	// Prompt the user whether or not to continue or to read a message
-	// (by message number).  Make use of the different prompt texts,
-	// depending whether we're at the beginning, in the middle, or at
-	// the end of the message list.
-	var userInput = "";
-	var allowedKeys = "?GS"; // ? = help, G = Go to message #, S = Select message(s), Ctrl-D: Batch delete
-	allowedKeys += this.msgListKeys.userSettings;
-	if (allowChgSubBoard)
-		allowedKeys += "C"; // Change to another message area
-	if (this.CanDelete() || this.CanDeleteLastMsg())
-	{
-		allowedKeys += "D"; // Delete
-		allowedKeys += this.enhReaderKeys.deleteMessage;
-		allowedKeys += '\x7f';
-		allowedKeys += '\x08';
-	}
-	if (this.CanEdit())
-		allowedKeys += "E"; // Edit
-	if (pStart && pEnd)
-	{
-		// This is the only page.
-		console.print(this.msgListOnlyOnePageContinuePrompt);
-		// Get input from the user.  Allow only Q (quit).
-		allowedKeys += "Q";
-	}
-	else if (pStart)
-	{
-		// We're on the first page.
-		console.print(this.sStartContinuePrompt);
-		// Get input from the user.  Allow only L (last), N (next), or Q (quit).
-		allowedKeys += "LNQ";
-	}
-	else if (pEnd)
-	{
-		// We're on the last page.
-		console.print(this.sEndContinuePrompt);
-		// Get input from the user.  Allow only F (first), P (previous), or Q (quit).
-		allowedKeys += "FPQ";
-	}
-	else
-	{
-		// We're neither on the first nor last page.  Allow F (first), L (last),
-		// N (next), P (previous), or Q (quit).
-		console.print(this.sContinuePrompt);
-		allowedKeys += "FLNPQ";
-	}
-	// Get the user's input.  Allow CTRL-D (batch delete) without echoing it.
-	// If the user didn't press CTRL-L, allow the keys in allowedKeys or a number from 1
-	// to the highest message number.
-	userInput = console.getkey(K_NOECHO);
-	if (userInput != this.enhReaderKeys.batchDelete) // CTRL_D
-	{
-		console.ungetstr(userInput);
-		userInput = console.getkeys(allowedKeys, this.HighestMessageNum()).toString();
-	}
-	if (userInput == "Q")
-		continueOn = false;
-
-	// If the user has typed all numbers, then read that message.
-	if ((userInput != "") && /^[0-9]+$/.test(userInput))
-	{
-		// If the user entered a valid message number, then let the user read the message.
-		// The message number might be invalid if there are search results that
-		// have non-continuous message numbers.
-		if (this.IsValidMessageNum(userInput))
-		{
-			// See if the current message header has our "isBogus" property and it's true.
-			// Only let the user read the message if it's not a bogus message header.
-			// The message header could have the "isBogus" property, for instance, if
-			// it's a vote message (introduced in Synchronet 3.17).
-			var tmpMsgHdr = this.GetMsgHdrByIdx(+(userInput-1), false);
-			if (tmpMsgHdr != null)
-			{
-				// Confirm with the user whether to read the message
-				var readMsg = true;
-				if (this.promptToReadMessage)
-				{
-					var sReadMsgConfirmText = this.colors["readMsgConfirmColor"]
-											+ "Read message "
-											+ this.colors["readMsgConfirmNumberColor"]
-											+ userInput + this.colors["readMsgConfirmColor"]
-											+ ": Are you sure";
-					readMsg = console.yesno(sReadMsgConfirmText);
-				}
-				if (readMsg)
-				{
-					// Update the message list screen variables
-					this.CalcMsgListScreenIdxVarsFromMsgNum(+userInput);
-					// Return from here so that the calling function can switch
-					// into reader mode.
-					retObj.continueOn = continueOn;
-					retObj.userInput = userInput;
-					retObj.selectedMsgOffset = userInput-1;
-					return retObj;
-				}
-				else
-					this.deniedReadingMessage = true;
-
-				// Prompt the user whether or not to continue listing
-				// messages.
-				if (this.promptToContinueListingMessages)
-				{
-					continueOn = console.yesno(this.colors["afterReadMsg_ListMorePromptColor"] +
-											   "Continue listing messages");
-				}
-			}
-			else
-			{
-				console.print("\x01n\x01h\x01yThat's not a readable message.\x01n");
-				console.crlf();
-				console.pause();
-			}
-		}
-		else
-		{
-			// The user entered an invalid message number.
-			console.print("\x01n\x01h\x01w" + userInput + " \x01y is not a valid message number.\x01n");
-			console.crlf();
-			console.pause();
-			continueOn = true;
-		}
-	}
-
-	// Make sure color highlighting is turned off
-	console.attributes = "N";
-
-	// Fill the return object with the required values, and return it.
-	retObj.continueOn = continueOn;
-	retObj.userInput = userInput;
-	return retObj;
-}
-// For the DigDistMsgReader Class: Given a message number of a message in the
-// current message area, lets the user read a message and allows the user to
-// respond, etc.  This is an enhanced version that allows scrolling up & down
-// the message with the up & down arrow keys, and the left & right arrow keys
-// will return from the function to allow calling code to navigate back & forth
-// through the message sub-board.
-//
-// Parameters:
-//  pOffset: The offset of the message to be read
-//  pAllowChgArea: Optional boolean - Whether or not to allow changing the
-//                 message area
-//
-// Return value: And object with the following properties:
-//               offsetValid: Boolean - Whether or not the passed-in offset was valid
-//               msgDeleted: Boolean - Whether or not the message is marked as deleted
-//                           (not deleted by the user in the reader)
-//               userReplied: Boolean - Whether or not the user replied to the message.
-//               lastKeypress: The last keypress from the user - For navigation purposes
-//               newMsgOffset: The offset of another message to read, if the user
-//                             input another message number.  If the user did not
-//                             input another message number, this will be -1.
-//               nextAction: The next action for the caller to take.  This will be
-//                           one of the values specified by the ACTION_* constants.
-//                           This defaults to ACTION_NONE on error.
-//               refreshEnhancedRdrHelpLine: Boolean - Whether or not to refresh the
-//                                           enhanced reader help line on the screen
-//                                           (for instance, if switched to the traditional
-//                                            non-scrolling interface to read the message)
-function DigDistMsgReader_ReadMessageEnhanced(pOffset, pAllowChgArea)
-{
-	var retObj = {
-		offsetValid: true,
-		msgNotReadable: false,
-		userReplied: false,
-		lastKeypress: "",
-		newMsgOffset: -1,
-		nextAction: ACTION_NONE,
-		refreshEnhancedRdrHelpLine: false
-	};
-	
-	// Get the message header.  Don't expand fields since we may need to save
-	// the header later with the MSG_READ attribute.
-	//var msgHeader = this.GetMsgHdrByIdx(pOffset, false);
-	// Get the message header.  Get expanded fields so that we can show any
-	// voting stats/responses that may be included with the message.
-	var msgHeader = this.GetMsgHdrByIdx(pOffset, true);
-	if (msgHeader == null)
-	{
-		//console.print("\x01n" + replaceAtCodesInStr(format(this.text.invalidMsgNumText, +(pOffset+1))) + "\x01n");
-		//console.crlf();
-		console.putmsg("\x01n" + format(this.text.invalidMsgNumText, +(pOffset+1)) + "\x01n");
-		console.inkey(K_NONE, ERROR_PAUSE_WAIT_MS);
-		retObj.offsetValid = false;
-		return retObj;
-	}
-
-	// If this message is not readable for the user (it's marked as deleted and
-	// the system is set to not show deleted messages, etc.), then don't let the
-	// user read it, and just silently return.
-	// TODO: If the message is not readable, this will end up causing an infinite loop.
-	retObj.msgNotReadable = !isReadableMsgHdr(msgHeader, this.subBoardCode);
-	if (retObj.msgNotReadable)
-		return retObj;
-
-	// Mark the message as read if reading personal email or if it was written to the current user
-	if (this.readingPersonalEmail || ((msgHeader.attr & MSG_READ) == 0) && (userHandleAliasNameMatch(msgHeader.to)))
-	{
-		// Using applyAttrsInMsgHdrInMessagbase(), which loads the header without
-		// expanded fields and saves the attributes with that header.
-		var saveRetObj = applyAttrsInMsgHdrInMessagbase(this.subBoardCode, msgHeader.number, MSG_READ);
-		if (this.SearchTypePopulatesSearchResults() && saveRetObj.saveSucceeded)
-			this.RefreshHdrInSavedArrays(pOffset, MSG_READ, true);
-	}
-	// For personal email, if we wanted to really check that it was written to the current sysop user:
-	//var personalEmailToCurrentSysopUser = this.readingPersonalEmail && user.is_sysop && msgHeader.to.toUpperCase().indexOf("SYSOP") == 0;
-	//if (((msgHeader.attr & MSG_READ) == 0) && (userHandleAliasNameMatch(msgHeader.to) || personalEmailToCurrentSysopUser))
-
-	// Updating message pointers etc.
-	updateScanPtrAndOrLastRead(this.subBoardCode, msgHeader, this.doingMsgScan);
-
-	// Update the message list index variables so that the message list is in
-	// the right spot for the message currently being read
-	this.CalcMsgListScreenIdxVarsFromMsgNum(pOffset+1);
-
-	// Check the pAllowChgArea parameter.  If it's a boolean, then use it.  If
-	// not, then check to see if we're reading personal mail - If not, then allow
-	// the user to change to a different message area.
-	var allowChgMsgArea = true;
-	if (typeof(pAllowChgArea) == "boolean")
-		allowChgMsgArea = pAllowChgArea;
-	else
-		allowChgMsgArea = (this.subBoardCode != "mail");
-
-	// Get the message text and see if it has any ANSI codes.  Remove any pause
-	// codes it might have.  If it has ANSI codes, then don't use the scrolling
-	// interface so that the ANSI gets displayed properly.
-	var getMsgBodyRetObj = this.GetMsgBody(msgHeader);
-	var messageText = getMsgBodyRetObj.msgBody;
-
-	if (msgHdrHasAttachmentFlag(msgHeader))
-	{
-		messageText = "\x01n\x01g\x01h- This message contains one or more attachments. Press CTRL-A to download.\x01n\r\n"
-		            + "\x01n\x01g\x01h--------------------------------------------------------------------------\x01n\r\n"
-		            + messageText;
-	}
-	var useScrollingInterface = this.scrollingReaderInterface && console.term_supports(USER_ANSI);
-	// If we switch to the non-scrolling interface here, then the calling method should
-	// refresh the enhanced reader help line on the screen.
-	retObj.refreshEnhancedRdrHelpLine = (this.scrollingReaderInterface && !useScrollingInterface);
-	// If the current message is new to the user, update the number of posts read this session.
-	if (pOffset > this.GetScanPtrMsgIdx())
-		++bbs.posts_read;
-	// Use the scrollable reader interface if the setting is enabled & the user's
-	// terminal supports ANSI.  Otherwise, use a more traditional user interface.
-	if (useScrollingInterface)
-		retObj = this.ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgArea, messageText, pOffset, getMsgBodyRetObj.pmode);
-	else
-		retObj = this.ReadMessageEnhanced_Traditional(msgHeader, allowChgMsgArea, messageText, pOffset, getMsgBodyRetObj.pmode);
-
-	return retObj;
-}
-// Helper method for ReadMessageEnhanced() - Does the scrollable reader interface
-function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgArea, messageText, pOffset, pmode)
-{
-	var retObj = {
-		offsetValid: true,
-		msgNotReadable: false,
-		userReplied: false,
-		lastKeypress: "",
-		newMsgOffset: -1,
-		nextAction: ACTION_NONE,
-		refreshEnhancedRdrHelpLine: false
-	};
-
-	// This is a scrollbar update function for use when viewing the message.
-	function msgScrollbarUpdateFn(pFractionToLastPage)
-	{
-		// Update the scrollbar position for the message, depending on the
-		// value of pFractionToLastMessage.
-		fractionToLastPage = pFractionToLastPage;
-		solidBlockStartRow = msgReaderObj.msgAreaTop + Math.floor(numNonSolidScrollBlocks * pFractionToLastPage);
-		if (solidBlockStartRow != solidBlockLastStartRow)
-			msgReaderObj.UpdateEnhancedReaderScrollbar(solidBlockStartRow, solidBlockLastStartRow, numSolidScrollBlocks);
-		solidBlockLastStartRow = solidBlockStartRow;
-		console.gotoxy(1, console.screen_rows);
-	}
-	// This is a scrollbar update function for use when viewing the header info/kludge lines.
-	function msgInfoScrollbarUpdateFn(pFractionToLastPage)
-	{
-		var infoSolidBlockStartRow = msgReaderObj.msgAreaTop + Math.floor(numNonSolidInfoScrollBlocks * pFractionToLastPage);
-		if (infoSolidBlockStartRow != lastInfoSolidBlockStartRow)
-			msgReaderObj.UpdateEnhancedReaderScrollbar(infoSolidBlockStartRow, lastInfoSolidBlockStartRow, numInfoSolidScrollBlocks);
-		lastInfoSolidBlockStartRow = infoSolidBlockStartRow;
-		console.gotoxy(1, console.screen_rows);
-	}
-
-	var msgAreaWidth = this.userSettings.useEnhReaderScrollbar ? this.msgAreaWidth : this.msgAreaWidth + 1;
-
-	// We could word-wrap the message to ensure words aren't split across lines, but
-	// doing so could make some messages look bad (i.e., messages with drawing characters),
-	// and word_wrap also might not handle ANSI or other color/attribute codes..
-	//if (!textHasDrawingChars(messageText))
-	//	messageText = word_wrap(messageText, msgAreaWidth);
-
-	// If the message has ANSI content, then use a Graphic object to help make
-	// the message look good.  Also, remove any ANSI clear screen codes from the
-	// message text.
-	var msgHasANSICodes = messageText.indexOf("\x1b[") >= 0;
-	if (msgHasANSICodes)
-	{
-		messageText = messageText.replace(/\u001b\[[012]J/gi, "");
-		//var graphic = new Graphic(msgAreaWidth, this.msgAreaHeight-1);
-		// To help ensure ANSI messages look good, it seems the Graphic object should have
-		// its with later set to 1 less than the width used to create it.
-		var graphicWidth = (msgAreaWidth < console.screen_columns ? msgAreaWidth+1 : console.screen_columns);
-		var graphic = new Graphic(graphicWidth, this.msgAreaHeight-1);
-		graphic.auto_extend = true;
-		graphic.ANSI = ansiterm.expand_ctrl_a(messageText);
-		//graphic.normalize();
-		graphic.width = graphicWidth - 1;
-		//messageText = graphic.MSG.split('\n');
-		messageText = graphic.MSG;
-	}
-
-	// Show the message header
-	this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
-
-	// Get the message text, interpret any @-codes in it, replace tabs with spaces
-	// to prevent weirdness when displaying the message lines, and word-wrap the
-	// text so that it looks good on the screen,
-	var msgInfo = this.GetMsgInfoForEnhancedReader(msgHeader, true, true, true, messageText);
-
-	var topMsgLineIdxForLastPage = msgInfo.topMsgLineIdxForLastPage;
-	var numSolidScrollBlocks = msgInfo.numSolidScrollBlocks;
-	var numNonSolidScrollBlocks = msgInfo.numNonSolidScrollBlocks;
-	var solidBlockStartRow = msgInfo.solidBlockStartRow;
-	var solidBlockLastStartRow = solidBlockStartRow;
-	var topMsgLineIdx = 0;
-	var fractionToLastPage = 0;
-	if (topMsgLineIdxForLastPage != 0)
-		fractionToLastPage = topMsgLineIdx / topMsgLineIdxForLastPage;
-
-	// If use of the scrollbar is enabled, draw an initial scrollbar on the rightmost
-	// column of the message area showing the fraction of the message shown and what
-	// part of the message is currently being shown.  The scrollbar will be updated
-	// minimally in the input loop to minimize screen redraws.
-	if (this.userSettings.useEnhReaderScrollbar)
-		this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
-
-	// Input loop (for scrolling the message up & down)
-	var msgLineFormatStr = "%-" + msgAreaWidth + "s";
-	var writeMessage = true;
-	// msgAreaHeight, msgReaderObj, and scrollbarUpdateFunction are for use
-	// with scrollTextLines().
-	var msgAreaHeight = this.msgAreaBottom - this.msgAreaTop + 1;
-	var msgReaderObj = this;
-
-	var msgHasAttachments = msgHdrHasAttachmentFlag(msgHeader);
-
-	// For editing a user account. Only for the sysop.
-	function userEdit(msgHeader, pOffset, pReader)
-	{
-		console.attributes = "N";
-		console.crlf();
-		console.print("- Edit user " + msgHeader.from);
-		console.crlf();
-		var editObj = editUser(msgHeader.from);
-		if (editObj.errorMsg.length != 0)
-		{
-			console.attributes = "N";
-			console.crlf();
-			console.print("\x01y\x01h" + editObj.errorMsg + "\x01n");
-			console.crlf();
-			console.pause();
-		}
-	}
-
-	// For viewing the hex dump information (for the sysop)
-	var msgHexInfo = null;
-
-	// User input loop
-	var continueOn = true;
-	while (continueOn)
-	{
-		// Display the message lines (depending on the value of writeMessage)
-		// and handle scroll keys via scrollTextLines().  Handle other keypresses
-		// here.
-		var scrollbarInfoObj = {
-			solidBlockLastStartRow: 0,
-			numSolidScrollBlocks: 0
-		};
-		scrollbarInfoObj.solidBlockLastStartRow = solidBlockLastStartRow;
-		scrollbarInfoObj.numSolidScrollBlocks = numSolidScrollBlocks;
-		var scrollRetObj = scrollTextLines(msgInfo.messageLines, topMsgLineIdx,
-									   this.colors.msgBodyColor, writeMessage,
-									   this.msgAreaLeft, this.msgAreaTop, msgAreaWidth,
-									   msgAreaHeight, 1, console.screen_rows,
-									   this.userSettings.useEnhReaderScrollbar,
-									   msgScrollbarUpdateFn, pmode);
-		topMsgLineIdx = scrollRetObj.topLineIdx;
-		retObj.lastKeypress = scrollRetObj.lastKeypress;
-		switch (retObj.lastKeypress)
-		{
-			case this.enhReaderKeys.deleteMessage: // Delete message
-			case '\x7f':
-			case '\x08':
-				var originalCurpos = console.getxy();
-				// The 2nd to last row of the screen is where the user will
-				// be prompted for confirmation to delete the message.
-				// Ideally, I'd like to put the cursor on the last row of
-				// the screen for this, but console.noyes() lets the enter
-				// key shift everything on screen up one row, and there's
-				// no way to avoid that.  So, to optimize screen refreshing,
-				// the cursor is placed on the 2nd to the last row on the
-				// screen to prompt for confirmation.
-				var promptPos = this.EnhReaderPrepLast2LinesForPrompt();
-
-				// Prompt the user for confirmation to delete the message.
-				// Note: this.PromptAndDeleteOrUndeleteMessage() will check to see if the user
-				// is a sysop or the message was posted by the user.
-				// If the message was deleted and the user can't view deleted messages,
-				// then exit this read method and return KEY_RIGHT as the last keypress
-				// so that the calling method will go to the next message/sub-board.
-				// Otherwise (if the message was not deleted), refresh the
-				// last 2 lines of the message on the screen.
-				var msgWasDeleted = this.PromptAndDeleteOrUndeleteMessage(pOffset, promptPos, true, true, msgAreaWidth, true);
-				if (msgWasDeleted && !canViewDeletedMsgs())
-				{
-					var msgSearchObj = this.LookForNextOrPriorNonDeletedMsg(pOffset);
-					continueOn = msgSearchObj.continueInputLoop;
-					retObj.newMsgOffset = msgSearchObj.newMsgOffset;
-					retObj.nextAction = msgSearchObj.nextAction;
-					if (msgSearchObj.promptGoToNextArea)
-					{
-						if (this.EnhReaderPromptYesNo(replaceAtCodesInStr(this.text.goToNextMsgAreaPromptText), msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr, solidBlockStartRow, numSolidScrollBlocks))
-						{
-							// Let this method exit and let the caller go to the next sub-board
-							continueOn = false;
-							retObj.nextAction = ACTION_GO_NEXT_MSG;
-						}
-						else
-							writeMessage = false; // No need to refresh the message
-					}
-				}
-				else
-				{
-					this.DisplayEnhReaderError("", msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr);
-					// Move the cursor back to its original position
-					console.gotoxy(originalCurpos);
-					writeMessage = false;
-				}
-				break;
-			case this.enhReaderKeys.selectMessage: // Select message (for batch delete, etc.)
-				var originalCurpos = console.getxy();
-				var promptPos = this.EnhReaderPrepLast2LinesForPrompt();
-				var selected = this.EnhReaderPromptYesNo("Select this message", msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr, solidBlockStartRow, numSolidScrollBlocks, true);
-				this.ToggleSelectedMessage(this.subBoardCode, pOffset, selected);
-				writeMessage = false; // No need to refresh the message
-				break;
-			case this.enhReaderKeys.batchDelete:
-				// TODO: Write this?  Not sure yet if it makes much sense to
-				// have batch delete in the reader interface.
-				// Prompt the user for confirmation, and use
-				// this.DeleteOrUndeleteSelectedMessages() to mark the selected messages
-				// as deleted.
-				// Returns an object with the following properties:
-				//  deletedAll: Boolean - Whether or not all messages were successfully marked
-				//              for deletion
-				//  failureList: An object containing indexes of messages that failed to get
-				//               marked for deletion, indexed by internal sub-board code, then
-				//               containing messages indexes as properties.  Reasons for failing
-				//               to mark messages deleted can include the user not having permission
-				//               to delete in a sub-board, failure to open the sub-board, etc.
-				writeMessage = false; // No need to refresh the message
-				break;
-			case this.enhReaderKeys.editMsg: // Edit the messaage
-				if (this.CanEdit())
-				{
-					// Move the cursor to the last line in the message area so
-					// the edit confirmation prompt will appear there.  Not using
-					// the last line on the screen because the yes/no prompt will
-					// output a carriage return and move everything on the screen
-					// up one line, which is not ideal in case the user says No.
-					var promptPos = this.EnhReaderPrepLast2LinesForPrompt();
-					// Let the user edit the message if they want to
-					var editReturnObj = this.EditExistingMsg(pOffset);
-					// If the user didn't confirm, then we only have to refresh the bottom
-					// help line.  Otherwise, we need to refresh everything on the screen.
-					if (!editReturnObj.userConfirmed)
-					{
-						// For some reason, the yes/no prompt erases the last character
-						// of the scrollbar - So, figure out which block was there and
-						// refresh it.
-						//var scrollBarBlock = "\x01n\x01h\x01k" + BLOCK1; // Dim block
-						// Dim block
-						if (this.userSettings.useEnhReaderScrollbar)
-						{
-							var scrollBarBlock = this.colors.scrollbarBGColor + this.text.scrollbarBGChar;
-							if (solidBlockStartRow + numSolidScrollBlocks - 1 == this.msgAreaBottom)
-							{
-								//scrollBarBlock = "\x01w" + BLOCK2; // Bright block
-								// Bright block
-								scrollBarBlock = this.colors.scrollbarScrollBlockColor + this.text.scrollbarScrollBlockChar;
-							}
-							else
-							{
-								// TODO
-							}
-							console.gotoxy(this.msgAreaRight+1, this.msgAreaBottom);
-							console.print(scrollBarBlock);
-						}
-						// Refresh the last 2 message lines on the screen, then display
-						// the key help line
-						this.DisplayEnhReaderError("", msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr);
-						this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
-						writeMessage = false;
-					}
-					else
-					{
-						// If the message was edited, then refresh the text lines
-						// array and update the other message-related variables.
-						if (editReturnObj.msgEdited && (editReturnObj.newMsgIdx > -1))
-						{
-							// When the message is edited, the old message will be
-							// deleted and the edited message will be posted as a new
-							// message.  So we should return to the caller and have it
-							// go directly to that new message.
-							this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
-							continueOn = false;
-							retObj.newMsgOffset = editReturnObj.newMsgIdx;
-						}
-						else
-						{
-							// The message was not edited.  Refresh everything on the screen.
-							// If the enhanced message header width is less than the console
-							// width, then clear the screen to remove anything that might be
-							// left on the screen by the message editor.
-							if (this.enhMsgHeaderWidth < console.screen_columns)
-								console.clear("\x01n");
-							// Display the message header and key help line
-							this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
-							this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
-							// Display the scrollbar again, and ensure it's in the correct position
-							if (this.userSettings.useEnhReaderScrollbar)
-							{
-								solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
-								this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
-							}
-							else
-							{
-								// TODO
-							}
-							writeMessage = true; // We want to refresh the message on the screen
-						}
-					}
-				}
-				else
-					writeMessage = false; // Don't write the current message again
-				break;
-			case this.enhReaderKeys.showHelp: // Show the help screen
-				this.DisplayEnhancedReaderHelp(allowChgMsgArea, msgHasAttachments);
-				// If the enhanced message header width is less than the console
-				// width, then clear the screen to remove anything left on the
-				// screen from the help screen.
-				if (this.enhMsgHeaderWidth < console.screen_columns)
-					console.clear("\x01n");
-				// Display the message header and key help line
-				this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
-				this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
-				// Display the scrollbar again, and ensure it's in the correct position
-				if (this.userSettings.useEnhReaderScrollbar)
-				{
-					solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
-					this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
-				}
-				else
-				{
-					// TODO
-				}
-				writeMessage = true; // We want to refresh the message on the screen
-				break;
-			case this.enhReaderKeys.reply: // Reply to the message
-			case this.enhReaderKeys.privateReply: // Private message reply
-				// If the user pressed the private reply key while reading private
-				// mail, then do nothing (allow only the regular reply key to reply).
-				var privateReply = (retObj.lastKeypress == this.enhReaderKeys.privateReply);
-				if (privateReply && this.readingPersonalEmail)
-					writeMessage = false; // Don't re-write the current message again
-				else
-				{
-					// Get the message header with fields expanded so we can get the most info possible.
-					//var extdMsgHdr = this.GetMsgHdrByAbsoluteNum(msgHeader.number, true);
-					var msgbase = new MsgBase(this.subBoardCode);
-					if (msgbase.open())
-					{
-						var extdMsgHdr = msgbase.get_msg_header(false, msgHeader.number, true);
-						msgbase.close();
-						// Let the user reply to the message.
-						var replyRetObj = this.ReplyToMsg(extdMsgHdr, messageText, privateReply, pOffset);
-						retObj.userReplied = replyRetObj.postSucceeded;
-						//retObj.msgNotReadable = replyRetObj.msgWasDeleted;
-						var msgWasDeleted = replyRetObj.msgWasDeleted;
-						//if (retObj.msgNotReadable)
-						if (msgWasDeleted && !canViewDeletedMsgs())
-						{
-							var msgSearchObj = this.LookForNextOrPriorNonDeletedMsg(pOffset);
-							continueOn = msgSearchObj.continueInputLoop;
-							retObj.newMsgOffset = msgSearchObj.newMsgOffset;
-							retObj.nextAction = msgSearchObj.nextAction;
-							if (msgSearchObj.promptGoToNextArea)
-							{
-								if (this.EnhReaderPromptYesNo(replaceAtCodesInStr(this.text.goToNextMsgAreaPromptText), msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr, solidBlockStartRow, numSolidScrollBlocks))
-								{
-									// Let this method exit and let the caller go to the next sub-board
-									continueOn = false;
-									retObj.nextAction = ACTION_GO_NEXT_MSG;
-								}
-								else
-									writeMessage = true; // We want to refresh the message on the screen
-							}
-						}
-						else
-						{
-							// If the enhanced message header width is less than the console
-							// width, then clear the screen to remove anything left on the
-							// screen by the message editor.
-							if (this.enhMsgHeaderWidth < console.screen_columns)
-								console.clear("\x01n");
-							// Display the message header and key help line again
-							this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
-							this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
-							// Display the scrollbar again to refresh it on the screen
-							if (this.userSettings.useEnhReaderScrollbar)
-							{
-								solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
-								this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
-							}
-							else
-							{
-								// TODO
-							}
-							writeMessage = true; // We want to refresh the message on the screen
-						}
-					}
-				}
-				break;
-			case this.enhReaderKeys.postMsg: // Post a message
-				if (!this.readingPersonalEmail)
-				{
-					// Let the user post a message.
-					if (bbs.post_msg(this.subBoardCode))
-					{
-						if (searchTypePopulatesSearchResults(this.searchType))
-						{
-							// TODO: If the user is doing a search, it might be
-							// useful to search their new message and add it to
-							// the search results if it's a match..  but maybe
-							// not?
-						}
-						console.pause();
-					}
-
-					// Refresh things on the screen
-					// Display the message header and key help line again
-					this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
-					this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
-					// Display the scrollbar again to refresh it on the screen
-					if (this.userSettings.useEnhReaderScrollbar)
-					{
-						solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
-						this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
-					}
-					else
-					{
-						// TODO
-					}
-					writeMessage = true; // We want to refresh the message on the screen
-				}
-				else
-					writeMessage = false; // Don't re-write the current message again
-				break;
-			// Numeric digit: The start of a number of a message to read
-			case "0":
-			case "1":
-			case "2":
-			case "3":
-			case "4":
-			case "5":
-			case "6":
-			case "7":
-			case "8":
-			case "9":
-				var originalCurpos = console.getxy();
-				// Put the user's input back in the input buffer to
-				// be used for getting the rest of the message number.
-				console.ungetstr(retObj.lastKeypress);
-				// Move the cursor to the 2nd to last row of the screen and
-				// prompt the user for the message number.  Ideally, I'd like
-				// to put the cursor on the last row of the screen for this, but
-				// console.getnum() lets the enter key shift everything on screen
-				// up one row, and there's no way to avoid that.  So, to optimize
-				// screen refreshing, the cursor is placed on the 2nd to the last
-				// row on the screen to prompt for the message number.
-				var promptPos = this.EnhReaderPrepLast2LinesForPrompt();
-				// Prompt for the message number
-				var msgNumInput = this.PromptForMsgNum(promptPos, replaceAtCodesInStr(this.text.readMsgNumPromptText), false, ERROR_PAUSE_WAIT_MS, false);
-				// Only allow reading the message if the message number is valid
-				// and it's not the same message number that was passed in.
-				if ((msgNumInput > 0) && (msgNumInput-1 != pOffset))
-				{
-					// If the message is marked as deleted, then output an error
-					if (this.MessageIsDeleted(msgNumInput-1))
-					{
-						writeWithPause(this.msgAreaLeft, console.screen_rows-1,
-									   "\x01n" + replaceAtCodesInStr(format(this.text.msgHasBeenDeletedText, msgNumInput)) + "\x01n",
-									   ERROR_PAUSE_WAIT_MS, "\x01n", true);
-					}
-					else
-					{
-						// Confirm with the user whether to read the message
-						var readMsg = true;
-						if (this.promptToReadMessage)
-						{
-							var sReadMsgConfirmText = this.colors["readMsgConfirmColor"]
-													+ "Read message "
-													+ this.colors["readMsgConfirmNumberColor"]
-													+ msgNumInput + this.colors["readMsgConfirmColor"]
-													+ ": Are you sure";
-							console.gotoxy(promptPos);
-							console.attributes = "N";
-							readMsg = console.yesno(sReadMsgConfirmText);
-						}
-						if (readMsg)
-						{
-							continueOn = false;
-							retObj.newMsgOffset = msgNumInput - 1;
-							retObj.nextAction = ACTION_GO_SPECIFIC_MSG;
-						}
-						else
-							writeMessage = false; // Don't re-write the current message again
-					}
-				}
-				else // Message number invalid or the same as what was passed in
-					writeMessage = false; // Don't re-write the current message again
-
-				// If the user chose to continue reading messages, then refresh
-				// the last 2 message lines in the last part of the message area
-				// and then put the cursor back to its original position.
-				if (continueOn)
-				{
-					this.DisplayEnhReaderError("", msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr);
-					// Move the cursor back to its original position
-					console.gotoxy(originalCurpos);
-				}
-				break;
-			case this.enhReaderKeys.prevMsgByTitle: // Previous message by title
-			case this.enhReaderKeys.prevMsgByAuthor: // Previous message by author
-			case this.enhReaderKeys.prevMsgByToUser: // Previous message by 'to user'
-			case this.enhReaderKeys.prevMsgByThreadID: // Previous message by thread ID
-				// Only allow this if we aren't doing a message search.
-				if (!this.SearchingAndResultObjsDefinedForCurSub())
-				{
-					var threadPrevMsgOffset = this.FindThreadPrevOffset(msgHeader,
-					                                                    keypressToThreadType(retObj.lastKeypress, this.enhReaderKeys),
-					                                                    true);
-					if (threadPrevMsgOffset > -1)
-					{
-						retObj.newMsgOffset = threadPrevMsgOffset;
-						retObj.nextAction = ACTION_GO_SPECIFIC_MSG;
-						continueOn = false;
-					}
-					else
-					{
-						// Refresh the help line at the bottom of the screen
-						//this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
-						writeMessage = false; // Don't re-write the current message again
-					}
-					// Make sure the help line on the bottom of the screen is
-					// drawn.
-					this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
-				}
-				else
-					writeMessage = false; // Don't re-write the current message again
-				break;
-			case this.enhReaderKeys.nextMsgByTitle: // Next message by title (subject)
-			case this.enhReaderKeys.nextMsgByAuthor: // Next message by author
-			case this.enhReaderKeys.nextMsgByToUser: // Next message by 'to user'
-			case this.enhReaderKeys.nextMsgByThreadID: // Next message by thread ID
-				// Only allow this if we aren't doing a message search.
-				if (!this.SearchingAndResultObjsDefinedForCurSub())
-				{
-					var threadPrevMsgOffset = this.FindThreadNextOffset(msgHeader,
-					                                                    keypressToThreadType(retObj.lastKeypress, this.enhReaderKeys),
-					                                                    true);
-					if (threadPrevMsgOffset > -1)
-					{
-						retObj.newMsgOffset = threadPrevMsgOffset;
-						retObj.nextAction = ACTION_GO_SPECIFIC_MSG;
-						continueOn = false;
-					}
-					else
-						writeMessage = false; // Don't re-write the current message again
-					// Make sure the help line on the bottom of the screen is
-					// drawn.
-					this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
-				}
-				else
-					writeMessage = false; // Don't re-write the current message again
-				break;
-			case this.enhReaderKeys.previousMsg: // Previous message
-				// Look for a prior message that isn't marked for deletion.  Even
-				// if we don't find one, we'll still want to return from this
-				// function (with message index -1) so that this script can go
-				// onto the previous message sub-board/group.
-				retObj.newMsgOffset = this.FindNextReadableMsgIdx(pOffset, false);
-				// As a screen redraw optimization: Only return if there is a valid new
-				// message offset or the user is allowed to change to a different sub-board.
-				// Otherwise, don't return, and don't refresh the message on the screen.
-				var goToPrevMessage = false;
-				if ((retObj.newMsgOffset > -1) || allowChgMsgArea)
-				{
-					if (retObj.newMsgOffset == -1 && !curMsgSubBoardIsLast())
-					{
-						goToPrevMessage = this.EnhReaderPromptYesNo(replaceAtCodesInStr(this.text.goToPrevMsgAreaPromptText),
-																	msgInfo.messageLines, topMsgLineIdx,
-																	msgLineFormatStr, solidBlockStartRow,
-																	numSolidScrollBlocks);
-					}
-					else
-					{
-						// We're not at the beginning of the sub-board, so it's okay to exit this
-						// method and go to the previous message.
-						goToPrevMessage = true;
-					}
-				}
-				if (goToPrevMessage)
-				{
-					continueOn = false;
-					retObj.nextAction = ACTION_GO_PREVIOUS_MSG;
-				}
-				else
-					writeMessage = false; // No need to refresh the message
-				break;
-			case this.enhReaderKeys.nextMsg: // Next message
-			case KEY_ENTER:
-				// Look for a later message that isn't marked for deletion.  Even
-				// if we don't find one, we'll still want to return from this
-				// function (with message index -1) so that this script can go
-				// onto the next message sub-board/group.
-				var findNextMsgRetObj = this.ScrollableReaderNextReadableMessage(pOffset, msgInfo, topMsgLineIdx, msgLineFormatStr, solidBlockStartRow, numSolidScrollBlocks);
-				if (findNextMsgRetObj.newMsgOffset > -1)
-					retObj.newMsgOffset = findNextMsgRetObj.newMsgOffset;
-				writeMessage = findNextMsgRetObj.writeMessage;
-				continueOn = findNextMsgRetObj.continueOn;
-				retObj.nextAction = findNextMsgRetObj.nextAction;
-				break;
-				// First & last message: Quit out of this input loop and let the
-				// calling function, this.ReadMessages(), handle the action.
-			case this.enhReaderKeys.firstMsg: // First message
-				// Only leave this function if we aren't already on the first message.
-				if (pOffset > 0)
-				{
-					continueOn = false;
-					retObj.nextAction = ACTION_GO_FIRST_MSG;
-				}
-				else
-					writeMessage = false; // Don't re-write the current message again
-				break;
-			case this.enhReaderKeys.lastMsg: // Last message
-				// Only leave this function if we aren't already on the last message.
-				if (pOffset < this.NumMessages() - 1)
-				{
-					continueOn = false;
-					retObj.nextAction = ACTION_GO_LAST_MSG;
-				}
-				else
-					writeMessage = false; // Don't re-write the current message again
-				break;
-			case this.enhReaderKeys.prevSubBoard: // Go to the previous message area
-				if (allowChgMsgArea)
-				{
-					continueOn = false;
-					retObj.nextAction = ACTION_GO_PREV_MSG_AREA;
-				}
-				else
-					writeMessage = false; // Don't re-write the current message again
-				break;
-			case this.enhReaderKeys.nextSubBoard: // Go to the next message area
-				if (allowChgMsgArea || this.doingMultiSubBoardScan)
-				{
-					continueOn = false;
-					retObj.nextAction = ACTION_GO_NEXT_MSG_AREA;
-				}
-				else
-					writeMessage = false; // Don't re-write the current message again
-				break;
-				// H and K: Display the extended message header info/kludge lines
-				// (for the sysop)
-			case this.enhReaderKeys.showHdrInfo:
-			case this.enhReaderKeys.showKludgeLines:
-				if (user.is_sysop)
-				{
-					writeMessage = this.ShowHdrOrKludgeLines_Scrollable(retObj.lastKeypress == this.enhReaderKeys.showKludgeLines, msgHeader, msgAreaWidth, msgAreaHeight);
-					// Display the scrollbar for the message to refresh it on the screen
-					if (this.userSettings.useEnhReaderScrollbar)
-					{
-						solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
-						this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
-					}
-					else
-					{
-						// TODO
-					}
-				}
-				else // The user is not a sysop
-					writeMessage = false;
-				break;
-				// Message list, change message area: Quit out of this input loop
-				// and let the calling function, this.ReadMessages(), handle the
-				// action.
-			case this.enhReaderKeys.showMsgList: // Message list
-				console.attributes = "N";
-				console.crlf();
-				console.print("Loading...");
-				retObj.nextAction = ACTION_DISPLAY_MSG_LIST;
-				continueOn = false;
-				break;
-			case this.enhReaderKeys.chgMsgArea: // Change message area, if allowed
-				if (allowChgMsgArea)
-				{
-					retObj.nextAction = ACTION_CHG_MSG_AREA;
-					continueOn = false;
-				}
-				else
-					writeMessage = false; // No need to refresh the message
-				break;
-			case this.enhReaderKeys.downloadAttachments: // Download attachments
-				if (msgHasAttachments)
-				{
-					console.attributes = "N";
-					console.gotoxy(1, console.screen_rows);
-					console.crlf();
-					allowUserToDownloadMessage_NewInterface(msgHeader, this.subBoardCode);
-
-					// Refresh things on the screen
-					console.clear("\x01n");
-					// Display the message header and key help line again
-					this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
-					this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
-					// Display the scrollbar again to refresh it on the screen
-					if (this.userSettings.useEnhReaderScrollbar)
-					{
-						solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
-						this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
-					}
-					else
-					{
-						// TODO
-					}
-					writeMessage = true; // We want to refresh the message on the screen
-				}
-				else
-					writeMessage = false;
-				break;
-			case this.enhReaderKeys.saveToBBSMachine:
-				// Save the message to the BBS machine - Only allow this
-				// if the user is a sysop.
-				if (user.is_sysop)
-				{
-					// Prompt the user for a filename to save the message to the
-					// BBS machine
-					var promptPos = this.EnhReaderPrepLast2LinesForPrompt();
-					console.print("\x01n\x01cFilename:\x01h");
-					var inputLen = console.screen_columns - 10; // 10 = "Filename:" length + 1
-					var filename = console.getstr(inputLen, K_NOCRLF);
-					console.attributes = "N";
-					if (filename.length > 0)
-					{
-						var saveMsgRetObj = this.SaveMsgToFile(msgHeader, filename, promptPos);
-						console.gotoxy(promptPos);
-						console.cleartoeol("\x01n");
-						console.gotoxy(promptPos);
-						if (saveMsgRetObj.succeeded)
-						{
-							var statusMsg = "\x01n\x01cThe message has been saved.";
-							if (msgHdrHasAttachmentFlag(msgHeader))
-								statusMsg += " Attachments not saved.";
-							statusMsg += "\x01n";
-							console.print(statusMsg);
-						}
-						else
-							console.print("\x01n\x01y\x01hFailed: " + saveMsgRetObj.errorMsg + "\x01n");
-						mswait(ERROR_PAUSE_WAIT_MS);
-					}
-					else
-					{
-						console.gotoxy(promptPos);
-						console.print("\x01n\x01y\x01hMessage not exported\x01n");
-						mswait(ERROR_PAUSE_WAIT_MS);
-					}
-					// Refresh the last 2 lines of the message on the screen to overwrite
-					// the file save prompt
-					this.DisplayEnhReaderError("", msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr);
-				}
-				writeMessage = false; // Don't write the whole message again
-				break;
-			case this.enhReaderKeys.userEdit: // Edit the user who wrote the message
-				if (user.is_sysop)
-				{
-					userEdit(msgHeader, pOffset, this);
-					// Refresh things on the screen
-					console.clear("\x01n");
-					// Display the message header and key help line again
-					this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
-					this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
-					// Display the scrollbar again to refresh it on the screen
-					if (this.userSettings.useEnhReaderScrollbar)
-					{
-						retObj.solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
-						this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
-					}
-					else
-					{
-						// TODO
-					}
-					writeMessage = true; // We want to refresh the message on the screen
-				}
-				else // The user is not a sysop
-					writeMessage = false;
-				break;
-			case this.enhReaderKeys.forwardMsg: // Forward the message
-				console.attributes = "N";
-				console.crlf();
-				console.print("\x01c- Forward message\x01n");
-				console.crlf();
-				var retStr = this.ForwardMessage(msgHeader, messageText);
-				if (retStr.length > 0)
-				{
-					console.print("\x01n\x01h\x01y* " + retStr + "\x01n");
-					console.crlf();
-					console.pause();
-				}
-
-				// Refresh things on the screen
-				console.clear("\x01n");
-				// Display the message header and key help line again
-				this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
-				this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
-				// Display the scrollbar again to refresh it on the screen
-				if (this.userSettings.useEnhReaderScrollbar)
-				{
-					solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
-					this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
-				}
-				else
-				{
-					// TODO
-				}
-				writeMessage = true; // We want to refresh the message on the screen
-				break;
-			case this.enhReaderKeys.vote: // Vote on the message
-				// Move the cursor to the last line in the message area so the
-				// vote question text prompt will appear there.
-				var promptPos = this.EnhReaderPrepLast2LinesForPrompt();
-				console.gotoxy(1, console.screen_rows-1);
-				// Let the user vote on the message
-				var voteRetObj = this.VoteOnMessage(msgHeader, true);
-				if (voteRetObj.BBSHasVoteFunction)
-				{
-					var msgIsPollVote = ((typeof(MSG_TYPE_POLL) != "undefined") && (msgHeader.type & MSG_TYPE_POLL) == MSG_TYPE_POLL);
-					if (!voteRetObj.userQuit)
-					{
-						// If the message is a poll vote, then output any error
-						// message on its own line and refresh the whole screen.
-						// Otherwise, use the last 2 rows for an error message
-						// and only refresh what's necessary.
-						if (msgIsPollVote)
-						{
-							console.attributes = "N";
-							console.gotoxy(1, console.screen_rows-1);
-							if (voteRetObj.errorMsg.length > 0)
-							{
-								if (voteRetObj.mnemonicsRequiredForErrorMsg)
-								{
-									console.mnemonics(voteRetObj.errorMsg);
-									console.attributes = "N";
-								}
-								else
-									console.print("\x01y\x01h* " + voteRetObj.errorMsg + "\x01n");
-								mswait(ERROR_PAUSE_WAIT_MS);
-							}
-							else if (!voteRetObj.savedVote)
-							{
-								console.print("\x01y\x01h* Failed to save the vote\x01n");
-								mswait(ERROR_PAUSE_WAIT_MS);
-							}
-						}
-						else
-						{
-							// Not a poll vote - Just an up/down vote
-							if ((voteRetObj.errorMsg.length > 0) || (!voteRetObj.savedVote))
-							{
-								console.attributes = "N";
-								console.gotoxy(1, console.screen_rows-1);
-								if (voteRetObj.errorMsg.length > 0)
-								{
-									if (voteRetObj.mnemonicsRequiredForErrorMsg)
-									{
-										console.mnemonics(voteRetObj.errorMsg);
-										console.attributes = "N";
-									}
-									else
-										console.print("\x01y\x01h* " + voteRetObj.errorMsg + "\x01n");
-								}
-								else if (!voteRetObj.savedVote)
-									console.print("\x01y\x01h* Failed to save the vote\x01n");
-							}
-							else
-								msgHeader = voteRetObj.updatedHdr; // To get updated vote information
-							mswait(ERROR_PAUSE_WAIT_MS);
-
-							writeMessage = false;
-						}
-						// Exit out of the reader and come back to read
-						// the same message again so that the voting results
-						// are re-loaded and displayed on the screen.
-						retObj.newMsgOffset = pOffset;
-						retObj.nextAction = ACTION_GO_SPECIFIC_MSG;
-						continueOn = false;
-						this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
-					}
-					else
-					{
-						// The user quit out of voting.  Refresh the screen.
-						// Exit out of the reader and come back to read
-						// the same message again so that the screen is refreshed.
-						retObj.newMsgOffset = pOffset;
-						retObj.nextAction = ACTION_GO_SPECIFIC_MSG;
-						continueOn = false;
-						this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
-					}
-				}
-				else
-					writeMessage = false;
-				break;
-			case this.enhReaderKeys.showVotes: // Show votes
-				var DVRetObj = this.ShowVoteInfo_Scrollable(msgHeader, msgAreaWidth, msgAreaHeight);
-				writeMessage = DVRetObj.writeMessage;
-				if (DVRetObj.hasVoteProps)
-				{
-					// Display the scrollbar for the message to refresh it on the screen
-					if (this.userSettings.useEnhReaderScrollbar)
-					{
-						solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
-						this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
-					}
-					else
-					{
-						// TODO
-					}
-				}
-				break;
-			case this.enhReaderKeys.closePoll: // Close a poll message
-				// Save the original cursor position
-				var originalCurPos = console.getxy();
-				var pollCloseMsg = "";
-				// If this message is a poll, then allow closing it.
-				if ((typeof(MSG_TYPE_POLL) != "undefined") && (msgHeader.type & MSG_TYPE_POLL) == MSG_TYPE_POLL)
-				{
-					if ((msgHeader.auxattr & POLL_CLOSED) == 0)
-					{
-						// Only let the user close the poll if they created it
-						if (userHandleAliasNameMatch(msgHeader.from))
-						{
-							// Prompt to confirm whether the user wants to close the poll
-							console.gotoxy(1, console.screen_rows-1);
-							printf("\x01n%" + +(console.screen_columns-1) + "s", "");
-							console.gotoxy(1, console.screen_rows-1);
-							if (!console.noyes("Close poll"))
-							{
-								// Close the poll (open the sub-board first)
-								var msgbase = new MsgBase(this.subBoardCode);
-								if (msgbase.open())
-								{
-									if (closePollWithOpenMsgbase(msgbase, msgHeader.number))
-									{
-										msgHeader.auxattr |= POLL_CLOSED;
-										pollCloseMsg = "\x01n\x01cThis poll was successfully closed.";
-									}
-									else
-										pollCloseMsg = "\x01n\x01r\x01h* Failed to close this poll!";
-									msgbase.close();
-								}
-								else
-									pollCloseMsg = "\x01n\x01y\x01hUnable to open sub-board to close the poll";
-							}
-						}
-						else
-							pollCloseMsg = "\x01n\x01y\x01hCan't close this poll because it's not yours";
-					}
-					else
-						pollCloseMsg = "\x01n\x01y\x01hThis poll is already closed";
-				}
-				else
-					pollCloseMsg = "This message is not a poll";
-
-				// Display the poll closing status message
-				this.DisplayEnhReaderError(pollCloseMsg, msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr);
-				console.gotoxy(originalCurPos);
-				writeMessage = false;
-				break;
-			case this.enhReaderKeys.validateMsg: // Validate the message
-				if (user.is_sysop && (this.subBoardCode != "mail") && msg_area.sub[this.subBoardCode].is_moderated)
-				{
-					var message = "";
-					if (this.ValidateMsg(this.subBoardCode, msgHeader.number))
-					{
-						message = "\x01n\x01cMessage validation successful";
-						// Refresh the message header in the arrays
-						this.RefreshMsgHdrInArrays(msgHeader.number);
-						// Exit out of the reader and come back to read
-						// the same message again so that the voting results
-						// are re-loaded and displayed on the screen.
-						retObj.newMsgOffset = pOffset;
-						retObj.nextAction = ACTION_GO_SPECIFIC_MSG;
-						continueOn = false;
-						this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
-					}
-					else
-						message = "\x01n\x01y\x01hMessage validation failed!";
-					this.DisplayEnhReaderError(message, msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr);
-				}
-				else
-					writeMessage = false;
-				break;
-			case this.enhReaderKeys.quickValUser: // Quick-validate the user
-				if (user.is_sysop)
-				{
-					var valRetObj = quickValidateLocalUser(msgHeader.from, this.scrollingReaderInterface && console.term_supports(USER_ANSI), this.quickUserValSetIndex);
-					if (valRetObj.needWholeScreenRefresh)
-					{
-						this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
-						if (this.userSettings.useEnhReaderScrollbar)
-							this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
-						else
-						{
-							// TODO
-						}
-						this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
-					}
-					else
-					{
-						writeMessage = false; // Don't refresh the whole message
-						if (valRetObj.refreshBottomLine)
-							this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
-						if (valRetObj.optionBoxTopLeftX > 0 && valRetObj.optionBoxTopLeftY > 0 && valRetObj.optionBoxWidth > 0 && valRetObj.optionBoxHeight > 0)
-							this.RefreshMsgAreaRectangle(msgInfo.messageLines, topMsgLineIdx, valRetObj.optionBoxTopLeftX, valRetObj.optionBoxTopLeftY, valRetObj.optionBoxWidth, valRetObj.optionBoxHeight);
-					}
-				}
-				else
-					writeMessage = false;
-				break;
-			case this.enhReaderKeys.bypassSubBoardInNewScan:
-				writeMessage = false; // TODO: Finish
-				/*
-				if (this.doingMsgScan)
-				{
-					console.attributes = "N";
-					var originalCurpos = console.getxy();
-					// The 2nd to last row of the screen is where the user will
-					// be prompted for confirmation to delete the message.
-					// Ideally, I'd like to put the cursor on the last row of
-					// the screen for this, but console.noyes() lets the enter
-					// key shift everything on screen up one row, and there's
-					// no way to avoid that.  So, to optimize screen refreshing,
-					// the cursor is placed on the 2nd to the last row on the
-					// screen to prompt for confirmation.
-					var promptPos = this.EnhReaderPrepLast2LinesForPrompt();
-					if (!console.noyes("Bypass this sub-board in newscans"))
-					{
-						continueOn = false;
-						msg_area.sub[this.subBoardCode].scan_cfg &= SCAN_CFG_NEW;
-					}
-					else
-					{
-						this.DisplayEnhReaderError("", msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr);
-						// Move the cursor back to its original position
-						console.gotoxy(originalCurpos);
-						this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
-					}
-				}
-				else
-					writeMessage = false;
-				*/
-				break;
-			case this.enhReaderKeys.userSettings:
-				// Make a backup copy of the this.userSettings.useEnhReaderScrollbar setting in case it changes, so we can tell if we need to refresh the scrollbar
-				var oldUseEnhReaderScrollbar = this.userSettings.useEnhReaderScrollbar;
-				var userSettingsRetObj = this.DoUserSettings_Scrollable(function(pReader) { pReader.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea); });
-				retObj.lastKeypress = "";
-				writeMessage = userSettingsRetObj.needWholeScreenRefresh;
-				// In case the user changed their twitlist, re-filter the messages for this sub-board
-				if (userSettingsRetObj.userTwitListChanged)
-				{
-					console.gotoxy(1, console.screen_rows);
-					console.crlf();
-					console.print("\x01nTwitlist changed; re-filtering..");
-					var tmpMsgbase = new MsgBase(this.subBoardCode);
-					if (tmpMsgbase.open())
-					{
-						continueOn = false;
-						writeMessage = false;
-						var tmpAllMsgHdrs = tmpMsgbase.get_all_msg_headers(true);
-						tmpMsgbase.close();
-						this.FilterMsgHdrsIntoHdrsForCurrentSubBoard(tmpAllMsgHdrs, true);
-						// If the user is currently reading a message a message by someone who is now
-						// in their twit list, change the message currently being viewed.
-						if (this.MsgHdrFromOrToInUserTwitlist(msgHeader))
-						{
-							var findNextMsgRetObj = this.ScrollableReaderNextReadableMessage(pOffset, msgInfo, topMsgLineIdx, msgLineFormatStr, solidBlockStartRow, numSolidScrollBlocks);
-							if (findNextMsgRetObj.newMsgOffset > -1)
-							{
-								retObj.newMsgOffset = findNextMsgRetObj.newMsgOffset;
-								retObj.nextAction = ACTION_GO_SPECIFIC_MSG;
-							}
-							else
-								retObj.nextAction = ACTION_GO_NEXT_MSG_AREA;
-						}
-						else
-						{
-							// If there are still messages in this sub-board, and the message offset is beyond the last
-							// message, then show the last message in the sub-board.  Otherwise, go to the next message area.
-							if (this.hdrsForCurrentSubBoard.length > 0)
-							{
-								if (pOffset > this.hdrsForCurrentSubBoard.length)
-								{
-									//this.hdrsForCurrentSubBoard[this.hdrsForCurrentSubBoard.length-1].number
-									retObj.newMsgOffset = this.hdrsForCurrentSubBoard.length - 1;
-									retObj.nextAction = ACTION_GO_SPECIFIC_MSG;
-								}
-							}
-							else
-								retObj.nextAction = ACTION_GO_NEXT_MSG_AREA;
-						}
-					}
-					else
-						console.print("\x01y\x01hFailed to open the messagbase!\x01\r\n\x01p");
-					this.SetUpLightbarMsgListVars();
-					writeMessage = true;
-				}
-				if (userSettingsRetObj.needWholeScreenRefresh)
-				{
-					this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
-					if (this.userSettings.useEnhReaderScrollbar)
-						this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
-					else
-					{
-						// TODO
-					}
-					this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
-				}
-				else
-				{
-					// If the message scrollbar was toggled, then draw/erase the scrollbar
-					if (this.userSettings.useEnhReaderScrollbar != oldUseEnhReaderScrollbar)
-					{
-						msgAreaWidth = this.userSettings.useEnhReaderScrollbar ? this.msgAreaWidth : this.msgAreaWidth + 1;
-						// If the message is ANSI, then re-create the Graphic object to account for the
-						// new width
-						if (msgHasANSICodes)
-						{
-							//var graphic = new Graphic(msgAreaWidth, this.msgAreaHeight-1);
-							// To help ensure ANSI messages look good, it seems the Graphic object should have
-							// its with later set to 1 less than the width used to create it.
-							var graphicWidth = (msgAreaWidth < console.screen_columns ? msgAreaWidth+1 : console.screen_columns);
-							var graphic = new Graphic(graphicWidth, this.msgAreaHeight-1);
-							graphic.auto_extend = true;
-							graphic.ANSI = ansiterm.expand_ctrl_a(messageText);
-							//graphic.width = msgAreaWidth;
-							graphic.width = graphicWidth - 1;
-							messageText = graphic.MSG;
-						}
-						// Display or erase the scrollbar
-						if (this.userSettings.useEnhReaderScrollbar)
-						{
-							solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
-							this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
-						}
-						else
-						{
-							// Erase the scrollbar
-							console.attributes = "N";
-							for (var screenY = this.msgAreaTop; screenY <= this.msgAreaBottom; ++screenY)
-							{
-								console.gotoxy(this.msgAreaRight+1, screenY);
-								console.print(" ");
-							}
-						}
-					}
-					this.RefreshMsgAreaRectangle(msgInfo.messageLines, topMsgLineIdx, userSettingsRetObj.optionBoxTopLeftX, userSettingsRetObj.optionBoxTopLeftY, userSettingsRetObj.optionBoxWidth, userSettingsRetObj.optionBoxHeight);
-				}
-				break;
-			case this.enhReaderKeys.showMsgHex:
-				if (user.is_sysop)
-				{
-					writeMessage = false;
-					if (msgHexInfo == null)
-						msgHexInfo = this.GetMsgHexInfo(messageText, true);
-					if (msgHexInfo.msgHexArray.length > 0)
-					{
-						this.ShowMsgHex_Scrolling(msgHexInfo);
-						if (this.userSettings.useEnhReaderScrollbar)
-							this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
-						writeMessage = true;
-					}
-				}
-				else // The user is not a sysop
-					writeMessage = false;
-				break;
-			case this.enhReaderKeys.hexDump:
-				// Save a hex dump of the message to the BBS machine - Only allow this
-				// if the user is a sysop.
-				if (user.is_sysop)
-				{
-					// Prompt the user for a filename to save the hex dump to the
-					// BBS machine
-					var promptPos = this.EnhReaderPrepLast2LinesForPrompt();
-					console.print("\x01n\x01cFilename:\x01h");
-					var inputLen = console.screen_columns - 10; // 10 = "Filename:" length + 1
-					var filename = console.getstr(inputLen, K_NOCRLF);
-					console.attributes = "N";
-					if (filename.length > 0)
-					{
-						console.gotoxy(promptPos);
-						console.cleartoeol("\x01n");
-						console.gotoxy(promptPos);
-						var saveHexRetObj = this.SaveMsgHexDumpToFile(msgHeader, filename);
-						if (saveHexRetObj.saveSucceeded)
-							console.print("\x01n\x01cThe hex dump has been saved.\x01n");
-						else if (saveHexRetObj.errorMsg != "")
-							console.print("\x01n\x01y\x01h" + saveHexRetObj.errorMsg + "\x01n");
-						else
-							console.print("\x01n\x01y\x01hFailed!\x01n");
-					}
-					else
-					{
-						console.gotoxy(promptPos);
-						console.print("\x01n\x01y\x01hHex dump not exported\x01n");
-					}
-					mswait(ERROR_PAUSE_WAIT_MS);
-					// Refresh the last 2 lines of the message on the screen to overwrite
-					// the file save prompt
-					this.DisplayEnhReaderError("", msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr);
-				}
-				writeMessage = false; // Don't write the whole message again
-				break;
-			case this.enhReaderKeys.operatorMenu: // Operator menu
-				writeMessage = false;
-				if (user.is_sysop)
-				{
-					var opRetObj = this.ShowReadModeOpMenuAndGetSelection();
-					// Refresh the message area where the option menu was
-					this.RefreshMsgAreaRectangle(msgInfo.messageLines, topMsgLineIdx, opRetObj.menuTopLeftX, opRetObj.menuTopLeftY, opRetObj.menuWidth, opRetObj.menuHeight);
-					if (opRetObj.chosenOption != null)
-					{
-						switch (opRetObj.chosenOption)
-						{
-							case this.readerOpMenuOptValues.validateMsg: // Validate the message
-								if (this.subBoardCode != "mail" && msg_area.sub[this.subBoardCode].is_moderated)
-								{
-									var message = "";
-									if (this.ValidateMsg(this.subBoardCode, msgHeader.number))
-									{
-										message = "\x01n\x01cMessage validation successful";
-										// Refresh the message header in the arrays
-										this.RefreshMsgHdrInArrays(msgHeader.number);
-										// Exit out of the reader and come back to read
-										// the same message again so that the voting results
-										// are re-loaded and displayed on the screen.
-										retObj.newMsgOffset = pOffset;
-										retObj.nextAction = ACTION_GO_SPECIFIC_MSG;
-										continueOn = false;
-										this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
-									}
-									else
-										message = "\x01n\x01y\x01hMessage validation failed!";
-									this.DisplayEnhReaderError(message, msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr);
-									writeMessage = true;
-								}
-								break;
-							case this.readerOpMenuOptValues.editAuthorUserAccount: // Edit the local user account
-								console.gotoxy(1, console.screen_rows);
-								userEdit(msgHeader, pOffset, this);
-								// Refresh things on the screen
-								console.clear("\x01n");
-								// Display the message header and key help line again
-								this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
-								this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
-								// Display the scrollbar again to refresh it on the screen
-								if (this.userSettings.useEnhReaderScrollbar)
-								{
-									retObj.solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
-									this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
-								}
-								else
-								{
-									// TODO
-								}
-								writeMessage = true; // We want to refresh the message on the screen
-								break;
-							case this.readerOpMenuOptValues.showHdrLines: // Show header or kludge lines
-							case this.readerOpMenuOptValues.showKludgeLines:
-								writeMessage = this.ShowHdrOrKludgeLines_Scrollable(opRetObj.chosenOption == this.readerOpMenuOptValues.showKludgeLines, msgHeader, msgAreaWidth, msgAreaHeight);
-								// Display the scrollbar for the message to refresh it on the screen
-								if (this.userSettings.useEnhReaderScrollbar)
-								{
-									solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
-									this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
-								}
-								else
-								{
-									// TODO
-								}
-								break;
-							case this.readerOpMenuOptValues.showTallyStats: // Show tally/vote stats/info
-								var DVRetObj = this.ShowVoteInfo_Scrollable(msgHeader, msgAreaWidth, msgAreaHeight);
-								writeMessage = DVRetObj.writeMessage;
-								if (DVRetObj.hasVoteProps)
-								{
-									// Display the scrollbar for the message to refresh it on the screen
-									if (this.userSettings.useEnhReaderScrollbar)
-									{
-										solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
-										this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
-									}
-									else
-									{
-										// TODO
-									}
-								}
-								break;
-							case this.readerOpMenuOptValues.showMsgHex:
-								writeMessage = false;
-								if (msgHexInfo == null)
-									msgHexInfo = this.GetMsgHexInfo(messageText, true);
-								if (msgHexInfo.msgHexArray.length > 0)
-								{
-									this.ShowMsgHex_Scrolling(msgHexInfo);
-									if (this.userSettings.useEnhReaderScrollbar)
-										this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
-									writeMessage = true;
-								}
-								break;
-							case this.readerOpMenuOptValues.saveMsgHexToFile:
-								// Prompt the user for a filename to save the hex dump to the
-								// BBS machine
-								var promptPos = this.EnhReaderPrepLast2LinesForPrompt();
-								console.print("\x01n\x01cFilename:\x01h");
-								var inputLen = console.screen_columns - 10; // 10 = "Filename:" length + 1
-								var filename = console.getstr(inputLen, K_NOCRLF);
-								console.attributes = "N";
-								if (filename.length > 0)
-								{
-									console.gotoxy(promptPos);
-									console.cleartoeol("\x01n");
-									console.gotoxy(promptPos);
-									var saveHexRetObj = this.SaveMsgHexDumpToFile(msgHeader, filename);
-									if (saveHexRetObj.saveSucceeded)
-										console.print("\x01n\x01cThe hex dump has been saved.\x01n");
-									else if (saveHexRetObj.errorMsg != "")
-										console.print("\x01n\x01y\x01h" + saveHexRetObj.errorMsg + "\x01n");
-									else
-										console.print("\x01n\x01y\x01hFailed!\x01n");
-								}
-								else
-								{
-									console.gotoxy(promptPos);
-									console.print("\x01n\x01y\x01hHex dump not exported\x01n");
-								}
-								mswait(ERROR_PAUSE_WAIT_MS);
-								// Refresh the last 2 lines of the message on the screen to overwrite
-								// the file save prompt
-								this.DisplayEnhReaderError("", msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr);
-								writeMessage = true;
-								break;
-							case this.readerOpMenuOptValues.quickValUser: // Quick validate the user
-								var valRetObj = quickValidateLocalUser(msgHeader.from, this.scrollingReaderInterface && console.term_supports(USER_ANSI), this.quickUserValSetIndex);
-								if (valRetObj.needWholeScreenRefresh)
-								{
-									this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
-									if (this.userSettings.useEnhReaderScrollbar)
-										this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
-									else
-									{
-										// TODO
-									}
-									this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
-								}
-								else
-								{
-									writeMessage = false; // Don't refresh the whole message
-									if (valRetObj.refreshBottomLine)
-										this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
-									if (valRetObj.optionBoxTopLeftX > 0 && valRetObj.optionBoxTopLeftY > 0 && valRetObj.optionBoxWidth > 0 && valRetObj.optionBoxHeight > 0)
-										this.RefreshMsgAreaRectangle(msgInfo.messageLines, topMsgLineIdx, valRetObj.optionBoxTopLeftX, valRetObj.optionBoxTopLeftY, valRetObj.optionBoxWidth, valRetObj.optionBoxHeight);
-								}
-								break;
-							case this.readerOpMenuOptValues.addAuthorToTwitList: // Add author to twit list
-								var promptTxt = format("Add %s to twit list", msgHeader.from);
-								if (this.EnhReaderPromptYesNo(promptTxt, msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr, solidBlockStartRow, numSolidScrollBlocks, true))
-								{
-									var statusMsg = "\x01n" + addToTwitList(msgHeader.from) ? "\x01w\x01hSuccessfully updated the twit list" : "\x01y\x01hFailed to update the twit list!"
-									writeWithPause(1, console.screen_rows, statusMsg, ERROR_PAUSE_WAIT_MS, "\x01n", true);
-									console.attributes = "N";
-									this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
-								}
-								break;
-							case this.readerOpMenuOptValues.addAuthorEmailToEmailFilter:
-								var fromEmailAddr = "";
-								if (typeof(msgHeader.from_net_addr) === "string" && msgHeader.from_net_addr.length > 0)
-								{
-									if (msgHeader.from_net_type == NET_INTERNET)
-										fromEmailAddr = msgHeader.from_net_addr;
-									else
-										fromEmailAddr = msgHeader.from + "@" + msgHeader.from_net_addr;
-								}
-								var promptTxt = format("Add %s to global email filter", fromEmailAddr);
-								if (this.EnhReaderPromptYesNo(promptTxt, msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr, solidBlockStartRow, numSolidScrollBlocks, true))
-								{
-									var statusMsg = "\x01n" + addToGlobalEmailFilter(fromEmailAddr) ? "\x01w\x01hSuccessfully updated the email filter" : "\x01y\x01hFailed to update the email filter!"
-									writeWithPause(1, console.screen_rows, statusMsg, ERROR_PAUSE_WAIT_MS, "\x01n", true);
-									console.attributes = "N";
-									this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
-								}
-								break;
-						}
-					}
-				}
-				break;
-			case this.enhReaderKeys.quit: // Quit
-			case KEY_ESC:
-				// Normally, if quitFromReaderGoesToMsgList is enabled, then do that
-				if (this.userSettings.quitFromReaderGoesToMsgList)
-				{
-					console.attributes = "N";
-					console.crlf();
-					console.print("Loading...");
-					retObj.nextAction = ACTION_DISPLAY_MSG_LIST;
-				}
-				else
-					retObj.nextAction = ACTION_QUIT;
-				continueOn = false;
-				break;
-			default:
-				writeMessage = false;
-				break;
-		}
-	}
-
-	return retObj;
-}
-// Helper method for ReadMessageEnhanced_Scrollable(): Shows header or kludge lines for the scrollable interface. For the sysop.
-function DigDistMsgReader_ShowHdrOrKludgeLines_Scrollable(pOnlyKludgeLines, msgHeader, msgAreaWidth, msgAreaHeight)
-{
-	var msgReaderObj = this;
-	// This is a scrollbar update function for use when viewing the header info/kludge lines.
-	function msgInfoScrollbarUpdateFn(pFractionToLastPage)
-	{
-		var infoSolidBlockStartRow = msgReaderObj.msgAreaTop + Math.floor(numNonSolidInfoScrollBlocks * pFractionToLastPage);
-		if (infoSolidBlockStartRow != lastInfoSolidBlockStartRow)
-			msgReaderObj.UpdateEnhancedReaderScrollbar(infoSolidBlockStartRow, lastInfoSolidBlockStartRow, numInfoSolidScrollBlocks);
-		lastInfoSolidBlockStartRow = infoSolidBlockStartRow;
-		console.gotoxy(1, console.screen_rows);
-	}
-
-	var writeMessage = false;
-
-	// Save the original cursor position
-	var originalCurPos = console.getxy();
-
-	// Get an array of the extended header info/kludge lines and then
-	// allow the user to scroll through them.
-	var extdHdrInfoLines = this.GetExtdMsgHdrInfo(this.subBoardCode, msgHeader.number, pOnlyKludgeLines);
-	if (extdHdrInfoLines.length > 0)
-	{
-		if (this.userSettings.useEnhReaderScrollbar)
-		{
-			// Calculate information for the scrollbar for the kludge lines
-			var infoFractionShown = this.msgAreaHeight / extdHdrInfoLines.length;
-			if (infoFractionShown > 1)
-				infoFractionShown = 1.0;
-			var numInfoSolidScrollBlocks = Math.floor(this.msgAreaHeight * infoFractionShown);
-			if (numInfoSolidScrollBlocks == 0)
-				numInfoSolidScrollBlocks = 1;
-			var numNonSolidInfoScrollBlocks = this.msgAreaHeight - numInfoSolidScrollBlocks;
-			var lastInfoSolidBlockStartRow = this.msgAreaTop;
-			// Display the kludge lines and let the user scroll through them
-			this.DisplayEnhancedReaderWholeScrollbar(this.msgAreaTop, numInfoSolidScrollBlocks);
-		}
-		scrollTextLines(extdHdrInfoLines, 0, this.colors.msgBodyColor, true, this.msgAreaLeft,
-						this.msgAreaTop, msgAreaWidth, msgAreaHeight, 1, console.screen_rows,
-						this.userSettings.useEnhReaderScrollbar, msgInfoScrollbarUpdateFn);
-		writeMessage = true; // We want to refresh the message on the screen
-	}
-	else
-	{
-		// There are no header/kludge lines for this message
-		var msgText = pOnlyKludgeLines ? this.text.noKludgeLinesForThisMsgText : this.text.noHdrLinesForThisMsgText;
-		this.DisplayEnhReaderError(replaceAtCodesInStr(msgText), msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr);
-		console.gotoxy(originalCurPos);
-	}
-
-	return writeMessage;
-}
-// For the DDMsgReader class: Shows vote/tally information, for the scrollable interface.
-function DigDistMsgReader_ShowVoteInfo_Scrollable(pMsgHeader, pMsgAreaWidth, pMsgAreaHeight)
-{
-	var msgReaderObj = this;
-	// This is a scrollbar update function for use when viewing the header info/kludge lines.
-	function msgInfoScrollbarUpdateFn(pFractionToLastPage)
-	{
-		var infoSolidBlockStartRow = msgReaderObj.msgAreaTop + Math.floor(numNonSolidInfoScrollBlocks * pFractionToLastPage);
-		if (infoSolidBlockStartRow != lastInfoSolidBlockStartRow)
-			msgReaderObj.UpdateEnhancedReaderScrollbar(infoSolidBlockStartRow, lastInfoSolidBlockStartRow, numInfoSolidScrollBlocks);
-		lastInfoSolidBlockStartRow = infoSolidBlockStartRow;
-		console.gotoxy(1, console.screen_rows);
-	}
-
-	var retObj = {
-		hasVoteProps: pMsgHeader.hasOwnProperty("total_votes") && pMsgHeader.hasOwnProperty("upvotes"),
-		writeMessage: false
-	};
-
-	// Save the original cursor position
-	var originalCurPos = console.getxy();
-	if (retObj.hasVoteProps)
-	{
-		var voteInfo = this.GetUpvoteAndDownvoteInfo(pMsgHeader);
-		// Display the vote info and let the user scroll through them
-		// (the console height should be enough, but do this just in case)
-		// Calculate information for the scrollbar for the vote info lines
-		if (this.userSettings.useEnhReaderScrollbar)
-		{
-			var infoFractionShown = this.pMsgAreaHeight / voteInfo.length;
-			if (infoFractionShown > 1)
-				infoFractionShown = 1.0;
-			var numInfoSolidScrollBlocks = Math.floor(this.pMsgAreaHeight * infoFractionShown);
-			if (numInfoSolidScrollBlocks == 0)
-				numInfoSolidScrollBlocks = 1;
-			var numNonSolidInfoScrollBlocks = this.pMsgAreaHeight - numInfoSolidScrollBlocks;
-			var lastInfoSolidBlockStartRow = this.msgAreaTop;
-			// Display the vote info lines and let the user scroll through them
-			this.DisplayEnhancedReaderWholeScrollbar(this.msgAreaTop, numInfoSolidScrollBlocks);
-		}
-		else
-		{
-			// TODO
-		}
-		scrollTextLines(voteInfo, 0, this.colors.msgBodyColor, true, this.msgAreaLeft, this.msgAreaTop, pMsgAreaWidth,
-		                pMsgAreaHeight, 1, console.screen_rows, this.userSettings.useEnhReaderScrollbar, msgInfoScrollbarUpdateFn);
-		retObj.writeMessage = true; // We want to refresh the message on the screen
-	}
-	else
-	{
-		this.DisplayEnhReaderError("There is no voting information for this message", msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr);
-		console.gotoxy(originalCurPos);
-	}
-	return retObj;
-}
-// Helper method for ReadMessageEnhanced_Scrollable(): Determines if there is a readable message after the
-// one at the given offset.
-//
-// Parameters:
-//  pOfset: The offset of the current message
-//  pMsgInfo: An object contaiining message information
-//  pTopMsgLineIdx: The index of the message line at the top of the reader area
-//  pMsgLineFormatStr: A format string for the message line
-//  pSolidBlockStartRow: The screen row of where the solid blocks start for the scrollbar
-//  pNumSolidScrollBlocks: The number of solid blocks in the scrollbar
-//
-// Return value: An object containing the following properties:
-//               newMsgOffset: The offset of the next readable message, if available.  If not available, this will be -1.
-//               writeMessage: Boolean - Whether or not to write the whole message again (for the scrollable interface)
-//               continueOn: Boolean - Whether or not to continue with the reader input loop
-//               nextAction: A value indicating the next action for the reader to take after leaving the reader function
-function DigDistMsgReader_ScrollableReaderNextReadableMessage(pOffset, pMsgInfo, pTopMsgLineIdx, pMsgLineFormatStr, pSolidBlockStartRow, pNumSolidScrollBlocks)
-{
-	var retObj = {
-		newMsgOffset: -1,
-		writeMessage: true,
-		continueOn: true,
-		nextAction: ACTION_NONE
-	};
-
-	// Look for the next readable message.  Even if we don't find one, we'll still
-	// want to return from this function (with message index -1) so that this script
-	// can go onto the next message sub-board/group.
-	retObj.newMsgOffset = this.FindNextReadableMsgIdx(pOffset, true);
-	// Note: Unlike the left arrow key, we want to exit this method when
-	// navigating to the next message, regardless of whether or not the
-	// user is allowed to change to a different sub-board, so that processes
-	// that require continuation (such as new message scan) can continue.
-	// Still, if there are no more readable messages in the current sub-board
-	// (and thus the user would go onto the next message area), prompt the
-	// user whether they want to continue onto the next message area.
-	if (retObj.newMsgOffset == -1 && !curMsgSubBoardIsLast())
-	{
-		// For personal mail, don't do anything, and don't refresh the
-		// message.  In a sub-board, ask the user if they want to go
-		// to the next one.
-		if (this.readingPersonalEmail)
-			retObj.writeMessage = false;
-		else
-		{
-			// If configured to allow the user to post in the sub-board
-			// instead of going to the next message area and we're not
-			// scanning, then do so.
-			if (this.readingPostOnSubBoardInsteadOfGoToNext && !this.doingMsgScan)
-			{
-				console.attributes = "N";
-				console.crlf();
-				// Ask the user if they want to post on the sub-board.
-				// If they say yes, then do so before exiting.
-				var grpNameAndDesc = this.GetGroupNameAndDesc();
-				if (!console.noyes(replaceAtCodesInStr(format(this.text.postOnSubBoard, grpNameAndDesc.grpName, grpNameAndDesc.grpDesc))))
-					bbs.post_msg(this.subBoardCode);
-				retObj.continueOn = false;
-				retObj.nextAction = ACTION_QUIT;
-			}
-			else
-			{
-				// Prompt the user whether they want to go to the next message area
-				if (this.EnhReaderPromptYesNo(replaceAtCodesInStr(this.text.goToNextMsgAreaPromptText), pMsgInfo.messageLines, pTopMsgLineIdx, pMsgLineFormatStr, pSolidBlockStartRow, pNumSolidScrollBlocks))
-				{
-					// Let this method exit and let the caller go to the next sub-board
-					retObj.continueOn = false;
-					retObj.nextAction = ACTION_GO_NEXT_MSG;
-				}
-				else
-					retObj.writeMessage = false; // No need to refresh the message
-			}
-		}
-	}
-	else
-	{
-		// We're not at the end of the sub-board, so it's okay to exit this
-		// method and go to the next message.
-		retObj.continueOn = false;
-		retObj.nextAction = ACTION_GO_NEXT_MSG;
-	}
-
-	return retObj;
-}
-// Helper method for ReadMessageEnhanced() - Determines the next keypress for a click
-// coordinate outside the scroll area.
-//
-// Parameters:
-//  pScrollRetObj: The return object of the message scroll function
-//  pEnhReadHelpLineClickCoords: An array of click coordinates & action strings
-//
-// Return value: An object containing the following properties:
-//               actionStr: A string containing the next action for the enhanced reader,
-//                          or an empty string if there was no valid action found.
-function DigDistMsgReader_ScrollReaderDetermineClickCoordAction(pScrollRetObj, pEnhReadHelpLineClickCoords)
-{
-	var retObj = {
-		actionStr: ""
-	};
-
-	for (var coordIdx = 0; coordIdx < pEnhReadHelpLineClickCoords.length; ++coordIdx)
-	{
-		if ((pScrollRetObj.mouse.x == pEnhReadHelpLineClickCoords[coordIdx].x) && (pScrollRetObj.mouse.y == pEnhReadHelpLineClickCoords[coordIdx].y))
-		{
-			// The up arrow, down arrow, PageUp, PageDown, Home, and End aren't handled
-			// here - Those are handled in scrollTextLines().
-			if (pEnhReadHelpLineClickCoords[coordIdx].actionStr == LEFT_ARROW)
-				retObj.actionStr = this.enhReaderKeys.previousMsg;
-			else if (pEnhReadHelpLineClickCoords[coordIdx].actionStr == RIGHT_ARROW)
-				retObj.actionStr = this.enhReaderKeys.nextMsg;
-			else if (pEnhReadHelpLineClickCoords[coordIdx].actionStr.indexOf("DEL") == 0)
-				retObj.actionStr = this.enhReaderKeys.deleteMessage;
-			else if (pEnhReadHelpLineClickCoords[coordIdx].actionStr.indexOf("E)") == 0)
-				retObj.actionStr = this.enhReaderKeys.editMsg;
-			else if (pEnhReadHelpLineClickCoords[coordIdx].actionStr.indexOf("F") == 0)
-				retObj.actionStr = this.enhReaderKeys.firstMsg;
-			else if (pEnhReadHelpLineClickCoords[coordIdx].actionStr.indexOf("L") == 0)
-				retObj.actionStr = this.enhReaderKeys.lastMsg;
-			else if (pEnhReadHelpLineClickCoords[coordIdx].actionStr.indexOf("R") == 0)
-				retObj.actionStr = this.enhReaderKeys.reply;
-			else if (pEnhReadHelpLineClickCoords[coordIdx].actionStr.indexOf("C") == 0)
-				retObj.actionStr = this.enhReaderKeys.chgMsgArea;
-			else if (pEnhReadHelpLineClickCoords[coordIdx].actionStr.indexOf("Q") == 0)
-				retObj.actionStr = this.enhReaderKeys.quit;
-			else if (pEnhReadHelpLineClickCoords[coordIdx].actionStr.indexOf("?") == 0)
-				retObj.actionStr = this.enhReaderKeys.showHelp;
-			break;
-		}
-	}
-	return retObj;
-}
-// Helper method for ReadMessageEnhanced() - Does the traditional (non-scrollable) reader interface
-function DigDistMsgReader_ReadMessageEnhanced_Traditional(msgHeader, allowChgMsgArea, messageText, pOffset, pmode)
-{
-	var retObj = {
-		offsetValid: true,
-		msgNotReadable: false,
-		userReplied: false,
-		lastKeypress: "",
-		newMsgOffset: -1,
-		nextAction: ACTION_NONE,
-		refreshEnhancedRdrHelpLine: false
-	};
-
-	// We could word-wrap the message to ensure words aren't split across lines, but
-	// doing so could make some messages look bad (i.e., messages with drawing characters),
-	// and word_wrap also might not handle ANSI or other color/attribute codes..
-	//if (!textHasDrawingChars(messageText))
-	//	messageText = word_wrap(messageText, this.msgAreaWidth);
-
-	var msgHasAttachments = msgHdrHasAttachmentFlag(msgHeader);
-
-	// Only interpret @-codes if the user is reading personal email and the sender is a sysop.  There
-	// are many @-codes that do some action such as move the cursor, execute a
-	// script, etc., and I don't want users on message networks to do anything
-	// malicious to users on other BBSes.
-	if (this.readingPersonalEmail && msgSenderIsASysop(msgHeader))
-		messageText = replaceAtCodesInStr(messageText); // Or this.ParseMsgAtCodes(messageText, msgHeader) to replace only some @ codes
-	var msgHasANSICodes = messageText.indexOf("\x1b[") >= 0;
-	var msgTextWrapped = (msgHasANSICodes ? messageText : word_wrap(messageText, console.screen_columns-1));
-
-	// Generate the key help text
-	var keyHelpText = "\x01n\x01c\x01h#\x01n\x01b, \x01c\x01hLeft\x01n\x01b, \x01c\x01hRight\x01n\x01b, ";
-	if (this.CanDelete() || this.CanDeleteLastMsg())
-		keyHelpText += "\x01c\x01hDEL\x01b, ";
-	if (this.CanEdit())
-		keyHelpText += "\x01c\x01hE\x01y)\x01n\x01cdit\x01b, ";
-	keyHelpText += "\x01c\x01hF\x01y)\x01n\x01cirst\x01b, \x01c\x01hL\x01y)\x01n\x01cast\x01b, \x01c\x01hR\x01y)\x01n\x01ceply\x01b, ";
-	// If the user is allowed to change to a different message area, then
-	// include that option.
-	if (allowChgMsgArea)
-	{
-		// If there's room for the private reply option, then include that
-		// before the change area option.
-		if (console.screen_columns >= 89)
-			keyHelpText += "\x01c\x01hP\x01y)\x01n\x01crivate reply\x01b, ";
-		keyHelpText += "\x01c\x01hC\x01y)\x01n\x01chg area\x01b, ";
-	}
-	else
-	{
-		// The user isn't allowed to change to a different message area.
-		// Go ahead and include the private reply option.
-		keyHelpText += "\x01c\x01hP\x01y)\x01n\x01crivate reply\x01b, ";
-	}
-	keyHelpText += "\x01c\x01hQ\x01y)\x01n\x01cuit\x01b, \x01c\x01h?\x01g: \x01c";
-
-	// For showing the message hex dump (for the sysop)
-	var msgHexInfo = null;
-
-	// User input loop
-	var writeMessage = true;
-	var writePromptText = true;
-	var continueOn = true;
-	while (continueOn)
-	{
-		if (writeMessage)
-		{
-			if (console.term_supports(USER_ANSI))
-				console.clear("\x01n");
-			// Write the message header & message body to the screen
-			this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
-			console.print("\x01n" + this.colors.msgBodyColor);
-			console.putmsg(msgTextWrapped, pmode|P_NOATCODES);
-		}
-		// Write the prompt text
-		if (writePromptText)
-			console.print(keyHelpText);
-		// Default the writing of the message & input prompt to true for the
-		// next iteration.
-		writeMessage = true;
-		writePromptText = true;
-		// Input a key from the user and take action based on the keypress.
-		//retObj.lastKeypress = getKeyWithESCChars(K_UPPER|K_NOCRLF|K_NOECHO|K_NOSPIN);
-		retObj.lastKeypress = getKeyWithESCChars(K_UPPER);
-		switch (retObj.lastKeypress)
-		{
-			case this.enhReaderKeys.deleteMessage: // Delete message
-			case '\x7f':
-			case '\x08':
-				console.crlf();
-				// Prompt the user for confirmation to delete the message.
-				// Note: this.PromptAndDeleteOrUndeleteMessage() will check to see if the user
-				// is a sysop or the message was posted by the user.
-				// If the message was deleted, then exit this read method
-				// and return KEY_RIGHT as the last keypress so that the
-				// calling method will go to the next message/sub-board.
-				// Otherwise (if the message was not deleted), refresh the
-				// last 2 lines of the message on the screen.
-				var msgWasDeleted = this.PromptAndDeleteOrUndeleteMessage(pOffset, null, true);
-				if (msgWasDeleted && !canViewDeletedMsgs())
-				{
-					var msgSearchObj = this.LookForNextOrPriorNonDeletedMsg(pOffset);
-					continueOn = msgSearchObj.continueInputLoop;
-					retObj.newMsgOffset = msgSearchObj.newMsgOffset;
-					retObj.nextAction = msgSearchObj.nextAction;
-					if (msgSearchObj.promptGoToNextArea)
-					{
-						if (console.yesno(replaceAtCodesInStr(this.text.goToNextMsgAreaPromptText)))
-						{
-							// Let this method exit and let the caller go to the next sub-board
-							continueOn = false;
-							retObj.nextAction = ACTION_GO_NEXT_MSG;
-						}
-						else
-							writeMessage = false; // No need to refresh the message
-					}
-				}
-				break;
-			case this.enhReaderKeys.selectMessage: // Select message (for batch delete, etc.)
-				console.crlf();
-				var selectMessage = !console.noyes("Select this message");
-				this.ToggleSelectedMessage(this.subBoardCode, pOffset, selectMessage);
-				break;
-			case this.enhReaderKeys.batchDelete:
-				// TODO: Write this?  Not sure yet if it makes much sense to
-				// have batch delete in the reader interface.
-				// Prompt the user for confirmation, and use
-				// this.DeleteOrUndeleteSelectedMessages() to mark the selected messages
-				// as deleted.
-				// Returns an object with the following properties:
-				//  deletedAll: Boolean - Whether or not all messages were successfully marked
-				//              for deletion
-				//  failureList: An object containing indexes of messages that failed to get
-				//               marked for deletion, indexed by internal sub-board code, then
-				//               containing messages indexes as properties.  Reasons for failing
-				//               to mark messages deleted can include the user not having permission
-				//               to delete in a sub-board, failure to open the sub-board, etc.
-				writeMessage = false; // No need to refresh the message
-				break;
-			case this.enhReaderKeys.editMsg: // Edit the message
-				if (this.CanEdit())
-				{
-					console.crlf();
-					// Let the user edit the message if they want to
-					var editReturnObj = this.EditExistingMsg(pOffset);
-					// If the user confirmed editing the message, then see if the
-					// message was edited and refresh the screen accordingly.
-					if (editReturnObj.userConfirmed)
-					{
-						// If the message was edited, then refresh the text lines
-						// array and update the other message-related variables.
-						if (editReturnObj.msgEdited && (editReturnObj.newMsgIdx > -1))
-						{
-							// When the message is edited, the old message will be
-							// deleted and the edited message will be posted as a new
-							// message.  So we should return to the caller and have it
-							// go directly to that new message.
-							continueOn = false;
-							retObj.newMsgOffset = editReturnObj.newMsgIdx;
-						}
-					}
-				}
-				else
-				{
-					writeMessage = false;
-					writePromptText = false;
-				}
-				break;
-			case this.enhReaderKeys.showHelp: // Show help
-				if (!console.term_supports(USER_ANSI))
-				{
-					console.crlf();
-					console.crlf();
-				}
-				this.DisplayEnhancedReaderHelp(allowChgMsgArea, msgHasAttachments);
-				if (!console.term_supports(USER_ANSI))
-				{
-					console.crlf();
-					console.crlf();
-				}
-				break;
-			case this.enhReaderKeys.reply: // Reply to the message
-			case this.enhReaderKeys.privateReply: // Private reply
-				// If the user pressed the private reply key while reading private
-				// mail, then do nothing (allow only the regular reply key to reply).
-				// If not reading personal email, go ahead and let the user reply
-				// with either the reply or private reply keypress.
-				var privateReply = (retObj.lastKeypress == this.enhReaderKeys.privateReply);
-				if (privateReply && this.readingPersonalEmail)
-				{
-					writeMessage = false; // Don't re-write the current message again
-					writePromptText = false; // Don't write the prompt text again
-				}
-				else
-				{
-					console.crlf();
-					// Get the message header with fields expanded so we can get the most info possible.
-					//var extdMsgHdr = this.GetMsgHdrByAbsoluteNum(msgHeader.number, true);
-					var msgbase = new MsgBase(this.subBoardCode);
-					if (msgbase.open())
-					{
-						var extdMsgHdr = msgbase.get_msg_header(false, msgHeader.number, true);
-						msgbase.close();
-						// Let the user reply to the message
-						var replyRetObj = this.ReplyToMsg(extdMsgHdr, messageText, privateReply, pOffset);
-						retObj.userReplied = replyRetObj.postSucceeded;
-						//retObj.msgNotReadable = replyRetObj.msgWasDeleted;
-						var msgWasDeleted = replyRetObj.msgWasDeleted;
-						if (msgWasDeleted && !canViewDeletedMsgs())
-						{
-							var msgSearchObj = this.LookForNextOrPriorNonDeletedMsg(pOffset);
-							continueOn = msgSearchObj.continueInputLoop;
-							retObj.newMsgOffset = msgSearchObj.newMsgOffset;
-							retObj.nextAction = msgSearchObj.nextAction;
-							if (msgSearchObj.promptGoToNextArea)
-							{
-								if (console.yesno(replaceAtCodesInStr(this.text.goToNextMsgAreaPromptText)))
-								{
-									// Let this method exit and let the caller go to the next sub-board
-									continueOn = false;
-									retObj.nextAction = ACTION_GO_NEXT_MSG;
-								}
-								else
-									writeMessage = true; // We want to refresh the message on the screen
-							}
-						}
-					}
-					else // msgbase failed to open
-					{
-						console.attributes = "N";
-						console.crlf();
-						console.print("\x01h\x01yFailed to open the sub-board.  Aborting.\x01n");
-						mswait(ERROR_PAUSE_WAIT_MS);
-					}
-				}
-				break;
-			case this.enhReaderKeys.postMsg: // Post a message
-				if (!this.readingPersonalEmail)
-				{
-					// Let the user post a message.
-					if (bbs.post_msg(this.subBoardCode))
-					{
-						// TODO: If the user is doing a search, it might be
-						// useful to search their new message and add it to
-						// the search results if it's a match..  but maybe
-						// not?
-					}
-
-					console.pause();
-
-					// We'll want to refresh the message & prompt text on the screen
-					writeMessage = true;
-					writePromptText = true;
-				}
-				else
-				{
-					// Don't write the current message or prompt text in the next iteration
-					writeMessage = false;
-					writePromptText = false;
-				}
-				break;
-			// Numeric digit: The start of a number of a message to read
-			case "0":
-			case "1":
-			case "2":
-			case "3":
-			case "4":
-			case "5":
-			case "6":
-			case "7":
-			case "8":
-			case "9":
-				console.crlf();
-				// Put the user's input back in the input buffer to
-				// be used for getting the rest of the message number.
-				console.ungetstr(retObj.lastKeypress);
-				// Prompt for the message number
-				var msgNumInput = this.PromptForMsgNum(null, replaceAtCodesInStr(this.text.readMsgNumPromptText), false, ERROR_PAUSE_WAIT_MS, false);
-				// Only allow reading the message if the message number is valid
-				// and it's not the same message number that was passed in.
-				if ((msgNumInput > 0) && (msgNumInput-1 != pOffset))
-				{
-					// If the message is marked as deleted, then output an error
-					if (this.MessageIsDeleted(msgNumInput-1))
-					{
-						console.crlf();
-						console.print("\x01n" + replaceAtCodesInStr(format(this.text.msgHasBeenDeletedText, msgNumInput)) + "\x01n");
-						console.crlf();
-						console.pause();
-					}
-					else
-					{
-						// Confirm with the user whether to read the message
-						var readMsg = true;
-						if (this.promptToReadMessage)
-						{
-							readMsg = console.yesno("\x01n" + this.colors["readMsgConfirmColor"]
-													+ "Read message "
-													+ this.colors["readMsgConfirmNumberColor"]
-													+ msgNumInput + this.colors["readMsgConfirmColor"]
-													+ ": Are you sure");
-						}
-						if (readMsg)
-						{
-							continueOn = false;
-							retObj.newMsgOffset = msgNumInput - 1;
-							retObj.nextAction = ACTION_GO_SPECIFIC_MSG;
-						}
-					}
-				}
-				break;
-			case this.enhReaderKeys.prevMsgByTitle: // Previous message by title
-			case this.enhReaderKeys.prevMsgByAuthor: // Previous message by author
-			case this.enhReaderKeys.prevMsgByToUser: // Previous message by 'to user'
-			case this.enhReaderKeys.prevMsgByThreadID: // Previous message by thread ID
-				// Only allow this if we aren't doing a message search.
-				if (!this.SearchingAndResultObjsDefinedForCurSub())
-				{
-					console.crlf(); // For the "Searching..." text
-					var threadPrevMsgOffset = this.FindThreadPrevOffset(msgHeader,
-																		keypressToThreadType(retObj.lastKeypress, this.enhReaderKeys),
-																		false);
-					if (threadPrevMsgOffset > -1)
-					{
-						retObj.newMsgOffset = threadPrevMsgOffset;
-						retObj.nextAction = ACTION_GO_SPECIFIC_MSG;
-						continueOn = false;
-					}
-				}
-				else
-				{
-					writeMessage = false;
-					writePromptText = false;
-				}
-				break;
-			case this.enhReaderKeys.nextMsgByTitle: // Next message by title (subject)
-			case this.enhReaderKeys.nextMsgByAuthor: // Next message by author
-			case this.enhReaderKeys.nextMsgByToUser: // Next message by 'to user'
-			case this.enhReaderKeys.nextMsgByThreadID: // Next message by thread ID
-				// Only allow this if we aren't doing a message search.
-				if (!this.SearchingAndResultObjsDefinedForCurSub())
-				{
-					console.crlf(); // For the "Searching..." text
-					var threadNextMsgOffset = this.FindThreadNextOffset(msgHeader,
-																		keypressToThreadType(retObj.lastKeypress, this.enhReaderKeys),
-																		false);
-					if (threadNextMsgOffset > -1)
-					{
-						retObj.newMsgOffset = threadNextMsgOffset;
-						retObj.nextAction = ACTION_GO_SPECIFIC_MSG;
-						continueOn = false;
-					}
-				}
-				else
-				{
-					writeMessage = false;
-					writePromptText = false;
-				}
-				break;
-			case this.enhReaderKeys.previousMsg: // Previous message
-				// TODO: Change the key for this?
-				// Look for a prior message that isn't marked for deletion.  Even
-				// if we don't find one, we'll still want to return from this
-				// function (with message index -1) so that this script can go
-				// onto the previous message sub-board/group.
-				retObj.newMsgOffset = this.FindNextReadableMsgIdx(pOffset, false);
-				var goToPrevMessage = false;
-				if ((retObj.newMsgOffset > -1) || allowChgMsgArea)
-				{
-					if (retObj.newMsgOffset == -1 && !curMsgSubBoardIsLast())
-					{
-						console.crlf();
-						goToPrevMessage = console.yesno(replaceAtCodesInStr(this.text.goToPrevMsgAreaPromptText));
-					}
-					else
-					{
-						// We're not at the beginning of the sub-board, so it's okay to exit this
-						// method and go to the previous message.
-						goToPrevMessage = true;
-					}
-				}
-				if (goToPrevMessage)
-				{
-					continueOn = false;
-					retObj.nextAction = ACTION_GO_PREVIOUS_MSG;
-				}
-				break;
-			case this.enhReaderKeys.nextMsg: // Next message
-			case KEY_ENTER:
-				// Look for a later message that isn't marked for deletion.  Even
-				// if we don't find one, we'll still want to return from this
-				// function (with message index -1) so that this script can go
-				// onto the next message sub-board/group.
-				retObj.newMsgOffset = this.FindNextReadableMsgIdx(pOffset, true);
-				// Note: Unlike the left arrow key, we want to exit this method when
-				// navigating to the next message, regardless of whether or not the
-				// user is allowed to change to a different sub-board, so that processes
-				// that require continuation (such as new message scan) can continue.
-				// Still, if there are no more readable messages in the current sub-board
-				// (and thus the user would go onto the next message area), prompt the
-				// user whether they want to continue onto the next message area.
-				if (retObj.newMsgOffset == -1 && !curMsgSubBoardIsLast())
-				{
-					console.attributes = "N";
-					console.crlf();
-					// If configured to allow the user to post in the sub-board
-					// instead of going to the next message area and we're not
-					// scanning, then do so.
-					if (this.readingPostOnSubBoardInsteadOfGoToNext && !this.doingMsgScan)
-					{
-						// Ask the user if they want to post on the sub-board.
-						// If they say yes, then do so before exiting.
-						var grpNameAndDesc = this.GetGroupNameAndDesc();
-						if (!console.noyes(replaceAtCodesInStr(format(this.text.postOnSubBoard, grpNameAndDesc.grpName, grpNameAndDesc.grpDesc))))
-							bbs.post_msg(this.subBoardCode);
-						continueOn = false;
-						retObj.nextAction = ACTION_QUIT;
-					}
-					else
-					{
-						if (console.yesno(replaceAtCodesInStr(this.text.goToNextMsgAreaPromptText)))
-						{
-							// Let this method exit and let the caller go to the next sub-board
-							continueOn = false;
-							retObj.nextAction = ACTION_GO_NEXT_MSG;
-						}
-					}
-				}
-				else
-				{
-					// We're not at the end of the sub-board, so it's okay to exit this
-					// method and go to the next message.
-					continueOn = false;
-					retObj.nextAction = ACTION_GO_NEXT_MSG;
-				}
-				break;
-			case this.enhReaderKeys.firstMsg: // First message
-				// Only leave this function if we aren't already on the first message.
-				if (pOffset > 0)
-				{
-					continueOn = false;
-					retObj.nextAction = ACTION_GO_FIRST_MSG;
-				}
-				else
-				{
-					writeMessage = false;
-					writePromptText = false;
-				}
-				break;
-			case this.enhReaderKeys.lastMsg: // Last message
-				// Only leave this function if we aren't already on the last message.
-				if (pOffset < this.NumMessages() - 1)
-				{
-					continueOn = false;
-					retObj.nextAction = ACTION_GO_LAST_MSG;
-				}
-				else
-				{
-					writeMessage = false;
-					writePromptText = false;
-				}
-				break;
-			case "-": // Go to the previous message area
-				if (allowChgMsgArea)
-				{
-					continueOn = false;
-					retObj.nextAction = ACTION_GO_PREV_MSG_AREA;
-				}
-				else
-				{
-					writeMessage = false;
-					writePromptText = false;
-				}
-				break;
-			case "+": // Go to the next message area
-				if (allowChgMsgArea || this.doingMultiSubBoardScan)
-				{
-					continueOn = false;
-					retObj.nextAction = ACTION_GO_NEXT_MSG_AREA;
-				}
-				else
-				{
-					writeMessage = false;
-					writePromptText = false;
-				}
-				break;
-			// H and K: Display the extended message header info/kludge lines
-			// (for the sysop)
-			case this.enhReaderKeys.showHdrInfo:
-			case this.enhReaderKeys.showKludgeLines:
-				if (user.is_sysop)
-				{
-					console.crlf();
-					// Get an array of the extended header info/kludge lines and then
-					// display them.
-					var extdHdrInfoLines = this.GetExtdMsgHdrInfo(this.subBoardCode, msgHeader.number, (retObj.lastKeypress == this.enhReaderKeys.showKludgeLines));
-					if (extdHdrInfoLines.length > 0)
-					{
-						console.crlf();
-						for (var infoIter = 0; infoIter < extdHdrInfoLines.length; ++infoIter)
-						{
-							console.print(extdHdrInfoLines[infoIter]);
-							console.crlf();
-						}
-						console.pause();
-					}
-					else
-					{
-						// There are no kludge lines for this message
-						console.print(replaceAtCodesInStr(this.text.noKludgeLinesForThisMsgText));
-						console.crlf();
-						console.pause();
-					}
-				}
-				else // The user is not a sysop
-				{
-					writeMessage = false;
-					writePromptText = false;
-				}
-				break;
-				// Message list, change message area: Quit out of this input loop
-				// and let the calling function, this.ReadMessages(), handle the
-				// action.
-			case this.enhReaderKeys.showMsgList: // Message list
-				console.attributes = "N";
-				console.crlf();
-				console.print("Loading...");
-				retObj.nextAction = ACTION_DISPLAY_MSG_LIST;
-				continueOn = false;
-				break;
-			case this.enhReaderKeys.chgMsgArea: // Change message area, if allowed
-				if (allowChgMsgArea)
-				{
-					retObj.nextAction = ACTION_CHG_MSG_AREA;
-					continueOn = false;
-				}
-				else
-				{
-					writeMessage = false;
-					writePromptText = false;
-				}
-				break;
-			case this.enhReaderKeys.downloadAttachments: // Download attachments
-				if (msgHasAttachments)
-				{
-					console.attributes = "N";
-					console.crlf();
-					console.print("\x01c- Download Attached Files -\x01n");
-					allowUserToDownloadMessage_NewInterface(msgHeader, this.subBoardCode);
-
-					// Ensure the message is refreshed on the screen
-					writeMessage = true;
-					writePromptText = true;
-				}
-				else
-				{
-					writeMessage = false;
-					writePromptText = false;
-				}
-				break;
-			case this.enhReaderKeys.saveToBBSMachine:
-				// Save the message to the BBS machine - Only allow this
-				// if the user is a sysop.
-				if (user.is_sysop)
-				{
-					console.crlf();
-					console.print("\x01n\x01cFilename:\x01h");
-					var inputLen = console.screen_columns - 10; // 10 = "Filename:" length + 1
-					var filename = console.getstr(inputLen, K_NOCRLF);
-					console.attributes = "N";
-					console.crlf();
-					if (filename.length > 0)
-					{
-						var saveMsgRetObj = this.SaveMsgToFile(msgHeader, filename);
-						if (saveMsgRetObj.succeeded)
-						{
-							console.print("\x01n\x01cThe message has been saved.\x01n");
-							if (msgHdrHasAttachmentFlag(msgHeader))
-								console.print(" Attachments not saved.");
-							console.attributes = "N";
-						}
-						else
-							console.print("\x01n\x01y\x01hFailed: " + saveMsgRetObj.errorMsg + "\x01n");
-						mswait(ERROR_PAUSE_WAIT_MS);
-					}
-					else
-					{
-						console.print("\x01n\x01y\x01hMessage not exported\x01n");
-						mswait(ERROR_PAUSE_WAIT_MS);
-					}
-					writeMessage = true;
-				}
-				else
-					writeMessage = false;
-				break;
-			case this.enhReaderKeys.userEdit: // Edit the user who wrote the message
-				if (user.is_sysop)
-				{
-					console.attributes = "N";
-					console.crlf();
-					console.print("- Edit user " + msgHeader.from);
-					console.crlf();
-					var editObj = editUser(msgHeader.from);
-					if (editObj.errorMsg.length != 0)
-					{
-						console.attributes = "N";
-						console.crlf();
-						console.print("\x01y\x01h" + editObj.errorMsg + "\x01n");
-						console.crlf();
-						console.pause();
-					}
-				}
-				writeMessage = true;
-				break;
-			case this.enhReaderKeys.forwardMsg: // Forward the message
-				console.attributes = "N";
-				console.crlf();
-				console.print("\x01c- Forward message\x01n");
-				console.crlf();
-				var retStr = this.ForwardMessage(msgHeader, messageText);
-				if (retStr.length > 0)
-				{
-					console.print("\x01n\x01h\x01y* " + retStr + "\x01n");
-					console.crlf();
-					console.pause();
-				}
-				writeMessage = true;
-				break;
-			case this.enhReaderKeys.vote: // Vote on the message
-				var voteRetObj = this.VoteOnMessage(msgHeader);
-				if (voteRetObj.BBSHasVoteFunction)
-				{
-					if (!voteRetObj.userQuit)
-					{
-						if ((voteRetObj.errorMsg.length > 0) || (!voteRetObj.savedVote))
-						{
-							console.attributes = "N";
-							console.crlf();
-							if (voteRetObj.errorMsg.length > 0)
-							{
-								if (voteRetObj.mnemonicsRequiredForErrorMsg)
-								{
-									console.mnemonics(voteRetObj.errorMsg);
-									console.attributes = "N";
-								}
-								else
-									console.print("\x01y\x01h* " + voteRetObj.errorMsg + "\x01n");
-							}
-							else if (!voteRetObj.savedVote)
-								console.print("\x01y\x01h* Failed to save the vote\x01n");
-							console.crlf();
-							console.pause();
-						}
-						else
-							msgHeader = voteRetObj.updatedHdr; // To get updated vote information
-					}
-
-					// If this message is a poll, then exit out of the reader
-					// and come back to read the same message again so that the
-					// voting results are re-loaded and displayed on the screen.
-					if ((typeof(MSG_TYPE_POLL) != "undefined") && (msgHeader.type & MSG_TYPE_POLL) == MSG_TYPE_POLL)
-					{
-						retObj.newMsgOffset = pOffset;
-						retObj.nextAction = ACTION_GO_SPECIFIC_MSG;
-						continueOn = false;
-					}
-					else
-						writeMessage = true; // We want to refresh the message on the screen
-				}
-				else
-					writeMessage = false;
-				break;
-			case this.enhReaderKeys.showVotes: // Show votes
-				if (msgHeader.hasOwnProperty("total_votes") && msgHeader.hasOwnProperty("upvotes"))
-				{
-					console.attributes = "N";
-					console.crlf();
-					var voteInfo = this.GetUpvoteAndDownvoteInfo(msgHeader);
-					for (var voteInfoIdx = 0; voteInfoIdx < voteInfo.length; ++voteInfoIdx)
-					{
-						console.print(voteInfo[voteInfoIdx]);
-						console.crlf();
-					}
-				}
-				else
-				{
-					console.print("\x01n\x01h\x01yThere is no voting information for this message\x01n");
-					console.crlf();
-				}
-				console.pause();
-				writeMessage = true;
-				break;
-			case this.enhReaderKeys.closePoll: // Close a poll message
-				var pollCloseMsg = "";
-				console.attributes = "N";
-				console.crlf();
-				// If this message is a poll, then allow closing it.
-				if ((typeof(MSG_TYPE_POLL) != "undefined") && (msgHeader.type & MSG_TYPE_POLL) == MSG_TYPE_POLL)
-				{
-					if ((msgHeader.auxattr & POLL_CLOSED) == 0)
-					{
-						// Only let the user close the poll if they created it
-						if (userHandleAliasNameMatch(msgHeader.from))
-						{
-							// Prompt to confirm whether the user wants to close the poll
-							if (!console.noyes("Close poll"))
-							{
-								// Close the poll (open the sub-board first)
-								var msgbase = new MsgBase(this.subBoardCode);
-								if (msgbase.open())
-								{
-									if (closePollWithOpenMsgbase(msgbase, msgHeader.number))
-									{
-										msgHeader.auxattr |= POLL_CLOSED;
-										pollCloseMsg = "\x01n\x01cThis poll was successfully closed.";
-									}
-									else
-										pollCloseMsg = "\x01n\x01r\x01h* Failed to close this poll!";
-									msgbase.close();
-								}
-								else
-									pollCloseMsg = "\x01n\x01r\x01h* Failed to open the sub-board!";
-							}
-						}
-						else
-							pollCloseMsg = "\x01n\x01y\x01hCan't close this poll because it's not yours";
-					}
-					else
-						pollCloseMsg = "\x01n\x01y\x01hThis poll is already closed";
-				}
-				else
-					pollCloseMsg = "This message is not a poll";
-
-				// Display the poll closing status message
-				if (strip_ctrl(pollCloseMsg).length > 0)
-				{
-					console.print("\x01n" + pollCloseMsg + "\x01n");
-					console.crlf();
-					console.pause();
-				}
-				writeMessage = true;
-				break;
-			case this.enhReaderKeys.validateMsg: // Validate the message
-				if (user.is_sysop && (this.subBoardCode != "mail") && msg_area.sub[this.subBoardCode].is_moderated)
-				{
-					var message = "";
-					if (this.ValidateMsg(this.subBoardCode, msgHeader.number))
-					{
-						message = "\x01n\x01cMessage validation successful";
-						// Refresh the message header in the arrays
-						this.RefreshMsgHdrInArrays(msgHeader.number);
-						// Exit out of the reader and come back to read
-						// the same message again so that the voting results
-						// are re-loaded and displayed on the screen.
-						retObj.newMsgOffset = pOffset;
-						retObj.nextAction = ACTION_GO_SPECIFIC_MSG;
-						continueOn = false;
-					}
-					else
-					{
-						message = "\x01n\x01y\x01hMessage validation failed!";
-						writeMessage = true;
-					}
-					console.crlf();
-					console.print(message + "\x01n");
-					console.crlf();
-					console.pause();
-				}
-				else
-					writeMessage = false;
-				break;
-			case this.enhReaderKeys.quickValUser: // Quick-validate the user
-				if (user.is_sysop)
-					quickValidateLocalUser(msgHeader.from, false, this.quickUserValSetIndex);
-				else
-					writeMessage = false;
-				break;
-			case this.enhReaderKeys.bypassSubBoardInNewScan:
-				// TODO: Finish
-				writeMessage = false;
-				/*
-				if (this.doingMsgScan)
-				{
-					console.attributes = "N";
-					console.crlf();
-					if (!console.noyes("Bypass this sub-board in newscans"))
-					{
-						continueOn = false;
-						msg_area.sub[this.subBoardCode].scan_cfg &= SCAN_CFG_NEW;
-					}
-					else
-						writeMessage = true;
-				}
-				else
-					writeMessage = false;
-				*/
-				break;
-			case this.enhReaderKeys.userSettings:
-				var userSettingsRetObj = this.DoUserSettings_Traditional();
-				// In case the user changed their twitlist, re-filter the messages for this sub-board
-				if (userSettingsRetObj.userTwitListChanged)
-				{
-					console.crlf();
-					console.print("\x01nTwitlist changed; re-filtering..");
-					var tmpMsgbase = new MsgBase(this.subBoardCode);
-					if (tmpMsgbase.open())
-					{
-						continueOn = false;
-						writeMessage = false;
-						var tmpAllMsgHdrs = tmpMsgbase.get_all_msg_headers(true);
-						tmpMsgbase.close();
-						this.FilterMsgHdrsIntoHdrsForCurrentSubBoard(tmpAllMsgHdrs, true);
-						// If the user is currently reading a message a message by someone who is now
-						// in their twit list, change the message currently being viewed.
-						if (this.MsgHdrFromOrToInUserTwitlist(msgHeader))
-						{
-							var newReadableMsgOffset = this.FindNextReadableMsgIdx(pOffset, true);
-							if (newReadableMsgOffset > -1)
-							{
-								retObj.newMsgOffset = newReadableMsgOffset;
-								retObj.offsetValid = true;
-								retObj.nextAction = ACTION_GO_SPECIFIC_MSG;
-							}
-							else
-								retObj.nextAction = ACTION_GO_NEXT_MSG_AREA;
-						}
-						else
-						{
-							// If there are still messages in this sub-board, and the message offset is beyond the last
-							// message, then show the last message in the sub-board.  Otherwise, go to the next message area.
-							if (this.hdrsForCurrentSubBoard.length > 0)
-							{
-								if (pOffset > this.hdrsForCurrentSubBoard.length)
-								{
-									//this.hdrsForCurrentSubBoard[this.hdrsForCurrentSubBoard.length-1].number
-									retObj.newMsgOffset = this.hdrsForCurrentSubBoard.length - 1;
-									retObj.offsetValid = true;
-									retObj.nextAction = ACTION_GO_SPECIFIC_MSG;
-								}
-							}
-							else
-								retObj.nextAction = ACTION_GO_NEXT_MSG_AREA;
-						}
-					}
-					else
-						console.print("\x01y\x01hFailed to open the messagbase!\x01\r\n\x01p");
-					this.SetUpLightbarMsgListVars();
-					writeMessage = true;
-				}
-				break;
-			case this.enhReaderKeys.showMsgHex: // Show message hex dump
-				writeMessage = false;
-				writePromptText = false;
-				if (user.is_sysop)
-				{
-					if (msgHexInfo == null)
-						msgHexInfo = this.GetMsgHexInfo(messageText, true);
-					if (msgHexInfo.msgHexArray.length > 0)
-					{
-						writeMessage = true;
-						writePromptText = true;
-						console.attributes = "N";
-						console.crlf();
-						console.print("\x01c\x01hMessage hex dump:\x01n\r\n");
-						console.print("=================\r\n");
-						for (var hexI = 0; hexI < msgHexInfo.msgHexArray.length; ++hexI)
-							console.print(msgHexInfo.msgHexArray[hexI] + "\r\n");
-						console.pause();
-					}
-				}
-				break;
-			case this.enhReaderKeys.hexDump:
-				writeMessage = false;
-				writePromptText = false;
-				// Save a hex dump of the message to the BBS machine - Only allow this
-				// if the user is a sysop.
-				if (user.is_sysop)
-				{
-					writeMessage = true;
-					writePromptText = true;
-					// Prompt the user for a filename to save the hex dump to the
-					// BBS machine
-					console.print("\x01n\r\n\x01cFilename:\x01h");
-					var inputLen = console.screen_columns - 10; // 10 = "Filename:" length + 1
-					var filename = console.getstr(inputLen, K_NOCRLF);
-					console.attributes = "N";
-					console.crlf();
-					if (filename.length > 0)
-					{
-						var saveHexRetObj = this.SaveMsgHexDumpToFile(msgHeader, filename);
-						if (saveHexRetObj.saveSucceeded)
-							console.print("\x01n\x01cThe hex dump has been saved.\x01n");
-						else if (saveHexRetObj.errorMsg != "")
-							console.print("\x01n\x01y\x01h" + saveHexRetObj.errorMsg + "\x01n");
-						else
-							console.print("\x01n\x01y\x01hFailed!\x01n");
-					}
-					else
-						console.print("\x01n\x01y\x01hHex dump not exported\x01n");
-					console.crlf();
-					console.pause();
-				}
-				break;
-			case this.enhReaderKeys.operatorMenu: // Operator menu
-				if (user.is_sysop)
-				{
-					writeMessage = true;
-					writePromptText = true;
-					console.crlf();
-					console.print("\x01w\x01h== Operator menu ==\x01n");
-					console.crlf();
-					var opRetObj = this.ShowReadModeOpMenuAndGetSelection();
-					if (opRetObj.chosenOption != null)
-					{
-						switch (opRetObj.chosenOption)
-						{
-							case this.readerOpMenuOptValues.validateMsg: // Validate the message
-								if (this.subBoardCode != "mail" && msg_area.sub[this.subBoardCode].is_moderated)
-								{
-									var message = "";
-									if (this.ValidateMsg(this.subBoardCode, msgHeader.number))
-									{
-										message = "\x01n\x01cMessage validation successful";
-										// Refresh the message header in the arrays
-										this.RefreshMsgHdrInArrays(msgHeader.number);
-										// Exit out of the reader and come back to read
-										// the same message again so that the voting results
-										// are re-loaded and displayed on the screen.
-										retObj.newMsgOffset = pOffset;
-										retObj.nextAction = ACTION_GO_SPECIFIC_MSG;
-										continueOn = false;
-									}
-									else
-										message = "\x01n\x01y\x01hMessage validation failed!";
-									console.crlf();
-									console.print(message + "\x01n");
-									console.crlf();
-									console.pause();
-								}
-								break;
-							case this.readerOpMenuOptValues.editAuthorUserAccount: // Edit author's user account
-								console.attributes = "N";
-								console.crlf();
-								console.print("- Edit user " + msgHeader.from);
-								console.crlf();
-								var editObj = editUser(msgHeader.from);
-								if (editObj.errorMsg.length != 0)
-								{
-									console.attributes = "N";
-									console.crlf();
-									console.print("\x01y\x01h" + editObj.errorMsg + "\x01n");
-									console.crlf();
-									console.pause();
-								}
-								console.attributes = "N";
-								break;
-							case this.readerOpMenuOptValues.showHdrLines: // Show header or kludge lines
-							case this.readerOpMenuOptValues.showKludgeLines:
-								console.crlf();
-								// Get an array of the extended header info/kludge lines and then
-								// display them.
-								var extdHdrInfoLines = this.GetExtdMsgHdrInfo(this.subBoardCode, msgHeader.number, (opRetObj.chosenOption == this.readerOpMenuOptValues.showKludgeLines));
-								if (extdHdrInfoLines.length > 0)
-								{
-									console.crlf();
-									for (var infoIter = 0; infoIter < extdHdrInfoLines.length; ++infoIter)
-									{
-										console.print(extdHdrInfoLines[infoIter]);
-										console.crlf();
-									}
-									console.pause();
-								}
-								else
-								{
-									// There are no kludge lines for this message
-									console.print(replaceAtCodesInStr(this.text.noKludgeLinesForThisMsgText));
-									console.crlf();
-									console.pause();
-								}
-								break;
-							case this.readerOpMenuOptValues.showTallyStats: // Show tally stats
-								if (msgHeader.hasOwnProperty("total_votes") && msgHeader.hasOwnProperty("upvotes"))
-								{
-									console.attributes = "N";
-									console.crlf();
-									var voteInfo = this.GetUpvoteAndDownvoteInfo(msgHeader);
-									for (var voteInfoIdx = 0; voteInfoIdx < voteInfo.length; ++voteInfoIdx)
-									{
-										console.print(voteInfo[voteInfoIdx]);
-										console.crlf();
-									}
-								}
-								else
-								{
-									console.print("\x01n\x01h\x01yThere is no voting information for this message\x01n");
-									console.crlf();
-								}
-								console.pause();
-								break;
-							case this.readerOpMenuOptValues.showMsgHex:
-								writeMessage = false;
-								writePromptText = false;
-								if (msgHexInfo == null)
-									msgHexInfo = this.GetMsgHexInfo(messageText, true);
-								if (msgHexInfo.msgHexArray.length > 0)
-								{
-									writeMessage = true;
-									writePromptText = true;
-									console.attributes = "N";
-									console.crlf();
-									console.print("\x01c\x01hMessage hex dump:\x01n\r\n");
-									console.print("=================\r\n");
-									for (var hexI = 0; hexI < msgHexInfo.msgHexArray.length; ++hexI)
-										console.print(msgHexInfo.msgHexArray[hexI] + "\r\n");
-									console.pause();
-								}
-								break;
-							case this.readerOpMenuOptValues.saveMsgHexToFile:
-								writeMessage = true;
-								writePromptText = true;
-								// Prompt the user for a filename to save the hex dump to the
-								// BBS machine
-								console.print("\x01n\r\n\x01cFilename:\x01h");
-								var inputLen = console.screen_columns - 10; // 10 = "Filename:" length + 1
-								var filename = console.getstr(inputLen, K_NOCRLF);
-								console.attributes = "N";
-								console.crlf();
-								if (filename.length > 0)
-								{
-									var saveHexRetObj = this.SaveMsgHexDumpToFile(msgHeader, filename);
-									if (saveHexRetObj.saveSucceeded)
-										console.print("\x01n\x01cThe hex dump has been saved.\x01n");
-									else if (saveHexRetObj.errorMsg != "")
-										console.print("\x01n\x01y\x01h" + saveHexRetObj.errorMsg + "\x01n");
-									else
-										console.print("\x01n\x01y\x01hFailed!\x01n");
-								}
-								else
-									console.print("\x01n\x01y\x01hHex dump not exported\x01n");
-								console.crlf();
-								console.pause();
-								break;
-							case this.readerOpMenuOptValues.quickValUser: // Quick-validate the user
-								quickValidateLocalUser(msgHeader.from, false, this.quickUserValSetIndex);
-								break;
-							case this.readerOpMenuOptValues.addAuthorToTwitList: // Add author to twit list
-								var promptTxt = format("Add %s to twit list", msgHeader.from);
-								if (!console.noyes(promptTxt))
-								{
-									var statusMsg = "\x01n" + addToTwitList(msgHeader.from) ? "\x01w\x01hSuccessfully updated the twit list" : "\x01y\x01hFailed to update the twit list!"
-									console.print(statusMsg);
-									console.attributes = "N";
-									console.crlf();
-									console.pause();
-								}
-								break;
-							case this.readerOpMenuOptValues.addAuthorEmailToEmailFilter:
-								var fromEmailAddr = "";
-								if (typeof(msgHeader.from_net_addr) === "string" && msgHeader.from_net_addr.length > 0)
-								{
-									if (msgHeader.from_net_type == NET_INTERNET)
-										fromEmailAddr = msgHeader.from_net_addr;
-									else
-										fromEmailAddr = msgHeader.from + "@" + msgHeader.from_net_addr;
-								}
-								var promptTxt = format("Add %s to global email filter", fromEmailAddr);
-								if (!console.noyes(promptTxt))
-								{
-									var statusMsg = "\x01n" + addToGlobalEmailFilter(fromEmailAddr) ? "\x01w\x01hSuccessfully updated the email filter" : "\x01y\x01hFailed to update the email filter!"
-									console.print(statusMsg);
-									console.attributes = "N";
-									console.crlf();
-									console.pause();
-								}
-								break;
-						}
-					}
-				}
-				else
-				{
-					writeMessage = false;
-					writePromptText = false;
-				}
-				break;
-			case this.enhReaderKeys.quit: // Quit
-			case KEY_ESC:
-				// Normally, if quitFromReaderGoesToMsgList is enabled, then do that, except
-				// in indexed mode, allow going back to the indexed mode menu.
-				if (this.userSettings.quitFromReaderGoesToMsgList && !this.indexedMode)
-				{
-					console.attributes = "N";
-					console.crlf();
-					console.print("Loading...");
-					retObj.nextAction = ACTION_DISPLAY_MSG_LIST;
-				}
-				else
-					retObj.nextAction = ACTION_QUIT;
-				continueOn = false;
-				break;
-			default:
-				// No need to do anything
-				writeMessage = false;
-				writePromptText = false;
-				break;
-		}
-	}
-
-	return retObj;
-}
-
-// For the DDMsgReader class: Does the operator mode for reading.
-//
-// Return value: An object with the following properties:
-//               menuTopLeftX: The horizontal component of the upper-left corner of the operator menu (for scrollable mode)
-//               menuTopLeftY: The vertical component of the upper-left corner of the operator menu (for scrollable mode)
-//               menuWidth: The width of the operator menu (for scrollable mode)
-//               menuHeight: The height of the operator menu (for scrollable mode)
-//               chosenOption: The user's chosen option from the menu (one of this.readerOpMenuOptValues), or null
-//                             if the user quit/aborted
-//               lastUserInput: The user's last key input from the menu (empty string if there is none)
-function DigDistMsgReader_ShowReadModeOpMenuAndGetSelection()
-{
-	var retObj = {
-		menuTopLeftX: 1,
-		menuTopLeftY: 1,
-		menuWidth: 0,
-		menuHeight: 0,
-		chosenOption: null,
-		lastUserInput: ""
-	};
-
-	// This is only for the sysop
-	if (!user.is_sysop)
-		return retObj;
-
-	// If using scrollable mode, create the operator menu & display it
-
-	var opMenu = this.CreateReadModeOpMenu();
-	retObj.menuTopLeftX = opMenu.pos.x;
-	retObj.menuTopLeftY = opMenu.pos.y;
-	retObj.menuWidth = opMenu.size.width;
-	retObj.menuHeight = opMenu.size.height;
-
-	//GetVal(pDraw, pSelectedItemIndexes)
-	retObj.chosenOption = opMenu.GetVal();
-	if (typeof(opMenu.lastUserInput) === "string")
-		retObj.lastUserInput = opMenu.lastUserInput;
-	// If the user pressed one of the additional quit keys set up for the
-	// menu, make sure the chosen option is null (it should be anyway)
-	if (retObj.lastUserInput.toUpperCase() == "Q" || retObj.lastUserInput == KEY_ESC) // Quit
-		retObj.chosenOption = null;
-
-	return retObj;
-}
-
-// For the DDMsgReader class: Creates the operator menu for read mode.
-// The menu should be created each time, because the options could be different for
-// each sub-board (i.e., if the sub-board is moderated, then the 'Validate message'
-// option will be available).
-function DigDistMsgReader_CreateReadModeOpMenu()
-{
-	//var itemFormatStr = "\x01n\x01c\x01h%s\x01y";
-	// We'll add the 'Validate message' option if the sub-board isn't email and is moderated
-	var subBoardIsModerated = (this.subBoardCode != "mail" && msg_area.sub[this.subBoardCode].is_moderated);
-
-	var opMenuWidth = 35;
-	var opMenuHeight = 11;
-	if (subBoardIsModerated)
-		++opMenuHeight;
-	var opMenuX = Math.floor(console.screen_columns/2) - Math.floor(opMenuWidth/2);
-	var opMenuY = this.msgAreaTop + 2;
-	var opMenu = new DDLightbarMenu(opMenuX, opMenuY, opMenuWidth, opMenuHeight);	
-	opMenu.borderEnabled = true;
-	opMenu.allowANSI = this.scrollingReaderInterface && console.term_supports(USER_ANSI);
-	opMenu.topBorderText = "\x01n\x01w\x01hOperator menu (reader mode)";
-	if (subBoardIsModerated)
-		opMenu.Add("&A: Validate the message", this.readerOpMenuOptValues.validateMsg);
-	// If the scrollbar/ANSI interface is being used, then add the menu items with hotkey characters.
-	// Otherwise don't use the hotkey characters (the menu will use numbered mode).
-	if (opMenu.allowANSI)
-	{
-		opMenu.Add("&U: Edit author's user account", this.readerOpMenuOptValues.editAuthorUserAccount);
-		opMenu.Add("&H: Show header lines", this.readerOpMenuOptValues.showHdrLines);
-		opMenu.Add("&K: Show kludge lines", this.readerOpMenuOptValues.showKludgeLines);
-		opMenu.Add("&T: Show tally stats", this.readerOpMenuOptValues.showTallyStats);
-		opMenu.Add("&X: Show message hex", this.readerOpMenuOptValues.showMsgHex);
-		opMenu.Add("&E: Save message hex to file", this.readerOpMenuOptValues.saveMsgHexToFile);
-		opMenu.Add("&A: Quick validate the user", this.readerOpMenuOptValues.quickValUser);
-		opMenu.Add("&I: Add author to twit list", this.readerOpMenuOptValues.addAuthorToTwitList);
-		opMenu.Add("&M: Add author to email filter", this.readerOpMenuOptValues.addAuthorEmailToEmailFilter);
-		// Use cyan for the item color, and cyan with blue background for selected item color
-		opMenu.colors.itemColor = "\x01n\x01c";
-		opMenu.colors.selectedItemColor = "\x01n\x01c\x014";
-	}
-	else
-	{
-		opMenu.Add("Edit author's user account", this.readerOpMenuOptValues.editAuthorUserAccount);
-		opMenu.Add("Show header lines", this.readerOpMenuOptValues.showHdrLines);
-		opMenu.Add("Show kludge lines", this.readerOpMenuOptValues.showKludgeLines);
-		opMenu.Add("Show tally stats", this.readerOpMenuOptValues.showTallyStats);
-		opMenu.Add("Show message hex", this.readerOpMenuOptValues.showMsgHex);
-		opMenu.Add("Save message hex to file", this.readerOpMenuOptValues.saveMsgHexToFile);
-		opMenu.Add("Quick validate the user", this.readerOpMenuOptValues.quickValUser);
-		opMenu.Add("Add author to twit list", this.readerOpMenuOptValues.addAuthorToTwitList);
-		opMenu.Add("Add author to email filter", this.readerOpMenuOptValues.addAuthorEmailToEmailFilter);
-		// Use green for the item color and high cyan for the item number color
-		opMenu.colors.itemColor = "\x01n\x01g";
-		opMenu.colors.itemNumColor = "\x01n\x01c\x01h";
-	}
-
-	opMenu.AddAdditionalQuitKeys("qQ" + this.enhReaderKeys.operatorMenu);
-
-	return opMenu;
-}
-
-// For the ReadMessageEnhanced methods: This function converts a thread navigation
-// key character to its corresponding thread type value
-function keypressToThreadType(pKeypress, pEnhReaderKeys)
-{
-	var threadType = THREAD_BY_ID;
-	switch (pKeypress)
-	{
-		case pEnhReaderKeys.prevMsgByTitle:
-		case pEnhReaderKeys.nextMsgByTitle:
-			threadType = THREAD_BY_TITLE;
-			break;
-		case pEnhReaderKeys.prevMsgByAuthor:
-		case pEnhReaderKeys.nextMsgByAuthor:
-			threadType = THREAD_BY_AUTHOR;
-			break;
-		case pEnhReaderKeys.prevMsgByToUser:
-		case pEnhReaderKeys.nextMsgByToUser:
-			threadType = THREAD_BY_TO_USER;
-			break;
-		case pEnhReaderKeys.prevMsgByThreadID:
-		case pEnhReaderKeys.nextMsgByThreadID:
-		default:
-			threadType = THREAD_BY_ID;
-			break;
-	}
-	return threadType;
-}
-
-// For the DigDistMsgReader class: For the enhanced reader method - Prepares the
-// last 2 lines on the screen for propmting the user for something.
-//
-// Return value: An object containing x and y values representing the cursor
-//               position, ready to prompt the user.
-function DigDistMsgReader_EnhReaderPrepLast2LinesForPrompt()
-{
-	var promptPos = { x: this.msgAreaLeft, y: this.msgAreaBottom };
-	// Write a line of characters above where the prompt will be placed,
-	// to help get the user's attention.
-	console.gotoxy(promptPos.x, promptPos.y-1);
-	console.print("\x01n" + this.colors.enhReaderPromptSepLineColor);
-	for (var lineCounter = 0; lineCounter < this.msgAreaWidth; ++lineCounter)
-		console.print(HORIZONTAL_SINGLE);
-	// Clear the inside of the message area, so as not to overwrite
-	// the scrollbar character
-	console.attributes = "N";
-	console.gotoxy(promptPos);
-	for (var lineCounter = 0; lineCounter < this.msgAreaWidth; ++lineCounter)
-		console.print(" ");
-	// Position the cursor at the prompt location
-	console.gotoxy(promptPos);
-	
-	return promptPos;
-}
-
-// For the DigDistMsgReader class: For the enhanced reader method - Looks for a
-// later method that isn't marked for deletion.  If none is found, looks for a
-// prior message that isn't marked for deletion.
-//
-// Parameters:
-//  pOffset: The offset of the message to start at
-//
-// Return value: An object with the following properties:
-//               newMsgOffset: The offset of the next readable message
-//               nextAction: The next action (code) for the enhanced reader
-//               continueInputLoop: Boolean - Whether or not to continue the input loop
-//               promptGoToNextArea: Boolean - Whether or not to prompt the user to go
-//                                   to the next message area
-function DigDistMsgReader_LookForNextOrPriorNonDeletedMsg(pOffset)
-{
-	var retObj = {
-		newMsgOffset: 0,
-		nextAction: ACTION_NONE,
-		continueInputLoop: true,
-		promptGoToNextArea: false
-	};
-
-	// Look for a later message that isn't marked for deletion.
-	// If none is found, then look for a prior message that isn't
-	// marked for deletion.
-	retObj.newMsgOffset = this.FindNextReadableMsgIdx(pOffset, true);
-	if (retObj.newMsgOffset > -1)
-	{
-		retObj.continueInputLoop = false;
-		retObj.nextAction = ACTION_GO_NEXT_MSG;
-	}
-	else
-	{
-		// No later message found, so look for a prior message.
-		retObj.newMsgOffset = this.FindNextReadableMsgIdx(pOffset, false);
-		if (retObj.newMsgOffset > -1)
-		{
-			retObj.continueInputLoop = false;
-			retObj.nextAction = ACTION_GO_PREVIOUS_MSG;
-		}
-		else
-		{
-			// No prior message found.  We'll want to return from the enhanced
-			// reader function (with message index -1) so that this script can
-			// go onto the next message sub-board/group.  Also, set the next
-			// action such that the calling method will go on to the next
-			// message/sub-board.
-			if (!curMsgSubBoardIsLast())
-			{
-				if (this.readingPersonalEmail)
-				{
-					retObj.continueInputLoop = false;
-					retObj.nextAction = ACTION_QUIT;
-				}
-				else
-					retObj.promptGoToNextArea =  true;
-			}
-			else
-			{
-				// We're not at the end of the sub-board or the current sub-board
-				// is the last, so go ahead and exit.
-				retObj.continueInputLoop = false;
-				retObj.nextAction = ACTION_GO_NEXT_MSG;
-			}
-		}
-	}
-	return retObj;
-}
-
-// For the DigDistMsgReader class: Writes the help line for enhanced reader
-// mode.
-//
-// Parameters:
-//  pScreenRow: Optional - The screen row to write the help line on.  If not
-//              specified, the last row on the screen will be used.
-//  pDisplayChgAreaOpt: Optional boolean - Whether or not to show the "change area" option.
-//                      Defaults to true.
-function DigDistMsgReader_DisplayEnhancedMsgReadHelpLine(pScreenRow, pDisplayChgAreaOpt)
-{
-	var displayChgAreaOpt = (typeof(pDisplayChgAreaOpt) == "boolean" ? pDisplayChgAreaOpt : true);
-	// Move the cursor to the desired location on the screen and display the help line
-	console.gotoxy(1, typeof(pScreenRow) == "number" ? pScreenRow : console.screen_rows);
-	// TODO: Mouse: console.print replaced with console.putmsg for mouse click hotspots
-	//console.print(displayChgAreaOpt ? this.enhReadHelpLine : this.enhReadHelpLineWithoutChgArea);
-	// console.putmsg() handles @-codes, which we use for mouse click tracking
-	console.putmsg(displayChgAreaOpt ? this.enhReadHelpLine : this.enhReadHelpLineWithoutChgArea);
-}
-
-// For the DigDistMsgReader class: Goes back to the prior readable sub-board
-// (accounting for search results, etc.).  Changes the object's subBoardCode,
-// msgbase object, etc.
-//
-// Parameters:
-//  pAllowChgMsgArea: Boolean - Whether or not the user is allowed to change
-//                    to another message area
-//  pPromptPrevIfNoResults: Optional boolean - Whether or not to prompt the user to
-//                          go to the previous area if there are no search results.
-//
-// Return value: An object with the following properties:
-//               changedMsgArea: Boolean - Whether or not this method successfully
-//                               changed to a prior message area
-//               msgIndex: The message index for the new sub-board.  Will be -1
-//                         if there is no new sub-board or otherwise invalid
-//                         scenario.
-//               shouldStopReading: Whether or not the script should stop letting
-//                                  the user read messages
-function DigDistMsgReader_GoToPrevSubBoardForEnhReader(pAllowChgMsgArea, pPromptPrevIfNoResults)
-{
-	var retObj = {
-		changedMsgArea: false,
-		msgIndex: -1,
-		shouldStopReading: false
-	};
-
-	// Only allow this if pAllowChgMsgArea is true and we're not reading personal
-	// email.  If we're reading personal email, then msg_area.sub is unavailable
-	// for the "mail" internal code.
-	if (pAllowChgMsgArea && (this.subBoardCode != "mail"))
-	{
-		// continueGoingToPrevSubBoard specifies whether or not to continue
-		// going to the previous sub-boards in case there is search text
-		// specified.
-		var continueGoingToPrevSubBoard = true;
-		while (continueGoingToPrevSubBoard)
-		{
-			// Allow going to the previous message sub-board/group.
-			var msgGrpIdx = msg_area.sub[this.subBoardCode].grp_index;
-			var subBoardIdx = msg_area.sub[this.subBoardCode].index;
-			var readMsgRetObj = findNextOrPrevNonEmptySubBoard(msgGrpIdx, subBoardIdx, false);
-			// If a different sub-board was found, then go to that sub-board.
-			if (readMsgRetObj.foundSubBoard && readMsgRetObj.subChanged)
-			{
-				bbs.cursub = 0;
-				bbs.curgrp = readMsgRetObj.grpIdx;
-				bbs.cursub = readMsgRetObj.subIdx;
-				this.setSubBoardCode(readMsgRetObj.subCode);
-				if (this.searchType == SEARCH_NONE || !this.SearchingAndResultObjsDefinedForCurSub())
-				{
-					continueGoingToPrevSubBoard = false; // No search results, so don't keep going to the previous sub-board.
-					// Go to the user's last read message.  If the message index ends up
-					// below 0, then go to the last message not marked as deleted.
-					// We probably shouldn't use GetMsgIdx() yet because the arrays of
-					// message headers have not been populated for the next area yet
-					retObj.msgIndex = this.AbsMsgNumToIdx(msg_area.sub[this.subBoardCode].last_read);
-					//retObj.msgIndex = this.GetMsgIdx(msg_area.sub[this.subBoardCode].last_read);
-					if (retObj.msgIndex >= 0)
-						retObj.changedMsgArea = true;
-					else
-					{
-						// Look for the last message not marked as deleted
-						var readableMsgIdx = this.FindNextReadableMsgIdx(this.NumMessages(), false);
-						// If a non-deleted message was found, then set retObj.msgIndex to it.
-						// Otherwise, tell the user there are no messages in this sub-board
-						// and return.
-						if (readableMsgIdx > -1)
-							retObj.msgIndex = readableMsgIdx;
-						else
-							retObj.msgIndex = this.NumMessages() - 1; // Shouldn't get here
-						var newLastRead = this.IdxToAbsMsgNum(retObj.msgIndex);
-						if (newLastRead > -1)
-							msg_area.sub[this.subBoardCode].last_read = newLastRead;
-					}
-				}
-				// Set the hotkey help line again, as this sub-board might have
-				// different settings for whether messages can be edited or deleted,
-				// then refresh it on the screen.
-				var oldHotkeyHelpLine = this.enhReadHelpLine;
-				this.SetEnhancedReaderHelpLine();
-				// If a search is is specified that would populate the search
-				// results, then populate this.msgSearchHdrs for the current
-				// sub-board if there is search text specified.  If there
-				// are no search results, then ask the user if they want
-				// to continue searching the message areas.
-				if (this.SearchTypePopulatesSearchResults())
-				{
-					if (this.PopulateHdrsIfSearch_DispErrorIfNoMsgs(false, true, false))
-					{
-						retObj.changedMsgArea = true;
-						continueGoingToPrevSubBoard = false;
-						retObj.msgIndex = this.NumMessages() - 1;
-						if (this.scrollingReaderInterface && console.term_supports(USER_ANSI))
-							this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, pAllowChgMsgArea);
-					}
-					else // No search results in this sub-board
-					{
-						var promptPrevIfNoResults = (typeof(pPromptPrevIfNoResults) === "boolean" ? pPromptPrevIfNoResults : true);
-						if (promptPrevIfNoResults)
-							continueGoingToPrevSubBoard = !console.noyes("Continue searching");
-						if (!continueGoingToPrevSubBoard)
-						{
-							retObj.shouldStopReading = true;
-							return retObj;
-						}
-					}
-				}
-				else
-				{
-					retObj.changedMsgArea = true;
-					this.PopulateHdrsForCurrentSubBoard();
-					if ((oldHotkeyHelpLine != this.enhReadHelpLine) && this.scrollingReaderInterface && console.term_supports(USER_ANSI))
-						this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, pAllowChgMsgArea);
-				}
-			}
-			else
-			{
-				// Didn't find a prior sub-board with readable messages.
-				// We could stop and exit the script here by doing the following,
-				// but I'd rather let the user exit when they want to.
-
-				continueGoingToPrevSubBoard = false;
-				// Show a message telling the user that there are no prior
-				// messages or sub-boards.  Then, refresh the hotkey help line.
-				writeWithPause(this.msgAreaLeft, console.screen_rows,
-									"\x01n\x01h\x01y* No prior messages or no message in prior message areas.",
-									ERROR_PAUSE_WAIT_MS, "\x01n", true);
-				if (this.scrollingReaderInterface && console.term_supports(USER_ANSI))
-					this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, pAllowChgMsgArea);
-			}
-		}
-	}
-
-	return retObj;
-}
-
-// For the DigDistMsgReader class: Goes to the next readable sub-board
-// (accounting for search results, etc.).  Changes the object's subBoardCode,
-// msgbase object, etc.
-//
-// Parameters:
-//  pAllowChgMsgArea: Boolean - Whether or not the user is allowed to change
-//                    to another message area
-//  pPromptNextIfNoResults: Optional boolean - Whether or not to prompt the user to
-//                          go to the next area if there are no search results.
-//                          Defaults to true.
-//
-// Return value: An object with the following properties:
-//               changedMsgArea: Boolean - Whether or not this method successfully
-//                               changed to a prior message area
-//               msgIndex: The message index for the new sub-board.  Will be -1
-//                         if there is no new sub-board or otherwise invalid
-//                         scenario.
-//               shouldStopReading: Whether or not the script should stop letting
-//                                  the user read messages
-function DigDistMsgReader_GoToNextSubBoardForEnhReader(pAllowChgMsgArea, pPromptNextIfNoResults)
-{
-	var retObj = {
-		changedMsgArea: false,
-		msgIndex: -1,
-		shouldStopReading: false
-	};
-
-	// Only allow this if pAllowChgMsgArea is true and we're not reading personal
-	// email.  If we're reading personal email, then msg_area.sub is unavailable
-	// for the "mail" internal code.
-	if (pAllowChgMsgArea && (this.subBoardCode != "mail"))
-	{
-		// continueGoingToNextSubBoard specifies whether or not to continue
-		// advancing to the next sub-boards in case there is search text
-		// specified.
-		var continueGoingToNextSubBoard = true;
-		while (continueGoingToNextSubBoard)
-		{
-			// Allow going to the next message sub-board/group.
-			var msgGrpIdx = msg_area.sub[this.subBoardCode].grp_index;
-			var subBoardIdx = msg_area.sub[this.subBoardCode].index;
-			var readMsgRetObj = findNextOrPrevNonEmptySubBoard(msgGrpIdx, subBoardIdx, true);
-			// If a different sub-board was found, then go to that sub-board.
-			if (readMsgRetObj.foundSubBoard && readMsgRetObj.subChanged)
-			{
-				retObj.msgIndex = 0;
-				bbs.cursub = 0;
-				bbs.curgrp = readMsgRetObj.grpIdx;
-				bbs.cursub = readMsgRetObj.subIdx;
-				this.setSubBoardCode(readMsgRetObj.subCode);
-				if ((this.searchType == SEARCH_NONE) || !this.SearchingAndResultObjsDefinedForCurSub())
-				{
-					continueGoingToNextSubBoard = false; // No search results, so don't keep going to the next sub-board.
-					// Go to the user's last read message.  If the message index ends up
-					// below 0, then go to the first message not marked as deleted.
-					retObj.msgIndex = this.AbsMsgNumToIdx(msg_area.sub[this.subBoardCode].last_read);
-					// We probably shouldn't use GetMsgIdx() yet because the arrays of
-					// message headers have not been populated for the next area yet
-					//retObj.msgIndex = this.GetMsgIdx(msg_area.sub[this.subBoardCode].last_read);
-					if (retObj.msgIndex >= 0)
-						retObj.changedMsgArea = true;
-					else
-					{
-						// Set the index of the message to display - Look for the
-						// first message not marked as deleted
-						var readableMsgIdx = this.FindNextReadableMsgIdx(this.NumMessages()-1, true);
-						// If a non-deleted message was found, then set retObj.msgIndex to it.
-						// Otherwise, tell the user there are no messages in this sub-board
-						// and return.
-						if (readableMsgIdx > -1)
-						{
-							retObj.msgIndex = readableMsgIdx;
-							retObj.changedMsgArea = true;
-							var newLastRead = this.IdxToAbsMsgNum(readableMsgIdx);
-							if (newLastRead > -1)
-								msg_area.sub[this.subBoardCode].last_read = newLastRead;
-						}
-					}
-				}
-				// Set the hotkey help line again, as this sub-board might have
-				// different settings for whether messages can be edited or deleted,
-				// then refresh it on the screen.
-				var oldHotkeyHelpLine = this.enhReadHelpLine;
-				this.SetEnhancedReaderHelpLine();
-				// If a search is is specified that would populate the search
-				// results, then populate this.msgSearchHdrs for the current
-				// sub-board if there is search text specified.  If there
-				// are no search results, then ask the user if they want
-				// to continue searching the message areas.
-				if (this.SearchTypePopulatesSearchResults())
-				{
-					if (this.PopulateHdrsIfSearch_DispErrorIfNoMsgs(false, true, false))
-					{
-						retObj.changedMsgArea = true;
-						continueGoingToNextSubBoard = false;
-						this.PopulateHdrsForCurrentSubBoard();
-						retObj.msgIndex = 0;
-						if (this.scrollingReaderInterface && console.term_supports(USER_ANSI))
-							this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, pAllowChgMsgArea);
-					}
-					else // No search results in this sub-board
-					{
-						var promptNextIfNoresults = (typeof(pPromptNextIfNoResults) === "boolean" ? pPromptNextIfNoResults : true);
-						if (promptNextIfNoresults)
-							continueGoingToNextSubBoard = !console.noyes("Continue searching");
-						if (!continueGoingToNextSubBoard)
-						{
-							retObj.shouldStopReading = true;
-							return retObj;
-						}
-					}
-				}
-				else
-				{
-					// There is no search.  Populate the arrays of all headers
-					// for this sub-board
-					this.PopulateHdrsForCurrentSubBoard();
-					retObj.msgIndex = this.GetMsgIdx(msg_area.sub[this.subBoardCode].last_read);
-					if (retObj.msgIndex == -1)
-						retObj.msgIndex = 0;
-				}
-			}
-			else
-			{
-				// Didn't find later sub-board with readable messages.
-				// We could stop and exit the script here by doing the following,
-				// but I'd rather let the user exit when they want to.
-				//retObj.shouldStopReading = true;
-				//return retObj;
-
-				continueGoingToNextSubBoard = false;
-				// Show a message telling the user that there are no more
-				// messages or sub-boards.  Then, refresh the hotkey help line.
-				writeWithPause(this.msgAreaLeft, console.screen_rows,
-				               "\x01n\x01h\x01y* No more messages or message areas.",
-				               ERROR_PAUSE_WAIT_MS, "\x01n", true);
-				if (this.scrollingReaderInterface && console.term_supports(USER_ANSI))
-					this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, pAllowChgMsgArea);
-			}
-		}
-	}
-
-	return retObj;
-}
-
-// For the DigDistMsgReader Class: Prepares the variables that keep track of the
-// traditional-interface message list position, current messsage number, etc.
-function DigDistMsgReader_SetUpTraditionalMsgListVars()
-{
-	// If a search is specified, then just start at the first message.
-	// If no search is specified, then get the index of the user's last read
-	// message.  Then, figure out which page it's on and set the lightbar list
-	// index & cursor position variables accordingly.
-	var lastReadMsgIdx = 0;
-	if (!this.SearchingAndResultObjsDefinedForCurSub())
-	{
-		lastReadMsgIdx = this.GetLastReadMsgIdxAndNum().lastReadMsgIdx;
-		if (lastReadMsgIdx == -1)
-			lastReadMsgIdx = 0;
-	}
-	var pageNum = findPageNumOfItemNum(lastReadMsgIdx+1, this.tradMsgListNumLines, this.NumMessages(),
-									   this.userSettings.listMessagesInReverse);
-	this.CalcTraditionalMsgListTopIdx(pageNum);
-	if (!this.userSettings.listMessagesInReverse && (this.tradListTopMsgIdx > lastReadMsgIdx))
-		this.tradListTopMsgIdx -= this.tradMsgListNumLines;
-}
-
-// For the DigDistMsgReader Class: Prepares the variables that keep track of the
-// lightbar message list position, current messsage number, etc.
-function DigDistMsgReader_SetUpLightbarMsgListVars()
-{
-	// If no search is specified or if reading personal email, then get the index
-	// of the user's last read message.  Then, figure out which page it's on and
-	// set the lightbar list index & cursor position variables accordingly.
-	var lastReadMsgIdx = 0;
-	if (!this.SearchingAndResultObjsDefinedForCurSub() || this.readingPersonalEmail)
-	{
-		lastReadMsgIdx = this.GetLastReadMsgIdxAndNum().lastReadMsgIdx;
-		if (lastReadMsgIdx == -1)
-			lastReadMsgIdx = 0;
-	}
-	else
-	{
-		// A search was specified.  If reading personal email, then set the
-		// message index to the last read message.
-		if (this.readingPersonalEmail)
-		{
-			lastReadMsgIdx = this.GetLastReadMsgIdxAndNum().lastReadMsgIdx;
-			if (lastReadMsgIdx == -1)
-				lastReadMsgIdx = 0;
-		}
-	}
-	var pageNum = findPageNumOfItemNum(lastReadMsgIdx+1, this.lightbarMsgListNumLines, this.NumMessages(),
-	                                   this.userSettings.listMessagesInReverse);
-	this.CalcLightbarMsgListTopIdx(pageNum);
-	var initialCursorRow = 0;
-	if (this.userSettings.listMessagesInReverse)
-		initialCursorRow = this.lightbarMsgListStartScreenRow+(this.lightbarListTopMsgIdx-lastReadMsgIdx);
-	else
-	{
-		if (this.lightbarListTopMsgIdx > lastReadMsgIdx)
-			this.lightbarListTopMsgIdx -= this.lightbarMsgListNumLines;
-		initialCursorRow = this.lightbarMsgListStartScreenRow+(lastReadMsgIdx-this.lightbarListTopMsgIdx);
-	}
-	if (this.userSettings.listMessagesInReverse)
-	{
-		this.lightbarListSelectedMsgIdx = this.NumMessages() - lastReadMsgIdx - 1;
-		if (this.lightbarListSelectedMsgIdx < 0)
-			this.lightbarListSelectedMsgIdx = 0;
-	}
-	else
-		this.lightbarListSelectedMsgIdx = lastReadMsgIdx;
-	this.lightbarListCurPos = { x: 1, y: initialCursorRow };
-}
-
-// For the DigDistMsgReader Class: Writes the message list column headers at the
-// top of the screen.
-function DigDistMsgReader_WriteMsgListScreenTopHeader()
-{
-	console.home();
-
-	// If we will be displaying the message group and sub-board in the
-	// header at the top of the screen (an additional 2 lines), then
-	// update nMaxLines and nListStartLine to account for this.
-	if (this.displayBoardInfoInHeader && canDoHighASCIIAndANSI()) // console.term_supports(USER_ANSI)
-	{
-		var curpos = console.getxy();
-		// Figure out the message group name & sub-board name
-		// For the message group name, we can also use msgbase.cfg.grp_name in
-		// Synchronet 3.12 and higher.
-		var msgbase = new MsgBase(this.subBoardCode);
-		var msgGroupName = "";
-		if (this.subBoardCode == "mail")
-			msgGroupName = "Mail";
-		else
-			msgGroupName = msg_area.grp_list[msgbase.cfg.grp_number].description;
-		var subBoardName = "Unspecified";
-		if (msgbase.open())
-		{
-			if (msgbase.cfg != null)
-				subBoardName = msgbase.cfg.description;
-			else if ((msgbase.subnum == -1) || (msgbase.subnum == 65535))
-				subBoardName = "Electronic Mail";
-			else
-				subBoardName = "Unspecified";
-			msgbase.close();
-		}
-
-		// Display the message group name
-		console.print(this.colors["msgListHeaderMsgGroupTextColor"] + "Msg group: " +
-		this.colors["msgListHeaderMsgGroupNameColor"] + msgGroupName);
-		console.cleartoeol(); // Fill to the end of the line with the current colors
-		// Display the sub-board name on the next line
-		++curpos.y;
-		console.gotoxy(curpos);
-		console.print(this.colors.msgListHeaderSubBoardTextColor + "Sub-board: " +
-		this.colors["msgListHeaderMsgSubBoardName"] + subBoardName);
-		console.cleartoeol(); // Fill to the end of the line with the current colors
-		++curpos.y;
-		console.gotoxy(curpos);
-	}
-
-	// Write the message listing column headers
-	if (this.showScoresInMsgList)
-		printf(this.colors.msgListColHeader + this.sMsgListHdrFormatStr, "Msg#", "From", "To", "Subject", "+/-", "Date", "Time");
-	else
-		printf(this.colors.msgListColHeader + this.sMsgListHdrFormatStr, "Msg#", "From", "To", "Subject", "Date", "Time");
-
-	// Set the normal text attribute
-	console.attributes = "N";
-}
-// For the DigDistMsgReader Class: Lists a screenful of message header information.
-//
-// Parameters:
-//  pTopIndex: The index (offset) of the top message
-//  pMaxLines: The maximum number of lines to output to the screen
-//
-// Return value: Boolean, whether or not the last message output to the
-//               screen is the last message in the sub-board.
-function DigDistMsgReader_ListScreenfulOfMessages(pTopIndex, pMaxLines)
-{
-	var atLastPage = false;
-
-	var curpos = console.getxy();
-	var msgIndex = 0;
-	if (this.userSettings.listMessagesInReverse)
-	{
-		var endIndex = pTopIndex - pMaxLines + 1; // The index of the last message to display
-		for (msgIndex = pTopIndex; (msgIndex >= 0) && (msgIndex >= endIndex); --msgIndex)
-		{
-			// The following line which sets console.line_counter to 0 is a
-			// kludge to disable Synchronet's automatic pausing after a
-			// screenful of text, so that this script can have more control
-			// over screen pausing.
-			console.line_counter = 0;
-
-			// Get the message header (it will be a MsgHeader object) and
-			// display it.
-			msgHeader = this.GetMsgHdrByIdx(msgIndex, this.showScoresInMsgList);
-			if (msgHeader == null)
-				continue;
-
-			// Display the message info
-			this.PrintMessageInfo(msgHeader, false, msgIndex+1);
-			if (console.term_supports(USER_ANSI))
-			{
-				++curpos.y;
-				console.gotoxy(curpos);
-			}
-			else
-				console.crlf();
-		}
-
-		atLastPage = (msgIndex < 0);
-	}
-	else
-	{
-		var endIndex = pTopIndex + pMaxLines; // One past the last message index to display
-		for (msgIndex = pTopIndex; (msgIndex < this.NumMessages()) && (msgIndex < endIndex); ++msgIndex)
-		{
-			// The following line which sets console.line_counter to 0 is a
-			// kludge to disable Synchronet's automatic pausing after a
-			// screenful of text, so that this script can have more control
-			// over screen pausing.
-			console.line_counter = 0;
-
-			// Get the message header (it will be a MsgHeader object) and
-			// display it.
-			var msgHeader = this.GetMsgHdrByIdx(msgIndex, this.showScoresInMsgList);
-			if (msgHeader == null)
-				continue;
-
-			// Display the message info
-			this.PrintMessageInfo(msgHeader, false, msgIndex+1);
-			if (console.term_supports(USER_ANSI))
-			{
-				++curpos.y;
-				console.gotoxy(curpos);
-			}
-			else
-				console.crlf();
-		}
-
-		atLastPage = (msgIndex == this.NumMessages());
-	}
-
-	return atLastPage;
-}
-// For the DigDistMsgReader Class: Displays the help screen for the message list.
-//
-// Parameters:
-//  pChgSubBoardAllowed: Whether or not changing to another sub-board is allowed
-//  pPauseAtEnd: Boolean, whether or not to pause at the end.
-function DigDistMsgReader_DisplayMsgListHelp(pChgSubBoardAllowed, pPauseAtEnd)
-{
-	DisplayProgramInfo();
-
-	// Display help specific to which interface is being used.
-	if (this.msgListUseLightbarListInterface)
-		this.DisplayLightbarMsgListHelp(false, pChgSubBoardAllowed);
-	else
-		this.DisplayTraditionalMsgListHelp(false, pChgSubBoardAllowed);
-
-	// If pPauseAtEnd is true, then output a newline and
-	// prompt the user whether or not to continue.
-	if (pPauseAtEnd && !console.aborted)
-		console.pause();
-
-	console.aborted = false;
-}
-// For the DigDistMsgReader Class: Displays help for the traditional-interface
-// message list
-//
-// Parameters:
-//  pDisplayHeader: Whether or not to display a help header at the beginning
-//  pChgSubBoardAllowed: Whether or not changing to another sub-board is allowed
-//  pPauseAtEnd: Boolean, whether or not to pause at the end.
-function DigDistMsgReader_DisplayTraditionalMsgListHelp(pDisplayHeader, pChgSubBoardAllowed, pPauseAtEnd)
-{
-	// If pDisplayHeader is true, then display the program information.
-	if (pDisplayHeader)
-		DisplayProgramInfo();
-
-	// Display information about the current sub-board and search results.
-	console.print("\x01n\x01cCurrently reading \x01g" + subBoardGrpAndName(this.subBoardCode));
-	console.crlf();
-	// If the user isn't reading personal messages (i.e., is reading a sub-board),
-	// then output the total number of messages in the sub-board.  We probably
-	// shouldn't output the total number of messages in the "mail" area, because
-	// that includes more than the current user's email.
-	if (this.subBoardCode != "mail")
-	{
-		var numOfMessages = 0;
-		var msgbase = new MsgBase(this.subBoardCode);
-		if (msgbase.open())
-		{
-			numOfMessages = msgbase.total_msgs;
-			msgbase.close();
-		}
-		console.print("\x01n\x01cThere are a total of \x01g" + numOfMessages + " \x01cmessages in the current area.");
-		console.crlf();
-	}
-	// If there is currently a search (which also includes personal messages),
-	// then output the number of search results/personal messages.
-	if (this.SearchingAndResultObjsDefinedForCurSub())
-	{
-		var numSearchResults = this.NumMessages();
-		var resultsWord = (numSearchResults > 1 ? "results" : "result");
-		console.print("\x01n\x01c");
-		if (this.readingPersonalEmail)
-			console.print("You have \x01g" + numSearchResults + " \x01c" + (numSearchResults == 1 ? "message" : "messages") + ".");
-		else
-		{
-			if (numSearchResults == 1)
-				console.print("There is \x01g1 \x01csearch result.");
-			else
-				console.print("There are \x01g" + numSearchResults + " \x01csearch results.");
-		}
-		console.crlf();
-	}
-	console.crlf();
-
-	console.print("\x01n" + this.colors.tradInterfaceHelpScreenColor);
-	displayTextWithLineBelow("Page navigation and message selection", false,
-	                         this.colors.tradInterfaceHelpScreenColor, "\x01k\x01h");
-	console.print(this.colors.tradInterfaceHelpScreenColor);
-	console.print("The message lister will display a page of message header information.  At\r\n");
-	console.print("the end of each page, a prompt is displayed, allowing you to navigate to\r\n");
-	console.print("the next page, previous page, first page, or the last page.  If you would\r\n");
-	console.print("like to read a message, you may type the message number, followed by\r\n");
-	console.print("the enter key if the message number is short.  To quit the listing, press\r\n");
-	console.print("the Q key.\r\n\r\n");
-	this.DisplayMessageListNotesHelp();
-	console.crlf();
-	console.crlf();
-	displayTextWithLineBelow("Summary of the keyboard commands:", false,
-	                         this.colors.tradInterfaceHelpScreenColor, "\x01k\x01h");
-	console.print(this.colors.tradInterfaceHelpScreenColor);
-	console.print("\x01n\x01h\x01cN" + this.colors.tradInterfaceHelpScreenColor + ": Go to the next page\r\n");
-	console.print("\x01n\x01h\x01cP" + this.colors.tradInterfaceHelpScreenColor + ": Go to the previous page\r\n");
-	console.print("\x01n\x01h\x01cF" + this.colors.tradInterfaceHelpScreenColor + ": Go to the first page\r\n");
-	console.print("\x01n\x01h\x01cL" + this.colors.tradInterfaceHelpScreenColor + ": Go to the last page\r\n");
-	console.print("\x01n\x01h\x01cG" + this.colors.tradInterfaceHelpScreenColor + ": Go to a specific message by number (the message will appear at the top\r\n" +
-	              "   of the list)\r\n");
-	console.print("\x01n\x01h\x01cNumber" + this.colors.tradInterfaceHelpScreenColor + ": Read the message corresponding with that number\r\n");
-	//console.print("The following commands are available only if you have permission to do so:\r\n");
-	if (this.CanDelete() || this.CanDeleteLastMsg())
-		console.print("\x01n\x01h\x01cD" + this.colors.tradInterfaceHelpScreenColor + ": Mark a message for deletion\r\n");
-	if (this.CanEdit())
-		console.print("\x01n\x01h\x01cE" + this.colors.tradInterfaceHelpScreenColor + ": Edit an existing message\r\n");
-	if (pChgSubBoardAllowed)
-		console.print("\x01n\x01h\x01cC" + this.colors.tradInterfaceHelpScreenColor + ": Change to another message sub-board\r\n");
-	console.print("\x01n\x01h\x01cS" + this.colors.tradInterfaceHelpScreenColor + ": Select messages (for batch delete, etc.)\r\n");
-	console.print("\x01n" + this.colors.tradInterfaceHelpScreenColor + "  A message number or multiple numbers can be entered separated by commas or\r\n");
-	console.print("\x01n" + this.colors.tradInterfaceHelpScreenColor + "  spaces.  Additionally, a range of numbers (separated by a dash) can be used.\r\n");
-	console.print("\x01n" + this.colors.tradInterfaceHelpScreenColor + "  Examples:\r\n");
-	console.print("\x01n" + this.colors.tradInterfaceHelpScreenColor + "  125\r\n");
-	console.print("\x01n" + this.colors.tradInterfaceHelpScreenColor + "  1,2,3\r\n");
-	console.print("\x01n" + this.colors.tradInterfaceHelpScreenColor + "  1 2 3\r\n");
-	console.print("\x01n" + this.colors.tradInterfaceHelpScreenColor + "  1,2,10-20\r\n");
-	console.print("\x01n\x01h\x01cCTRL-U" + this.colors.tradInterfaceHelpScreenColor + ": Change your user settings\r\n");
-	console.print("\x01n\x01h\x01cCTRL-D" + this.colors.tradInterfaceHelpScreenColor + ": Batch delete selected messages\r\n");
-	console.print("\x01n\x01h\x01cQ" + this.colors.tradInterfaceHelpScreenColor + ": Quit\r\n");
-	if (this.indexedMode)
-		console.print(" Currently in indexed mode; quitting will quit back to the index list.\r\n");
-	console.print("\x01n\x01h\x01c?" + this.colors.tradInterfaceHelpScreenColor + ": Show this help screen\r\n\r\n");
-
-	// If pPauseAtEnd is true, then output a newline and
-	// prompt the user whether or not to continue.
-	if (pPauseAtEnd && !console.aborted)
-		console.pause();
-
-	// Don't set this here - This is only ever called by DisplayMsgListHelp()
-	//console.aborted = false;
-}
-// For the DigDistMsgReader Class: Displays help for the lightbar message list
-//
-// Parameters:
-//  pDisplayHeader: Whether or not to display a help header at the beginning
-//  pChgSubBoardAllowed: Whether or not changing to another sub-board is allowed
-//  pPauseAtEnd: Boolean, whether or not to pause at the end.
-function DigDistMsgReader_DisplayLightbarMsgListHelp(pDisplayHeader, pChgSubBoardAllowed, pPauseAtEnd)
-{
-	// If pDisplayHeader is true, then display the program information.
-	if (pDisplayHeader)
-		DisplayProgramInfo();
-
-	// Display information about the current sub-board and search results.
-	console.print("\x01n\x01cCurrently reading \x01g" + subBoardGrpAndName(this.subBoardCode));
-	console.crlf();
-	// If the user isn't reading personal messages (i.e., is reading a sub-board),
-	// then output the total number of messages in the sub-board.  We probably
-	// shouldn't output the total number of messages in the "mail" area, because
-	// that includes more than the current user's email.
-	if (this.subBoardCode != "mail")
-	{
-		var numOfMessages = 0;
-		var msgbase = new MsgBase(this.subBoardCode);
-		if (msgbase.open())
-		{
-			numOfMessages = msgbase.total_msgs;
-			msgbase.close();
-		}
-		console.print("\x01n\x01cThere are a total of \x01g" + numOfMessages + " \x01cmessages in the current sub-board.");
-		console.crlf();
-	}
-	// If there is currently a search (which also includes personal messages),
-	// then output the number of search results/personal messages.
-	if (this.SearchingAndResultObjsDefinedForCurSub())
-	{
-		var numSearchResults = this.NumMessages();
-		var resultsWord = (numSearchResults > 1 ? "results" : "result");
-		console.print("\x01n\x01c");
-		if (this.readingPersonalEmail)
-			console.print("You have \x01g" + numSearchResults + " \x01c" + (numSearchResults == 1 ? "message" : "messages") + ".");
-		else
-		{
-			if (numSearchResults == 1)
-				console.print("There is \x01g1 \x01csearch result.");
-			else
-				console.print("There are \x01g" + numSearchResults + " \x01csearch results.");
-		}
-		console.crlf();
-	}
-	console.crlf();
-
-	displayTextWithLineBelow("Lightbar interface: Page navigation and message selection",
-	                         false, this.colors.tradInterfaceHelpScreenColor, "\x01k\x01h");
-	console.print(this.colors.tradInterfaceHelpScreenColor);
-	console.print("The message lister will display a page of message header information.  You\r\n");
-	console.print("may use the up and down arrows to navigate the list of messages.  The\r\n");
-	console.print("currently-selected message will be highlighted as you navigate through\r\n");
-	console.print("the list.  To read a message, navigate to the desired message and press\r\n");
-	console.print("the enter key.  You can also read a message by typing its message number.\r\n");
-	console.print("To quit out of the message list, press the Q key.\r\n\r\n");
-	this.DisplayMessageListNotesHelp();
-	console.crlf();
-	console.crlf();
-	displayTextWithLineBelow("Summary of the keyboard commands:", false, this.colors.tradInterfaceHelpScreenColor, "\x01k\x01h");
-	console.print(this.colors.tradInterfaceHelpScreenColor);
-	console.print("\x01n\x01h\x01cDown arrow" + this.colors.tradInterfaceHelpScreenColor + ": Move the cursor down/select the next message\r\n");
-	console.print("\x01n\x01h\x01cUp arrow" + this.colors.tradInterfaceHelpScreenColor + ": Move the cursor up/select the previous message\r\n");
-	console.print("\x01n\x01h\x01cN" + this.colors.tradInterfaceHelpScreenColor + ": Go to the next page\r\n");
-	console.print("\x01n\x01h\x01cP" + this.colors.tradInterfaceHelpScreenColor + ": Go to the previous page\r\n");
-	console.print("\x01n\x01h\x01cF" + this.colors.tradInterfaceHelpScreenColor + ": Go to the first page\r\n");
-	console.print("\x01n\x01h\x01cL" + this.colors.tradInterfaceHelpScreenColor + ": Go to the last page\r\n");
-	console.print("\x01n\x01h\x01cG" + this.colors.tradInterfaceHelpScreenColor + ": Go to a specific message by number (the message will be highlighted and\r\n" +
-	              "   may appear at the top of the list)\r\n");
-	console.print("\x01n\x01h\x01cENTER" + this.colors.tradInterfaceHelpScreenColor + ": Read the selected message\r\n");
-	console.print("\x01n\x01h\x01cNumber" + this.colors.tradInterfaceHelpScreenColor + ": Read the message corresponding with that number\r\n");
-	if (this.CanDelete() || this.CanDeleteLastMsg())
-		console.print("\x01n\x01h\x01cDEL" + this.colors.tradInterfaceHelpScreenColor + ": Mark the selected message for deletion\r\n");
-	if (this.CanEdit())
-		console.print("\x01n\x01h\x01cE" + this.colors.tradInterfaceHelpScreenColor + ": Edit the selected message\r\n");
-	if (pChgSubBoardAllowed)
-		console.print("\x01n\x01h\x01cC" + this.colors.tradInterfaceHelpScreenColor + ": Change to another message sub-board\r\n");
-	console.print("\x01n\x01h\x01cSpacebar" + this.colors.tradInterfaceHelpScreenColor + ": Select message (for batch delete, etc.)\r\n");
-	console.print("\x01n\x01h\x01cCTRL-A" + this.colors.tradInterfaceHelpScreenColor + ": Select/de-select all messages\r\n");
-	console.print("\x01n\x01h\x01cCTRL-D" + this.colors.tradInterfaceHelpScreenColor + ": Batch delete selected messages\r\n");
-	console.print("\x01n\x01h\x01cCTRL-U" + this.colors.tradInterfaceHelpScreenColor + ": Change your user settings\r\n");
-	console.print("\x01n\x01h\x01cQ" + this.colors.tradInterfaceHelpScreenColor + ": Quit\r\n");
-	if (this.indexedMode)
-		console.print(" Currently in indexed mode; quitting will quit back to the index list.\r\n");
-	console.print("\x01n\x01h\x01c?" + this.colors.tradInterfaceHelpScreenColor + ": Show this help screen\r\n");
-
-	// If pPauseAtEnd is true, then pause.
-	if (pPauseAtEnd && !console.aborted)
-		console.pause();
-
-	// Don't set this here - This is only ever called by DisplayMsgListHelp()
-	//console.aborted = false;
-}
-// For the DigDistMsgReader class: Displays the message list notes for the
-// help screens.
-function DigDistMsgReader_DisplayMessageListNotesHelp()
-{
-	displayTextWithLineBelow("Notes about the message list:", false,
-	                         this.colors["tradInterfaceHelpScreenColor"], "\x01n\x01k\x01h")
-	console.print(this.colors.tradInterfaceHelpScreenColor);
-	var helpLines = [
-		"Between the message number and 'From' name, a message could have the following status indicators:",
-		"\x01n\x01h\x01r\x01i" + this.msgListStatusChars.deleted + "\x01n" + this.colors.tradInterfaceHelpScreenColor + ": Message has been marked for deletion",
-		this.msgListStatusChars.attachments + ": The message has attachments",
-		this.msgListStatusChars.unread + ": The message is unread"
-	];
-	if (this.userSettings.displayMsgRepliedChar)
-		helpLines.push(this.msgListStatusChars.replied + ": You have replied to the message");
-	var wrapLen = console.screen_columns-1;
-	for (var i = 0; i < helpLines.length; ++i)
-	{
-		var wrappedLines = word_wrap(helpLines[i], wrapLen).split("\n");
-		for (var j = 0; j < wrappedLines.length; ++j)
-		{
-			if (wrappedLines[j].length == 0) continue;
-			console.print(wrappedLines[j] + "\r\n");
-		}
-	}
-}
-// For the DigDistMsgReader Class: Sets the traditional UI pause prompt text
-// strings, sLightbarModeHelpLine, the text string for the lightbar help line,
-// for the message lister interface.  This checks with the MsgBase object to determine
-// if the user is allowed to delete or edit messages, and if so, adds the
-// appropriate keys to the prompt & help text.
-function DigDistMsgReader_SetMsgListPauseTextAndLightbarHelpLine()
-{
-
-
-	// Set the traditional UI pause prompt text.
-	// If the user can delete messages, then append D as a valid key.
-	// If the user can edit messages, then append E as a valid key.
-	this.sStartContinuePrompt = "\x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "N\x01n\x01c)"
-	                          + this.colors["tradInterfaceContPromptMainColor"]
-	                          + "ext, \x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "L\x01n\x01c)"
-	                          + this.colors["tradInterfaceContPromptMainColor"]
-	                          + "ast, \x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "G\x01n\x01c)"
-	                          + this.colors["tradInterfaceContPromptMainColor"] + "o, ";
-	if (this.CanDelete() || this.CanDeleteLastMsg())
-	{
-		this.sStartContinuePrompt += "\x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"]
-		                          + "D\x01n\x01c)" + this.colors["tradInterfaceContPromptMainColor"] + "el, ";
-	}
-	if (this.CanEdit())
-	{
-		this.sStartContinuePrompt += "\x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"]
-		                          + "E\x01n\x01c)" + this.colors["tradInterfaceContPromptMainColor"] + "dit, ";
-	}
-	this.sStartContinuePrompt += "\x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "S\x01n\x01c)"
-	                     + this.colors["tradInterfaceContPromptMainColor"]
-	                     + "el, ";
-	this.sStartContinuePrompt += "\x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "Q\x01n\x01c)"
-	                          + this.colors["tradInterfaceContPromptMainColor"]
-	                          + "uit, msg" + this.colors["tradInterfaceContPromptHotkeyColor"] + "#" +
-	                          this.colors["tradInterfaceContPromptMainColor"] + ", " + this.colors["tradInterfaceContPromptHotkeyColor"]
-	                          + "?" + this.colors["tradInterfaceContPromptMainColor"] + ": "
-	                          		+ this.colors["tradInterfaceContPromptUserInputColor"];
-
-	this.sContinuePrompt = "\x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "N\x01n\x01c)"
-	                     + this.colors["tradInterfaceContPromptMainColor"]
-	                     + "ext, \x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "P\x01n\x01c)"
-	                     + this.colors["tradInterfaceContPromptMainColor"]
-	                     + "rev, \x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "F\x01n\x01c)"
-	                     + this.colors["tradInterfaceContPromptMainColor"]
-	                     + "irst, \x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "L\x01n\x01c)"
-	                     + this.colors["tradInterfaceContPromptMainColor"]
-	                     + "ast, \x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "G\x01n\x01c)"
-	                     + this.colors["tradInterfaceContPromptMainColor"] + "o, ";
-	if (this.CanDelete() || this.CanDeleteLastMsg())
-	{
-		this.sContinuePrompt += "\x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"]
-		                     + "D\x01n\x01c)" + this.colors["tradInterfaceContPromptMainColor"] + "el, ";
-	}
-	if (this.CanEdit())
-	{
-		this.sContinuePrompt += "\x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"]
-		                     + "E\x01n\x01c)" + this.colors["tradInterfaceContPromptMainColor"] + "dit, ";
-	}
-	this.sContinuePrompt += "\x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "S\x01n\x01c)"
-	                     + this.colors["tradInterfaceContPromptMainColor"]
-	                     + "el, ";
-	this.sContinuePrompt += "\x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "Q\x01n\x01c)"
-	                     + this.colors["tradInterfaceContPromptMainColor"]
-	                     + "uit, msg" + this.colors["tradInterfaceContPromptHotkeyColor"] + "#"
-	                     + this.colors["tradInterfaceContPromptMainColor"] + ", " + this.colors["tradInterfaceContPromptHotkeyColor"]
-	                     + "?" + this.colors["tradInterfaceContPromptMainColor"] + ": "
-	                     + this.colors["tradInterfaceContPromptUserInputColor"];
-
-	this.sEndContinuePrompt = "\x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "P\x01n\x01c)"
-	                        + this.colors["tradInterfaceContPromptMainColor"]
-	                        + "rev, \x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "F\x01n\x01c)"
-	                        + this.colors["tradInterfaceContPromptMainColor"]
-	                        + "irst, \x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "G\x01n\x01c)"
-	                        + this.colors["tradInterfaceContPromptMainColor"] + "o, ";
-	if (this.CanDelete() || this.CanDeleteLastMsg())
-	{
-		this.sEndContinuePrompt += "\x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"]
-		                        + "D\x01n\x01c)" + this.colors["tradInterfaceContPromptMainColor"] + "el, ";
-	}
-	if (this.CanEdit())
-	{
-		this.sEndContinuePrompt += "\x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"]
-		                        + "E\x01n\x01c)" + this.colors["tradInterfaceContPromptMainColor"] + "dit, ";
-	}
-	this.sEndContinuePrompt += "\x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "S\x01n\x01c)"
-	                        + this.colors["tradInterfaceContPromptMainColor"]
-	                        + "el, ";
-	this.sEndContinuePrompt += "\x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "Q\x01n\x01c)"
-	                        + this.colors["tradInterfaceContPromptMainColor"]
-	                        + "uit, msg" + this.colors["tradInterfaceContPromptHotkeyColor"] + "#"
-	                        + this.colors["tradInterfaceContPromptMainColor"] + ", " + this.colors["tradInterfaceContPromptHotkeyColor"]
-	                        + "?" + this.colors["tradInterfaceContPromptMainColor"] + ": "
-	                        + this.colors["tradInterfaceContPromptUserInputColor"];
-
-	this.msgListOnlyOnePageContinuePrompt = "\x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "G\x01n\x01c)"
-	                                + this.colors["tradInterfaceContPromptMainColor"] + "o, ";
-	if (this.CanDelete() || this.CanDeleteLastMsg())
-	{
-		this.msgListOnlyOnePageContinuePrompt += "\x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"]
-		                                + "D\x01n\x01c)" + this.colors["tradInterfaceContPromptMainColor"] + "el, ";
-	}
-	if (this.CanEdit())
-	{
-		this.msgListOnlyOnePageContinuePrompt += "\x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"]
-		                                + "E\x01n\x01c)" + this.colors["tradInterfaceContPromptMainColor"] + "dit, ";
-	}
-	this.msgListOnlyOnePageContinuePrompt += "\x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "S\x01n\x01c)"
-	                     + this.colors["tradInterfaceContPromptMainColor"]
-	                     + "el, ";
-	this.msgListOnlyOnePageContinuePrompt += "\x01n\x01c(" + this.colors["tradInterfaceContPromptHotkeyColor"] + "Q\x01n\x01c)"
-	                                + this.colors["tradInterfaceContPromptMainColor"]
-	                                + "uit, msg" + this.colors["tradInterfaceContPromptHotkeyColor"] + "#"
-	                                + this.colors["tradInterfaceContPromptMainColor"] + ", " + this.colors["tradInterfaceContPromptHotkeyColor"]
-	                                + "?" + this.colors["tradInterfaceContPromptMainColor"] + ": "
-	                                + this.colors["tradInterfaceContPromptUserInputColor"];
-
-	// Set the lightbar help text for message listing.  The @-codes are for mouse click tracking.
-	// For PageUp, normally I'd think KEY_PAGEUP should work, but that triggers sending a telegram instead.  \x1b[V seems to work though.
-	this.msgListLightbarModeHelpLine = this.colors.lightbarMsgListHelpLineHotkeyColor + "@CLEAR_HOT@@`" + UP_ARROW + "`" + KEY_UP + "@"
-	                           + this.colors.lightbarMsgListHelpLineGeneralColor + ", "
-							   + this.colors.lightbarMsgListHelpLineHotkeyColor + "@`" + DOWN_ARROW + "`" + KEY_DOWN + "@"
-	                           + this.colors.lightbarMsgListHelpLineGeneralColor + ", "
-							   + this.colors.lightbarMsgListHelpLineHotkeyColor + "@`PgUp`" + "\x1b[V" + "@"
-	                           + this.colors.lightbarMsgListHelpLineGeneralColor + "/"
-							   + this.colors.lightbarMsgListHelpLineHotkeyColor + "@`Dn`" + KEY_PAGEDN + "@"
-	                           + this.colors.lightbarMsgListHelpLineGeneralColor + ", "
-							   + this.colors.lightbarMsgListHelpLineHotkeyColor + "@`ENTER`" + KEY_ENTER + "@"
-	                           + this.colors.lightbarMsgListHelpLineGeneralColor + ", "
-							   + this.colors.lightbarMsgListHelpLineHotkeyColor + "@`HOME`" + KEY_HOME + "@"
-	                           + this.colors.lightbarMsgListHelpLineGeneralColor + ", "
-							   + this.colors.lightbarMsgListHelpLineHotkeyColor + "@`END`" + KEY_END + "@";
-	var lbHelpLineLen = 31;
-	// If the user can delete messages, then append DEL as a valid key.
-	if (this.CanDelete() || this.CanDeleteLastMsg())
-	{
-		this.msgListLightbarModeHelpLine += this.colors.lightbarMsgListHelpLineGeneralColor + ", "
-		                           + this.colors.lightbarMsgListHelpLineHotkeyColor + "@`DEL`" + KEY_DEL + "@";
-		lbHelpLineLen += 5;
-	}
-	this.msgListLightbarModeHelpLine += this.colors.lightbarMsgListHelpLineGeneralColor
-	                                 + ", " + this.colors.lightbarMsgListHelpLineHotkeyColor
-	                                 + "#" + this.colors.lightbarMsgListHelpLineGeneralColor + ", ";
-	lbHelpLineLen += 5;
-	// If the user can edit messages, then append E as a valid key.
-	if (this.CanEdit())
-	{
-		this.msgListLightbarModeHelpLine += this.colors.lightbarMsgListHelpLineHotkeyColor
-		                           + "@`E`E@" + this.colors.lightbarMsgListHelpLineParenColor
-								   + ")" + this.colors.lightbarMsgListHelpLineGeneralColor
-								   + "dit, ";
-		lbHelpLineLen += 7;
-	}
-	this.msgListLightbarModeHelpLine += this.colors.lightbarMsgListHelpLineHotkeyColor + "@`G`G@"
-							   + this.colors.lightbarMsgListHelpLineParenColor + ")"
-	                           + this.colors.lightbarMsgListHelpLineGeneralColor + "o, "
-	                           + this.colors.lightbarMsgListHelpLineHotkeyColor + "@`Q`Q@"
-							   + this.colors.lightbarMsgListHelpLineParenColor + ")"
-	                           + this.colors.lightbarMsgListHelpLineGeneralColor + "uit, "
-							   + this.colors.lightbarMsgListHelpLineHotkeyColor + "@`?`?@  ";
-	lbHelpLineLen += 15;
-
-	// Add spaces to the end of sLightbarModeHelpLine up until one char
-	// less than the width of the screen.
-	//var lbHelpLineLen = console.strlen(this.msgListLightbarModeHelpLine);
-	var numChars = console.screen_columns - lbHelpLineLen - 1;
-	if (numChars > 0)
-	{
-		// Add characters on the left and right of the line so that the
-		// text is centered.
-		var numLeft = Math.floor(numChars / 2);
-		var numRight = numChars - numLeft;
-		for (var i = 0; i < numLeft; ++i)
-			this.msgListLightbarModeHelpLine = " " + this.msgListLightbarModeHelpLine;
-		this.msgListLightbarModeHelpLine = "\x01n"
-		                             + this.colors.lightbarMsgListHelpLineBkgColor
-		                             + this.msgListLightbarModeHelpLine;
-		this.msgListLightbarModeHelpLine += "\x01n" + this.colors.lightbarMsgListHelpLineBkgColor;
-		for (var i = 0; i < numRight; ++i)
-			this.msgListLightbarModeHelpLine += ' ';
-	}
-}
-// For the DigDistMsgReader Class: Sets the hotkey help line for the enhanced
-// reader mode
-function DigDistMsgReader_SetEnhancedReaderHelpLine()
-{
-	// For PageUp, normally I'd think KEY_PAGEUP should work, but that triggers sending a telegram instead.  \x1b[V seems to work though.
-	this.enhReadHelpLine = this.colors.enhReaderHelpLineHotkeyColor + "@CLEAR_HOT@@`" + UP_ARROW + "`" + KEY_UP + "@"
-						 + this.colors.enhReaderHelpLineGeneralColor + ", "
-						 + this.colors.enhReaderHelpLineHotkeyColor + "@`" + DOWN_ARROW + "`" + KEY_DOWN + "@"
-						 + this.colors.enhReaderHelpLineGeneralColor + ", "
-						 + this.colors.enhReaderHelpLineHotkeyColor + "@`" + LEFT_ARROW + "`" + KEY_LEFT + "@"
-						 + this.colors.enhReaderHelpLineGeneralColor +", "
-						 + this.colors.enhReaderHelpLineHotkeyColor + "@`" + RIGHT_ARROW + "`" + KEY_RIGHT + "@"
-						 + this.colors.enhReaderHelpLineGeneralColor + ", "
-						 + this.colors.enhReaderHelpLineHotkeyColor + "@`PgUp`" + "\x1b[V" + "@"
-						 + this.colors.enhReaderHelpLineGeneralColor + "/"
-						 + this.colors.enhReaderHelpLineHotkeyColor + "@`Dn`" + KEY_PAGEDN + "@"
-						 + this.colors.enhReaderHelpLineGeneralColor + ", "
-						 + this.colors.enhReaderHelpLineHotkeyColor + "@`HOME`" + KEY_HOME + "@"
-						 + this.colors.enhReaderHelpLineGeneralColor + ", "
-						 + this.colors.enhReaderHelpLineHotkeyColor + "@`END`" + KEY_END + "@"
-						 + this.colors.enhReaderHelpLineGeneralColor + ", "
-						 + this.colors.enhReaderHelpLineHotkeyColor;
-	if (this.CanDelete() || this.CanDeleteLastMsg())
-		this.enhReadHelpLine += "@`DEL`" + KEY_DEL + "@" + this.colors.enhReaderHelpLineGeneralColor + ", " + this.colors.enhReaderHelpLineHotkeyColor;
-	if (this.CanEdit() && (console.screen_columns > 87))
-		this.enhReadHelpLine += "@`E`E@" + this.colors.enhReaderHelpLineParenColor + ")" + this.colors.enhReaderHelpLineGeneralColor + "dit, " + this.colors.enhReaderHelpLineHotkeyColor;
-	this.enhReadHelpLine += "@`F`F@" + this.colors.enhReaderHelpLineParenColor + ")"
-						 + this.colors.enhReaderHelpLineGeneralColor + "irst, "
-						 + this.colors.enhReaderHelpLineHotkeyColor + "@`L`L@" 
-						 + this.colors.enhReaderHelpLineParenColor + ")"
-						 + this.colors.enhReaderHelpLineGeneralColor + "ast, "
-						 + this.colors.enhReaderHelpLineHotkeyColor + "@`R`R@"
-						 + this.colors.enhReaderHelpLineParenColor + ")"
-						 + this.colors.enhReaderHelpLineGeneralColor + "eply, "
-						 + this.colors.enhReaderHelpLineHotkeyColor + "@`C`C@"
-						 + this.colors.enhReaderHelpLineParenColor + ")"
-						 + this.colors.enhReaderHelpLineGeneralColor + "hg area, "
-						 + this.colors.enhReaderHelpLineHotkeyColor + "@`Q`Q@"
-						 + this.colors.enhReaderHelpLineParenColor + ")"
-						 + this.colors.enhReaderHelpLineGeneralColor + "uit, "
-						 + this.colors.enhReaderHelpLineHotkeyColor + "@`?`?@";
-	// Center the help text based on the console width
-	//var numHotkeyChars = 92;
-	var numHotkeyChars = 89;
-	//var numCharsRemaining = console.screen_columns - (console.strlen(this.enhReadHelpLine) - numHotkeyChars) - 1;
-	var helpLineScreenLen = (console.strlen(this.enhReadHelpLine) - numHotkeyChars);
-	var numCharsRemaining = console.screen_columns - helpLineScreenLen - 1;
-	var frontPaddingLen = Math.floor(numCharsRemaining/2);
-	var padding = format("%*s", frontPaddingLen, "");
-	this.enhReadHelpLine = padding + this.enhReadHelpLine;
-	this.enhReadHelpLine = "\x01n" + this.colors.enhReaderHelpLineBkgColor + this.enhReadHelpLine;
-	if (console.screen_columns > 80)
-	{
-		//helpLineScreenLen += frontPaddingLen;
-		//numCharsRemaining = console.screen_columns - helpLineScreenLen - 1;
-		helpLineScreenLen = (console.strlen(this.enhReadHelpLine) - numHotkeyChars);
-		//numCharsRemaining = console.screen_columns - helpLineScreenLen - 2;
-		// Adding 3 as a correction factor for wide terminals (this is a kludge)
-		numCharsRemaining = console.screen_columns - helpLineScreenLen + 3;
-		if (numCharsRemaining > 0)
-		{
-			//this.enhReadHelpLine += format("%" + numCharsRemaining + "s", "");
-			this.enhReadHelpLine += format("%*s", numCharsRemaining, "");
-		}
-	}
-
-	// Create a version without the change area option
-	// For PageUp, normally I'd think KEY_PAGEUP should work, but that triggers sending a telegram instead.  \x1b[V seems to work though.
-	this.enhReadHelpLineWithoutChgArea = this.colors.enhReaderHelpLineHotkeyColor + "@CLEAR_HOT@  @`" + UP_ARROW + "`" + KEY_UP + "@"
-									   + this.colors.enhReaderHelpLineGeneralColor + ", "
-									   + this.colors.enhReaderHelpLineHotkeyColor + "@`" + DOWN_ARROW + "`" + KEY_DOWN + "@"
-									   + this.colors.enhReaderHelpLineGeneralColor + ", "
-									   + this.colors.enhReaderHelpLineHotkeyColor + "@`" + LEFT_ARROW + "`" + KEY_LEFT + "@"
-									   + this.colors.enhReaderHelpLineGeneralColor + ", "
-									   + this.colors.enhReaderHelpLineHotkeyColor + "@`" + RIGHT_ARROW + "`" + KEY_RIGHT + "@"
-									   + this.colors.enhReaderHelpLineGeneralColor + ", "
-									   + this.colors.enhReaderHelpLineHotkeyColor + "@`PgUp`" + "\x1b[V" + "@"
-									   + this.colors.enhReaderHelpLineGeneralColor + "/"
-									   + this.colors.enhReaderHelpLineHotkeyColor + "@`Dn`" + KEY_PAGEDN + "@"
-									   + this.colors.enhReaderHelpLineGeneralColor + ", "
-									   + this.colors.enhReaderHelpLineHotkeyColor + "@`HOME`" + KEY_HOME + "@"
-									   + this.colors.enhReaderHelpLineGeneralColor + ", "
-									   + this.colors.enhReaderHelpLineHotkeyColor + "@`END`" + KEY_END + "@"
-									   + this.colors.enhReaderHelpLineGeneralColor + ", "
-									   + this.colors.enhReaderHelpLineHotkeyColor;
-	if (this.CanDelete() || this.CanDeleteLastMsg())
-		this.enhReadHelpLineWithoutChgArea += "@`DEL`" + KEY_DEL + "@" + this.colors.enhReaderHelpLineGeneralColor + ", " + this.colors.enhReaderHelpLineHotkeyColor;
-	if (this.CanEdit())
-		this.enhReadHelpLineWithoutChgArea += "@`E`E@" + this.colors.enhReaderHelpLineParenColor + ")" + this.colors.enhReaderHelpLineGeneralColor + "dit, " + this.colors.enhReaderHelpLineHotkeyColor;
-	this.enhReadHelpLineWithoutChgArea += "@`F`F@" + this.colors.enhReaderHelpLineParenColor + ")"
-									   + this.colors.enhReaderHelpLineGeneralColor + "irst, "
-									   + this.colors.enhReaderHelpLineHotkeyColor + "@`L`L@"
-									   + this.colors.enhReaderHelpLineParenColor + ")"
-									   + this.colors.enhReaderHelpLineGeneralColor + "ast, "
-									   + this.colors.enhReaderHelpLineHotkeyColor + "@`R`R@"
-									   + this.colors.enhReaderHelpLineParenColor + ")"
-									   + this.colors.enhReaderHelpLineGeneralColor + "eply, "
-									   + this.colors.enhReaderHelpLineHotkeyColor + "@`Q`Q@"
-									   + this.colors.enhReaderHelpLineParenColor + ")"
-									   + this.colors.enhReaderHelpLineGeneralColor + "uit, "
-									   + this.colors.enhReaderHelpLineHotkeyColor + "@`?`?@  ";
-
-	// Center the help text based on the console width
-	numHotkeyChars = 84;
-	//var numCharsRemaining = console.screen_columns - (console.strlen(this.enhReadHelpLineWithoutChgArea) - numHotkeyChars) - 1;
-	helpLineScreenLen = (console.strlen(this.enhReadHelpLineWithoutChgArea) - numHotkeyChars);
-	numCharsRemaining = console.screen_columns - helpLineScreenLen - 1;
-	if (numCharsRemaining > 0)
-	{
-		frontPaddingLen = Math.floor(numCharsRemaining/2);
-		//padding = format("%" + frontPaddingLen + "s", "");
-		padding = format("%*s", frontPaddingLen, "");
-		this.enhReadHelpLineWithoutChgArea = padding + this.enhReadHelpLineWithoutChgArea;
-	}
-	this.enhReadHelpLineWithoutChgArea = "\x01n" + this.colors.enhReaderHelpLineBkgColor + this.enhReadHelpLineWithoutChgArea;
-	if (console.screen_columns > 80)
-	{
-		helpLineScreenLen = (console.strlen(this.enhReadHelpLineWithoutChgArea) - numHotkeyChars);
-		// Adding 3 as a correction factor for wide terminals (this is a kludge)
-		numCharsRemaining = console.screen_columns - helpLineScreenLen + 3;
-		if (numCharsRemaining > 0)
-		{
-			//this.enhReadHelpLineWithoutChgArea += format("%" + numCharsRemaining + "s", "");
-			this.enhReadHelpLineWithoutChgArea += format("%*s", numCharsRemaining, "");
-		}
-	}
-}
-function stripCtrlFromEnhReadHelpLine_ReplaceArrowChars(pHelpLine)
-{
-	var helpLineNoAttrs = strip_ctrl(pHelpLine);
-	var charsToPutBack = [UP_ARROW, DOWN_ARROW, LEFT_ARROW, RIGHT_ARROW];
-	var helpLineIdx = -1;
-	for (var i = 0; i < charsToPutBack.length; ++i)
-	{
-		helpLineIdx = helpLineNoAttrs.indexOf(",", helpLineIdx+1);
-		if (helpLineIdx > -1)
-		{
-			helpLineNoAttrs = helpLineNoAttrs.substr(0, helpLineIdx) + charsToPutBack[i] + helpLineNoAttrs.substr(helpLineIdx);
-			++helpLineIdx;
-		}
-	}
-	return helpLineNoAttrs;
-}
-// For the DigDistMsgReader class: Reads the configuration file (by default,
-// DDMsgReader.cfg) and sets the object properties
-// accordingly.
-function DigDistMsgReader_ReadConfigFile()
-{
-	this.cfgFileSuccessfullyRead = false;
-
-	var themeFilename = ""; // In case a theme filename is specified
-
-	// Open the main configuration file.  First look for it in the sbbs/mods
-	// directory, then sbbs/ctrl, then in the same directory as this script.
-	var cfgFilename = file_cfgname(system.mods_dir, this.cfgFilename);
-	if (!file_exists(cfgFilename))
-		cfgFilename = file_cfgname(system.ctrl_dir, this.cfgFilename);
-	if (!file_exists(cfgFilename))
-		cfgFilename = file_cfgname(gStartupPath, this.cfgFilename);
-	var cfgFile = new File(cfgFilename);
-	if (cfgFile.open("r"))
-	{
-		this.cfgFileSuccessfullyRead = true;
-		var settingsObj = cfgFile.iniGetObject();
-		cfgFile.close();
-
-		var numSpaces = parseInt(settingsObj["tabSpaces"]);
-		if (!isNaN(numSpaces) && numSpaces > 0)
-			this.numTabSpaces = numSpaces;
-		var maxNumLines = parseInt(settingsObj["areaChooserHdrMaxLines"]);
-		if (!isNaN(maxNumLines) && maxNumLines > 0)
-			this.areaChooserHdrMaxLines = maxNumLines;
-		var numberVal = parseInt(settingsObj["quickUserValSetIndex"]);
-		if (!isNaN(numberVal) && numberVal > -1)
-			this.quickUserValSetIndex = numberVal;
-		if (settingsObj.hasOwnProperty("listInterfaceStyle") && typeof(settingsObj.listInterfaceStyle) === "string")
-			this.msgListUseLightbarListInterface = (settingsObj.listInterfaceStyle.toUpperCase() == "LIGHTBAR");
-		if (typeof(settingsObj["readerInterfaceStyle"]) === "string")
-			this.scrollingReaderInterface = (settingsObj.readerInterfaceStyle.toUpperCase() == "SCROLLABLE");
-		if (typeof(settingsObj["displayBoardInfoInHeader"]) === "boolean")
-			this.displayBoardInfoInHeader = settingsObj.displayBoardInfoInHeader;
-		if (typeof(settingsObj["promptToContinueListingMessages"]) === "boolean")
-			this.promptToContinueListingMessages = settingsObj.promptToContinueListingMessages;
-		if (typeof(settingsObj["promptConfirmReadMessage"]) === "boolean")
-			this.promptToReadMessage = settingsObj.promptConfirmReadMessage;
-		if (typeof(settingsObj["msgListDisplayTime"]) === "string")
-			this.msgList_displayMessageDateImported = (settingsObj.msgListDisplayTime.toUpperCase() == "IMPORTED");
-		if (typeof(settingsObj["msgAreaList_lastImportedMsg_time"]) === "string")
-			this.msgAreaList_lastImportedMsg_showImportTime = (settingsObj.msgAreaList_lastImportedMsg_time.toUpperCase() == "IMPORTED");
-		if (typeof(settingsObj["startMode"]) === "string")
-		{
-			var valueUpper = settingsObj.startMode.toUpperCase();
-			if ((valueUpper == "READER") || (valueUpper == "READ"))
-				this.startMode = READER_MODE_READ;
-			else if ((valueUpper == "LISTER") || (valueUpper == "LIST"))
-				this.startMode = READER_MODE_LIST;
-		}
-		if (typeof(settingsObj["pauseAfterNewMsgScan"]) === "boolean")
-			this.pauseAfterNewMsgScan = settingsObj.pauseAfterNewMsgScan;
-		if (typeof(settingsObj["readingPostOnSubBoardInsteadOfGoToNext"]) === "boolean")
-			this.readingPostOnSubBoardInsteadOfGoToNext = settingsObj.readingPostOnSubBoardInsteadOfGoToNext;
-		if (typeof(settingsObj["areaChooserHdrFilenameBase"]) === "string")
-			this.areaChooserHdrFilenameBase = settingsObj.areaChooserHdrFilenameBase;
-		if (typeof(settingsObj["displayAvatars"]) === "boolean")
-			this.displayAvatars = settingsObj.displayAvatars;
-		if (typeof(settingsObj["rightJustifyAvatars"]) === "boolean")
-			this.rightJustifyAvatar = settingsObj.rightJustifyAvatars;
-		if (typeof(settingsObj["msgListSort"]) === "string")
-		{
-			if (settingsObj.msgListSort.toUpperCase() == "WRITTEN")
-				this.msgListSort = MSG_LIST_SORT_DATETIME_WRITTEN;
-		}
-		if (typeof(settingsObj["convertYStyleMCIAttrsToSync"]) === "boolean")
-			this.convertYStyleMCIAttrsToSync = settingsObj.convertYStyleMCIAttrsToSync;
-		if (typeof(settingsObj["prependFowardMsgSubject"]) === "boolean")
-			this.prependFowardMsgSubject = settingsObj.prependFowardMsgSubject;
-		if (typeof(settingsObj["enableIndexedModeMsgListCache"]) === "boolean")
-			this.enableIndexedModeMsgListCache = settingsObj.enableIndexedModeMsgListCache;
-		if (typeof(settingsObj["themeFilename"]) === "string")
-		{
-			// First look for the theme config file in the sbbs/mods
-			// directory, then sbbs/ctrl, then the same directory as
-			// this script.
-			themeFilename = system.mods_dir + settingsObj.themeFilename;
-			if (!file_exists(themeFilename))
-				themeFilename = system.ctrl_dir + settingsObj.themeFilename;
-			if (!file_exists(themeFilename))
-				themeFilename = gStartupPath + settingsObj.themeFilename;
-		}
-		if (typeof(settingsObj["saveAllHdrsWhenSavingMsgToBBSPC"]) === "boolean")
-			this.saveAllHdrsWhenSavingMsgToBBSPC = settingsObj.saveAllHdrsWhenSavingMsgToBBSPC;
-		// User setting defaults
-		if (typeof(settingsObj.reverseListOrder === "boolean"))
-			this.userSettings.listMessagesInReverse = settingsObj.reverseListOrder;
-		if (typeof(settingsObj.useIndexedModeForNewscan) === "boolean")
-			this.userSettings.useIndexedModeForNewscan = settingsObj.useIndexedModeForNewscan;
-		if (typeof(settingsObj.newscanOnlyShowNewMsgs) === "boolean")
-			this.userSettings.newscanOnlyShowNewMsgs = settingsObj.newscanOnlyShowNewMsgs;
-		if (typeof(settingsObj.indexedModeMenuSnapToFirstWithNew) === "boolean")
-			this.userSettings.indexedModeMenuSnapToFirstWithNew = settingsObj.indexedModeMenuSnapToFirstWithNew;
-		if (typeof(settingsObj.promptDelPersonalEmailAfterReply) === "boolean")
-			this.userSettings.promptDelPersonalEmailAfterReply = settingsObj.promptDelPersonalEmailAfterReply;
-		if (typeof(settingsObj.displayIndexedModeMenuIfNoNewMessages) === "boolean")
-			this.userSettings.displayIndexedModeMenuIfNoNewMessages = settingsObj.displayIndexedModeMenuIfNoNewMessages;
-	}
-	else
-	{
-		// Was unable to read the configuration file.  Output a warning to the user
-		// that defaults will be used and to notify the sysop.
-		console.attributes = "N";
-		console.crlf();
-		console.print("\x01w\x01hUnable to open the configuration file: \x01y" + this.cfgFilename);
-		console.crlf();
-		console.print("\x01wDefault settings will be used.  Please notify the sysop.");
-		mswait(2000);
-	}
-	
-	// If a theme filename was specified, then read the colors & strings
-	// from it.
-	if (themeFilename.length > 0)
-	{
-		var onlySyncAttrsRegexWholeWord = new RegExp("^[\x01krgybmcw01234567hinq,;\.dtlasz]+$", 'i');
-
-		var themeFile = new File(themeFilename);
-		if (themeFile.open("r"))
-		{
-			var themeSettingsObj = themeFile.iniGetObject();
-			themeFile.close();
-
-			// Set any color values specified
-			for (var prop in this.colors)
-			{
-				if (themeSettingsObj.hasOwnProperty(prop))
-				{
-					// Trim spaces from the color value
-					var value = trimSpaces(themeSettingsObj[prop].toString(), true, true, true);
-					value = value.replace(/\\x01/g, "\x01"); // Replace "\x01" with control character
-					// If the value doesn't have any control characters, then add the control character
-					// before attribute characters
-					if (!/\x01/.test(value))
-						value = attrCodeStr(value);
-					if (onlySyncAttrsRegexWholeWord.test(value))
-						this.colors[prop] = value;
-				}
-			}
-			// Set any text strings specified
-			for (var prop in this.text)
-			{
-				if (typeof(themeSettingsObj[prop]) === "string" && themeSettingsObj[prop].length > 0)
-				{
-					// Replace any instances of "\x01" with the Synchronet
-					// attribute control character
-					this.text[prop] = themeSettingsObj[prop].replace(/\\x01/g, "\x01");
-				}
-			}
-			// Append the hotkey help line colors with their background color, to ensure that
-			// the background always gets set for all of them (in case a 'normal' attribute
-			// appears in any of the colors)
-			// Message list
-			this.colors.lightbarMsgListHelpLineGeneralColor += this.colors.lightbarMsgListHelpLineBkgColor;
-			this.colors.lightbarMsgListHelpLineHotkeyColor += this.colors.lightbarMsgListHelpLineBkgColor;
-			this.colors.lightbarMsgListHelpLineParenColor += this.colors.lightbarMsgListHelpLineBkgColor;
-			// Area chooser
-			this.colors.lightbarAreaChooserHelpLineGeneralColor += this.colors.lightbarAreaChooserHelpLineBkgColor;
-			this.colors.lightbarAreaChooserHelpLineHotkeyColor += this.colors.lightbarAreaChooserHelpLineBkgColor;
-			this.colors.lightbarAreaChooserHelpLineParenColor += this.colors.lightbarAreaChooserHelpLineBkgColor;
-			// Reader
-			this.colors.enhReaderHelpLineGeneralColor += this.colors.enhReaderHelpLineBkgColor;
-			this.colors.enhReaderHelpLineHotkeyColor += this.colors.enhReaderHelpLineBkgColor;
-			this.colors.enhReaderHelpLineParenColor += this.colors.enhReaderHelpLineBkgColor;
-			// Indexed mode newscan
-			this.colors.lightbarIndexedModeHelpLineHotkeyColor += this.colors.lightbarIndexedModeHelpLineBkgColor;
-			this.colors.lightbarIndexedModeHelpLineGeneralColor += this.colors.lightbarIndexedModeHelpLineBkgColor;
-			this.colors.lightbarIndexedModeHelpLineParenColor += this.colors.lightbarIndexedModeHelpLineBkgColor;
-
-			// Ensure that scrollbarBGChar and scrollbarScrollBlockChar are
-			// only one character.  If they're longer, use only the first
-			// character.
-			if (this.text.scrollbarBGChar.length > 1)
-				this.text.scrollbarBGChar = this.text.scrollbarBGChar.substr(0, 1);
-			if (this.text.scrollbarScrollBlockChar.length > 1)
-				this.text.scrollbarScrollBlockChar = this.text.scrollbarScrollBlockChar.substr(0, 1);
-		}
-		else
-		{
-			// Was unable to read the theme file.  Output a warning to the user
-			// that defaults will be used and to notify the sysop.
-			this.cfgFileSuccessfullyRead = false;
-			console.attributes = "N";
-			console.crlf();
-			console.print("\x01w\x01hUnable to open the theme file: \x01y" + themeFilename);
-			console.crlf();
-			console.print("\x01wDefault settings will be used.  Please notify the sysop.");
-			mswait(2000);
-		}
-	}
-}
-// For the DigDistMsgReader class: Reads the user settings file
-//
-// Parameters:
-//  pOnlyTwitlist: Optional boolean - Whether or not to only read the user's twitlist. Defaults to false.
-function DigDistMsgReader_ReadUserSettingsFile(pOnlyTwitlist)
-{
-	var onlyTwitList = (typeof(pOnlyTwitlist) === "boolean" ? pOnlyTwitlist : false);
-	// Open the user's personal twit list file, if it exists
-	var userTwitlistFile = new File(gUserTwitListFilename);
-	if (userTwitlistFile.open("r"))
-	{
-		while (!userTwitlistFile.eof)
-		{
-			// Read the next line from the config file.
-			var fileLine = userTwitlistFile.readln(2048);
-
-			// fileLine should be a string, but I've seen some cases
-			// where for some reason it isn't.  If it's not a string,
-			// then continue onto the next line.
-			if (typeof(fileLine) != "string")
-				continue;
-
-			// If the line starts with with a semicolon (the comment
-			// character) or is blank, then skip it.
-			if ((fileLine.substr(0, 1) == ";") || (fileLine.length == 0))
-				continue;
-
-			// In case there are any commas, split on commas. Add all names to the user's twitlist
-			var names = fileLine.split(",");
-			for (var i = 0; i < names.length; ++i)
-			{
-				var twitListNameEntry = names[i].trim().toLowerCase(); // Lowercase for case-insensitive comparisons
-				if (twitListNameEntry.length > 0)
-					this.userSettings.twitList.push(twitListNameEntry);
-			}
-		}
-		userTwitlistFile.close();
-	}
-
-	if (!onlyTwitList)
-	{
-		// Open and read the user settings file, if it exists
-		var userSettingsFile = new File(gUserSettingsFilename);
-		if (userSettingsFile.open("r"))
-		{
-			// Variables in this.userSettings are initialized in the DigDistMsgReader constructor. Then, default user
-			// settings are set when reading DDMsgReader.cfg, which is read before the user settings file. So for each
-			// user setting (except for twitlist), try to read it from the user settings file, but heave the default be
-			// whatever it's currently set to.
-			for (var settingName in this.userSettings)
-			{
-				if (settingName == "twitList") continue;
-				this.userSettings[settingName] = userSettingsFile.iniGetValue("BEHAVIOR", settingName, this.userSettings[settingName]);
-			}
-
-			userSettingsFile.close();
-		}
-	}
-}
-
-// For the DigDistMessageReader class: Writes the user settings file.
-//
-// Return value: Boolean - Whether or not the write succeeded
-function DigDistMsgReader_WriteUserSettingsFile()
-{
-	var writeSucceeded = false;
-	// Open the user settings file, if it exists
-	var userSettingsFile = new File(gUserSettingsFilename);
-	if (userSettingsFile.open(userSettingsFile.exists ? "r+" : "w+"))
-	{
-		// Variables in this.userSettings are initialized in the DigDistMsgReader constructor. For each
-		// user setting (except for twitlist), save the setting in the user's settings file. The user's
-		// twit list is an array that is saved to a separate file.
-		for (var settingName in this.userSettings)
-		{
-			if (settingName == "twitList") continue;
-			userSettingsFile.iniSetValue("BEHAVIOR", settingName, this.userSettings[settingName]);
-		}
-		userSettingsFile.close();
-		writeSucceeded = true;
-	}
-	return writeSucceeded;
-}
-
-// For the DigDistMsgReader class: Lets the user edit an existing message.
-//
-// Parameters:
-//  pMsgIndex: The index of the message to edit
-//
-// Return value: An object with the following parameters:
-//               userCannotEdit: Boolean - True if the user can't edit, false if they can
-//               userConfirmed: Boolean - Whether or not the user confirmed editing
-//               msgEdited: Boolean - Whether or not the message was edited
-//               newMsgIdx: The index (offset) of the new (edited) message that was saved.
-//                          If the message wasn't edited/saved, this will be -1.
-function DigDistMsgReader_EditExistingMsg(pMsgIndex)
-{
-	var returnObj = {
-		userCannotEdit: false,
-		userConfirmed: false,
-		msgEdited: false,
-		newMsgIdx: -1
-	};
-
-	// Open the sub-board
-	var msgbase = new MsgBase(this.subBoardCode);
-	if (!msgbase.open())
-	{
-		console.print("\x01n\x01h\x01wCan't open the sub-board\x01n");
-		console.crlf();
-		console.pause();
-		return returnObj;
-	}
-
-	// Only let the user edit the message if they're a sysop or
-	// if they wrote the message.
-	var msgHeader = this.GetMsgHdrByIdx(pMsgIndex, false, msgbase);
-	if (msgHeader == null)
-	{
-		console.print("\x01n\x01h\x01wInvalid message header\x01n");
-		console.crlf();
-		console.pause();
-		return returnObj;
-	}
-	if (!user.is_sysop && (msgHeader.from != user.name) && (msgHeader.from != user.alias) && (msgHeader.from != user.handle))
-	{
-		console.print("\x01n\x01h\x01wCannot edit message #\x01y" + +(pMsgIndex+1) +
-		              " \x01wbecause it's not yours or you're not a sysop.\x01n");
-		console.crlf();
-		console.pause();
-		returnObj.userCannotEdit = true;
-		return returnObj;
-	}
-
-	// Confirm the action with the user (default to no).
-	returnObj.userConfirmed = !console.noyes(replaceAtCodesInStr(format(this.text.msgEditConfirmText, +(pMsgIndex+1))));
-	if (!returnObj.userConfirmed)
-	{
-		msgbase.close();
-		return returnObj;
-	}
-
-	// Make use of bbs.edit_msg() if the function exists (it was added in
-	// Synchronet 3.18c).  Otherwise, edit the old way.
-	if (typeof(bbs.edit_msg) === "function")
-	{
-		if (!bbs.edit_msg(msgHeader))
-		{
-			var grpIdx = msg_area.sub[this.subBoardCode].grp_index;
-			var areaDesc = msg_area.grp_list[grpIdx].description + " - " + msg_area.sub[this.subBoardCode].description;
-			var logMsg = user.alias + " was unable to edit message number " + msgHeader.number + " in " + areaDesc;
-			log(LOG_ERROR, logMsg);
-			bbs.log_str(logMsg);
-		}
-	}
-	else
-		this.EditExistingMessageOldWay(msgbase, msgHeader, pMsgIndex);
-
-	msgbase.close();
-
-	return returnObj;
-}
-// Helper for DigDistMsgReader_EditExistingMsg(): Edits an existing message by writing it
-// to a temporary file, having the user edit that, and saving it as a new message.
-// This was done before the bbs.edit_msg() function existed (it was added in Synchronet
-// 3.18c).
-//
-// Parameters:
-//  pMsgbase: The MessageBase object.  Assumed to be open.
-//  pOrigMsgHdr: The header of the original message
-//  pMsgIndex: The index of the message to edit
-function DigDistMsgReader_EditExistingMessageOldWay(pMsgbase, pOrigMsgHdr, pMsgIndex)
-{
-	// Dump the message body to a temporary file in the node dir
-	//var originalMsgBody = pMsgbase.get_msg_body(true, pMsgIndex, false, false, true, true);
-	var originalMsgBody;
-	var tmpMsgHdr = this.GetMsgHdrByIdx(pMsgIndex, false, pMsgbase);
-	if (tmpMsgHdr == null)
-		originalMsgBody = pMsgbase.get_msg_body(true, pMsgIndex, false, false, true, true);
-	else
-		originalMsgBody = pMsgbase.get_msg_body(false, tmpMsgHdr.number, false, false, true, true);
-	var tempFilename = system.node_dir + "DDMsgLister_message.txt";
-	var tmpFile = new File(tempFilename);
-	if (tmpFile.open("w"))
-	{
-		var wroteToTempFile = tmpFile.write(word_wrap(originalMsgBody, 79));
-		tmpFile.close();
-		// If we were able to write to the temp file, then let the user
-		// edit the file.
-		if (wroteToTempFile)
-		{
-			// The following lines set some attributes in the bbs object
-			// in an attempt to make the "To" name and subject appear
-			// correct in the editor.
-			// TODO: On May 14, 2013, Digital Man said bbs.msg_offset will
-			// probably be removed because it doesn't provide any benefit.
-			// bbs.msg_number is a unique message identifier that won't
-			// change, so it's probably best for scripts to use bbs.msg_number
-			// instead of offsets.
-			bbs.msg_to = pOrigMsgHdr.to;
-			bbs.msg_to_ext = pOrigMsgHdr.to_ext;
-			bbs.msg_subject = pOrigMsgHdr.subject;
-			bbs.msg_offset = pOrigMsgHdr.offset;
-			bbs.msg_number = pOrigMsgHdr.number;
-
-			// Let the user edit the temporary file
-			console.editfile(tempFilename);
-			// Load the temp file back into msgBodyColor and have pMsgbase
-			// save the message.
-			if (tmpFile.open("r"))
-			{
-				var newMsgBody = tmpFile.read();
-				tmpFile.close();
-				// If the new message body is different from the original message
-				// body, then go ahead and save the message and mark the original
-				// message for deletion. (Checking the new & original message
-				// bodies seems to be the only way to check to see if the user
-				// aborted out of the message editor.)
-				if (newMsgBody != originalMsgBody)
-				{
-					var newHdr = { to: pOrigMsgHdr.to, to_ext: pOrigMsgHdr.to_ext, from: pOrigMsgHdr.from,
-					               from_ext: pOrigMsgHdr.from_ext, attr: pOrigMsgHdr.attr,
-					               subject: pOrigMsgHdr.subject };
-					var savedNewMsg = pMsgbase.save_msg(newHdr, newMsgBody);
-					// If the message was successfully saved, then mark the original
-					// message for deletion and output a message to the user.
-					if (savedNewMsg)
-					{
-						returnObj.msgEdited = true;
-						returnObj.newMsgIdx = pMsgbase.total_msgs - 1;
-						var message = "\x01n\x01cThe edited message has been saved as a new message.";
-						if (pMsgbase.remove_msg(true, pMsgIndex))
-							message += "  The original has been\r\nmarked for deletion.";
-						else
-							message += "  \x01h\x01yHowever, the original\r\ncould not be marked for deletion.";
-						message += "\r\n\x01p";
-						console.print(message);
-					}
-					else
-						console.print("\r\n\x01n\x01h\x01yError: \x01wFailed to save the new message\r\n\x01p");
-				}
-			}
-			else
-			{
-				console.print("\r\n\x01n\x01h\x01yError: \x01wUnable to read the temporary file\r\n");
-				console.print("Filename: \x01b" + tempFilename + "\r\n");
-				console.pause();
-			}
-		}
-		else
-		{
-			console.print("\r\n\x01n\x01h\x01yError: \x01wUnable to write to temporary file\r\n");
-			console.print("Filename: \x01b" + tempFilename + "\r\n");
-			console.pause();
-		}
-	}
-	else
-	{
-		console.print("\r\n\x01n\x01h\x01yError: \x01wUnable to open a temporary file for writing\r\n");
-		console.print("Filename: \x01b" + tempFilename + "\r\n");
-		console.pause();
-	}
-	// Delete the temporary file from disk.
-	tmpFile.remove();
-}
-
-// For the DigDistMsgReader Class: Returns whether or not the user can delete
-// their messages in the sub-board (distinct from being able to delete only
-// their last message).
-function DigDistMsgReader_CanDelete()
-{
-	// Deleting messages is allowed if the user is the sysop or reading personal email.
-	// If not, check the sub-board configuration.
-	var canDelete = user.is_sysop || this.readingPersonalEmail;
-	if (!canDelete)
-		canDelete = Boolean(msg_area.sub[this.subBoardCode].settings & SUB_DEL);
-	return canDelete;
-}
-// For the DigDistMsgReader Class: Returns whether or not the user can delete
-// the last message they posted in the sub-board.
-function DigDistMsgReader_CanDeleteLastMsg()
-{
-	// Sysops can delete the last message by default. If not, check the sub-board configuration.
-	var canDelete = user.is_sysop;
-	if (!canDelete && !this.readingPersonalEmail)
-		canDelete = Boolean(msg_area.sub[this.subBoardCode].settings & SUB_DELLAST);
-	return canDelete;
-}
-// For the DigDistMsgReader Class: Returns whether or not the user can edit
-// messages.
-function DigDistMsgReader_CanEdit()
-{
-	// Sysops can edit by default. If not, check the sub-board configuration.
-	var canEdit = user.is_sysop;
-	if (!canEdit && !this.readingPersonalEmail)
-		canEdit = Boolean(msg_area.sub[this.subBoardCode].settings & SUB_EDIT);
-	return canEdit;
-}
-// For the DigDistMsgReader Class: Returns whether or not message quoting
-// is enabled.
-function DigDistMsgReader_CanQuote()
-{
-	// Sysops and users reading personal email can quote by default.
-	// If not, check the sub-board configuration.
-	var canQuote = user.is_sysop || this.readingPersonalEmail;
-	if (!canQuote)
-		canQuote = Boolean(msg_area.sub[this.subBoardCode].settings & SUB_QUOTE);
-	return canQuote;
-}
-
-// For the DigDistMsgReader Class: Displays the stock Synchronet message header file for
-// a given message header.
-//
-// Parameters:
-//  pMsgHdr: The message header object
-function DigDistMsgReader_DisplaySyncMsgHeader(pMsgHdr)
-{
-	if ((pMsgHdr == null) || (typeof(pMsgHdr) != "object"))
-		return;
-
-	// Note: The message header has the following fields:
-	// 'number': The message number
-	// 'offset': The message offset
-	// 'to': Who the message is directed to (string)
-	// 'from' Who wrote the message (string)
-	// 'subject': The message subject (string)
-	// 'date': The date - Full text (string)
-
-	// Generate a string containing the message's import date & time.
-	//var dateTimeStr = strftime("%Y-%m-%d %H:%M:%S", msgHeader.when_imported_time)
-	// Use the date text in the message header, without the time
-	// zone offset at the end.
-	var dateTimeStr = pMsgHdr["date"].replace(/ [-+][0-9]+$/, "");
-
-	// Check to see if there is a msghdr file in the sbbs/text/menu
-	// directory.  If there is, then use it to display the message
-	// header information.  Otherwise, output a default message header.
-	var msgHdrFileOpened = false;
-	var msgHdrFilename = this.GetMsgHdrFilenameFull();
-	if (msgHdrFilename.length > 0)
-	{
-		var msgHdrFile = new File(msgHdrFilename);
-		if (msgHdrFile.open("r"))
-		{
-			msgHdrFileOpened = true;
-			var fileLine = null; // To store a line read from the file
-			while (!msgHdrFile.eof)
-			{
-				// Read the next line from the header file
-				fileLine = msgHdrFile.readln(2048);
-
-				// fileLine should be a string, but I've seen some cases
-				// where it isn't, so check its type.
-				if (typeof(fileLine) != "string")
-					continue;
-
-				// Since we're displaying the message information ouside of Synchronet's
-				// message read prompt, this script now has to parse & replace some of
-				// the @-codes in the message header line, since Synchronet doesn't know
-				// that the user is reading a message.
-				console.putmsg(this.ParseMsgAtCodes(fileLine, pMsgHdr, null, dateTimeStr, false, true));
-				console.crlf();
-			}
-			msgHdrFile.close();
-		}
-	}
-
-	// If the msghdr file didn't open (or doesn't exist), then output the default
-	// header.
-	if (!msgHdrFileOpened)
-	{
-		// Generate a string describing the message attributes, then output the default
-		// header.
-		var allMsgAttrStr = makeAllMsgAttrStr(pMsgHdr);
-		console.print("\x01n\x01w" + charStr(HORIZONTAL_DOUBLE, 78));
-		console.crlf();
-		var horizSingleFive = charStr(HORIZONTAL_SINGLE, 5);
-		console.print("\x01n\x01w" + horizSingleFive + "\x01cFrom\x01w\x01h: \x01b" + pMsgHdr["from"].substr(0, console.screen_columns-12));
-		console.crlf();
-		console.print("\x01n\x01w" + horizSingleFive + "\x01cTo  \x01w\x01h: \x01b" + pMsgHdr["to"].substr(0, console.screen_columns-12));
-		console.crlf();
-		console.print("\x01n\x01w" + horizSingleFive + "\x01cSubj\x01w\x01h: \x01b" + pMsgHdr["subject"].substr(0, console.screen_columns-12));
-		console.crlf();
-		console.print("\x01n\x01w" + horizSingleFive + "\x01cDate\x01w\x01h: \x01b" + dateTimeStr.substr(0, console.screen_columns-12));
-		console.crlf();
-		console.print("\x01n\x01w" + horizSingleFive + "\x01cAttr\x01w\x01h: \x01b" + allMsgAttrStr.substr(0, console.screen_columns-12));
-		console.crlf();
-	}
-}
-
-// For the DigDistMsgReader class: Returns the name of the msghdr file in the
-// sbbs/text/menu directory. If the user's terminal supports ANSI, this first
-// checks to see if an .ans version exists.  Otherwise, checks to see if an
-// .asc version exists.  If neither are found, this function will return an
-// empty string.
-function DigDistMsgReader_GetMsgHdrFilenameFull()
-{
-  // If the user's terminal supports ANSI and msghdr.ans exists
-  // in the text/menu directory, then use that one.  Otherwise,
-  // if msghdr.asc exists, then use that one.
-  var ansiFileName = "menu/msghdr.ans";
-  var asciiFileName = "menu/msghdr.asc";
-  var msgHdrFilename = "";
-  if (console.term_supports(USER_ANSI) && file_exists(system.text_dir + ansiFileName))
-    msgHdrFilename = system.text_dir + ansiFileName;
-  else if (file_exists(system.text_dir + asciiFileName))
-    msgHdrFilename = system.text_dir + asciiFileName;
-  return msgHdrFilename;
-}
-
-// For the DigDistMsgReader class: Returns the number of messages in the current
-// sub-board.  This will be either the number of headers in this.msgSearchHdrs
-// for the current sub-board (if non-empty and a search type specified) or
-// msgbase.total_msgs.
-//
-// Parameters:
-//  pMsgbase: Optional - A MessageBase object
-//
-// Return value: The number of messages
-function DigDistMsgReader_NumMessages(pMsgbase)
-{
-	var numMsgs = 0;
-	if (this.SearchingAndResultObjsDefinedForCurSub())
-		numMsgs = this.msgSearchHdrs[this.subBoardCode].indexed.length;
-	else if (this.hdrsForCurrentSubBoard.length > 0)
-		numMsgs = this.hdrsForCurrentSubBoard.length;
-	else
-	{
-		var closeMsgbaseInThisFunc = false;
-		var msgbase = null;
-		if (pMsgbase != null && typeof(pMsgbase) === "object")
-			msgbase = pMsgbase;
-		else
-		{
-			closeMsgbaseInThisFunc = true;
-			msgbase = new MsgBase(this.subBoardCode);
-			msgbase.open();
-		}
-		if (msgbase.is_open)
-		{
-			//numMsgs = msgbase.total_msgs;
-			// Count the number of readable messages in the messagebase (i.e.,
-			// messages that are not deleted, unvalidated, or null headers)
-			numMsgs = 0;
-			var indexRecords = msgbase.get_index();
-			if (indexRecords != null)
-			{
-				for (var i = 0; i < indexRecords.length; ++i)
-				{
-					if (isReadableMsgHdr(indexRecords[i], this.subBoardCode))
-						++numMsgs;
-				}
-			}
-			if (closeMsgbaseInThisFunc)
-				msgbase.close();
-		}
-	}
-
-	return numMsgs;
-}
-
-// For the DigDistMsgReader class: Returns whether there are any non-deleted
-// messages in the current sub-board.
-//
-// Return value: Boolean - Whether or not there are any non-deleted messages
-//               in the current sub-board.
-function DigDistMsgReader_NonDeletedMessagesExist()
-{
-	var messagesExist = false;
-
-	var numMsgs = this.NumMessages();
-	if (numMsgs > 0)
-	{
-		var msgHdr;
-		for (var msgIdx = 0; (msgIdx < numMsgs) && !messagesExist; ++msgIdx)
-		{
-			msgHdr = this.GetMsgHdrByIdx(msgIdx);
-			if (msgHdr != null && (msgHdr.attr & MSG_DELETE) == 0)
-			{
-				messagesExist = true;
-				break;
-			}
-		}
-	}
-
-	return messagesExist;
-}
-
-// For the DigDistMsgReader class: Returns the highest message number (1-based), either from this.msgSearchHdrs
-// (if it has search results for the current sub-board) or msgbase.  If
-// there are no search results for the current sub-board in this.msgSearchHdrs,
-// the highest message number is the same as the total number of messages
-// in the sub-board (unless the Synchronet standard ever changes..).
-function DigDistMsgReader_HighestMessageNum()
-{
-	var highestMessageNum = 0;
-	var msgbase = new MsgBase(this.subBoardCode);
-	if (msgbase.open())
-	{
-		highestMessageNum = msgbase.total_msgs;
-		msgbase.close();
-	}
-	if (this.msgSearchHdrs.hasOwnProperty(this.subBoardCode) && (this.msgSearchHdrs[this.subBoardCode].indexed.length > 0))
-		highestMessageNum = this.msgSearchHdrs[this.subBoardCode].indexed.length;
-	return highestMessageNum;
-}
-
-// For the DigDistMsgReader class: Returns whether or not a message number (1-based)
-// is a valid and existing message number.  This is intended for validating user
-// input where not all the message numbers are consecutive.
-//
-// Parameters:
-//  pMsgNum: The message number to validate
-//
-// Return value: Boolean - Whether or not the message number 
-function DigDistMsgReader_IsValidMessageNum(pMsgNum)
-{
-	// The message numbers start at 1
-	if (pMsgNum < 1)
-		return false;
-	// If there are search results for the current sub-board, then check to see if
-	// the message number exists in its indexed array.  Otherwise, check with
-	// msgbase.
-	var msgNumIsValid = false;
-	if (this.SearchingAndResultObjsDefinedForCurSub())
-		msgNumIsValid = ((pMsgNum > 0) && (pMsgNum <= this.msgSearchHdrs[this.subBoardCode].indexed.length));
-	else
-	{
-		var msgbase = new MsgBase(this.subBoardCode);
-		if (msgbase.open())
-		{
-			msgNumIsValid = ((pMsgNum > 0) && (pMsgNum <= msgbase.total_msgs));
-			msgbase.close();
-		}
-	}
-	return msgNumIsValid;
-}
-
-// For the DigDistMsgReader class: Returns a message header by index.  Will look
-// in this.msgSearchHdrs if it's not empty, then in this.hdrsForCurrentSubBoard
-// if it's not empty, then from msgbase.
-//
-// Parameters:
-//  pMsgIdx: The message index (0-based)
-//  pExpandFields: Whether or not to expand fields.  Defaults to false.
-//  pMsgbase: Optional - An open MsgBase object.  If not passed, the sub-board coould be opened in this method.
-function DigDistMsgReader_GetMsgHdrByIdx(pMsgIdx, pExpandFields, pMsgbase)
-{
-	var expandFields = (typeof(pExpandFields) == "boolean" ? pExpandFields : false);
-
-	var msgHdr = null;
-	if (this.msgSearchHdrs.hasOwnProperty(this.subBoardCode) && (this.msgSearchHdrs[this.subBoardCode].indexed.length > 0))
-	{
-		if ((pMsgIdx >= 0) && (pMsgIdx < this.msgSearchHdrs[this.subBoardCode].indexed.length))
-		{
-			if (expandFields)
-				msgHdr = this.msgSearchHdrs[this.subBoardCode].indexed[pMsgIdx];
-			else
-				msgHdr = getHdrFromMsgbase(pMsgbase, this.subBoardCode, false, this.msgSearchHdrs[this.subBoardCode].indexed[pMsgIdx].number, expandFields);
-		}
-	}
-	else if (this.hdrsForCurrentSubBoard.length > 0)
-	{
-		if ((pMsgIdx >= 0) && (pMsgIdx < this.hdrsForCurrentSubBoard.length))
-		{
-			if (expandFields)
-				msgHdr = this.hdrsForCurrentSubBoard[pMsgIdx];
-			else
-				msgHdr = getHdrFromMsgbase(pMsgbase, this.subBoardCode, false, this.hdrsForCurrentSubBoard[pMsgIdx].number, expandFields);
-		}
-	}
-	else
-		msgHdr = getHdrFromMsgbase(pMsgbase, this.subBoardCode, true, pMsgIdx, pExpandFields);
-	return msgHdr;
-}
-
-// For the DigDistMsgReader class: Returns a message header by message number
-// (1-based).  Will look in this.msgSearchHdrs if it's not empty, then in
-// this.hdrsForCurrentSubBoard if it's not empty, then from msgbase.
-//
-// Parameters:
-//  pMsgNum: The message number (1-based)
-//  pExpandFields: Whether or not to expand fields.  Defaults to false.
-//
-// Return value: The message header for the message number, or null on error
-function DigDistMsgReader_GetMsgHdrByMsgNum(pMsgNum, pExpandFields)
-{
-	var msgHdr = null;
-	if (this.msgSearchHdrs.hasOwnProperty(this.subBoardCode) &&
-			(this.msgSearchHdrs[this.subBoardCode].indexed.length > 0))
-	{
-		if ((pMsgNum > 0) && (pMsgNum <= this.msgSearchHdrs[this.subBoardCode].indexed.length))
-			msgHdr = this.msgSearchHdrs[this.subBoardCode].indexed[pMsgNum-1];
-	}
-	else if (this.hdrsForCurrentSubBoard.length > 0)
-	{
-		if ((pMsgNum > 0) && (pMsgNum <= this.hdrsForCurrentSubBoard.length))
-			msgHdr = this.hdrsForCurrentSubBoard.length[pMsgNum-1];
-	}
-	else
-	{
-		var msgbase = new MsgBase(this.subBoardCode);
-		if (msgbase.open())
-		{
-			if ((pMsgNum > 0) && (pMsgNum <= msgbase.total_msgs))
-			{
-				var expandFields = (typeof(pExpandFields) == "boolean" ? pExpandFields : false);
-				msgHdr = msgbase.get_msg_header(true, pMsgNum-1, expandFields);
-			}
-			msgbase.close();
-		}
-	}
-	return msgHdr;
-}
-
-// For the DigDistMsgReader class: Returns a message header by absolute message
-// number.  If there is a problem, this method will return null.
-//
-// Parameters:
-//  pMsgNum: The absolute message number
-//  pExpandFields: Whether or not to expand fields.  Defaults to false.
-//  pGetVoteInfo: Whether or not to get voting information.  Defaults to false.
-//
-// Return value: The message header for the message number, or null on error
-function DigDistMsgReader_GetMsgHdrByAbsoluteNum(pMsgNum, pExpandFields, pGetVoteInfo)
-{
-	var msgHdr = null;
-	var msgbase = new MsgBase(this.subBoardCode);
-	if (msgbase.open())
-	{
-		var expandFields = (typeof(pExpandFields) === "boolean" ? pExpandFields : false);
-		var getVoteInfo = (typeof(pGetVoteInfo) === "boolean" ? pGetVoteInfo : false);
-		msgHdr = msgbase.get_msg_header(false, pMsgNum, expandFields, getVoteInfo);
-		msgbase.close();
-	}
-	return msgHdr;
-}
-function DumpMsgHdr(pSubCode, pMsgNum, pExpandFields, pGetVoteInfo)
-{
-	var hdrLines = [];
-	var msgHdr = null;
-	var msgbase = new MsgBase(pSubCode);
-	if (msgbase.open())
-	{
-		var expandFields = (typeof(pExpandFields) === "boolean" ? pExpandFields : false);
-		var getVoteInfo = (typeof(pGetVoteInfo) === "boolean" ? pGetVoteInfo : false);
-		msgHdr = msgbase.get_msg_header(false, pMsgNum, expandFields, getVoteInfo);
-		if (msgHdr != null)
-			hdrLines = msgbase.dump_msg_header(msgHdr);
-		msgbase.close();
-	}
-	return hdrLines;
-}
-
-// For the DigDistMsgReader class: Takes an absolute message number and returns
-// its message index (offset).  On error, returns -1.
-//
-// Parameters:
-//  pMsgNum: The absolute message number
-//
-// Return value: The message's index.  On error, returns -1.
-function DigDistMsgReader_AbsMsgNumToIdx(pMsgNum)
-{
-	//return absMsgNumToIdx(this.subBoardCode, pMsgNum);
-	//if (this.doingNewscan && this.userSettings.newscanOnlyShowNewMsgs)
-
-	// Check this.msgNumToIdxMap and/or this.hdrsForCurrentSubBoard if
-	// they're populated and return the index from that. Otherwise,
-	// check the messagebase directly.
-	var msgIdx = -1;
-	if (this.msgNumToIdxMap.hasOwnProperty(pMsgNum))
-		msgIdx = this.msgNumToIdxMap[pMsgNum];
-	else if (this.hdrsForCurrentSubBoard.length > 0)
-	{
-		for (var i = 0; i < this.hdrsForCurrentSubBoard.length; ++i)
-		{
-			if (this.hdrsForCurrentSubBoard[i].number == pMsgNum)
-			{
-				msgIdx = this.hdrsForCurrentSubBoard[i].number;
-				break;
-			}
-		}
-	}
-	else
-		msgIdx = absMsgNumToIdx(this.subBoardCode, pMsgNum);
-	return msgIdx;
-}
-
-// For the DigDistMsgReader class: Takes a message index and returns
-// its absolute message number.  On error, returns -1.
-//
-// Parameters:
-//  pMsgIdx: The message index
-//
-// Return value: The message's absolute message number.  On error, returns -1.
-function DigDistMsgReader_IdxToAbsMsgNum(pMsgIdx)
-{
-	var msgIdx = -1;
-	// Check this.hdrsForCurrentSubBoard if it's populated and return
-	// the message number from the header at the given index if exists.
-	// therwise, check the messagebase directly.
-	if (this.hdrsForCurrentSubBoard.length > 0)
-	{
-		if (pMsgIdx >= 0 && pMsgIdx < this.hdrsForCurrentSubBoard.length)
-			msgIdx = this.hdrsForCurrentSubBoard[pMsgIdx].number;
-	}
-	else
-	{
-		var msgbase = new MsgBase(this.subBoardCode);
-		if (msgbase.open())
-		{
-			msgIdx = idxToAbsMsgNum(msgbase, pMsgIdx);
-			msgbase.close();
-		}
-	}
-	return msgIdx;
-}
-
-// Prompts the user for a message number and keeps repeating if they enter and
-// invalid message number.  It is assumed that the cursor has already been
-// placed where it needs to be for the prompt text, but a cursor position is
-// one of the parameters to this function so that this function can write an
-// error message if the message number inputted from the user is invalid.
-//
-// Parameters:
-//  pCurPos: The starting position of the cursor (used for positioning the
-//           cursor at the prompt text location to display an error message).
-//           If not needed (i.e., in a traditional user interface or non-ANSI
-//           terminal), this parameter can be null.
-//  pPromptText: The text to use to prompt the user
-//  pClearToEOLAfterPrompt: Whether or not to clear to the end of the line after
-//                          writing the prompt text (boolean)
-//  pErrorPauseTimeMS: The time (in milliseconds) to pause after displaying the
-//                     error that the message number is invalid
-//  pRepeat: Boolean - Whether or not to ask repeatedly until the user enters a
-//           valid message number.  Optional; defaults to false.
-//
-// Return value: The message number entered by the user.  If the user didn't
-//               enter a message number, this will be 0.
-function DigDistMsgReader_PromptForMsgNum(pCurPos, pPromptText, pClearToEOLAfterPrompt,
-                                          pErrorPauseTimeMS, pRepeat)
-{
-	var curPosValid = ((pCurPos != null) && (typeof(pCurPos) == "object") &&
-	                   pCurPos.hasOwnProperty("x") && (typeof(pCurPos.x) == "number") &&
-	                   pCurPos.hasOwnProperty("y") && (typeof(pCurPos.y) == "number"));
-	var useCurPos = (console.term_supports(USER_ANSI) && curPosValid);
-
-	var msgNum = 0;
-	var promptCount = 0;
-	var lastErrorLen = 0; // The length of the last error message
-	var promptTextLen = console.strlen(pPromptText);
-	var continueAskingMsgNum = false;
-	do
-	{
-		++promptCount;
-		msgNum = 0;
-		if (promptTextLen > 0)
-			console.print(pPromptText);
-		if (pClearToEOLAfterPrompt && useCurPos)
-		{
-			// The first time the user is being prompted, clear the line to the
-			// end of the line.  For subsequent times, clear the line from the
-			// prompt text length to the error text length;
-			if (promptCount == 1)
-				console.cleartoeol("\x01n");
-			else
-			{
-				if (lastErrorLen > promptTextLen)
-				{
-					console.attributes = "N";
-					console.gotoxy(pCurPos.x + promptTextLen, pCurPos.y);
-					var clearLen = lastErrorLen - promptTextLen;
-					printf("%-*s", clearLen, "");
-				}
-			}
-			console.gotoxy(pCurPos.x + promptTextLen, pCurPos.y);
-		}
-		msgNum = console.getnum(this.NumMessages()); // this.HighestMessageNum()
-		// If the message number is invalid, then output an error message.
-		if (msgNum != 0)
-		{
-			if (!this.IsValidMessageNum(msgNum) && msgNum != -1) // msgNum would be -1 if the user pressed Q to quit
-			{
-				// Output an error message that the message number is invalid
-				if (useCurPos)
-				{
-					// TODO: Update to optionally not clear all of the line before writing
-					// the error text, or specify how much of the line to clear.  I want
-					// to do that for the enhanced message reader when prompting for a
-					// message number to read - I don't want this to clear the whole line
-					// because that would erase the scrollbar character on the right.
-					writeWithPause(pCurPos.x, pCurPos.y,
-						"\x01n" + replaceAtCodesInStr(format(this.text.invalidMsgNumText, msgNum)) + "\x01n",
-						pErrorPauseTimeMS, "\x01n", true);
-					console.gotoxy(pCurPos);
-				}
-				else
-				{
-					console.print("\x01n\x01w\x01h" + msgNum + " \x01yis an invalid message number.");
-					console.crlf();
-					if (pErrorPauseTimeMS > 0)
-						mswait(pErrorPauseTimeMS);
-				}
-				// Set msgNum back to 0 to signify that the user didn't enter a (valid)
-				// message number.
-				msgNum = 0;
-				lastErrorLen = 24 + msgNum.toString().length;
-				continueAskingMsgNum = pRepeat;
-			}
-			else
-				continueAskingMsgNum = false;
-		}
-		else
-			continueAskingMsgNum = false;
-	} while (continueAskingMsgNum);
-	return msgNum;
-}
-
-// For the DigDistMsgReader class: Looks for complex @-code strings in a text line and
-// parses & replaces them appropriately with the appropriate info from the message header
-// object and/or message base object.  This is more complex than simple text substitution
-// because message subject @-codes could include something like @MSG_SUBJECT-L######@
-// or @MSG_SUBJECT-R######@ or @MSG_SUBJECT-L20@ or @MSG_SUBJECT-R20@.
-//
-// Parameters:
-//  pTextLine: The line of text to search
-//  pMsgHdr: The message header object
-//  pDisplayMsgNum: The message number, if different from the number in the header
-//                  object (i.e., if doing a message search).  This parameter can
-//                  be null, in which case the number in the header object will be
-//                  used.
-//  pDateTimeStr: Optional formatted string containing the date & time.  If this is
-//                not provided, the current date & time will be used.
-//  pBBSLocalTimeZone: Optional boolean - Whether or not pDateTimeStr is in the BBS's
-//                     local time zone.  Defaults to false.
-//  pAllowCLS: Optional boolean - Whether or not to allow the @CLS@ code.
-//             Defaults to false.
-//
-// Return value: A string with the complex @-codes substituted in the line with the
-// appropriate message header information.
-function DigDistMsgReader_ParseMsgAtCodes(pTextLine, pMsgHdr, pDisplayMsgNum, pDateTimeStr,
-                                          pBBSLocalTimeZone, pAllowCLS)
-{
-	var textLine = pTextLine;
-	var dateTimeStr = "";
-	var useBBSLocalTimeZone = false;
-	if (typeof(pDateTimeStr) == "string")
-	{
-		dateTimeStr = pDateTimeStr;
-		if (typeof(pBBSLocalTimeZone) == "boolean")
-			useBBSLocalTimeZone = pBBSLocalTimeZone;
-	}
-	else
-		dateTimeStr = strftime("%Y-%m-%d %H:%M:%S", pMsgHdr.when_written_date);
-	// Message attribute strings
-	var allMsgAttrStr = makeAllMsgAttrStr(pMsgHdr);
-	var mainMsgAttrStr = makeMainMsgAttrStr(pMsgHdr.attr);
-	var auxMsgAttrStr = makeAuxMsgAttrStr(pMsgHdr.auxattr);
-	var netMsgAttrStr = makeNetMsgAttrStr(pMsgHdr.netattr);
-	// An array of @-code strings without the trailing @, to be used for constructing
-	// regular expressions to look for versions with justification & length specifiers.
-	// The order of the strings in this array matters.  For instance, @MSG_NUM_AND_TOTAL
-	// needs to come before @MSG_NUM so that it gets processed properly, since they
-	// both start out with the same text.
-	var atCodeStrBases = ["@MSG_FROM", "@MSG_FROM_AND_FROM_NET", "@MSG_FROM_EXT", "@MSG_TO", "@MSG_TO_NAME", "@MSG_TO_EXT",
-	                      "@MSG_SUBJECT", "@MSG_DATE", "@MSG_ATTR", "@MSG_AUXATTR", "@MSG_NETATTR",
-	                      "@MSG_ALLATTR", "@MSG_NUM_AND_TOTAL", "@MSG_NUM", "@MSG_ID",
-	                      "@MSG_REPLY_ID", "@MSG_TIMEZONE", "@GRP", "@GRPL", "@SUB", "@SUBL",
-						  "@BBS", "@BOARDNAME", "@ALIAS", "@SYSOP", "@CONF", "@DATE", "@DIR", "@DIRL",
-						  "@USER", "@NAME"];
-	// For each @-code, look for a version with justification & length specified and
-	// replace accordingly.
-	for (var atCodeStrBaseIdx in atCodeStrBases)
-	{
-		var atCodeStrBase = atCodeStrBases[atCodeStrBaseIdx];
-		// Synchronet @-codes can specify justification with -L or -R and width using a series
-		// of non-numeric non-space characters (i.e., @MSG_SUBJECT-L#####@ or @MSG_SUBJECT-R######@).
-		// So look for these types of format specifiers for the message subject and if found,
-		// parse and replace appropriately.
-		var multipleCharLenRegex = new RegExp(atCodeStrBase + "-[LR][^0-9 ]+@", "gi");
-		var atCodeMatchArray = textLine.match(multipleCharLenRegex);
-		if ((atCodeMatchArray != null) && (atCodeMatchArray.length > 0))
-		{
-			for (var idx in atCodeMatchArray)
-			{
-				// In this case, the subject length is the length of the whole format specifier.
-				var substLen = atCodeMatchArray[idx].length;
-				textLine = this.ReplaceMsgAtCodeFormatStr(pMsgHdr, pDisplayMsgNum, textLine, substLen,
-				                                     atCodeMatchArray[idx], pDateTimeStr, useBBSLocalTimeZone,
-				                                     mainMsgAttrStr, auxMsgAttrStr, netMsgAttrStr, allMsgAttrStr);
-			}
-		}
-		// Now, look for subject formatters with the length specified (i.e., @MSG_SUBJECT-L20@ or @MSG_SUBJECT-R20@)
-		var numericLenSearchRegex = new RegExp(atCodeStrBase + "-[LR][0-9]+@", "gi");
-		atCodeMatchArray = textLine.match(numericLenSearchRegex);
-		if ((atCodeMatchArray != null) && (atCodeMatchArray.length > 0))
-		{
-			for (var idx in atCodeMatchArray)
-			{
-				// Extract the length specified between the -L or -R and the final @.
-				var dashJustifyIndex = findDashJustifyIndex(atCodeMatchArray[idx]);
-				var substLen = atCodeMatchArray[idx].substring(dashJustifyIndex+2, atCodeMatchArray[idx].length-1);
-				textLine = this.ReplaceMsgAtCodeFormatStr(pMsgHdr, pDisplayMsgNum, textLine, substLen, atCodeMatchArray[idx],
-				                                     pDateTimeStr, useBBSLocalTimeZone, mainMsgAttrStr, mainMsgAttrStr,
-				                                     auxMsgAttrStr, netMsgAttrStr, allMsgAttrStr, dashJustifyIndex);
-			}
-		}
-	}
-
-	// In case there weren't any complex @-codes, do replacements for the basic
-	// @-codes.  Set the group & sub-board information as Personal Mail or the
-	// sub-board currently being read.
-	var grpIdx = -1;
-	var groupName = "";
-	var groupDesc = "";
-	var subName = "";
-	var subDesc = "";
-	var msgConf = "";
-	var fileDir = "";
-	var fileDirLong = "";
-	if (this.readingPersonalEmail)
-	{
-		var subName = "Personal mail";
-		var subDesc = "Personal mail";
-	}
-	else
-	{
-		grpIdx = msg_area.sub[this.subBoardCode].grp_index;
-		groupName = msg_area.sub[this.subBoardCode].grp_name;
-		groupDesc = msg_area.grp_list[grpIdx].description;
-		subName = msg_area.sub[this.subBoardCode].name;
-		subDesc = msg_area.sub[this.subBoardCode].description;
-		msgConf = msg_area.grp_list[msg_area.sub[this.subBoardCode].grp_index].name + " "
-		        + msg_area.sub[this.subBoardCode].name;
-	}
-	if ((typeof(bbs.curlib) == "number") && (typeof(bbs.curdir) == "number"))
-	{
-		if ((typeof(file_area.lib_list[bbs.curlib]) == "object") && (typeof(file_area.lib_list[bbs.curlib].dir_list[bbs.curdir]) == "object"))
-		{
-			fileDir = file_area.lib_list[bbs.curlib].dir_list[bbs.curdir].name;
-			fileDirLong = file_area.lib_list[bbs.curlib].dir_list[bbs.curdir].description;
-		}
-	}
-	var messageNum = (typeof(pDisplayMsgNum) == "number" ? pDisplayMsgNum : pMsgHdr["offset"]+1);
-	var msgVoteInfo = getMsgUpDownvotesAndScore(pMsgHdr);
-	// This list has some custom @-codes:
-	var newTxtLine = textLine.replace(/@MSG_SUBJECT@/gi, pMsgHdr["subject"])
-	                         .replace(/@MSG_TO@/gi, pMsgHdr["to"])
-	                         .replace(/@MSG_TO_NAME@/gi, pMsgHdr["to"])
-	                         .replace(/@MSG_TO_EXT@/gi, (typeof(pMsgHdr["to_ext"]) == "string" ? pMsgHdr["to_ext"] : ""))
-	                         .replace(/@MSG_DATE@/gi, pDateTimeStr)
-	                         .replace(/@MSG_ATTR@/gi, mainMsgAttrStr)
-	                         .replace(/@MSG_AUXATTR@/gi, auxMsgAttrStr)
-	                         .replace(/@MSG_NETATTR@/gi, netMsgAttrStr)
-	                         .replace(/@MSG_ALLATTR@/gi, allMsgAttrStr)
-	                         .replace(/@MSG_NUM_AND_TOTAL@/gi, messageNum.toString() + "/" + this.NumMessages())
-	                         .replace(/@MSG_NUM@/gi, messageNum.toString())
-	                         .replace(/@MSG_ID@/gi, (typeof(pMsgHdr["id"]) == "string" ? pMsgHdr["id"] : ""))
-	                         .replace(/@MSG_REPLY_ID@/gi, (typeof(pMsgHdr["reply_id"]) == "string" ? pMsgHdr["reply_id"] : ""))
-	                         .replace(/@MSG_FROM_NET@/gi, (typeof(pMsgHdr["from_net_addr"]) == "string" ? pMsgHdr["from_net_addr"] : ""))
-	                         .replace(/@MSG_TO_NET@/gi, (typeof(pMsgHdr["to_net_addr"]) == "string" ? pMsgHdr["to_net_addr"] : ""))
-	                         .replace(/@MSG_TIMEZONE@/gi, (useBBSLocalTimeZone ? system.zonestr(system.timezone) : system.zonestr(pMsgHdr["when_written_zone"])))
-	                         .replace(/@GRP@/gi, groupName)
-	                         .replace(/@GRPL@/gi, groupDesc)
-	                         .replace(/@SUB@/gi, subName)
-	                         .replace(/@SUBL@/gi, subDesc)
-							 .replace(/@CONF@/gi, msgConf)
-							 .replace(/@SYSOP@/gi, system.operator)
-							 .replace(/@DATE@/gi, system.datestr())
-							 .replace(/@LOCATION@/gi, system.location)
-							 .replace(/@DIR@/gi, fileDir)
-							 .replace(/@DIRL@/gi, fileDirLong)
-							 .replace(/@ALIAS@/gi, user.alias)
-							 .replace(/@NAME@/gi, (user.name.length > 0 ? user.name : user.alias))
-							 .replace(/@USER@/gi, user.alias)
-							 .replace(/@MSG_UPVOTES@/gi, msgVoteInfo.upvotes)
-							 .replace(/@MSG_DOWNVOTES@/gi,msgVoteInfo.downvotes)
-							 .replace(/@MSG_SCORE@/gi, msgVoteInfo.voteScore);
-	// If the user is not the sysop and the message was posted anonymously,
-	// then replace the from name @-codes with "Anonymous".  Otherwise,
-	// replace the from name @-codes with the actual from name.
-	if (!user.is_sysop && ((pMsgHdr.attr & MSG_ANONYMOUS) == MSG_ANONYMOUS))
-	{
-		newTxtLine = newTxtLine.replace(/@MSG_FROM@/gi, "Anonymous")
-		                       .replace(/@MSG_FROM_AND_FROM_NET@/gi, "Anonymous")
-		                       .replace(/@MSG_FROM_EXT@/gi, "Anonymous");
-	}
-	else
-	{
-		newTxtLine = newTxtLine.replace(/@MSG_FROM@/gi, pMsgHdr["from"])
-		                       .replace(/@MSG_FROM_AND_FROM_NET@/gi, pMsgHdr["from"] + (typeof(pMsgHdr["from_net_addr"]) == "string" ? " (" + pMsgHdr["from_net_addr"] + ")" : ""))
-		                       .replace(/@MSG_FROM_EXT@/gi, (typeof(pMsgHdr["from_ext"]) == "string" ? pMsgHdr["from_ext"] : ""));
-	}
-	if (!pAllowCLS)
-		newTxtLine = newTxtLine.replace(/@CLS@/gi, "");
-	newTxtLine = replaceAtCodesInStr(newTxtLine);
-	return newTxtLine;
-}
-// For the DigDistMsgReader class: Helper for ParseMsgAtCodes(): Replaces a
-// given @-code format string in a text line with the appropriate message header
-// info or BBS system info.
-//
-// Parameters:
-//  pMsgHdr: The object containing the message header information
-//  pDisplayMsgNum: The message number, if different from the number in the header
-//                  object (i.e., if doing a message search).  This parameter can
-//                  be null, in which case the number in the header object will be
-//                  used.
-//  pTextLine: The text line in which to perform the replacement
-//  pSpecifiedLen: The length extracted from the @-code format string
-//  pAtCodeStr: The @-code format string, which will be replaced with the actual message info
-//  pDateTimeStr: Formatted string containing the date & time
-//  pUseBBSLocalTimeZone: Boolean - Whether or not pDateTimeStr is in the BBS's local time zone.
-//  pMsgMainAttrStr: A string describing the main message attributes ('attr' property of header)
-//  pMsgAuxAttrStr: A string describing the auxiliary message attributes ('auxattr' property of header)
-//  pMsgNetAttrStr: A string describing the network message attributes ('netattr' property of header)
-//  pMsgAllAttrStr: A string describing all message attributes
-//  pDashJustifyIdx: Optional - The index of the -L or -R in the @-code string
-function DigDistMsgReader_ReplaceMsgAtCodeFormatStr(pMsgHdr, pDisplayMsgNum, pTextLine, pSpecifiedLen,
-                                  pAtCodeStr, pDateTimeStr, pUseBBSLocalTimeZone, pMsgMainAttrStr, pMsgAuxAttrStr,
-								  pMsgNetAttrStr, pMsgAllAttrStr, pDashJustifyIdx)
-{
-	if (typeof(pDashJustifyIdx) != "number")
-		pDashJustifyIdx = findDashJustifyIndex(pAtCodeStr);
-	// Specify the format string with left or right justification based on the justification
-	// character (either L or R).
-	var formatStr = ((/L/i).test(pAtCodeStr.charAt(pDashJustifyIdx+1)) ? "%-" : "%") + pSpecifiedLen + "s";
-	// Specify the replacement text depending on the @-code string
-	var replacementTxt = "";
-	if (pAtCodeStr.indexOf("@MSG_FROM_AND_FROM_NET") > -1)
-	{
-		// If the user is not the sysop and the message was posted anonymously,
-		// then replace the from name @-codes with "Anonymous".  Otherwise,
-		// replace the from name @-codes with the actual from name.
-		if (!user.is_sysop && ((pMsgHdr.attr & MSG_ANONYMOUS) == MSG_ANONYMOUS))
-			replacementTxt = "Anonymous".substr(0, pSpecifiedLen);
-		else
-		{
-			var fromWithNet = pMsgHdr["from"] + (typeof(pMsgHdr["from_net_addr"]) == "string" ? " (" + pMsgHdr["from_net_addr"] + ")" : "");
-			replacementTxt = fromWithNet.substr(0, pSpecifiedLen);
-		}
-	}
-	else if (pAtCodeStr.indexOf("@MSG_FROM") > -1)
-	{
-		// If the user is not the sysop and the message was posted anonymously,
-		// then replace the from name @-codes with "Anonymous".  Otherwise,
-		// replace the from name @-codes with the actual from name.
-		if (!user.is_sysop && ((pMsgHdr.attr & MSG_ANONYMOUS) == MSG_ANONYMOUS))
-			replacementTxt = "Anonymous".substr(0, pSpecifiedLen);
-		else
-			replacementTxt = pMsgHdr["from"].substr(0, pSpecifiedLen);
-	}
-	else if (pAtCodeStr.indexOf("@MSG_FROM_EXT") > -1)
-	{
-		// If the user is not the sysop and the message was posted anonymously,
-		// then replace the from name @-codes with "Anonymous".  Otherwise,
-		// replace the from name @-codes with the actual from name.
-		if (!user.is_sysop && ((pMsgHdr.attr & MSG_ANONYMOUS) == MSG_ANONYMOUS))
-			replacementTxt = "Anonymous".substr(0, pSpecifiedLen);
-		else
-			replacementTxt = (typeof pMsgHdr["from_ext"] === "undefined" ? "" : pMsgHdr["from_ext"].substr(0, pSpecifiedLen));
-	}
-	else if ((pAtCodeStr.indexOf("@MSG_TO") > -1) || (pAtCodeStr.indexOf("@MSG_TO_NAME") > -1))
-		replacementTxt = pMsgHdr["to"].substr(0, pSpecifiedLen);
-	else if (pAtCodeStr.indexOf("@MSG_TO_EXT") > -1)
-		replacementTxt = (typeof pMsgHdr["to_ext"] === "undefined" ? "" : pMsgHdr["to_ext"].substr(0, pSpecifiedLen));
-	else if (pAtCodeStr.indexOf("@MSG_SUBJECT") > -1)
-		replacementTxt = pMsgHdr["subject"].substr(0, pSpecifiedLen);
-	else if (pAtCodeStr.indexOf("@MSG_DATE") > -1)
-		replacementTxt = pDateTimeStr.substr(0, pSpecifiedLen);
-	else if (pAtCodeStr.indexOf("@MSG_ATTR") > -1)
-		replacementTxt = pMsgMainAttrStr.substr(0, pSpecifiedLen);
-	else if (pAtCodeStr.indexOf("@MSG_AUXATTR") > -1)
-		replacementTxt = pMsgAuxAttrStr.substr(0, pSpecifiedLen);
-	else if (pAtCodeStr.indexOf("@MSG_NETATTR") > -1)
-		replacementTxt = pMsgNetAttrStr.substr(0, pSpecifiedLen);
-	else if (pAtCodeStr.indexOf("@MSG_ALLATTR") > -1)
-		replacementTxt = pMsgAllAttrStr.substr(0, pSpecifiedLen);
-	else if (pAtCodeStr.indexOf("@MSG_NUM_AND_TOTAL") > -1)
-	{
-		var messageNum = (typeof(pDisplayMsgNum) == "number" ? pDisplayMsgNum : pMsgHdr["offset"]+1);
-		replacementTxt = (messageNum.toString() + "/" + this.NumMessages()).substr(0, pSpecifiedLen); // "number" is also absolute number
-	}
-	else if (pAtCodeStr.indexOf("@MSG_NUM") > -1)
-	{
-		var messageNum = (typeof(pDisplayMsgNum) == "number" ? pDisplayMsgNum : pMsgHdr["offset"]+1);
-		replacementTxt = messageNum.toString().substr(0, pSpecifiedLen); // "number" is also absolute number
-	}
-	else if (pAtCodeStr.indexOf("@MSG_ID") > -1)
-		replacementTxt = (typeof pMsgHdr["id"] === "undefined" ? "" : pMsgHdr["id"].substr(0, pSpecifiedLen));
-	else if (pAtCodeStr.indexOf("@MSG_REPLY_ID") > -1)
-		replacementTxt = (typeof pMsgHdr["reply_id"] === "undefined" ? "" : pMsgHdr["reply_id"].substr(0, pSpecifiedLen));
-	else if (pAtCodeStr.indexOf("@MSG_TIMEZONE") > -1)
-	{
-		if (pUseBBSLocalTimeZone)
-			replacementTxt = system.zonestr(system.timezone).substr(0, pSpecifiedLen);
-		else
-			replacementTxt = system.zonestr(pMsgHdr["when_written_zone"]).substr(0, pSpecifiedLen);
-	}
-	else if (pAtCodeStr.indexOf("@GRP") > -1)
-	{
-		if (this.readingPersonalEmail)
-			replacementTxt = "Personal mail".substr(0, pSpecifiedLen);
-		else
-			replacementTxt = msg_area.sub[this.subBoardCode].grp_name.substr(0, pSpecifiedLen);
-			
-	}
-	else if (pAtCodeStr.indexOf("@GRPL") > -1)
-	{
-		if (this.readingPersonalEmail)
-			replacementTxt = "Personal mail".substr(0, pSpecifiedLen);
-		else
-		{
-			var grpIdx = msg_area.sub[this.subBoardCode].grp_index;
-			replacementTxt = msg_area.grp_list[grpIdx].description.substr(0, pSpecifiedLen);
-		}
-	}
-	else if (pAtCodeStr.indexOf("@SUB") > -1)
-	{
-		if (this.readingPersonalEmail)
-			replacementTxt = "Personal mail".substr(0, pSpecifiedLen);
-		else
-
-			replacementTxt = msg_area.sub[this.subBoardCode].name.substr(0, pSpecifiedLen);
-	}
-	else if (pAtCodeStr.indexOf("@SUBL") > -1)
-	{
-		if (this.readingPersonalEmail)
-			replacementTxt = "Personal mail".substr(0, pSpecifiedLen);
-		else
-			replacementTxt = msg_area.sub[this.subBoardCode].description.substr(0, pSpecifiedLen);
-	}
-	else if ((pAtCodeStr.indexOf("@BBS") > -1) || (pAtCodeStr.indexOf("@BOARDNAME") > -1))
-		replacementTxt = system.name.substr(0, pSpecifiedLen);
-	else if (pAtCodeStr.indexOf("@SYSOP") > -1)
-		replacementTxt = system.operator.substr(0, pSpecifiedLen);
-	else if (pAtCodeStr.indexOf("@CONF") > -1)
-	{
-		var msgConfDesc = msg_area.grp_list[msg_area.sub[this.subBoardCode].grp_index].name + " "
-		                + msg_area.sub[this.subBoardCode].name;
-		replacementTxt = msgConfDesc.substr(0, pSpecifiedLen);
-	}
-	else if (pAtCodeStr.indexOf("@DATE") > -1)
-		replacementTxt = system.datestr().substr(0, pSpecifiedLen);
-	else if (pAtCodeStr.indexOf("@DIR") > -1)
-	{
-		if ((typeof(bbs.curlib) == "number") && (typeof(bbs.curdir) == "number"))
-			replacementTxt = file_area.lib_list[bbs.curlib].dir_list[bbs.curdir].name.substr(0, pSpecifiedLen);
-		else
-			replacementTxt = "";
-	}
-	else if (pAtCodeStr.indexOf("@DIRL") > -1)
-	{
-		if ((typeof(bbs.curlib) == "number") && (typeof(bbs.curdir) == "number"))
-			replacementTxt = file_area.lib_list[bbs.curlib].dir_list[bbs.curdir].description.substr(0, pSpecifiedLen);
-		else
-			replacementTxt = "";
-	}
-	else if ((pAtCodeStr.indexOf("@ALIAS") > -1) || (pAtCodeStr.indexOf("@USER") > -1))
-		replacementTxt = user.alias.substr(0, pSpecifiedLen);
-	else if (pAtCodeStr.indexOf("@NAME") > -1) // User name or alias
-	{
-		var userNameOrAlias = (user.name.length > 0 ? user.name : user.alias);
-		replacementTxt = userNameOrAlias.substr(0, pSpecifiedLen);
-	}
-
-	// Do the text replacement (escape special characters in the @-code string so we can do a literal search)
-	var searchRegex = new RegExp(pAtCodeStr.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"), "gi");
-	return pTextLine.replace(searchRegex, format(formatStr, replacementTxt));
-}
-// Helper for DigDistMsgReader_ParseMsgAtCodes() and
-// DigDistMsgReader_ReplaceMsgAtCodeFormatStr(): Returns the index of the -L or -R in
-// one of the @-code strings.
-//
-// Parameters:
-//  pAtCodeStr: The @-code string to search
-//
-// Return value: The index of the -L or -R, or -1 if not found
-function findDashJustifyIndex(pAtCodeStr)
-{
-	var strIndex = pAtCodeStr.indexOf("-");
-	if (strIndex > -1)
-	{
-		// If this part of the string is not -L or -R, then set strIndex to -1
-		// to signify that it was not found.
-		var checkStr = pAtCodeStr.substr(strIndex, 2).toUpperCase();
-		if ((checkStr != "-L") && (checkStr != "-R"))
-			strIndex = -1;
-	}
-	return strIndex;
-}
-
-// Finds the offset (index) of the next message prior to or after a given offset
-// that is readable (not marked as deleted or the user can read deleted messages,
-// etc.).  If none is found, the return value will be -1.
-//
-// Parameters:
-//  pOffset: The message offset to search prior/after
-//  pForward: Boolean - Whether or not to search forward (true) or backward (false).
-//            If this is not specified, the default will be true (forward).
-//  pUseCachedHdrs: Optional boolean - Whether or not to use the cached message headers
-//                  (which are filtered with deleted messages removed, etc). Defaults to true.
-//
-// Return value: The index of the next message prior/later that is not marked
-//               as deleted, or -1 if none is found.
-function DigDistMsgReader_FindNextReadableMsgIdx(pOffset, pForward, pUseCachedHdrs)
-{
-	// Sanity checking for the parameters & other things
-	if (typeof(pOffset) != "number")
-		return -1;
-	var searchForward = (typeof(pForward) == "boolean" ? pForward : true);
-	var msgbase = new MsgBase(this.subBoardCode);
-	if (!msgbase.open())
-		return -1;
-
-	var useCachedHdrs = (typeof(pUseCachedHdrs) === "boolean" ? pUseCachedHdrs : true);
-	var newMsgIdx = -1;
-	if (searchForward)
-	{
-		// Search forward for a message that isn't marked for deletion (and check if the user
-		// can read deleted messages)
-		if (useCachedHdrs)
-		{
-			var numOfMessages = this.NumMessages(msgbase);
-			if (pOffset < numOfMessages - 1)
-			{
-				var hdrIsBogus;
-				for (var messageIdx = pOffset+1; (messageIdx < numOfMessages) && (newMsgIdx == -1); ++messageIdx)
-				{
-					var nextMsgHdr = this.GetMsgHdrByIdx(messageIdx, false, msgbase);
-					if (nextMsgHdr != null && !isVoteHdr(nextMsgHdr))
-					{
-						var canRead = true;
-						if ((nextMsgHdr.attr & MSG_DELETE) == MSG_DELETE)
-							canRead = canViewDeletedMsgs();
-						if (canRead)
-							newMsgIdx = messageIdx;
-					}
-				}
-			}
-		}
-		else
-		{
-			// TODO
-		}
-	}
-	else
-	{
-		// Search backward for a message that isn't marked for deletion (and check if the user
-		// can read deleted messages)
-		if (pOffset > 0)
-		{
-			if (useCachedHdrs)
-			{
-				var hdrIsBogus;
-				for (var messageIdx = pOffset-1; (messageIdx >= 0) && (newMsgIdx == -1); --messageIdx)
-				{
-					var prevMsgHdr = this.GetMsgHdrByIdx(messageIdx, false, msgbase);
-					if (prevMsgHdr != null && !isVoteHdr(prevMsgHdr))
-					{
-						var canRead = true;
-						if ((prevMsgHdr.attr & MSG_DELETE) == MSG_DELETE)
-							canRead = canViewDeletedMsgs();
-						if (canRead)
-							newMsgIdx = messageIdx;
-					}
-				}
-			}
-			else
-			{
-				// TODO
-			}
-		}
-	}
-	msgbase.close();
-	return newMsgIdx;
-}
-
-// For the DigDistMsgReader class: Sets up the message base object for a new
-// sub-board.  Leaves the msgbase object open.
-//
-// Parameters:
-//  pNewSubBoardCode: The internal code of the new sub-board.
-//                    If this is not a string, then this function
-//                    will use bbs.cursub_code instead.
-//
-// Return value: An object with the following properties:
-//               succeeded: Boolean - True if succeeded or false if not
-//               lastReadMsgIdx: The index of the last-read message in the sub-board
-//               lastReadMsgNum: The message number of the last-read message in the sub-board
-// 
-function DigDistMsgReader_ChangeSubBoard(pNewSubBoardCode)
-{
-	var retObj = {
-		succeeded: false,
-		lastReadMsgIdx: 0,
-		lastReadMsgNum: 0
-	};
-
-	var newSubBoardCode = bbs.cursub_code;
-	if (typeof(pNewSubBoardCode) == "string")
-		newSubBoardCode = pNewSubBoardCode;
-	if (typeof(msg_area.sub[newSubBoardCode]) != "object")
-	{
-		console.clear("\x01n");
-		console.print("\x01n\x01h\x01y* \x01wSomething has gone wrong.  An invalid message sub-board code was");
-		console.crlf();
-		console.print("specified: " + newSubBoardCode);
-		console.crlf();
-		console.pause();
-		return retObj;
-	}
-
-	// If the new sub-board code is different from the currently-set
-	// sub-board code, then go ahead and change it.
-	if (newSubBoardCode != this.subBoardCode)
-	{
-		this.setSubBoardCode(newSubBoardCode);
-		this.PopulateHdrsForCurrentSubBoard();
-	}
-
-	// If there are no messages to display in the current sub-board, then just
-	// return.  Note: A message regarding there being no messages would have
-	// already been shown, so we don't need to show an 'Empty sub-board' message
-	// here.
-	if (this.NumMessages() == 0)
-		return retObj;
-
-	// Get the index of the user's last-read message in this sub-board.
-	var lastReadMsgInfo = this.GetLastReadMsgIdxAndNum();
-	retObj.lastReadMsgIdx = lastReadMsgInfo.lastReadMsgIdx;
-	retObj.lastReadMsgNum = lastReadMsgInfo.lastReadMsgNum;
-	if (retObj.lastReadMsgIdx == -1)
-		retObj.lastReadMsgIdx = 0;
-
-	retObj.succeeded = true;
-
-	return retObj;
-}
-
-// For the enhanced reader functionality of the DigDistMsgReader class: Sets up
-// the message base object for a new sub-board and refreshes the reader hotkey
-// help line on the bottom of the screen.  Leaves the msgbase object open.
-//
-// Parameters:
-//  pNewSubBoardCode: The internal code of the new sub-board.
-//                    If this is not a string, then this function
-//                    will use bbs.cursub_code instead.
-//
-// Return value: An object with the following properties:
-//               succeeded: Boolean - True if succeeded or false if not
-//               lastReadMsgIdx: The index of the last message read in the sub-board.
-//                               Will be 0 on error.
-function DigDistMsgReader_EnhancedReaderChangeSubBoard(pNewSubBoardCode)
-{
-	var retObj = this.ChangeSubBoard(pNewSubBoardCode);
-	if (retObj.succeeded && (this.NumMessages() > 0))
-	{
-		// Clear the screen and refresh the help line at the bottom of the screen
-		console.clear("\x01n");
-		this.DisplayEnhancedMsgReadHelpLine(console.screen_rows);
-	}
-	return retObj;
-}
-
-// For the DigDistMsgReader class: Allows the user to reply to a message
-//
-// Parameters:
-//  pMsgHdr: The header of the message to reply to.  This needs to be a header with
-//           fields expanded.
-//  pMsgText: The text (body) of the message
-//  pPrivate: Optional - Boolean to specify whether not this should be a private
-//            reply using the user's QWK or FIDO, etc. address.  Defaults to
-//            false if not specified.
-//  pMsgIdx: The message index (if there are search results, this might be
-//           different than the message offset in the messagebase).  This
-//           is intended for use in deleting a private email after reading it.
-//
-// Return value: An object containing the following properties:
-//               postSucceeded: Boolean - Whether or not the message post succeeded
-//               msgWasDeleted: Boolean - Whether or not the message was deleted after
-//                              the user replied to it
-function DigDistMsgReader_ReplyToMsg(pMsgHdr, pMsgText, pPrivate, pMsgIdx)
-{
-	var retObj = {
-		postSucceeded: false,
-		msgWasDeleted: false
-	};
-
-	if (pMsgHdr == null || typeof(pMsgHdr) !== "object")
-		return retObj;
-
-	// If the "no-reply" attribute is enabled for the message, then don't
-	// let the user rpely.
-	if (Boolean(pMsgHdr.attr & MSG_NOREPLY))
-	{
-		console.crlf();
-		console.print("\x01n\x01y\x01hReplies are not allowed for this message.");
-		console.crlf();
-		console.pause();
-		return retObj;
-	}
-
-	// Make sure the user is allowed to post a message before allowing them to
-	// reply.  If posting on a message sub-board, then we can check the can_read
-	// property of the sub-board; otherwise, the user should be allowed to post
-	// a message.
-	var replyPrivately = (typeof(pPrivate) == "boolean" ? pPrivate : false);
-	var canPost = true;
-	if (!replyPrivately && (this.subBoardCode != "mail"))
-		canPost = msg_area.sub[this.subBoardCode].can_post;
-	if (!canPost)
-	{
-		console.crlf();
-		console.print("\x01n\x01y\x01hYou are not allowed to post in this message area.");
-		console.crlf();
-		console.pause();
-		return retObj;
-	}
-
-	retObj.postSucceeded = true;
-
-	// No special behavior in the reply
-	var replyMode = WM_NONE;
-
-	// If quoting is allowed in the sub-board, then write QUOTES.TXT in
-	// the node directory to allow the user to quote the original message.
-	// TODO: Handle things when reading another user's email (for the sysop) - "mail" as a sub-board code might
-	// not work
-	var msgbase = new MsgBase(this.subBoardCode);
-	if (msgbase.open())
-	{
-		var quoteFile = null;
-		if (this.CanQuote())
-		{
-			// Get the user's setting for whether or not to wrap quote lines (and how long) from
-			// their external editor settings
-			var editorQuoteCfg = getExternalEditorQuoteWrapCfgFromSCFG(user.editor);
-			// Write the message text to the quotes file
-			quoteFile = new File(system.node_dir + "QUOTES.TXT");
-			if (quoteFile.open("w"))
-			{
-				var msgNum = (typeof(pMsgIdx) === "number" ? pMsgIdx+1 : null);
-				var msgText = "";
-				if (typeof(pMsgText) == "string")
-					msgText = pMsgText;
-				else
-					msgText = msgbase.get_msg_body(false, pMsgHdr.number, false, false, true, true);
-				if (editorQuoteCfg.quoteWrapEnabled && editorQuoteCfg.quoteWrapCols > 0)
-					msgText = word_wrap(msgText, editorQuoteCfg.quoteWrapCols, msgText.length, false);
-				quoteFile.write(msgText);
-
-				quoteFile.close();
-				// Let the user quote in the reply
-				replyMode |= WM_QUOTE;
-			}
-		}
-	}
-
-	// Strip any control characters that the subject line might have
-	pMsgHdr.subject = strip_ctrl(pMsgHdr.subject);
-
-	// If the user is listing personal e-mail, then we need to call
-	// bbs.email() to leave a reply; otherwise, use bbs.post_msg().
-	if (this.readingPersonalEmail)
-	{
-		var privReplRetObj = this.DoPrivateReply(pMsgHdr, pMsgIdx, replyMode);
-		retObj.postSucceeded = privReplRetObj.sendSucceeded;
-		retObj.msgWasDeleted = privReplRetObj.msgWasDeleted;
-		// If the user successfully saved the message and the message wasn't deleted,
-		// then apply the 'replied' attribute to the message Header
-		if (privReplRetObj.sendSucceeded && !privReplRetObj.msgWasDeleted)
-		{
-			var saveRetObj = applyAttrsInMsgHdrInMessagbase(this.subBoardCode, pMsgHdr.number, MSG_REPLIED);
-			if (saveRetObj.saveSucceeded)
-				this.RefreshHdrInSavedArrays(pMsgIdx, MSG_REPLIED, true);
-		}
-	}
-	else
-	{
-		// The user is posting in a public message sub-board.
-		// Open a file in the node directory and write some information
-		// about the current sub-board and message being read:
-		// - The highest message number in the sub-board (last message)
-		// - The total number of messages in the sub-board
-		// - The number of the message being read
-		// - The current sub-board code
-		// This is for message editors that need to access the message
-		// base (i.e., SlyEdit).  Normally (in Synchronet's message read
-		// propmt), this information is stored in bbs.smb_last_msg,
-		// bbs.smb_total_msgs, and bbs.smb_curmsg, but this message lister
-		// can't change those values.  Thus, we need to write them to a file.
-		var msgBaseInfoFile = new File(system.node_dir + "DDML_SyncSMBInfo.txt");
-		if (msgBaseInfoFile.open("w"))
-		{
-			msgBaseInfoFile.writeln(msgbase.last_msg.toString()); // Highest message #
-			msgBaseInfoFile.writeln(this.NumMessages(msgbase).toString()); // Total # messages
-			// Message number (Note: For SlyEdit, requires SlyEdit 1.27 or newer).
-			msgBaseInfoFile.writeln(pMsgHdr.number.toString()); // # of the message being read (New: 2013-05-14)
-			msgBaseInfoFile.writeln(this.subBoardCode); // Sub-board code
-			msgBaseInfoFile.close();
-		}
-
-		// Store the current total number of messages so that we can search new
-		// messages if needed after the message is posted
-		var numMessagesBefore = msgbase.total_msgs;
-
-		// Let the user post the message.  Then, delete the message base info
-		// file.  To be safe, and to ensure the messagebase object gets refreshed
-		// with the latest information, close the messagebase object before
-		// posting the message and re-open it afterward.  On Linux, the messagebase
-		// object doesn't seem to get refreshed with the number of messages in the
-		// sub-board, etc., but on Windows that doesn't seem to be an issue.
-		// If we are to send a private message, then let the user send the reply
-		// as a private email.  Otherwise, let the user post the reply as a public
-		// message.
-		// 2016-08-26: Updated to not close the messagebase because a private
-		// reply on a networked sub-board needs to be able to get a message
-		// header with fields expanded.
-		msgbase.close();
-		if (replyPrivately)
-		{
-			var privReplRetObj = this.DoPrivateReply(pMsgHdr, pMsgIdx, replyMode);
-			retObj.postSucceeded = privReplRetObj.sendSucceeded;
-			retObj.msgWasDeleted = privReplRetObj.msgWasDeleted;
-		}
-		else
-		{
-			// Not a private message - Post as a public message
-			// TODO: Saw this error message once on the next line:
-			// Error: Error -300 adding RFC822MSGID field to message header
-			retObj.postSucceeded = bbs.post_msg(this.subBoardCode, replyMode, pMsgHdr);
-			console.pause();
-		}
-		msgBaseInfoFile.remove();
-		var msgbaseReOpened = msgbase.open();
-
-		// If the user replied to the message and a message search was done that
-		// would populate the search results, then search the last messages to
-		// include the user's reply in the message matches or other new messages
-		// that may have been posted that match the user's search.
-		if (retObj.postSucceeded && msgbaseReOpened && (msgbase.total_msgs > numMessagesBefore))
-		{
-			// If doing a newscan and the user setting for only showing new messages during a newscan
-			// is enabled, then get the last message header (which should be the message the user
-			// just posted) and add it to the cached array of message headers
-			if (this.doingNewscan && this.userSettings.newscanOnlyShowNewMsgs)
-			{
-				var lastMsgHdr = msgbase.get_msg_header(false, msgbase.last_msg);
-				if (msgIsFromUser(lastMsgHdr))
-				{
-					this.hdrsForCurrentSubBoard.push(lastMsgHdr);
-					this.msgNumToIdxMap[lastMsgHdr.number] = this.hdrsForCurrentSubBoard.length - 1;
-				}
-			}
-			// If doing a search and the search headers has the current sub-board, then add the posted
-			// message to the search headers
-			else if (this.SearchTypePopulatesSearchResults() && this.msgSearchHdrs.hasOwnProperty(this.subBoardCode))
-			{
-				if (!this.msgSearchHdrs.hasOwnProperty(this.subBoardCode))
-					this.msgSearchHdrs[this.subBoardCode] = searchMsgbase(this.subBoardCode, this.searchType, this.searchString, this.readingPersonalEmailFromUser);
-				else
-				{
-					var msgHeaders = searchMsgbase(this.subBoardCode, this.searchType, this.searchString, this.readingPersonalEmailFromUser, numMessagesBefore, msgbase.total_msgs);
-					var msgNum = 0;
-					for (var i = 0; i < msgHeaders.indexed.length; ++i)
-					{
-						this.msgSearchHdrs[this.subBoardCode].indexed.push(msgHeaders.indexed[i]);
-						msgNum = msgHeaders.indexed[i].offset + 1;
-					}
-				}
-			}
-			// If we have cached message headers, add the user's just-posted message
-			else if (this.hdrsForCurrentSubBoard.length > 0)
-			{
-				//this.FilterMsgHdrsIntoHdrsForCurrentSubBoard(msgbase.get_all_msg_headers(true), true);
-				var lastMsgHdr = msgbase.get_msg_header(false, msgbase.last_msg);
-				if (msgIsFromUser(lastMsgHdr))
-				{
-					this.hdrsForCurrentSubBoard.push(lastMsgHdr);
-					this.msgNumToIdxMap[lastMsgHdr.number] = this.hdrsForCurrentSubBoard.length - 1;
-				}
-			}
-		}
-	}
-
-	// Delete the quote file
-	if (quoteFile != null)
-		quoteFile.remove();
-
-	msgbase.close();
-
-	return retObj;
-}
-
-// For the DigDistMsgReader class: This function performs a private message reply.  Takes a message header
-// and returns a boolean for whether or not it succeeded in sending the reply.
-//
-// Parameters:
-//  pMsgHdr: A message header object.  This needs to be a header with expanded fields.
-//  pMsgIdx: The message index (if there are search results, this might be
-//           different than the message offset in the messagebase).  This
-//           is intended for use in deleting a private email after reading it.
-//  pReplyMode: Optional - A bitfield containing reply mode bits to use
-//              in addition to the default network/email reply mode
-//
-// Return value: An object containing the following properties:
-//               sendSucceeded: Boolean - Whether or not the message post succeeded
-//               msgWasDeleted: Boolean - Whether or not the message was deleted after
-//                              the user replied to it
-function DigDistMsgReader_DoPrivateReply(pMsgHdr, pMsgIdx, pReplyMode)
-{
-	var retObj = {
-		sendSucceeded: true,
-		msgWasDeleted: false
-	};
-	
-	if (pMsgHdr == null)
-	{
-		retObj.sendSucceeded = false;
-		return retObj;
-	}
-
-	// Set up the initial reply mode bits
-	var replyMode = WM_NONE;
-	if (typeof(pReplyMode) == "number")
-		replyMode |= pReplyMode;
-
-	// If the message is a networked message, then try to address the message
-	// to the network address.
-	var couldNotDetermineNetAddr = true;
-	var wasNetMailOrigin = false;
-	if ((typeof(pMsgHdr.from_net_type) != "undefined") && (pMsgHdr.from_net_type != NET_NONE))
-	{
-		wasNetMailOrigin = true;
-		if ((typeof(pMsgHdr.from_net_addr) == "string") && (pMsgHdr.from_net_addr.length > 0))
-		{
-			couldNotDetermineNetAddr = false;
-			// Build the email address to reply to.  If the original message is
-			// internet email, then simply use the from_net_addr field from the
-			// message header.  Otherwise (i.e., on a networked sub-board), use
-			// username@from_net_addr.
-			var emailAddr = "";
-			if (typeof(pMsgHdr.from_net_addr) === "string" && pMsgHdr.from_net_addr.length > 0)
-			{
-				if (pMsgHdr.from_net_type == NET_INTERNET)
-					emailAddr = pMsgHdr.from_net_addr;
-				else
-					emailAddr = pMsgHdr.from + "@" + pMsgHdr.from_net_addr;
-			}
-			// Prompt the user to verify the receiver's email address
-			console.putmsg(bbs.text(Email), P_SAVEATR);
-			emailAddr = console.getstr(emailAddr, 60, K_LINE|K_EDIT);
-			if ((typeof(emailAddr) == "string") && (emailAddr.length > 0))
-			{
-				replyMode |= WM_NETMAIL;
-				retObj.sendSucceeded = bbs.netmail(emailAddr, replyMode, null, pMsgHdr);
-				console.pause();
-			}
-			else
-			{
-				retObj.sendSucceeded = false;
-				console.putmsg(bbs.text(Aborted), P_SAVEATR);
-				console.pause();
-			}
-		}
-	}
-	// If we could not determine the network mail address, we may need to try to look up the user to
-	// reply locally.
-	if (couldNotDetermineNetAddr)
-	{
-		// Most likely replying to a local user
-		replyMode |= WM_EMAIL;
-		// Look up the user number of the "from" user name in the message header
-		var userNumber = findUserNumWithName(pMsgHdr.from); // Used to use system.matchuser(pMsgHdr.from)
-		if (userNumber != 0)
-		{
-			// Output a newline to avoid ugly overwriting of text on the screen in
-			// case the sender wants to forward to netmail, then send email to the
-			// sender.  Note that if the send failed, that could be because the
-			// user aborted the message.
-			console.crlf();
-			retObj.sendSucceeded = bbs.email(userNumber, replyMode, null, null, pMsgHdr);
-			console.pause();
-		}
-		else
-		{
-			// If the 'from' username is blank (which can be the case if a guest sent the email), then
-			// ask the user where or to whom they want to send the message
-			if (pMsgHdr.from.length == 0)
-			{
-				console.attributes = "NC";
-				console.crlf();
-				console.print("Sender is unknown. Enter user name/number/email/netmail address\x01h:\x01n");
-				console.crlf();
-				var msgDest = console.getstr(console.screen_columns - 1, K_LINE);
-				if (msgDest != "")
-				{
-					var recipientMatched = true;
-					var sendViaNetmail = false;
-					// See if the user entered a netmail address
-					if (gEmailRegex.test(msgDest) || gFTNEmailRegex.test(msgDest))
-						sendViaNetmail = true;
-					// Check for a valid user number
-					else if (/^[0-9]+/.test(msgDest))
-					{
-						if (system.username(+msgDest) != "")
-							userNumber = +msgDest;
-						else
-							recipientMatched = false;
-					}
-					// Match local user by name/alias
-					else
-					{
-						userNumber = findUserNumWithName(msgDest);
-						if (userNumber <= 0)
-							recipientMatched = false;
-					}
-					// If no recipient was matched, then output an error.  Otherwise, do a reply.
-					if (!recipientMatched)
-					{
-						retObj.sendSucceeded = false;
-						console.crlf();
-						var errorMsg = "\x01n\x01h\x01yThe recipient (\x01w" + msgDest + "\x01y) was not found";
-						if (wasNetMailOrigin)
-							errorMsg += " and no network address was found for this message";
-						errorMsg += "\x01n";
-						console.print(errorMsg);
-						console.crlf();
-						console.pause();
-					}
-					else if (sendViaNetmail)
-					{
-						replyMode |= WM_NETMAIL;
-						retObj.sendSucceeded = bbs.netmail(msgDest, replyMode, null, pMsgHdr);
-						console.pause();
-					}
-					else
-					{
-						console.crlf();
-						retObj.sendSucceeded = bbs.email(userNumber, replyMode, null, null, pMsgHdr);
-						console.pause();
-					}
-				}
-				else
-				{
-					console.attributes = "N";
-					console.print("Canceled");
-					console.crlf();
-				}
-			}
-			else
-			{
-				retObj.sendSucceeded = false;
-				console.crlf();
-				var errorMsg = "\x01n\x01h\x01yThe recipient (\x01w" + pMsgHdr.from + "\x01y) was not found";
-				if (wasNetMailOrigin)
-					errorMsg += " and no network address was found for this message";
-				errorMsg += "\x01n";
-				console.print(errorMsg);
-				console.crlf();
-				console.pause();
-			}
-		}
-	}
-
-	// If the user replied to a personal email, and the user setting to prompt
-	// to delete the message after replying is enabled, then ask the user if
-	// they want to delete the message that was just replied to; and if so, delete it.
-	if (retObj.sendSucceeded && this.readingPersonalEmail && (typeof(pMsgIdx) == "number") && this.userSettings.promptDelPersonalEmailAfterReply)
-	{
-		// Get the delete mail confirmation text from text.dat and replace
-		// the %s with the "from" name in the message header, and use that
-		// as the confirmation text.
-		// Note: If the message was deleted, the DeleteMessage() method will
-		// refresh the header in the search results, if there are any search
-		// results.
-		if (!console.noyes(bbs.text(DeleteMailQ).replace("%s", pMsgHdr.from)))
-			retObj.msgWasDeleted = this.PromptAndDeleteOrUndeleteMessage(pMsgIdx, null, true, null, null, false);
-	}
-
-	return retObj;
-}
-
-// For the DigDistMsgReader class: Displays the enhanced reader mode help screen.
-//
-// Parameters:
-//  pDisplayChgAreaOpt: Optional boolean - Whether or not to show the "change area" option.
-//                      Defaults to true.
-//  pDisplayDLAttachmentOpt: Optional boolean - Whether or not to display the "download attachments"
-//                           option.  Defaults to false.
-function DigDistMsgReader_DisplayEnhancedReaderHelp(pDisplayChgAreaOpt, pDisplayDLAttachmentOpt)
-{
-	if (console.term_supports(USER_ANSI))
-		console.clear("\x01n");
-
-	var displayChgAreaOpt = (typeof(pDisplayChgAreaOpt) == "boolean" ? pDisplayChgAreaOpt : true);
-	var displayDLAttachmentsOpt = (typeof(pDisplayDLAttachmentOpt) == "boolean" ? pDisplayDLAttachmentOpt : false);
-
-	DisplayProgramInfo();
-	console.crlf();
-
-	// Display information about the current sub-board and search results.
-	console.print("\x01n\x01cCurrently reading \x01g" + subBoardGrpAndName(this.subBoardCode));
-	console.crlf();
-	// If the user isn't reading personal messages (i.e., is reading a sub-board),
-	// then output the total number of messages in the sub-board.  We probably
-	// shouldn't output the total number of messages in the "mail" area, because
-	// that includes more than the current user's email.
-	if (!this.readingPersonalEmail)
-	{
-		var numOfMessages = 0;
-		var msgbase = new MsgBase(this.subBoardCode);
-		if (msgbase.open())
-		{
-			numOfMessages = msgbase.total_msgs;
-			msgbase.close();
-		}
-		console.print("\x01n\x01cThere are a total of \x01g" + numOfMessages + " \x01cmessages in the current sub-board.");
-		console.crlf();
-	}
-	// If there is currently a search (which also includes personal messages),
-	// then output the number of search results/personal messages.
-	if (this.SearchingAndResultObjsDefinedForCurSub())
-	{
-		var numSearchResults = this.NumMessages();
-		var resultsWord = (numSearchResults > 1 ? "results" : "result");
-		console.print("\x01n\x01c");
-		if (this.readingPersonalEmail)
-			console.print("You have \x01g" + numSearchResults + " \x01c" + (numSearchResults == 1 ? "message" : "messages") + ".");
-		else
-		{
-			if (numSearchResults == 1)
-				console.print("There is \x01g1 \x01csearch result.");
-			else
-				console.print("There are \x01g" + numSearchResults + " \x01csearch results.");
-		}
-		console.crlf();
-	}
-
-	// Display the enhanced reader keys
-	console.crlf();
-	console.print("\x01n\x01cEnhanced reader mode keys");
-	console.crlf();
-	console.print("\x01h\x01k");
-	for (var i = 0; i < 25; ++i)
-		console.print(HORIZONTAL_SINGLE);
-	console.crlf();
-	var keyHelpLines = ["\x01h\x01cDown\x01g/\x01cup arrow    \x01g: \x01n\x01cScroll down\x01g/\x01cup in the message",
-	                    "\x01h\x01cLeft\x01g/\x01cright arrow \x01g: \x01n\x01cGo to the previous\x01g/\x01cnext message",
-						"\x01h\x01cEnter            \x01g: \x01n\x01cGo to the next message",
-	                    "\x01h\x01cPageUp\x01g/\x01cPageDown  \x01g: \x01n\x01cScroll up\x01g/\x01cdown a page in the message",
-	                    "\x01h\x01cHOME             \x01g: \x01n\x01cGo to the top of the message",
-	                    "\x01h\x01cEND              \x01g: \x01n\x01cGo to the bottom of the message"];
-	keyHelpLines.push("\x01h\x01cCtrl-U           \x01g: \x01n\x01cChange your user settings");
-	if (user.is_sysop)
-	{
-		keyHelpLines.push("\x01h\x01cDEL              \x01g: \x01n\x01cDelete the current message");
-		keyHelpLines.push("\x01h\x01cCtrl-S           \x01g: \x01n\x01cSave the message (to the BBS machine)");
-		keyHelpLines.push("\x01h\x01c" + this.enhReaderKeys.validateMsg + "                \x01g: \x01n\x01cValidate the message");
-		keyHelpLines.push("\x01h\x01cCtrl-O           \x01g: \x01n\x01cShow operator menu");
-		keyHelpLines.push("\x01h\x01cX                \x01g: \x01n\x01cShow message hex dump");
-		keyHelpLines.push("\x01h\x01cCtrl-X           \x01g: \x01n\x01cSave message hex dump to a file");
-		var quickValUserLine = "\x01h\x01cCtrl-Q           \x01g: \x01n\x01cQuick-validate user (must be local)";
-		if (this.quickUserValSetIndex >= 0 && this.quickUserValSetIndex < 10)
-			quickValUserLine += "; Set index: " + this.quickUserValSetIndex;
-		keyHelpLines.push(quickValUserLine);
-	}
-	else if (this.CanDelete() || this.CanDeleteLastMsg())
-		keyHelpLines.push("\x01h\x01cDEL              \x01g: \x01n\x01cDelete the current message (if it's yours)");
-	if (displayDLAttachmentsOpt)
-		keyHelpLines.push("\x01h\x01cCtrl-A           \x01g: \x01n\x01cDownload attachments");
-	// If not reading personal email or doing a search/scan, then include the
-	// text for the message threading keys.
-	if (!this.readingPersonalEmail && !this.SearchingAndResultObjsDefinedForCurSub())
-	{
-		// Thread ID keys: For Synchronet 3.16 and above, include the text "thread ID"
-		// in the help line, since Synchronet 3.16 has the thread_id field in the message
-		// headers.
-		var threadIDLine = "\x01h\x01c" + this.enhReaderKeys.prevMsgByThreadID + " \x01n\x01cor \x01h" + this.enhReaderKeys.nextMsgByThreadID + "           \x01g: \x01n\x01cGo to the previous\x01g/\x01cnext message in the thread";
-		/*
-		if (system.version_num >= 31600)
-			threadIDLine += " (thread ID)";
-		*/
-		keyHelpLines.push(threadIDLine);
-		keyHelpLines.push("\x01h\x01c" + this.enhReaderKeys.prevMsgByTitle + " \x01n\x01cor \x01h" + this.enhReaderKeys.nextMsgByTitle + "           \x01g: \x01n\x01cGo to the previous\x01g/\x01cnext message by title (subject)");
-		keyHelpLines.push("\x01h\x01c" + this.enhReaderKeys.prevMsgByAuthor + " \x01n\x01cor \x01h" + this.enhReaderKeys.nextMsgByAuthor + "           \x01g: \x01n\x01cGo to the previous\x01g/\x01cnext message by author");
-		keyHelpLines.push("\x01h\x01c" + this.enhReaderKeys.prevMsgByToUser + " \x01n\x01cor \x01h" + this.enhReaderKeys.nextMsgByToUser + "           \x01g: \x01n\x01cGo to the previous\x01g/\x01cnext message by 'To user'");
-	}
-	keyHelpLines.push("\x01h\x01c" + this.enhReaderKeys.firstMsg + " \x01n\x01cor \x01h" + this.enhReaderKeys.lastMsg + "           \x01g: \x01n\x01cGo to the first\x01g/\x01clast message in the sub-board");
-	if (displayChgAreaOpt)
-	{
-		if (this.doingMultiSubBoardScan)
-			keyHelpLines.push("\x01h\x01c" + this.enhReaderKeys.nextSubBoard + "                \x01g: \x01n\x01cGo to the next message sub-board");
-		else
-			keyHelpLines.push("\x01h\x01c" + this.enhReaderKeys.prevSubBoard + "\x01n\x01c or \x01h" + this.enhReaderKeys.nextSubBoard + "           \x01g: \x01n\x01cGo to the previous\x01g/\x01cnext message sub-board");
-		keyHelpLines.push("\x01h\x01c" + this.enhReaderKeys.chgMsgArea + "                \x01g: \x01n\x01cChange to a different message sub-board");
-	}
-	else if (this.doingMultiSubBoardScan)
-		keyHelpLines.push("\x01h\x01c" + this.enhReaderKeys.nextSubBoard + "                \x01g: \x01n\x01cGo to the next message sub-board");
-	if (user.is_sysop)
-	{
-		keyHelpLines.push("\x01h\x01c" + this.enhReaderKeys.editMsg + "                \x01g: \x01n\x01cEdit the current message");
-		keyHelpLines.push("\x01h\x01c" + this.enhReaderKeys.userEdit + "                \x01g: \x01n\x01cEdit the user who wrote the message");
-	}
-	keyHelpLines.push("\x01h\x01c" + this.enhReaderKeys.showMsgList + "                \x01g: \x01n\x01cList messages in the current sub-board");
-	if (user.is_sysop)
-		keyHelpLines.push("\x01h\x01c" + this.enhReaderKeys.showHdrInfo + " \x01n\x01cor \x01h" + this.enhReaderKeys.showKludgeLines + "           \x01g: \x01n\x01cDisplay extended header info\x01g/\x01ckludge lines for the message");
-	keyHelpLines.push("\x01h\x01c" + this.enhReaderKeys.reply + "                \x01g: \x01n\x01cReply to the current message");
-	if (!this.readingPersonalEmail)
-	{
-		keyHelpLines.push("\x01h\x01c" + this.enhReaderKeys.privateReply + "                \x01g: \x01n\x01cPrivately reply to the current message (via email/NetMail)");
-		keyHelpLines.push("\x01h\x01c" + this.enhReaderKeys.postMsg + "                \x01g: \x01n\x01cPost a message on the sub-board");
-	}
-	keyHelpLines.push("\x01h\x01cNumber           \x01g: \x01n\x01cGo to a specific message by number");
-	keyHelpLines.push("\x01h\x01cSpacebar         \x01g: \x01n\x01cSelect message (for batch delete, etc.)");
-	keyHelpLines.push("                   \x01n\x01cFor batch delete, open the message list and use CTRL-D.");
-	keyHelpLines.push("\x01h\x01c" + this.enhReaderKeys.forwardMsg + "                \x01g: \x01n\x01cForward the message to user/email");
-	if (typeof((new MsgBase(this.subBoardCode)).vote_msg) === "function")
-	{
-		keyHelpLines.push("\x01h\x01c" + this.enhReaderKeys.vote + "                \x01g: \x01n\x01cVote on the message");
-		keyHelpLines.push("\x01h\x01c" + this.enhReaderKeys.closePoll + "                \x01g: \x01n\x01cClose a poll");
-	}
-	keyHelpLines.push("\x01h\x01c" + this.enhReaderKeys.showVotes + "                \x01g: \x01n\x01cShow vote (tally) stats for the message");
-	keyHelpLines.push("\x01h\x01c" + this.enhReaderKeys.quit + "                \x01g: \x01n\x01cQuit back to the BBS");
-	if (this.indexedMode)
-		keyHelpLines.push(" \x01n\x01cCurrently in indexed mode; quitting will quit back to the index list.");
-	for (var idx = 0; idx < keyHelpLines.length; ++idx)
-	{
-		console.print("\x01n" + keyHelpLines[idx]);
-		console.crlf();
-	}
-
-	// Pause and let the user press a key to continue.  Note: For some reason,
-	// with console.pause(), not all of the message on the screen would get
-	// refreshed.  So instead, we display the system's pause text and input a
-	// key from the user.  Calling getKeyWithESCChars() to input a key from the
-	// user to allow for multi-key sequence inputs like PageUp, PageDown, F1,
-	// etc. without printing extra characters on the screen.
-	//console.print("\x01n" + this.pausePromptText);
-	//getKeyWithESCChars(K_NOSPIN|K_NOCRLF|K_NOECHO);
-	// I'm not sure the above is needed anymore.  Should be able to use
-	// console.pause(), which easily supports custom pause scripts being loaded.
-	console.pause();
-
-	console.aborted = false;
-}
-
-// For the DigDistMsgReader class: Displays the enhanced reader mode message
-// header information for a particular message header.
-//
-// Parameters:
-//  pMsgHdr: The message header object containing message header info
-//  pDisplayMsgNum: The message number to display, if different from the number
-//                  in the header object.  This can be null, in which case the
-//                  number in the header object will be used.
-//  pStartScreenRow: The row on the screen at which to start displaying the
-//                   header information.  Will be used if the user's terminal
-//                   supports ANSI.
-function DigDistMsgReader_DisplayEnhancedMsgHdr(pMsgHdr, pDisplayMsgNum, pStartScreenRow)
-{
-	if ((pMsgHdr == null) || (typeof(pMsgHdr) != "object"))
-		return;
-	// For the set of enhanced header lines, choose the regular set or the set with
-	// the highlighted 'to' user, dependin on whether the message was written to the
-	// logged-in (reading) user.
-	var enhMsgHdrLines = (userHandleAliasNameMatch(pMsgHdr.to) ? this.enhMsgHeaderLinesToReadingUser : this.enhMsgHeaderLines);
-	if (enhMsgHdrLines == null)
-		return;
-	if ((enhMsgHdrLines.length == 0) || (this.enhMsgHeaderWidth == 0))
-		return;
-
-	// Create a formatted date & time string.  Adjust the message's time to
-	// the BBS local time zone if possible.
-	var dateTimeStr = "";
-	var msgWrittenLocalTime = msgWrittenTimeToLocalBBSTime(pMsgHdr);
-	var useBBSLocalTimeZone = false;
-	if (msgWrittenLocalTime != -1)
-	{
-		dateTimeStr = strftime("%a, %d %b %Y %H:%M:%S", msgWrittenLocalTime);
-		useBBSLocalTimeZone = true;
-	}
-	else
-		dateTimeStr = pMsgHdr.date.replace(/ [-+][0-9]+$/, "");
-	
-	var enhHdrLines = enhMsgHdrLines.slice(0);
-	// Do some things if using the internal header (not loaded externally)
-	if (this.usingInternalEnhMsgHdr)
-	{
-		// If the message is not a poll and contains the properties total_votes and upvotes,
-		// then put some information in the header containing information about the message's
-		// voting results.
-		// Only add the vote information if the total_votes value is non-zero
-		var msgIsAPoll = false;
-		if (typeof(MSG_POLL) != "undefined")
-			msgIsAPoll = Boolean(pMsgHdr.attr & MSG_POLL);
-		// TODO: Fix the issue with not showing votes.  With this, it seems to think it has 0 votes
-		//var hdrWithVotes = getMsgHdr(this.subBoardCode, false, pMsgHdr.number, true, true);
-		if (!msgIsAPoll && pMsgHdr.hasOwnProperty("total_votes") && pMsgHdr.hasOwnProperty("upvotes") && pMsgHdr.total_votes != 0)
-		//if (!msgIsAPoll && hdrWithVotes.hasOwnProperty("total_votes") && hdrWithVotes.hasOwnProperty("upvotes") && hdrWithVotes.total_votes != 0)
-		{
-			var voteInfo = getMsgUpDownvotesAndScore(pMsgHdr);
-			//var voteInfo = getMsgUpDownvotesAndScore(hdrWithVotes);
-			var voteStatsTxt = "\x01n\x01c" + RIGHT_T_SINGLE + "\x01h\x01gS\x01n\x01gcore\x01h\x01c: \x01b" + voteInfo.voteScore + " (+" + voteInfo.upvotes + ", -" + voteInfo.downvotes + ")\x01n\x01c" + LEFT_T_SINGLE;
-			enhHdrLines[6] = enhHdrLines[6].substring(0, 10) + "\x01n\x01c" + voteStatsTxt + "\x01n\x01c" + HORIZONTAL_SINGLE + "\x01h\x01k" + enhHdrLines[6].substring(17 + strip_ctrl(voteStatsTxt).length);
-		}
-
-		// If this is a personal email that has been replied to, then
-		// put the word "Replied" toward the right of the last line
-		if (this.readingPersonalEmail && Boolean(pMsgHdr.attr & MSG_REPLIED))
-		{
-			enhHdrLines[6] = enhHdrLines[6].substr(0, enhHdrLines[6].length-17) + "\x01wReplied\x01k" + enhHdrLines[6].substr(enhHdrLines[6].length-10);
-		}
-	}
-
-	// If the user's terminal supports ANSI, we can move the cursor and
-	// display the header where specified.
-	if (console.term_supports(USER_ANSI))
-	{
-		// Display the header starting on the first column and the given screen row.
-		var screenX = 1;
-		var screenY = (typeof(pStartScreenRow) == "number" ? pStartScreenRow : 1);
-		for (var hdrFileIdx = 0; hdrFileIdx < enhHdrLines.length; ++hdrFileIdx)
-		{
-			console.gotoxy(screenX, screenY++);
-			console.putmsg(this.ParseMsgAtCodes(enhHdrLines[hdrFileIdx], pMsgHdr,
-			               pDisplayMsgNum, dateTimeStr, useBBSLocalTimeZone, false));
-		}
-		// Older - Used to center the header lines, but I'm not sure this is necessary,
-		// and it might even make the header off by one, which could be bad.
-		// Display the message header information.  Make sure the header lines are
-		// centered properly in case the user's terminal is more than 80 characters
-		// wide.
-		/*
-		var screenX = Math.floor(console.screen_columns/2)
-		            - Math.floor(this.enhMsgHeaderWidth/2);
-		if (console.screen_columns > 80)
-			++screenX;
-		*/
-		// If avatars are available, then show the sender's avatar on the right
-		// side
-		if (this.displayAvatars && (gAvatar != null) && ((pMsgHdr.attr & MSG_ANONYMOUS) == 0))
-		{
-			console.gotoxy(1, screenY-1);
-			//gAvatar.draw(pMsgHdr.from_ext, pMsgHdr.from, pMsgHdr.from_net_addr, /* above: */true, /* right-justified: */true);
-			gAvatar.draw(pMsgHdr.from_ext, pMsgHdr.from, pMsgHdr.from_net_addr, /* above: */true, /* right-justified: */this.rightJustifyAvatar);
-			console.attributes = 0;	// Clear the background attribute as the next line might scroll, filling with BG attribute
-			// If using the traditional (non-scrolling) user interface, then
-			// put the cursor where it should be.  (If using the scrolling
-			// interface, the cursor will be placed where it should be elsewhere.)
-			if (!this.scrollingReaderInterface)
-				console.gotoxy(1, screenY);
-		}
-	}
-	else
-	{
-		// The user's terminal doesn't support ANSI - So just output the header
-		// lines.
-		for (var hdrFileIdx = 0; hdrFileIdx < enhHdrLines.length; ++hdrFileIdx)
-		{
-			console.putmsg(this.ParseMsgAtCodes(enhHdrLines[hdrFileIdx], pMsgHdr,
-			               pDisplayMsgNum, dateTimeStr, useBBSLocalTimeZone, false));
-		}
-		// Note: Avatar display is only supported for ANSI
-	}
-}
-
-// For the DigDistMsgReader class: Displays the area chooser header
-//
-// Parameters:
-//  pStartScreenRow: The row on the screen at which to start displaying the
-//                   header information.  Will be used if the user's terminal
-//                   supports ANSI.
-//  pClearRowsFirst: Optional boolean - Whether or not to clear the rows first.
-//                   Defaults to true.  Only valid if the user's terminal supports
-//                   ANSI.
-function DigDistMsgReader_DisplayAreaChgHdr(pStartScreenRow, pClearRowsFirst)
-{
-	if (this.areaChangeHdrLines == null)
-		return;
-	if (this.areaChangeHdrLines.length == 0)
-		return;
-
-	// If the user's terminal supports ANSI and pStartScreenRow is a number, then
-	// we can move the cursor and display the header where specified.
-	if (console.term_supports(USER_ANSI) && (typeof(pStartScreenRow) == "number"))
-	{
-		// If specified to clear the rows first, then do so.
-		var screenX = 1;
-		var screenY = pStartScreenRow;
-		var clearRowsFirst = (typeof(pClearRowsFirst) == "boolean" ? pClearRowsFirst : true);
-		if (clearRowsFirst)
-		{
-			console.attributes = "N";
-			for (var hdrFileIdx = 0; hdrFileIdx < this.areaChangeHdrLines.length; ++hdrFileIdx)
-			{
-				console.gotoxy(screenX, screenY++);
-				console.cleartoeol();
-			}
-		}
-		// Display the header starting on the first column and the given screen row.
-		screenX = 1;
-		screenY = pStartScreenRow;
-		for (var hdrFileIdx = 0; hdrFileIdx < this.areaChangeHdrLines.length; ++hdrFileIdx)
-		{
-			console.gotoxy(screenX, screenY++);
-			console.print(this.areaChangeHdrLines[hdrFileIdx]);
-			//console.putmsg(this.areaChangeHdrLines[hdrFileIdx]);
-			//console.cleartoeol("\x01n"); // Shouldn't do this, as it resets color attributes
-		}
-	}
-	else
-	{
-		// The user's terminal doesn't support ANSI or pStartScreenRow is not a
-		// number - So just output the header lines.
-		for (var hdrFileIdx = 0; hdrFileIdx < this.areaChangeHdrLines.length; ++hdrFileIdx)
-		{
-			console.print(this.areaChangeHdrLines[hdrFileIdx]);
-			//console.putmsg(this.areaChangeHdrLines[hdrFileIdx]);
-			//console.cleartoeol("\x01n"); // Shouldn't do this, as it resets color attributes
-			console.crlf();
-		}
-	}
-}
-
-// For the DigDistMsgReader class: Displays the whole/initial scrollbar for a message
-// in enhanced reader mode.
-//
-// Parameters:
-//  pSolidBlockStartRow: The starting row for the solid/bright blocks
-//  pNumSolidBlocks: The number of solid/bright blocks to write
-function DigDistMsgReader_DisplayEnhancedReaderWholeScrollbar(pSolidBlockStartRow, pNumSolidBlocks)
-{
-	//console.attributes = "N";
-	var numSolidBlocksWritten = 0;
-	var wroteBrightBlockColor = false;
-	var wroteDimBlockColor = false;
-	for (var screenY = this.msgAreaTop; screenY <= this.msgAreaBottom; ++screenY)
-	{
-		console.gotoxy(this.msgAreaRight+1, screenY);
-		if ((screenY >= pSolidBlockStartRow) && (numSolidBlocksWritten < pNumSolidBlocks))
-		{
-			if (!wroteBrightBlockColor)
-			{
-				//console.print("\x01h\x01w");
-				console.print("\x01n" + this.colors.scrollbarScrollBlockColor);
-				wroteBrightBlockColor = true;
-				wroteDimBlockColor = false;
-			}
-			console.print(BLOCK2);
-			//console.print(this.text.scrollbarScrollBlockChar); // TODO: This doesn't seem to be working
-			++numSolidBlocksWritten;
-		}
-		else
-		{
-			if (!wroteDimBlockColor)
-			{
-				//console.print("\x01h\x01k");
-				console.print("\x01n" + this.colors.scrollbarBGColor);
-				wroteDimBlockColor = true;
-			}
-			console.print(BLOCK1);
-			//console.print(this.text.scrollbarBGChar); // TODO: This doesn't seem to be working
-		}
-	}
-}
-
-// For the DigDistMsgReader class: Updates the scrollbar for a message, for use
-// in enhanced reader mode.  This does only the necessary character updates to
-// minimize the number of characters that need to be updated on the screen.
-//
-// Parameters:
-//  pNewStartRow: The new (current) start row for solid/bright blocks
-//  pOldStartRow: The old start row for solid/bright blocks
-//  pNumSolidBlocks: The number of solid/bright blocks
-function DigDistMsgReader_UpdateEnhancedReaderScrollbar(pNewStartRow, pOldStartRow, pNumSolidBlocks)
-{
-	// Calculate the difference in the start row.  If the difference is positive,
-	// then the solid block section has moved down; if the diff is negative, the
-	// solid block section has moved up.
-	var solidBlockStartRowDiff = pNewStartRow - pOldStartRow;
-	var oldLastRow = pOldStartRow + pNumSolidBlocks - 1;
-	var newLastRow = pNewStartRow + pNumSolidBlocks - 1;
-	if (solidBlockStartRowDiff > 0)
-	{
-		// The solid block section has moved down
-		if (pNewStartRow > oldLastRow)
-		{
-			// No overlap
-			// Write dim blocks over the old solid block section
-			//console.print("\x01n\x01h\x01k");
-			console.print("\x01n" + this.colors.scrollbarBGColor);
-			for (var screenY = pOldStartRow; screenY <= oldLastRow; ++screenY)
-			{
-				console.gotoxy(this.msgAreaRight+1, screenY);
-				console.print(BLOCK1);
-				//console.print(this.text.scrollbarBGChar); // TODO: This doesn't seem to be working
-			}
-			// Write solid blocks in the new locations
-			//console.print("\x01w");
-			console.print("\x01n" + this.colors.scrollbarScrollBlockColor);
-			for (var screenY = pNewStartRow; screenY <= newLastRow; ++screenY)
-			{
-				console.gotoxy(this.msgAreaRight+1, screenY);
-				console.print(BLOCK2);
-				//console.print(this.text.scrollbarScrollBlockChar);  // TODO: This doesn't seem to be working
-			}
-		}
-		else
-		{
-			// There is some overlap
-			// Write dim blocks on top
-			//console.print("\x01n\x01h\x01k");
-			console.print("\x01n" + this.colors.scrollbarBGColor);
-			for (var screenY = pOldStartRow; screenY < pNewStartRow; ++screenY)
-			{
-				console.gotoxy(this.msgAreaRight+1, screenY);
-				console.print(BLOCK1);
-				//console.print(this.text.scrollbarBGChar); // TODO: This doesn't seem to be working
-			}
-			// Write bright blocks on the bottom
-			//console.print("\x01w");
-			console.print("\x01n" + this.colors.scrollbarScrollBlockColor);
-			for (var screenY = oldLastRow+1; screenY <= newLastRow; ++screenY)
-			{
-				console.gotoxy(this.msgAreaRight+1, screenY);
-				console.print(BLOCK2);
-				//console.print(this.text.scrollbarScrollBlockChar); // TODO: This doesn't seem to be working
-			}
-		}
-	}
-	else if (solidBlockStartRowDiff < 0)
-	{
-		// The solid block section has moved up
-		if (pOldStartRow > newLastRow)
-		{
-			// No overlap
-			// Write dim blocks over the old solid block section
-			//console.print("\x01n\x01h\x01k");
-			console.print("\x01n" + this.colors.scrollbarBGColor);
-			for (var screenY = pOldStartRow; screenY <= oldLastRow; ++screenY)
-			{
-				console.gotoxy(this.msgAreaRight+1, screenY);
-				console.print(BLOCK1);
-				//console.print(this.text.scrollbarBGChar); // TODO: This doesn't seem to be working
-			}
-			// Write solid blocks in the new locations
-			//console.print("\x01w");
-			console.print("\x01n" + this.colors.scrollbarScrollBlockColor);
-			for (var screenY = pNewStartRow; screenY <= newLastRow; ++screenY)
-			{
-				console.gotoxy(this.msgAreaRight+1, screenY);
-				console.print(BLOCK2);
-				//console.print(this.text.scrollbarScrollBlockChar); // TODO: This doesn't seem to be working
-			}
-		}
-		else
-		{
-			// There is some overlap
-			// Write bright blocks on top
-			//console.print("\x01n\x01h\x01w");
-			console.print("\x01n" + this.colors.scrollbarScrollBlockColor);
-			var endRow = pOldStartRow;
-			for (var screenY = pNewStartRow; screenY < endRow; ++screenY)
-			{
-				console.gotoxy(this.msgAreaRight+1, screenY);
-				console.print(BLOCK2);
-				//console.print(this.text.scrollbarScrollBlockChar); // TODO: This doesn't seem to be working
-			}
-			// Write dim blocks on the bottom
-			//console.print("\x01k");
-			console.print("\x01n" + this.colors.scrollbarBGColor);
-			endRow = pOldStartRow + pNumSolidBlocks;
-			for (var screenY = pNewStartRow+pNumSolidBlocks; screenY < endRow; ++screenY)
-			{
-				console.gotoxy(this.msgAreaRight+1, screenY);
-				console.print(BLOCK1);
-				//console.print(this.text.scrollbarBGChar); // TODO: This doesn't seem to be working
-			}
-		}
-	}
-}
-
-// For the DigDistMsgReader class: Returns whether a particular message is
-// marked as deleted.  If the message base object is not open or the given
-// offset is out of bounds, this method will return true.
-//
-// Parameters:
-//  pOffset: The offset of the message to check
-//
-// Return value: Boolean - Whether or not the message is marked as deleted
-function DigDistMsgReader_MessageIsDeleted(pOffset)
-{
-	var msgDeleted = false;
-	var msgbase = new MsgBase(this.subBoardCode);
-	if (msgbase.open())
-	{
-		if ((pOffset < 0) || (pOffset >= this.NumMessages(msgbase)))
-			msgDeleted = true;
-		else
-		{
-			// Get the message's header and see if it's marked as deleted
-			var msgHdr = this.GetMsgHdrByIdx(pOffset, false, msgbase);
-			if (msgHdr != null)
-				msgDeleted = ((msgHdr.attr & MSG_DELETE) == MSG_DELETE);
-		}
-		msgbase.close();
-	}
-
-	return msgDeleted;
-}
-
-// For the DigDistMsgReader class: Returns whether a particular message is the
-// last post in the sub-board from the current logged-in user.
-//
-// Parameters:
-//  pOffset: The offset of the message to check
-//
-// Return value: Boolean - Whether or not the message is the last post in the
-//               sub-board from the current logged-in user.
-function DigDistMsgReader_MessageIsLastFromUser(pOffset)
-{
-	var msgIstLastFromUser = false;
-	var msgbase = new MsgBase(this.subBoardCode);
-	if (msgbase.open())
-	{
-		if (msgbase.cfg != null)
-		{
-			// TODO: Update to handle search results?
-			if ((pOffset >= 0) && (pOffset < msgbase.total_msgs))
-			{
-				// First, see if the message at pOffset was posted by the user.  If it
-				// is, then look for the last message posted by the logged-in user and
-				// if found, see if that message has the same offset as the offset
-				// passed in.
-				var msgIdx = msgbase.get_msg_index(true, pOffset, false);
-				if (msgIdx != null && userHandleAliasNameMatch(msgIdx.to))
-				{
-					var lastMsgOffsetFromUser = -1;
-					for (var msgOffset = msgbase.total_msgs-1; (msgOffset >= pOffset) && (lastMsgOffsetFromUser == -1); --msgOffset)
-					{
-						msgIdx = msgbase.get_msg_index(true, msgOffset, false);
-						if (msgIdx != null && userHandleAliasNameMatch(msgIdx.to))
-							lastMsgOffsetFromUser = msgOffset;
-					}
-					// See if the passed-in offset is the last message we found from
-					// the logged-in user.
-					msgIstLastFromUser = (lastMsgOffsetFromUser == pOffset);
-				}
-			}
-		}
-		msgbase.close();
-	}
-	return msgIstLastFromUser;
-}
-
-// For the DigDistMsgReader class enhanced reader mode: Displays an error at the
-// bottom of the message area for a moment, then refreshes the last 2 lines in
-// the message area.  If the message string that is passed in is empty or not
-// a string, then this will simply refresh the last 2 lines of the message area.
-//
-// Parameters:
-//  pErrorMsg: The error message to show
-//  pMessageLines: The array of lines from the message being displayed
-//  pTopLineIdx: The index of the line being displayed at the top of the message area
-//  pMsgLineFormatStr: Optional - The format string for message lines
-function DigDistMsgReader_DisplayEnhReaderError(pErrorMsg, pMessageLines, pTopLineIdx,
-                                                pMsgLineFormatStr)
-{
-   var msgLineFormatStr = "";
-   if (typeof(pMsgLineFormatStr) == "string")
-      msgLineFormatStr = pMsgLineFormatStr;
-   else
-      msgLineFormatStr = "%-" + this.msgAreaWidth + "s";
-
-   var originalCurpos = console.getxy();
-   // Move the cursor to the 2nd to last row of the screen and
-   // show the error.  Ideally, I'd like
-   // to put the cursor on the last row of the screen for this, but
-   // console.getnum() lets the enter key shift everything on screen
-   // up one row, and there's no way to avoid that.  So, to optimize
-   // screen refreshing, the cursor is placed on the 2nd to the last
-   // row on the screen to prompt for the message number.
-   var promptPos = { x: this.msgAreaLeft, y: console.screen_rows-1 };
-   // Write a line of characters above where the prompt will be placed,
-   // to help get the user's attention.
-   console.gotoxy(promptPos.x, promptPos.y-1);
-   console.print("\x01n" + this.colors.enhReaderPromptSepLineColor);
-   for (var lineCounter = 0; lineCounter < this.msgAreaWidth; ++lineCounter)
-      console.print(HORIZONTAL_SINGLE);
-   // Clear the inside of the message area, so as not to overwrite
-   // the scrollbar character
-   console.attributes = "N";
-   console.gotoxy(promptPos);
-   for (var lineCounter = 0; lineCounter < this.msgAreaWidth; ++lineCounter)
-      console.print(" ");
-   // Show the error if a valid error message was passed in
-   if ((typeof(pErrorMsg) == "string") && (console.strlen(pErrorMsg) > 0))
-   {
-      writeWithPause(this.msgAreaLeft, console.screen_rows-1, pErrorMsg,
-                     ERROR_PAUSE_WAIT_MS, "\x01n", true);
-   }
-   // Figure out the indexes of the message for the last lines of
-   // the message and update that line on the screen.
-   // If the index is valid, then output that message line; otherwise,
-   // output a blank line.
-   --promptPos.y;
-   var msgLine = null;
-   var msgLineIndex = pTopLineIdx + this.msgAreaHeight - 2;
-   for (; msgLineIndex <= pTopLineIdx + this.msgAreaHeight - 1; ++msgLineIndex)
-   {
-      console.gotoxy(promptPos);
-      console.print("\x01n" + this.colors["msgBodyColor"]);
-      if ((msgLineIndex >= 0) && (msgLineIndex < pMessageLines.length))
-      {
-         console.print(pMessageLines[msgLineIndex]); // Already shortened to fit
-         console.print("\x01n" + this.colors["msgBodyColor"]); // In case colors changed
-         // Clear the rest of the line
-         printf("%" + +(this.msgAreaWidth-console.strlen(pMessageLines[msgLineIndex])) + "s", "");
-      }
-      else
-         printf(msgLineFormatStr, "");
-      ++promptPos.y;
-   }
-   // Move the cursor back to its original position
-   console.gotoxy(originalCurpos);
-}
-
-// For the DigDistMsgReader class enhanced reader mode: Prompts for a yes/no
-// question at the bottom of the message area and refreshes the last 2 lines in
-// the message area when the user has given an answer.  If the question string
-// that is passed in is empty or not a string, then this will simply refresh the
-// last 2 lines of the message area, and the return value will default to true.
-//
-// Parameters:
-//  pQuestion: The yes/no question to display, without the ? on the end
-//  pMessageLines: The array of lines from the message being displayed
-//  pTopLineIdx: The index of the line being displayed at the top of the message area
-//  pMsgLineFormatStr: The format string for message lines
-//  pSolidScrollBlockStartRow: The starting row for solid scroll blocks (purely for
-//                             the kludge of updating the last scrollbar block on the
-//                             screen because the yes/no function erases it)
-//  pNumSolidScrollBlocks: The number of solid scroll blocks (purely for the kludge of
-//                         updating the last scrollbar block on the screen because the
-//                         yes/no function erases it)
-//  pNoYes: Optional boolean - Whether the default response should be No instead of
-//          Yes.  Defaults to false (for a default Yes response).
-//
-// Return value: Boolean - True if the user selected yes, or false if the user selected no.
-//               If the question string passed in is 0-length or not a valid string, the
-//               return value will be true.
-function DigDistMsgReader_EnhReaderPromptYesNo(pQuestion, pMessageLines, pTopLineIdx,
-                                                pMsgLineFormatStr, pSolidScrollBlockStartRow,
-                                                pNumSolidScrollBlocks, pDefaultNo)
-{
-	var msgLineFormatStr = "";
-	if (typeof(pMsgLineFormatStr) == "string")
-		msgLineFormatStr = pMsgLineFormatStr;
-	else
-		msgLineFormatStr = "%-" + +(this.msgAreaWidth) + "s";
-
-	var originalCurpos = console.getxy();
-	// Move the cursor to the 2nd to last row of the screen and
-	// show the error.  Ideally, I'd like
-	// to put the cursor on the last row of the screen for this, but
-	// console.getnum() lets the enter key shift everything on screen
-	// up one row, and there's no way to avoid that.  So, to optimize
-	// screen refreshing, the cursor is placed on the 2nd to the last
-	// row on the screen to prompt for the message number.
-	var promptPos = { x: this.msgAreaLeft, y: console.screen_rows-1 };
-	// Write a line of characters above where the prompt will be placed,
-	// to help get the user's attention.
-	console.gotoxy(promptPos.x, promptPos.y-1);
-	console.print("\x01n" + this.colors.enhReaderPromptSepLineColor);
-	for (var lineCounter = 0; lineCounter < this.msgAreaWidth; ++lineCounter)
-	console.print(HORIZONTAL_SINGLE);
-	// Clear the inside of the message area, so as not to overwrite
-	// the scrollbar character
-	console.attributes = "N";
-	console.gotoxy(promptPos);
-	for (var lineCounter = 0; lineCounter < this.msgAreaWidth; ++lineCounter)
-	console.print(" ");
-	// Prompt the question if a valid question string was passed in
-	var yesNoResponse = true;
-	if ((typeof(pQuestion) == "string") && (console.strlen(pQuestion) > 0))
-	{
-		console.gotoxy(this.msgAreaLeft, console.screen_rows-1);
-		var defaultResponseNo = (typeof(pDefaultNo) == "boolean" ? pDefaultNo : false);
-		if (defaultResponseNo)
-			yesNoResponse = !console.noyes(pQuestion);
-		else
-			yesNoResponse = console.yesno(pQuestion);
-		// Kludge: Update the last scroll block on the screen, since the yes/no
-		// prompt erases it.
-		//var scrollBlockChar = "\x01n\x01h\x01k" + BLOCK1; // Dim scroll block
-		// Dim scroll block
-		var scrollBlockChar = this.colors.scrollbarBGColor + this.text.scrollbarBGChar;
-		if ((pSolidScrollBlockStartRow >= console.screen_rows-1) ||
-		    (pSolidScrollBlockStartRow + pNumSolidScrollBlocks - 1 >= console.screen_rows-1))
-		{
-			//scrollBlockChar = "\x01n\x01h\x01w" + BLOCK2; // Bright, solid scroll block
-			// Bright, solid scroll block
-			scrollBlockChar = this.colors.scrollbarScrollBlockColor + this.text.scrollbarScrollBlockChar;
-		}
-		console.gotoxy(console.screen_columns, console.screen_rows-1);
-		console.print(scrollBlockChar);
-	}
-	// Figure out the indexes of the message for the last lines of
-	// the message and update that line on the screen.
-	// If the index is valid, then output that message line; otherwise,
-	// output a blank line.
-	--promptPos.y;
-	var msgLine = null;
-	var msgLineIndex = pTopLineIdx + this.msgAreaHeight - 2;
-	for (; msgLineIndex <= pTopLineIdx + this.msgAreaHeight - 1; ++msgLineIndex)
-	{
-		console.gotoxy(promptPos);
-		console.print("\x01n" + this.colors["msgBodyColor"]);
-		if ((msgLineIndex >= 0) && (msgLineIndex < pMessageLines.length))
-		{
-			console.print(pMessageLines[msgLineIndex]); // Already shortened to fit
-			console.print("\x01n" + this.colors["msgBodyColor"]); // In case colors changed
-			// Clear the rest of the line
-			printf("%" + +(this.msgAreaWidth-console.strlen(pMessageLines[msgLineIndex])) + "s", "");
-		}
-		else
-			printf(msgLineFormatStr, "");
-		++promptPos.y;
-	}
-	// Move the cursor back to its original position
-	console.gotoxy(originalCurpos);
-
-	return yesNoResponse;
-}
-
-// For the DigDistMsgReader class: Allows the user to delete or undelete a message.  Checks
-// whether the message was posted by the user and prompt for confirmation to
-// delete it.  Checks for delete or delete_last permission.  If the sub-board has
-// delete_last permission enabled, this checks whether the message is the user's
-// last post on the sub-board and only lets them delete if so.
-//
-// Parameters:
-//  pOffset: The offset of the message to be deleted
-//  pPromptLoc: Optional - An object containing x and y properties for the location
-//              on the console of the prompt/error messages
-//  pDelete: Optional boolean: If true (default), deletes messages; if false, undeletes messages.
-//  pClearPromptRowAtFirstUse: Optional - A boolean to specify whether or not to
-//                             clear the remainder of the prompt row the first
-//                             time text is written in that row.
-//  pPromptRowWidth: Optional - The width of the prompt row (if pProptLoc is valid)
-//  pConfirm: Optional boolean - Whether or not to confirm deleting/undeleting the message. Defaults to true.
-//
-// Return value: Boolean - Whether or not the message was deleted
-function DigDistMsgReader_PromptAndDeleteOrUndeleteMessage(pOffset, pPromptLoc, pDelete, pClearPromptRowAtFirstUse,
-                                                           pPromptRowWidth, pConfirm)
-{
-	// Sanity checking
-	if ((pOffset == null) || (typeof(pOffset) != "number"))
-		return false;
-	if (!this.CanDelete() && !this.CanDeleteLastMsg())
-		return false;
-	var msgbase = new MsgBase(this.subBoardCode);
-	if (!msgbase.open())
-		return false;
-	if ((pOffset < 0) || (pOffset >= this.NumMessages(msgbase)))
-	{
-		msgbase.close();
-		return false;
-	}
-	var promptLocValid = ((pPromptLoc != null) && (typeof(pPromptLoc) == "object") &&
-	                      (typeof(pPromptLoc.x) == "number") && (typeof(pPromptLoc.y) == "number"));
-
-	var deleteMsg = (typeof(pDelete) === "boolean" ? pDelete : true);
-
-	var opSucceeded = false;
-
-	var msgNum = pOffset + 1;
-	var msgHeader = this.GetMsgHdrByIdx(pOffset, false, msgbase);
-	if (msgHeader == null)
-	{
-		msgbase.close();
-		return false;
-	}
-
-	var errorMessage = "";
-	var continueOn = false;
-	if (deleteMsg)
-	{
-		// If it's already marked for deletion, then nothing needs to be done
-		if ((msgHeader.attr & MSG_DELETE) == MSG_DELETE)
-			opSucceeded = true;
-		else
-		{
-			// The message is not marked for deletion.
-			if (this.CanDelete())
-			{
-				if (msgHeader != null)
-					continueOn = user.is_sysop || userHandleAliasNameMatch(msgHeader.from) || this.readingPersonalEmail;
-				else
-					continueOn = false;
-			}
-			else if (this.CanDeleteLastMsg())
-			{
-				continueOn = user.is_sysop || this.MessageIsLastFromUser(pOffset);
-				if (!continueOn)
-					errorMessage = replaceAtCodesInStr(format(this.text.cannotDeleteMsgText_notLastPostedMsg, msgNum));
-			}
-			else
-				errorMessage = replaceAtCodesInStr(format(this.text.cannotDeleteMsgText_notYoursNotASysop, msgNum));
-		}
-	}
-	else
-	{
-		// Undeleting a message marked for deletion
-		if ((msgHeader.attr & MSG_DELETE) == MSG_DELETE)
-			continueOn = true;
-		else
-			opSucceeded = true; // No action needed
-	}
-	if (continueOn)
-	{
-		// Determine whether or not to delete/undelete the message.  First, if we are to
-		// have the user confirm whether to delete the message, then ask the
-		// user to confirm first.  If we're not to have the user confirm, then
-		// go ahead and delete the message.
-		continueOn = true; // True in case of not confirming deletion
-		var confirmOp = (typeof(pConfirm) == "boolean" ? pConfirm : true);
-		if (confirmOp)
-		{
-			var confirmText = "\x01n";
-			if (deleteMsg)
-				confirmText += replaceAtCodesInStr(format(this.text.msgDelConfirmText, msgNum));
-			else
-				confirmText += replaceAtCodesInStr(format(this.text.msgUndelConfirmText, msgNum));
-			if (promptLocValid)
-			{
-				// If the caller wants to clear the remainder of the row where the prompt
-				// text will be, then do it.
-				if (pClearPromptRowAtFirstUse)
-				{
-					// Adding 5 to the prompt text to account for the ? and "[X] " that
-					// will be added when console.noyes() is called
-					var promptTxtLen = console.strlen(confirmText) + 5;
-					var numCharsRemaining = 0;
-					if (typeof(pPromptRowWidth) == "number")
-						numCharsRemaining = pPromptRowWidth - promptTxtLen;
-					else
-						numCharsRemaining = console.screen_columns - pPromptLoc.x - promptTxtLen;
-					console.attributes = "N";
-					console.gotoxy(pPromptLoc.x+promptTxtLen, pPromptLoc.y);
-					for (var i = 0; i < numCharsRemaining; ++i)
-						console.print(" ");
-				}
-				// Move the cursor to the prompt location
-				console.gotoxy(pPromptLoc);
-			}
-			continueOn = !console.noyes(confirmText);
-		}
-		// If we are to delete/undelete the message, then do it.
-		if (continueOn)
-		{
-			if (deleteMsg)
-			{
-				//opSucceeded = msgbase.remove_msg(true, msgHeader.offset);
-				opSucceeded = msgbase.remove_msg(false, msgHeader.number);
-			}
-			else
-			{
-				var tmpMsgHdr = msgbase.get_msg_header(false, msgHeader.number, false);
-				if (tmpMsgHdr != null)
-				{
-					tmpMsgHdr.attr = tmpMsgHdr.attr ^ MSG_DELETE;
-					opSucceeded = msgbase.put_msg_header(false, msgHeader.number, tmpMsgHdr);
-				}
-				else
-					opSucceeded = false;
-			}
-			if (opSucceeded)
-			{
-				// Delete/undelete any vote response messages for this message
-				// Delete/undelete vote message headers
-				var voteDelRetObj = toggleVoteMsgsDeleted(msgbase, msgHeader.number, msgHeader.id, deleteMsg, (this.subBoardCode == "mail"));
-				// In case there are search results or saved message headers, refresh the header in
-				// those arrays to enable the deleted attribute.
-				this.RefreshHdrInSavedArrays(pOffset, MSG_DELETE, deleteMsg);
-				if (!voteDelRetObj.allVoteMsgsAffected)
-				{
-					console.attributes = "N";
-					console.crlf();
-					console.print("\x01y\x01h* Failed to " + (deleteMsg ? "delete" : "undelete") + " all vote response messages for message " + msgNum + "\x01n");
-					console.crlf();
-					console.pause();
-				}
-
-				// Output a message saying the message has been marked for deletion/undeletion
-				if (promptLocValid)
-					console.gotoxy(pPromptLoc);
-				if (deleteMsg)
-					console.print("\x01n" + replaceAtCodesInStr(format(this.text.msgDeletedText, msgNum)));
-				else
-					console.print("\x01n" + replaceAtCodesInStr(format(this.text.msgUndeletedText, msgNum)));
-				if (promptLocValid)
-					console.inkey(K_NOSPIN|K_NOCRLF|K_NOECHO, ERROR_PAUSE_WAIT_MS);
-				else
-				{
-					console.crlf();
-					console.pause();
-				}
-			}
-		}
-	}
-	else
-	{
-		if (errorMessage.length > 0)
-		{
-			if (promptLocValid)
-				console.gotoxy(pPromptLoc);
-			console.print(errorMessage);
-			if (promptLocValid)
-				console.inkey(K_NOSPIN|K_NOCRLF|K_NOECHO, ERROR_PAUSE_WAIT_MS);
-			else
-			{
-				console.crlf();
-				console.pause();
-			}
-		}
-	}
-	msgbase.close();
-	return opSucceeded;
-}
-
-// For the DigDistMsgReader class: Allows the user to batch delete selected messages.
-// Prompts the user for confirmation to delete the selected messages.
-//
-// Parameters:
-//  pPromptLoc: Optional - An object containing x and y properties for the location
-//              on the console of the prompt/error messages
-//  pDelete: Optional boolean: If true (default), deletes messages; if false, undeletes messages.
-//  pClearPromptRowAtFirstUse: Optional - A boolean to specify whether or not to
-//                             clear the remainder of the prompt row the first
-//                             time text is written in that row.
-//  pPromptRowWidth: Optional - The width of the prompt row (if pProptLoc is valid)
-//  pConfirm: Optional boolean - Whether or not to confirm deleting/undeleting the messages. Defaults to true.
-//
-// Return value: Boolean - Whether or not all messages were deleted
-function DigDistMsgReader_PromptAndDeleteOrUndeleteSelectedMessages(pPromptLoc, pDelete, pClearPromptRowAtFirstUse,
-                                                          pPromptRowWidth, pConfirm)
-{
-	var promptLocValid = ((pPromptLoc != null) && (typeof(pPromptLoc) === "object") &&
-	                      (typeof(pPromptLoc.x) === "number") && (typeof(pPromptLoc.y) === "number"));
-
-	var doDelete = (typeof(pDelete) === "boolean" ? pDelete : true);
-	var allMsgsOpSuccessful = false;
-
-	var errorMsg = ""; // In case anything goes wrong
-	var continueOn = true; // Whether or not to continue with deletion/undeletion, depending on user confirmation etc.
-	if (doDelete)
-	{
-		// If not all the messages can be deleted, then don't allow it.
-		continueOn = this.AllSelectedMessagesCanBeDeleted();
-		if (!this.AllSelectedMessagesCanBeDeleted())
-		{
-			continueOn = false;
-			errorMsg = replaceAtCodesInStr(this.text.cannotDeleteAllSelectedMsgsText);
-		}
-	}
-	if (continueOn)
-	{
-		// Determine whether or not to delete the message.  First, if we are to
-		// have the user confirm whether to delete the message, then ask the
-		// user to confirm first.  If we're not to have the user confirm, then
-		// go ahead and delete the message.
-		var confirmDoIt = (typeof(pConfirm) === "boolean" ? pConfirm : true);
-		if (confirmDoIt)
-		{
-			if (promptLocValid)
-			{
-				var promptText = "";
-				if (doDelete)
-					promptText = replaceAtCodesInStr(this.text.delSelectedMsgsConfirmText);
-				else
-					promptText = replaceAtCodesInStr(this.text.undelSelectedMsgsConfirmText);
-				// If the caller wants to clear the remainder of the row where the prompt
-				// text will be, then do it.
-				if (pClearPromptRowAtFirstUse)
-				{
-					// Adding 5 to the prompt text to account for the ? and "[X] " that
-					// will be added when console.noyes() is called
-					var promptTxtLen = console.strlen(promptText) + 5;
-					var numCharsRemaining = 0;
-					if (typeof(pPromptRowWidth) == "number")
-						numCharsRemaining = pPromptRowWidth - promptTxtLen;
-					else
-						numCharsRemaining = console.screen_columns - pPromptLoc.x - promptTxtLen;
-					console.attributes = "N";
-					console.gotoxy(pPromptLoc.x+promptTxtLen, pPromptLoc.y);
-					for (var i = 0; i < numCharsRemaining; ++i)
-						console.print(" ");
-				}
-				// Move the cursor to the prompt location
-				console.gotoxy(pPromptLoc);
-				continueOn = !console.noyes(promptText);
-			}
-		}
-		// If we are to delete/undelete the messages, then do so.
-		if (continueOn)
-		{
-			// TODO: Return status & error message
-			var deleteRetObj = this.DeleteOrUndeleteSelectedMessages(doDelete);
-			allMsgsOpSuccessful = deleteRetObj.opSuccessful;
-			// If all selected messages were successfully deleted, then output
-			// a success message.  Otherwise, output an error.
-			var statusMsg = "";
-			if (deleteRetObj.opSuccessful)
-				statusMsg = "\x01n\x01cAll selected messages were " + (doDelete ? "deleted." : "undeleted.");
-			else
-				statusMsg = "\x01n\x01h\x01y* Failure to " + (doDelete ? "delete" : "undelete") + " all selected messages";
-			if (promptLocValid)
-			{
-				console.gotoxy(pPromptLoc);
-				console.attributes = "N";
-				console.cleartoeol();
-			}
-			else
-				console.crlf();
-			console.print(statusMsg);
-			if (promptLocValid)
-				console.inkey(K_NOSPIN|K_NOCRLF|K_NOECHO, ERROR_PAUSE_WAIT_MS);
-			else
-			{
-				console.crlf();
-				console.pause();
-			}
-		}
-	}
-
-	// If there was an error, then display it
-	if (errorMsg.length > 0)
-	{
-		if (promptLocValid)
-			console.gotoxy(pPromptLoc);
-		console.print(replaceAtCodesInStr(this.text.cannotDeleteAllSelectedMsgsText));
-		if (promptLocValid)
-			console.inkey(K_NOSPIN|K_NOCRLF|K_NOECHO, ERROR_PAUSE_WAIT_MS);
-		else
-		{
-			console.crlf();
-			console.pause();
-		}
-	}
-
-	return allMsgsOpSuccessful;
-}
-
-///////////////////////////////////////////////////////////////////////////////////
-// Methods for message group/sub-board choosing
-
-// For the DigDistMsgReader class: Writes the line of key help at the bottom
-// row of the screen.
-function DigDistMsgReader_WriteLightbarChgMsgAreaKeysHelpLine()
-{
-   console.gotoxy(1, console.screen_rows);
-   //console.print(this.lightbarAreaChooserHelpLine);
-   console.putmsg(this.lightbarAreaChooserHelpLine); // console.putmsg() can process @-codes, which we use for mouse click tracking
-   console.attributes = "N";
-}
-
-// For the DigDistMsgReader class: Outputs the header line to appear above
-// the list of message groups.
-//
-// Parameters:
-//  pNumPages: The number of pages.  This is optional; if this is
-//             not passed, then it won't be used.
-//  pPageNum: The page number.  This is optional; if this is not passed,
-//            then it won't be used.
-function DigDistMsgReader_WriteGrpListTopHdrLine1(pNumPages, pPageNum)
-{
-	var descStr = "Description";
-	if ((typeof(pPageNum) == "number") && (typeof(pNumPages) == "number"))
-		descStr += "    (Page " + pPageNum + " of " + pNumPages + ")";
-	else if ((typeof(pPageNum) == "number") && (typeof(pNumPages) != "number"))
-		descStr += "    (Page " + pPageNum + ")";
-	else if ((typeof(pPageNum) != "number") && (typeof(pNumPages) == "number"))
-		descStr += "    (" + pNumPages + (pNumPages == 1 ? " page)" : " pages)");
-	printf(this.msgGrpListHdrPrintfStr, "Group#", descStr, "# Sub-Boards");
-	console.cleartoeol("\x01n");
-}
-
-// For the DigDistMsgReader class: Outputs the first header line to appear
-// above the sub-board list for a message group.
-//
-// Parameters:
-//  pGrpIndex: The index of the message group (assumed to be valid)
-//  pNumPages: The number of pages.  This is optional; if this is
-//             not passed, then it won't be used.
-//  pPageNum: The page number.  This is optional; if this is not passed,
-//            then it won't be used.
-function DigDistMsgReader_WriteSubBrdListHdrLine(pGrpIndex, pNumPages, pPageNum)
-{
-	var descFormatStr = "\x01n" + this.colors.areaChooserSubBoardHeaderColor + "Sub-boards of \x01h%-25s     \x01n"
-	                  + this.colors.areaChooserSubBoardHeaderColor;
-	if ((typeof(pPageNum) == "number") && (typeof(pNumPages) == "number"))
-		descFormatStr += "(Page " + pPageNum + " of " + pNumPages + ")";
-	else if ((typeof(pPageNum) == "number") && (typeof(pNumPages) != "number"))
-		descFormatStr += "(Page " + pPageNum + ")";
-	else if ((typeof(pPageNum) != "number") && (typeof(pNumPages) == "number"))
-		descFormatStr += "(" + pNumPages + (pNumPages == 1 ? " page)" : " pages)");
-	printf(descFormatStr, msg_area.grp_list[pGrpIndex].description.substr(0, 25));
-	console.cleartoeol("\x01n");
-}
-
-// For the DigDistMsgReader class: Lets the user choose a message group and
-// sub-board via numeric input, using a lightbar interface (if enabled and
-// if the user's terminal uses ANSI) or a traditional user interface.
-function DigDistMsgReader_SelectMsgArea()
-{
-	if (this.msgListUseLightbarListInterface && console.term_supports(USER_ANSI))
-		this.SelectMsgArea_Lightbar();
-	else
-		this.SelectMsgArea_Traditional();
-}
-
-// For the DigDistMsgReader class: Lets the user choose a message group and
-// sub-board via numeric input, using a lightbar user interface.
-//
-// Parameters:
-//  pMsgGrp: Optional boolean - Whether to let the user choose a message group first.
-//           For internal use.  Defaults to true.
-//  pGrpIdx: If pMsgGrp is true, then this specifies the group index so that
-//           sub-boards can be displayed.
-function DigDistMsgReader_SelectMsgArea_Lightbar(pMsgGrp, pGrpIdx)
-{
-	// If there are no message groups, then don't let the user
-	// choose one.
-	if (msg_area.grp_list.length == 0)
-	{
-		console.clear("\x01n");
-		console.print("\x01y\x01hThere are no message groups.\r\n\x01p");
-		return;
-	}
-
-	// Make a backup of the current message group & sub-board indexes so
-	// that later we can tell if the user chose something different.
-	var oldGrp = msg_area.sub[this.subBoardCode].grp_index;
-	var oldSub = msg_area.sub[this.subBoardCode].index;
-
-	var chooseMsgGrp = (typeof(pMsgGrp) == "boolean" ? pMsgGrp : true);
-
-	// This function displays the header line(s) above the list
-	function displayListHdrLines(pStartRow, pChooseMsgGrp, pReader)
-	{
-		console.gotoxy(1, pStartRow);
-		if (pChooseMsgGrp)
-			pReader.WriteGrpListHdrLine1();
-		else
-		{
-			pReader.WriteSubBrdListHdrLine(pGrpIdx);
-			console.gotoxy(1, pStartRow+1);
-			printf(pReader.subBoardListHdrPrintfStr, "Sub #", "Name", "# Posts", "Latest date & time");
-		}
-	}
-
-	// Clear the screen & write the header lines, help line and group list header
-	console.clear("\x01n");
-	this.DisplayAreaChgHdr(1);
-	displayListHdrLines(this.areaChangeHdrLines.length+1, chooseMsgGrp, this);
-	this.WriteChgMsgAreaKeysHelpLine();
-
-	// Create a menu of message groups or sub-boards
-	var msgAreaMenu = (chooseMsgGrp ? this.CreateLightbarMsgGrpMenu() : this.CreateLightbarSubBoardMenu(pGrpIdx));
-	var drawMenu = true;
-	var lastSearchText = "";
-	var lastSearchFoundIdx = -1;
-	var chosenIdx = -1;
-	var continueOn = true;
-	// Let the user choose a group, and also respond to other user choices
-	while (continueOn)
-	{
-		chosenIdx = -1;
-		var msgGrpIdx = msgAreaMenu.GetVal(drawMenu);
-		drawMenu = true;
-		var lastUserInputUpper = (typeof(msgAreaMenu.lastUserInput) == "string" ? msgAreaMenu.lastUserInput.toUpperCase() : msgAreaMenu.lastUserInput);
-		if (typeof(msgGrpIdx) == "number")
-			chosenIdx = msgGrpIdx;
-		// If userChoice is not a number, then it should be null in this case,
-		// and the user would have pressed one of the additional quit keys set
-		// up for the menu.  So look at the menu's lastUserInput and do the
-		// appropriate thing.
-		else if ((lastUserInputUpper == "Q") || (lastUserInputUpper == KEY_ESC)) // Quit
-			continueOn = false;
-		else if ((lastUserInputUpper == "/") || (lastUserInputUpper == CTRL_F)) // Start of find
-		{
-			console.gotoxy(1, console.screen_rows);
-			console.cleartoeol("\x01n");
-			console.gotoxy(1, console.screen_rows);
-			var promptText = "Search: ";
-			console.print(promptText);
-			var searchText = getStrWithTimeout(K_UPPER|K_NOCRLF|K_GETSTR|K_NOSPIN|K_LINE, console.screen_columns - promptText.length - 1, SEARCH_TIMEOUT_MS);
-			lastSearchText = searchText;
-			// If the user entered text, then do the search, and if found,
-			// found, go to the page and select the item indicated by the
-			// search.
-			if (searchText.length > 0)
-			{
-				var oldLastSearchFoundIdx = lastSearchFoundIdx;
-				var oldSelectedItemIdx = msgAreaMenu.selectedItemIdx;
-				var idx = -1;
-				if (chooseMsgGrp)
-					idx = findMsgGrpIdxFromText(searchText, msgAreaMenu.selectedItemIdx);
-				else
-					idx = findSubBoardIdxFromText(pGrpIdx, searchText, msgAreaMenu.selectedItemIdx+1);
-				lastSearchFoundIdx = idx;
-				if (idx > -1)
-				{
-					// Set the currently selected item in the menu, and ensure it's
-					// visible on the page
-					msgAreaMenu.selectedItemIdx = idx;
-					if (msgAreaMenu.selectedItemIdx >= msgAreaMenu.topItemIdx+msgAreaMenu.GetNumItemsPerPage())
-						msgAreaMenu.topItemIdx = msgAreaMenu.selectedItemIdx - msgAreaMenu.GetNumItemsPerPage() + 1;
-					else if (msgAreaMenu.selectedItemIdx < msgAreaMenu.topItemIdx)
-						msgAreaMenu.topItemIdx = msgAreaMenu.selectedItemIdx;
-					else
-					{
-						// If the current index and the last index are both on the same page on the
-						// menu, then have the menu only redraw those items.
-						msgAreaMenu.nextDrawOnlyItems = [msgAreaMenu.selectedItemIdx, oldLastSearchFoundIdx, oldSelectedItemIdx];
-					}
-				}
-				else
-				{
-					if (chooseMsgGrp)
-						idx = findMsgGrpIdxFromText(searchText, 0);
-					else
-						idx = findSubBoardIdxFromText(pGrpIdx, searchText, 0);
-					lastSearchFoundIdx = idx;
-					if (idx > -1)
-					{
-						// Set the currently selected item in the menu, and ensure it's
-						// visible on the page
-						msgAreaMenu.selectedItemIdx = idx;
-						if (msgAreaMenu.selectedItemIdx >= msgAreaMenu.topItemIdx+msgAreaMenu.GetNumItemsPerPage())
-							msgAreaMenu.topItemIdx = msgAreaMenu.selectedItemIdx - msgAreaMenu.GetNumItemsPerPage() + 1;
-						else if (msgAreaMenu.selectedItemIdx < msgAreaMenu.topItemIdx)
-							msgAreaMenu.topItemIdx = msgAreaMenu.selectedItemIdx;
-						else
-						{
-							// The current index and the last index are both on the same page on the
-							// menu, so have the menu only redraw those items.
-							msgAreaMenu.nextDrawOnlyItems = [msgAreaMenu.selectedItemIdx, oldLastSearchFoundIdx, oldSelectedItemIdx];
-						}
-					}
-					else
-					{
-						this.WriteLightbarKeyHelpErrorMsg("Not found");
-						drawMenu = false;
-					}
-				}
-			}
-			else
-				drawMenu = false;
-			this.WriteChgMsgAreaKeysHelpLine();
-		}
-		else if (lastUserInputUpper == "N") // Next search result (requires an existing search term)
-		{
-			// This works but seems a little strange sometimes.
-			// - Should this always start from the selected index?
-			// - If it wraps around to one of the items on the first page,
-			//   should it always set the top index to 0?
-			if ((lastSearchText.length > 0) && (lastSearchFoundIdx > -1))
-			{
-				var oldLastSearchFoundIdx = lastSearchFoundIdx;
-				var oldSelectedItemIdx = msgAreaMenu.selectedItemIdx;
-				// Do the search, and if found, go to the page and select the item
-				// indicated by the search.
-				var idx = 0;
-				if (chooseMsgGrp)
-					idx = findMsgGrpIdxFromText(searchText, lastSearchFoundIdx+1);
-				else
-					idx = findSubBoardIdxFromText(pGrpIdx, searchText, lastSearchFoundIdx+1);
-				if (idx > -1)
-				{
-					lastSearchFoundIdx = idx;
-					// Set the currently selected item in the menu, and ensure it's
-					// visible on the page
-					msgAreaMenu.selectedItemIdx = idx;
-					if (msgAreaMenu.selectedItemIdx >= msgAreaMenu.topItemIdx+msgAreaMenu.GetNumItemsPerPage())
-					{
-						msgAreaMenu.topItemIdx = msgAreaMenu.selectedItemIdx - msgAreaMenu.GetNumItemsPerPage() + 1;
-						if (msgAreaMenu.topItemIdx < 0)
-							msgAreaMenu.topItemIdx = 0;
-					}
-					else if (msgAreaMenu.selectedItemIdx < msgAreaMenu.topItemIdx)
-						msgAreaMenu.topItemIdx = msgAreaMenu.selectedItemIdx;
-					else
-					{
-						// The current index and the last index are both on the same page on the
-						// menu, so have the menu only redraw those items.
-						msgAreaMenu.nextDrawOnlyItems = [msgAreaMenu.selectedItemIdx, oldLastSearchFoundIdx, oldSelectedItemIdx];
-					}
-				}
-				else
-				{
-					if (chooseMsgGrp)
-						idx = findMsgGrpIdxFromText(searchText, 0);
-					else
-						idx = findSubBoardIdxFromText(pGrpIdx, searchText, 0);
-					lastSearchFoundIdx = idx;
-					if (idx > -1)
-					{
-						// Set the currently selected item in the menu, and ensure it's
-						// visible on the page
-						msgAreaMenu.selectedItemIdx = idx;
-						if (msgAreaMenu.selectedItemIdx >= msgAreaMenu.topItemIdx+msgAreaMenu.GetNumItemsPerPage())
-						{
-							msgAreaMenu.topItemIdx = msgAreaMenu.selectedItemIdx - msgAreaMenu.GetNumItemsPerPage() + 1;
-							if (msgAreaMenu.topItemIdx < 0)
-								msgAreaMenu.topItemIdx = 0;
-						}
-						else if (msgAreaMenu.selectedItemIdx < msgAreaMenu.topItemIdx)
-							msgAreaMenu.topItemIdx = msgAreaMenu.selectedItemIdx;
-						else
-						{
-							// The current index and the last index are both on the same page on the
-							// menu, so have the menu only redraw those items.
-							msgAreaMenu.nextDrawOnlyItems = [msgAreaMenu.selectedItemIdx, oldLastSearchFoundIdx, oldSelectedItemIdx];
-						}
-					}
-					else
-					{
-						this.WriteLightbarKeyHelpErrorMsg("Not found");
-						drawMenu = false;
-						this.WriteChgMsgAreaKeysHelpLine();
-					}
-				}
-			}
-			else
-			{
-				this.WriteLightbarKeyHelpErrorMsg("There is no previous search", REFRESH_MSG_AREA_CHG_LIGHTBAR_HELP_LINE);
-				drawMenu = false;
-				this.WriteChgMsgAreaKeysHelpLine();
-			}
-		}
-		else if (lastUserInputUpper == "?") // Show help
-		{
-			this.ShowChooseMsgAreaHelpScreen(true, true);
-			console.pause();
-			// Refresh the screen
-			console.clear("\x01n");
-			console.gotoxy(1, 1);
-			this.DisplayAreaChgHdr(1);
-			displayListHdrLines(this.areaChangeHdrLines.length+1, chooseMsgGrp, this);
-			this.WriteChgMsgAreaKeysHelpLine();
-		}
-		// If the user entered a numeric digit, then treat it as
-		// the start of the message group number.
-		if (lastUserInputUpper.match(/[0-9]/))
-		{
-			// Put the user's input back in the input buffer to
-			// be used for getting the rest of the message number.
-			console.ungetstr(lastUserInputUpper);
-			// Move the cursor to the bottom of the screen and
-			// prompt the user for the message number.
-			console.gotoxy(1, console.screen_rows);
-			console.clearline("\x01n");
-			console.print("\x01cChoose group #: \x01h");
-			var userInput = console.getnum(msg_area.grp_list.length);
-			if (userInput > 0)
-				chosenIdx = userInput - 1;
-			else
-			{
-				// The user didn't make a selection.  So, we need to refresh
-				// the screen due to everything being moved up one line.
-				displayListHdrLines(this.areaChangeHdrLines.length+1, chooseMsgGrp, this);
-				this.WriteChgMsgAreaKeysHelpLine();
-			}
-		}
-
-		// If a group/sub-board was chosen, then deal with it.
-		if (chosenIdx > -1)
-		{
-			// If choosing a message group, then let the user choose a
-			// sub-board within the group.  Otherwise, return the user's
-			// chosen sub-board.
-			if (chooseMsgGrp)
-			{
-				//SelectMsgArea_Lightbar(pMsgGrp, pGrpIdx)
-				// Show a "Loading..." text in case there are many sub-boards in
-				// the chosen message group
-				console.crlf();
-				console.print("\x01nLoading...");
-				console.line_counter = 0; // To prevent a pause before the message list comes up
-				// Ensure that the sub-board printf information is created for
-				// the chosen message group.
-				this.BuildSubBoardPrintfInfoForGrp(chosenIdx);
-				var chosenSubBoardIdx = this.SelectMsgArea_Lightbar(false, chosenIdx);
-				if (chosenSubBoardIdx > -1)
-				{
-					// Set the current group & sub-board
-					bbs.curgrp = chosenIdx;
-					bbs.cursub = chosenSubBoardIdx;
-					continueOn = false;
-				}
-				else
-				{
-					// A sub-board was not chosen, so we'll have to re-draw
-					// the header and list of message groups.
-					displayListHdrLines(this.areaChangeHdrLines.length+1, chooseMsgGrp, this);
-				}
-			}
-			else
-				return chosenIdx; // Return the chosen sub-board index
-		}
-	}
-
-	// If the user chose a different message group & sub-board, then reset the
-	// lister index & cursor variables, as well as this.subBoardCode, etc.
-	if ((bbs.curgrp != oldGrp) || (bbs.cursub != oldSub))
-	{
-		this.tradListTopMsgIdx = -1;
-		this.lightbarListTopMsgIdx = -1;
-		this.lightbarListSelectedMsgIdx = -1;
-		this.lightbarListCurPos = null;
-		this.setSubBoardCode(msg_area.grp_list[bbs.curgrp].sub_list[bbs.cursub].code);
-	}
-}
-
-// For the DigDistMsgReader class: Lets the user choose a message group and
-// sub-board via numeric input, using a traditional user interface.
-function DigDistMsgReader_SelectMsgArea_Traditional()
-{
-	// TODO: Allow searching
-	// If there are no message groups, then don't let the user
-	// choose one.
-	if (msg_area.grp_list.length == 0)
-	{
-		console.clear("\x01n");
-		console.print("\x01y\x01hThere are no message groups.\r\n\x01p");
-		return;
-	}
-
-	// Make a backup of the current message group & sub-board indexes so
-	// that later we can tell if the user chose something different.
-	var oldGrp = msg_area.sub[this.subBoardCode].grp_index;
-	var oldSub = msg_area.sub[this.subBoardCode].index;
-	// Older:
-	/*
-	var oldGrp = bbs.curgrp;
-	var oldSub = bbs.cursub;
-	*/
-
-	// Show the message groups & sub-boards and let the user choose one.
-	var selectedGrp = 0;      // The user's selected message group
-	var selectedSubBoard = 0; // The user's selected sub-board
-	var grpSearchText = "";
-	var continueChoosingMsgGroup = true;
-	while (continueChoosingMsgGroup)
-	{
-		// Clear the BBS command string to make sure there are no extra
-		// commands in there that could cause weird things to happen.
-		bbs.command_str = "";
-
-		console.clear("\x01n");
-		this.DisplayAreaChgHdr();
-		//console.crlf();
-		this.ListMsgGrps(grpSearchText);
-		console.crlf();
-		console.print("\x01n\x01b\x01h" + TALL_UPPER_MID_BLOCK + " \x01n\x01cWhich, \x01h/\x01n\x01c or \x01hCTRL-F\x01n\x01c, \x01hQ\x01n\x01cuit, or [\x01h" +
-		              +(msg_area.sub[this.subBoardCode].grp_index+1) + "\x01n\x01c]: \x01h");
-		// Accept Q (quit), / or CTRL_F (Search) or a file library number
-		selectedGrp = console.getkeys("Q/" + CTRL_F, msg_area.grp_list.length);
-
-		// If the user just pressed enter (selectedGrp would be blank),
-		// default to the current group.
-		if (selectedGrp.toString() == "")
-			selectedGrp = msg_area.sub[this.subBoardCode].grp_index + 1;
-		// Older:
-		/*
-		if (selectedGrp.toString() == "")
-			selectedGrp = bbs.curgrp + 1;
-		*/
-
-		if (selectedGrp.toString() == "Q")
-			continueChoosingMsgGroup = false;
-		// / or CTRL-F: Search
-		else if ((selectedGrp.toString() == "/") || (selectedGrp.toString() == CTRL_F))
-		{
-			console.crlf();
-			var searchPromptText = "\x01n\x01c\x01hSearch\x01g: \x01n";
-			console.print(searchPromptText);
-			var searchText = console.getstr("", console.screen_columns-strip_ctrl(searchPromptText).length-1, K_UPPER|K_NOCRLF|K_GETSTR|K_NOSPIN|K_LINE);
-			if (searchText.length > 0)
-				grpSearchText = searchText;
-		}
-		else
-		{
-			// If the user specified a message group number, then
-			// set it and let the user choose a sub-board within
-			// the group.
-			if (selectedGrp > 0)
-			{
-				// Set the default sub-board #: The current sub-board, or if the
-				// user chose a different group, then this should be set
-				// to the first sub-board.
-				var defaultSubBoard = msg_area.sub[this.subBoardCode].index + 1;
-				if (selectedGrp-1 != msg_area.sub[this.subBoardCode].grp_index)
-					defaultSubBoard = 1;
-				// Older:
-				/*
-				var defaultSubBoard = bbs.cursub + 1;
-				if (selectedGrp-1 != bbs.curgrp)
-					defaultSubBoard = 1;
-				*/
-
-				var subSearchText = "";
-				var continueChoosingSubBoard = true;
-				while (continueChoosingSubBoard)
-				{
-					console.clear("\x01n");
-					this.DisplayAreaChgHdr();
-					this.ListSubBoardsInMsgGroup(selectedGrp-1, defaultSubBoard-1, null, subSearchText);
-					console.crlf();
-					console.print("\x01n\x01b\x01h" + TALL_UPPER_MID_BLOCK + " \x01n\x01cWhich, \x01h/\x01n\x01c or \x01hCTRL-F\x01n\x01c, \x01hQ\x01n\x01cuit, or [\x01h" +
-					              defaultSubBoard + "\x01n\x01c]: \x01h");
-					// Accept Q (quit), / or CTRL_F (Search) or a sub-board number
-					selectedSubBoard = console.getkeys("Q/" + CTRL_F, msg_area.grp_list[selectedGrp - 1].sub_list.length);
-
-					// If the user just pressed enter (selectedSubBoard would be blank),
-					// default the selected directory.
-					if (selectedSubBoard.toString() == "")
-						selectedSubBoard = defaultSubBoard;
-
-					// If the user chose to quit out of the sub-board list, then
-					// return to the message group list.
-					if (selectedSubBoard.toString() == "Q")
-						continueChoosingSubBoard = false;
-					// / or CTRL-F: Search
-					else if ((selectedSubBoard.toString() == "/") || (selectedSubBoard.toString() == CTRL_F))
-					{
-						console.crlf();
-						var searchPromptText = "\x01n\x01c\x01hSearch\x01g: \x01n";
-						console.print(searchPromptText);
-						var searchText = console.getstr("", console.screen_columns-strip_ctrl(searchPromptText).length-1, K_UPPER|K_NOCRLF|K_GETSTR|K_NOSPIN|K_LINE);
-						if (searchText.length > 0)
-							subSearchText = searchText;
-					}
-					// If the user chose a message sub-board, then validate the user's
-					// sub-board choice; if that succeeds, then change the user's
-					// sub-board to that and quit out of the chooser loops.
-					else if (selectedSubBoard > 0)
-					{
-						// Validate the sub-board choice.  If a search is specified, the
-						// validator function will search for messages in the selected
-						// sub-board and will return true if there are messages to read
-						// there or false if not.  If there is no search specified,
-						// the validator function will return a 'true' value.
-						var selectedGrpIdx = selectedGrp - 1;
-						var selectedSubIdx = selectedSubBoard - 1;
-						var msgAreaValidRetval = this.ValidateMsgAreaChoice(selectedGrpIdx, selectedSubIdx);
-						if (msgAreaValidRetval.msgAreaGood)
-						{
-							bbs.curgrp = selectedGrpIdx;
-							bbs.cursub = selectedSubIdx;
-							continueChoosingSubBoard = false;
-							continueChoosingMsgGroup = false;
-						}
-						else
-						{
-							// Output the error returned by the validator function
-							console.print("\x01n\x01h\x01y" + msgAreaValidRetval.errorMsg);
-							mswait(ERROR_PAUSE_WAIT_MS);
-							// Set our loop variables to continue allowing the user to
-							// choose a message sub-board
-							continueChoosingSubBoard = true;
-							continueChoosingMsgGroup = true;
-						}
-					}
-				}
-			}
-		}
-	}
-
-	// If the user chose a different message group & sub-board, then reset the
-	// lister index & cursor variables, as well as this.subBoardCode, etc.
-	//msg_area.sub[this.subBoardCode].grp_index
-	if ((bbs.curgrp != oldGrp) || (bbs.cursub != oldSub))
-	{
-		this.tradListTopMsgIdx = -1;
-		this.lightbarListTopMsgIdx = -1;
-		this.lightbarListSelectedMsgIdx = -1;
-		this.lightbarListCurPos = null;
-		this.setSubBoardCode(msg_area.grp_list[bbs.curgrp].sub_list[bbs.cursub].code);
-	}
-}
-
-// For the DigDistMsgReader class: Lists all message groups (for the traditional
-// user interface).
-//
-// Parameters:
-//  pSearchText: Optional - Search text for the message groups
-function DigDistMsgReader_ListMsgGrps_Traditional(pSearchText)
-{
-	// Print the header
-	this.WriteGrpListHdrLine1();
-	console.attributes = "N";
-
-	var searchText = (typeof(pSearchText) == "string" ? pSearchText.toUpperCase() : "");
-
-	// List the message groups
-	var printIt = true;
-	for (var i = 0; i < msg_area.grp_list.length; ++i)
-	{
-		if (searchText.length > 0)
-			printIt = ((msg_area.grp_list[i].name.toUpperCase().indexOf(searchText) >= 0) || (msg_area.grp_list[i].description.toUpperCase().indexOf(searchText) >= 0));
-		else
-			printIt = true;
-
-		if (printIt)
-		{
-			console.crlf();
-			this.WriteMsgGroupLine(i, false);
-		}
-	}
-}
-
-// For the DigDistMsgReader class: Lists the sub-boards in a message group,
-// for the traditional user interface.
-//
-// Parameters:
-//  pGrpIndex: The index of the message group (0-based)
-//  pMarkIndex: An index of a message group to highlight.  This
-//                   is optional; if left off, this will default to
-//                   the current sub-board.
-//  pSortType: Optional - A string describing how to sort the list (if desired):
-//             "none": Default behavior - Sort by sub-board #
-//             "dateAsc": Sort by date, ascending
-//             "dateDesc": Sort by date, descending
-//             "description": Sort by description
-//  pSearchText: Optional - Search text for the message sub-boards
-function DigDistMsgReader_ListSubBoardsInMsgGroup_Traditional(pGrpIndex, pMarkIndex, pSortType, pSearchText)
-{
-	// Default to the current message group & sub-board if pGrpIndex
-	// and pMarkIndex aren't specified.
-	var grpIndex = bbs.curgrp;
-	if ((pGrpIndex != null) && (typeof(pGrpIndex) == "number"))
-		grpIndex = pGrpIndex;
-	var highlightIndex = bbs.cursub;
-	if ((pMarkIndex != null) && (typeof(pMarkIndex) == "number"))
-		highlightIndex = pMarkIndex;
-
-	// Make sure grpIndex and highlightIndex are valid (they might not be for
-	// brand-new users).
-	if ((grpIndex == null) || (typeof(grpIndex) == "undefined"))
-		grpIndex = 0;
-	if ((highlightIndex == null) || (typeof(highlightIndex) == "undefined"))
-		highlightIndex = 0;
-
-	// Ensure that the sub-board printf information is created for
-	// this message group.
-	this.BuildSubBoardPrintfInfoForGrp(grpIndex);
-
-	// Print the headers
-	this.WriteSubBrdListHdrLine(grpIndex);
-	console.crlf();
-	printf(this.subBoardListHdrPrintfStr, "Sub #", "Name", "# Posts", "Latest date & time");
-	console.attributes = "N";
-
-	// List each sub-board in the message group.
-	var searchText = (typeof(pSearchText) == "string" ? pSearchText.toUpperCase() : "");
-	var subBoardArray = null;       // For sorting, if desired
-	var newestDate = {}; // For storing the date of the newest post in a sub-board
-	var msgBase = null;    // For opening the sub-boards with a MsgBase object
-	var msgHeader = null;  // For getting the date & time of the newest post in a sub-board
-	var subBoardNum = 0;   // 0-based sub-board number (because the array index is the number as a str)
-	var includeSubBoard = true;
-	// If a sort type is specified, then add the sub-board information to
-	// subBoardArray so that it can be sorted.
-	if ((typeof(pSortType) == "string") && (pSortType != "") && (pSortType != "none"))
-	{
-		subBoardArray = [];
-		var subBoardInfo = null;
-		for (var arrSubBoardNum in msg_area.grp_list[grpIndex].sub_list)
-		{
-			if (searchText.length > 0)
-				includeSubBoard = ((msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].name.toUpperCase().indexOf(searchText) >= 0) || (msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].description.toUpperCase().indexOf(searchText) >= 0));
-			else
-				includeSubBoard = true;
-			if (!includeSubBoard)
-				continue;
-
-			// Open the current sub-board with the msgBase object.
-			msgBase = new MsgBase(msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].code);
-			if (msgBase.open())
-			{
-				subBoardInfo = new MsgSubBoardInfo();
-				subBoardInfo.subBoardNum = +(arrSubBoardNum);
-				subBoardInfo.description = msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].description;
-				// Note: numReadableMsgs() is slow because it goes through and
-				// checks for deleted messages, etc., so just use msgBase.total_msgs
-				//subBoardInfo.numPosts = numReadableMsgs(msgBase, msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].code);
-				subBoardInfo.numPosts = msgBase.total_msgs;
-
-				// Get the date & time when the last message was imported.
-				if (subBoardInfo.numPosts > 0)
-				{
-					var msgIdx = msgBase.total_msgs-1;
-					msgHeader = msgBase.get_msg_index(true, msgIdx, false);
-					while (!isReadableMsgHdr(msgHeader, msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].code) && (msgIdx >= 0))
-						msgHeader = msgBase.get_msg_index(true, --msgIdx, true);
-					if (msgHeader != null)
-						msgHeader = msgBase.get_msg_header(true, msgIdx, false);
-					if (msgHeader != null)
-					{
-						if (this.msgAreaList_lastImportedMsg_showImportTime)
-							subBoardInfo.newestPostDate = msgHeader.when_imported_time;
-						else
-						{
-							//subBoardInfo.newestPostDate = msgHeader.when_written_time;
-							var msgWrittenLocalTime = msgWrittenTimeToLocalBBSTime(msgHeader);
-							if (msgWrittenLocalTime != -1)
-								subBoardInfo.newestPostDate = msgWrittenTimeToLocalBBSTime(msgHeader);
-							else
-								subBoardInfo.newestPostDate = msgHeader.when_written_time;
-						}
-					}
-				}
-			}
-			msgBase.close();
-			subBoardArray.push(subBoardInfo);
-		}
-
-		// Possibly sort the sub-board list.
-		if (pSortType == "dateAsc")
-		{
-			subBoardArray.sort(function(pA, pB)
-			{
-				// Return -1, 0, or 1, depending on whether pA's date comes
-				// before, is equal to, or comes after pB's date.
-				var returnValue = 0;
-				if (pA.newestPostDate < pB.newestPostDate)
-					returnValue = -1;
-				else if (pA.newestPostDate > pB.newestPostDate)
-					returnValue = 1;
-				return returnValue;
-			});
-		}
-		else if (pSortType == "dateDesc")
-		{
-			subBoardArray.sort(function(pA, pB)
-			{
-				// Return -1, 0, or 1, depending on whether pA's date comes
-				// after, is equal to, or comes before pB's date.
-				var returnValue = 0;
-				if (pA.newestPostDate > pB.newestPostDate)
-					returnValue = -1;
-				else if (pA.newestPostDate < pB.newestPostDate)
-					returnValue = 1;
-				return returnValue;
-			});
-		}
-		else if (pSortType == "description")
-		{
-			// Binary safe string comparison  
-			// 
-			// version: 909.322
-			// discuss at: http://phpjs.org/functions/strcmp    // +   original by: Waldo Malqui Silva
-			// +      input by: Steve Hilder
-			// +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
-			// +    revised by: gorthaur
-			// *     example 1: strcmp( 'waldo', 'owald' );    // *     returns 1: 1
-			// *     example 2: strcmp( 'owald', 'waldo' );
-			// *     returns 2: -1
-			subBoardArray.sort(function(pA, pB)
-			{
-				return ((pA.description == pB.description) ? 0 : ((pA.description > pB.description) ? 1 : -1));
-			});
-		}
-
-		// Display the sub-board list.
-		for (var i = 0; i < subBoardArray.length; ++i)
-		{
-			console.crlf();
-			console.print((subBoardArray[i].subBoardNum == highlightIndex) ? "\x01n" +
-			              this.colors.areaChooserMsgAreaMarkColor + "*" : " ");
-			printf(this.subBoardListPrintfInfo[grpIndex].printfStr, +(subBoardArray[i].subBoardNum+1),
-			       subBoardArray[i].description.substr(0, this.subBoardNameLen),
-			       subBoardArray[i].numPosts, strftime("%Y-%m-%d", subBoardArray[i].newestPostDate),
-			       strftime("%H:%M:%S", subBoardArray[i].newestPostDate));
-		}
-	}
-	// If no sort type is specified, then output the sub-board information in
-	// order of sub-board number.
-	else
-	{
-		for (var arrSubBoardNum in msg_area.grp_list[grpIndex].sub_list)
-		{
-			if (searchText.length > 0)
-				includeSubBoard = ((msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].name.toUpperCase().indexOf(searchText) >= 0) || (msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].description.toUpperCase().indexOf(searchText) >= 0));
-			else
-				includeSubBoard = true;
-			if (!includeSubBoard)
-				continue;
-
-			// Open the current sub-board with the msgBase object.
-			msgBase = new MsgBase(msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].code);
-			if (msgBase.open())
-			{
-				// Get the date & time when the last message was imported.
-				// Note: numReadableMsgs() is slow because it goes through and
-				// checks for deleted messages, etc., so just use msgBase.total_msgs
-				//var numMsgs = numReadableMsgs(msgBase, msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].code);
-				var numMsgs = msgBase.total_msgs;
-				if (numMsgs > 0)
-				{
-					var msgIdx = msgBase.total_msgs-1;
-					msgHeader = msgBase.get_msg_index(true, msgIdx, false);
-					while (!isReadableMsgHdr(msgHeader, msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].code) && (msgIdx >= 0))
-					  msgHeader = msgBase.get_msg_index(true, --msgIdx, true);
-					if (msgHeader != null)
-						msgHeader = msgBase.get_msg_header(true, msgIdx, false);
-					if (msgHeader != null)
-					{
-						// Construct the date & time strings of the latest post
-						if (this.msgAreaList_lastImportedMsg_showImportTime)
-						{
-							newestDate.date = strftime("%Y-%m-%d", msgHeader.when_imported_time);
-							newestDate.time = strftime("%H:%M:%S", msgHeader.when_imported_time);
-						}
-						else
-						{
-							//newestDate.date = strftime("%Y-%m-%d", msgHeader.when_written_time);
-							//newestDate.time = strftime("%H:%M:%S", msgHeader.when_written_time);
-							var msgWrittenLocalTime = msgWrittenTimeToLocalBBSTime(msgHeader);
-							if (msgWrittenLocalTime != -1)
-							{
-								newestDate.date = strftime("%Y-%m-%d", msgWrittenLocalTime);
-								newestDate.time = strftime("%H:%M:%S", msgWrittenLocalTime);
-							}
-							else
-							{
-								newestDate.date = strftime("%Y-%m-%d", msgHeader.when_written_time);
-								newestDate.time = strftime("%H:%M:%S", msgHeader.when_written_time);
-							}
-						}
-					}
-				}
-				else
-					newestDate.date = newestDate.time = "";
-
-				// Print the sub-board information
-				subBoardNum = +(arrSubBoardNum);
-				console.crlf();
-				console.print((subBoardNum == highlightIndex) ? "\x01n" +
-				              this.colors.areaChooserMsgAreaMarkColor + "*" : " ");
-				printf(this.subBoardListPrintfInfo[grpIndex].printfStr, +(subBoardNum+1),
-				       msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].description.substr(0, this.subBoardListPrintfInfo[grpIndex].nameLen),
-				       numMsgs, newestDate.date, newestDate.time);
-
-				msgBase.close();
-			}
-		}
-	}
-}
-
-//////////////////////////////////////////////
-// Message group list stuff (lightbar mode) //
-//////////////////////////////////////////////
-
-// For the DigDistMsgReader class - Writes a message group information line.
-//
-// Parameters:
-//  pGrpIndex: The index of the message group to write (assumed to be valid)
-//  pHighlight: Boolean - Whether or not to write the line highlighted.
-function DigDistMsgReader_writeMsgGroupLine(pGrpIndex, pHighlight)
-{
-	// TODO: If pHighlight is true, that causes the screen to be cleared
-	// and the line is written on the first row of the console.
-	console.attributes = "N";
-	// Write the highlight background color if pHighlight is true.
-	if (pHighlight)
-	console.print(this.colors.areaChooserMsgAreaBkgHighlightColor);
-
-	// Write the message group information line
-	console.print(((typeof(bbs.curgrp) == "number") && (pGrpIndex == msg_area.sub[this.subBoardCode].grp_index)) ? this.colors.areaChooserMsgAreaMarkColor + "*" : " ");
-	printf((pHighlight ? this.msgGrpListHilightPrintfStr : this.msgGrpListPrintfStr),
-	       +(pGrpIndex+1),
-	       msg_area.grp_list[pGrpIndex].description.substr(0, this.msgGrpDescLen),
-	       msg_area.grp_list[pGrpIndex].sub_list.length);
-	console.cleartoeol("\x01n");
-}
-
-//////////////////////////////////////////////////
-// Message sub-board list stuff (lightbar mode) //
-//////////////////////////////////////////////////
-
-// Updates the page number text in the group list header line on the screen.
-//
-// Parameters:
-//  pPageNum: The page number
-//  pNumPages: The total number of pages
-//  pGroup: Boolean - Whether or not this is for the group header.  If so,
-//          then this will go to the right location for the group page text
-//          and use this.colors.areaChooserMsgAreaHeaderColor for the text.
-//          Otherwise, this will go to the right place for the sub-board page
-//          text and use the sub-board header color.
-//  pRestoreCurPos: Optional - Boolean - If true, then move the cursor back
-//                  to the position where it was before this function was called
-function DigDistMsgReader_updateMsgAreaPageNumInHeader(pPageNum, pNumPages, pGroup, pRestoreCurPos)
-{
-	var originalCurPos = null;
-	if (pRestoreCurPos)
-		originalCurPos = console.getxy();
-
-	if (pGroup)
-	{
-		console.gotoxy(29, 1+this.areaChangeHdrLines.length);
-		console.print("\x01n" + this.colors.areaChooserMsgAreaHeaderColor + pPageNum + " of " +
-		              pNumPages + ")   ");
-	}
-	else
-	{
-		console.gotoxy(51, 1+this.areaChangeHdrLines.length);
-		console.print("\x01n" + this.colors.areaChooserSubBoardHeaderColor + pPageNum + " of " +
-		              pNumPages + ")   ");
-	}
-
-	if (pRestoreCurPos)
-		console.gotoxy(originalCurPos);
-}
-
-// For the DigDistMsgReader class: Returns a formatted string with sub-board
-// information for the message area chooser functionality.
-//
-// Parameters:
-//  pGrpIndex: The index of the message group (assumed to be valid)
-//  pSubIndex: The index of the sub-board within the message group (assumed to be valid)
-//  pHighlight: Boolean - Whether or not to write the line highlighted.
-//
-// Return value: A string with the sub-board information
-function DigDistMsgReader_GetMsgSubBrdLine(pGrpIndex, pSubIndex, pHighlight)
-{
-	// Determine if pGrpIndex and pSubIndex specify the user's
-	// currently-selected group and sub-board.
-	var currentSub = false;
-	if ((typeof(bbs.curgrp) == "number") && (typeof(bbs.cursub) == "number"))
-		currentSub = ((pGrpIndex == msg_area.sub[this.subBoardCode].grp_index) && (pSubIndex == msg_area.sub[this.subBoardCode].index));
-
-	var subBoardStr = "";
-	// Use the highlight background color if pHighlight is true.
-	if (pHighlight)
-		subBoardStr += this.colors.areaChooserMsgAreaBkgHighlightColor;
-
-	var subBoardInfo = getSubBoardInfo(pGrpIndex, pSubIndex, this.msgAreaList_lastImportedMsg_showImportTime);
-	var latestDateStr = strftime("%Y-%m-%d", subBoardInfo.newestTime);
-	var latestTimeStr = strftime("%H:%M:%S", subBoardInfo.newestTime);
-	subBoardStr += (currentSub ? this.colors.areaChooserMsgAreaMarkColor + "*" : " ");
-	subBoardStr += format((pHighlight ? this.subBoardListPrintfInfo[pGrpIndex].highlightPrintfStr : this.subBoardListPrintfInfo[pGrpIndex].printfStr),
-						  +(pSubIndex+1),
-						  msg_area.grp_list[pGrpIndex].sub_list[pSubIndex].description.substr(0, this.subBoardListPrintfInfo[pGrpIndex].nameLen),
-						  subBoardInfo.numItems, latestDateStr, latestTimeStr);
-	return subBoardStr;
-}
-
-///////////////////////////////////////////////
-// Other functions for the msg. area chooser //
-///////////////////////////////////////////////
-
-// For the DigDistMsgReader class: Shows the help screen
-//
-// Parameters:
-//  pLightbar: Boolean - Whether or not to show lightbar help.  If
-//             false, then this function will show regular help.
-//  pClearScreen: Boolean - Whether or not to clear the screen first
-function DigDistMsgReader_showChooseMsgAreaHelpScreen(pLightbar, pClearScreen)
-{
-	if (pClearScreen && console.term_supports(USER_ANSI))
-		console.clear("\x01n");
-	else
-		console.attributes = "N";
-	DisplayProgramInfo();
-	console.crlf();
-	console.print("\x01n\x01c\x01hMessage area (sub-board) chooser");
-	console.crlf();
-	console.print("\x01k" + charStr(HORIZONTAL_SINGLE, 32) + "\x01n");
-	console.crlf();
-	console.print("\x01cFirst, a listing of message groups is displayed.  One can be chosen by typing");
-	console.crlf();
-	console.print("its number.  Then, a listing of sub-boards within that message group will be");
-	console.crlf();
-	console.print("shown, and one can be chosen by typing its number.");
-	console.crlf();
-
-	console.crlf();
-	console.print("Keyboard commands:");
-	console.crlf();
-	console.print("\x01k\x01h" + charStr(HORIZONTAL_SINGLE, 18) + "\x01n");
-	console.crlf();
-	console.print("\x01n\x01c\x01h/\x01n\x01c or \x01hCTRL-F\x01n\x01c: Find group/sub-board");
-	console.crlf();
-	console.print("\x01n\x01c\x01h?\x01n\x01c: Show this help screen");
-	console.crlf();
-	console.print("\x01hQ\x01n\x01c: Quit");
-	console.crlf();
-
-	if (pLightbar)
-	{
-		console.crlf();
-		console.print("\x01n\x01cThe lightbar interface also allows up & down navigation through the lists:");
-		console.crlf();
-		console.print("\x01k\x01h" + charStr(HORIZONTAL_SINGLE, 74));
-		console.crlf();
-		console.print("\x01n\x01c\x01hUp\x01n\x01c/\x01hdown arrow\x01n\x01c: Move the cursor up/down one line");
-		console.crlf();
-		console.print("\x01hPageUp\x01n\x01c/\x01hPageDown\x01n\x01c: Move up/down a page");
-		console.crlf();
-		console.print("\x01hENTER\x01n\x01c: Select the current group/sub-board");
-		console.crlf();
-		console.print("\x01hHOME\x01n\x01c: Go to the first item on the screen");
-		console.crlf();
-		console.print("\x01hEND\x01n\x01c: Go to the last item on the screen");
-		console.crlf();
-		console.print("\x01hF\x01n\x01c: Go to the first page");
-		console.crlf();
-		console.print("\x01hL\x01n\x01c: Go to the last page");
-		console.crlf();
-		console.print("\x01hN\x01n\x01c: Next search result");
-		console.crlf();
-	}
-}
-
-// Builds sub-board printf format information for a message group.
-// The widths of the description & # messages columns are calculated
-// based on the greatest number of messages in a sub-board for the
-// message group.
-//
-// Parameters:
-//  pGrpIndex: The index of the message group
-function DigDistMsgReader_BuildSubBoardPrintfInfoForGrp(pGrpIndex)
-{
-   // If the array of sub-board printf strings doesn't contain the printf
-   // strings for this message group, then figure out the largest number
-   // of messages in the message group and add the printf strings.
-   if (typeof(this.subBoardListPrintfInfo[pGrpIndex]) == "undefined")
-   {
-      var greatestNumMsgs = getGreatestNumMsgs(pGrpIndex);
-
-      this.subBoardListPrintfInfo[pGrpIndex] = {};
-      this.subBoardListPrintfInfo[pGrpIndex].numMsgsLen = greatestNumMsgs.toString().length;
-      // Sub-board name length: With a # items length of 4, this should be
-      // 47 for an 80-column display.
-      this.subBoardListPrintfInfo[pGrpIndex].nameLen = console.screen_columns -
-                                   this.areaNumLen -
-                                   this.subBoardListPrintfInfo[pGrpIndex].numMsgsLen -
-                                   this.dateLen - this.timeLen - 7;
-      // Create the printf strings
-      this.subBoardListPrintfInfo[pGrpIndex].printfStr =
-               " " + this.colors.areaChooserMsgAreaNumColor
-               + "%" + this.areaNumLen + "d "
-               + this.colors.areaChooserMsgAreaDescColor + "%-"
-               + this.subBoardListPrintfInfo[pGrpIndex].nameLen + "s "
-               + this.colors.areaChooserMsgAreaNumItemsColor + "%"
-               + this.subBoardListPrintfInfo[pGrpIndex].numMsgsLen + "d "
-               + this.colors.areaChooserMsgAreaLatestDateColor + "%" + this.dateLen + "s "
-               + this.colors.areaChooserMsgAreaLatestTimeColor + "%" + this.timeLen + "s";
-      this.subBoardListPrintfInfo[pGrpIndex].highlightPrintfStr =
-                              "\x01n" + this.colors.areaChooserMsgAreaBkgHighlightColor + " "
-                              + "\x01n" + this.colors.areaChooserMsgAreaBkgHighlightColor
-                              + this.colors.areaChooserMsgAreaNumHighlightColor
-                              + "%" + this.areaNumLen + "d \x01n"
-                              + this.colors.areaChooserMsgAreaBkgHighlightColor
-                              + this.colors.areaChooserMsgAreaDescHighlightColor + "%-"
-                              + this.subBoardListPrintfInfo[pGrpIndex].nameLen + "s \x01n"
-                              + this.colors.areaChooserMsgAreaBkgHighlightColor
-                              + this.colors.areaChooserMsgAreaNumItemsHighlightColor + "%"
-                              + this.subBoardListPrintfInfo[pGrpIndex].numMsgsLen + "d \x01n"
-                              + this.colors.areaChooserMsgAreaBkgHighlightColor
-                              + this.colors.areaChooserMsgAreaDateHighlightColor + "%" + this.dateLen + "s \x01n"
-                              + this.colors.areaChooserMsgAreaBkgHighlightColor
-                              + this.colors.areaChooserMsgAreaTimeHighlightColor + "%" + this.timeLen + "s\x01n";
-   }
-}
-
-// Returns an array of strings containing extended message header information,
-// such as the kludge lines (for FidoNet-style networks), etc.
-// For each kludge line, there will be a label string for the info line, the
-// info line itself (wrapped to fit the message area width), then a blank
-// line (except for the last kludge line).  The info lines that this method
-// retrieves will only be retrieved if they exist in the given message header.
-//
-// Parameters:
-//  pSubCodeOrMsgbase: An internal sub-board code of the messagebase, or a MsgBase object representing
-//                     the sub-board the message is in (assumed to be open)
-//  pMsgNum: The message number of the message headers to retrieve
-//  pKludgeOnly: Boolean - Whether or not to only get the kludge lines.  If false,
-//               then all header fields will be retrieved.
-//  pUseColors: Optional boolean: Whether or not to add colors to the strings. Defaults to true.
-//  pWordWrap: Optional boolean: Whether or not to word-wrap the header lines to the user's
-//             terminal width. Defaults to true.
-//  pPrependHdrLabelLines: Optional boolean - Whether or not to prepend a couple lines labeling
-//                         that these are header/kludge lines. Defaults to true.
-//
-// Return value: An array of strings containing the extended message header information
-function DigDistMsgReader_GetExtdMsgHdrInfo(pSubCodeOrMsgbase, pMsgNum, pKludgeOnly, pUseColors, pWordWrap, pPrependHdrLabelLines)
-{
-	if (typeof(pMsgNum) !== "number")
-		return [];
-
-	// Get the message header with fields expanded so we can get the most info possible.
-	var msgHdr = null;
-	if (typeof(pSubCodeOrMsgbase) === "string")
-	{
-		var msgbase = new MsgBase(pSubCodeOrMsgbase);
-		if (msgbase.open())
-		{
-			// TODO: I think we should be able to call get_msg_header() and get valid vote information,
-			// but that doesn't seem to be the case:
-			msgHdr = msgbase.get_msg_header(false, pMsgNum, true, true);
-			msgbase.close();
-		}
-	}
-	else if (typeof(pSubCodeOrMsgbase) === "object" && pSubCodeOrMsgbase.hasOwnProperty("get_msg_header") && pSubCodeOrMsgbase.is_open)
-		msgHdr = pSubCodeOrMsgbase.get_msg_header(false, pMsgNum, true, true);
-
-	if (msgHdr == null)
-		return [];
-
-	// The message header retrieved that way might not have vote information,
-	// so copy any additional header information from this.hdrsForCurrentSubBoard
-	// if there's a header there for this message.
-	if (this.msgNumToIdxMap.hasOwnProperty(pMsgNum))
-	{
-		var tmpHdrIdx = this.msgNumToIdxMap[pMsgNum];
-		if (this.hdrsForCurrentSubBoard.hasOwnProperty(tmpHdrIdx))
-		{
-			for (var hdrProp in this.hdrsForCurrentSubBoard[tmpHdrIdx])
-			{
-				if (!msgHdr.hasOwnProperty(hdrProp))
-					msgHdr[hdrProp] = this.hdrsForCurrentSubBoard[tmpHdrIdx][hdrProp];
-			}
-		}
-	}
-
-	var kludgeOnly = (typeof(pKludgeOnly) == "boolean" ? pKludgeOnly : false);
-	var useColors = (typeof(pUseColors) === "boolean" ? pUseColors : true);
-	var wordWrap = (typeof(pWordWrap) === "boolean" ? pWordWrap : true);
-	var prependHdrLabelLines = (typeof(pPrependHdrLabelLines) === "boolean" ? pPrependHdrLabelLines : true);
-
-
-
-	var msgHdrInfoLines = [];
-
-	var formatStr;
-	if (useColors)
-		formatStr = "\x01n" + this.colors.hdrLineLabelColor + "%s: \x01n" + this.colors.hdrLineValueColor + "%-s";
-	else
-		formatStr = "%s: %-s";
-
-	// Make an array of objects with 'prop' and 'textArray' fields; this array will be sorted by prop
-	var msgInfoObjs = [];
-	for (var prop in msgHdr)
-	{
-		if (prop == "field_list")
-		{
-			//var fieldListLines = this.GetMsgHdrFieldListText(msgHdr[prop], true);
-			//for (var i = 0; i < fieldListLines.length; ++i)
-			//	msgHdrInfoLines.push(fieldListLines[i]);
-			
-			var fieldListObj = GetMsgHdrFieldListObj(msgHdr.field_list);
-			var fieldListKeys = Object.keys(msgHdr);
-			fieldListKeys.sort();
-			for (var fieldListProp in fieldListObj)
-			//for (var fieldListKeyIdx = 0; fieldListKeyIdx < fieldListKeys.length; ++fieldListKeyIdx)
-			{
-				//var fieldListProp = fieldListKeys[fieldListKeyIdx];
-				//msgHdrInfoLines.push(format(formatStr, fieldListProp, fieldListObj[fieldListProp]));
-				if (Array.isArray(fieldListObj[fieldListProp]))
-				{
-					var tmpTxtLines = [];
-					if (fieldListObj[fieldListProp].length > 0)
-					{
-						//msgHdrInfoLines.push(format(formatStr, fieldListProp, fieldListObj[fieldListProp][0]));
-						tmpTxtLines.push(format(formatStr, fieldListProp, fieldListObj[fieldListProp][0]));
-						for (var i = 1; i < fieldListObj[fieldListProp].length; ++i)
-						{
-							//msgHdrInfoLines.push(fieldListObj[fieldListProp][i]);
-							tmpTxtLines.push(fieldListObj[fieldListProp][i]);
-						}
-					}
-					msgInfoObjs.push({
-						'prop': fieldListProp,
-						'textArray': tmpTxtLines
-					});
-				}
-				else
-				{
-					//msgHdrInfoLines.push(format(formatStr, fieldListProp, fieldListObj[fieldListProp]));
-					msgInfoObjs.push({
-						'prop': fieldListProp,
-						'textArray': [ format(formatStr, fieldListProp, fieldListObj[fieldListProp]) ]
-					})
-				}
-			}
-			
-		}
-		else
-		{
-			var addIt = kludgeOnly ? MsgHdrPropIsKludgeLine(prop) : true;
-			if (addIt)
-			{
-				// Remove underscores from the property for the label
-				var propLabel = prop.replace(/_/g, " ");
-				// Apply good-looking capitalization to the property label
-				if ((propLabel == "id") || (propLabel == "ftn tid"))
-					propLabel = propLabel.toUpperCase();
-				else if (propLabel == "ftn area")
-					propLabel = "FTN Area";
-				else if (propLabel == "ftn pid")
-					propLabel = "Program ID";
-				else if (propLabel == "thread id")
-					propLabel = "Thread ID";
-				else if (propLabel == "attr")
-					propLabel = "Attributes";
-				else if (propLabel == "auxattr")
-					propLabel = "Auxiliary attributes";
-				else if (propLabel == "netattr")
-					propLabel = "Network attributes";
-				else
-					propLabel = capitalizeFirstChar(propLabel);
-
-				// Value
-				var propValue = "";
-				if (typeof(msgHdr[prop]) === "function") // Such as get_rfc822_header
-					continue;
-				if (prop == "when_written_time") //itemValue = system.timestr(msgHdr.when_written_time);
-					propValue = system.timestr(msgHdr.when_written_time) + " " + system.zonestr(msgHdr.when_written_zone);
-				else if (prop == "when_imported_time") //propValue = system.timestr(msgHdr.when_imported_time);
-					propValue = system.timestr(msgHdr.when_imported_time) + " " + system.zonestr(msgHdr.when_imported_zone);
-				else if ((prop == "when_imported_zone") || (prop == "when_written_zone"))
-					propValue = system.zonestr(msgHdr[prop]);
-				else if (prop == "attr")
-					propValue = makeMainMsgAttrStr(msgHdr[prop], "None");
-				else if (prop == "auxattr")
-					propValue = makeAuxMsgAttrStr(msgHdr[prop], "None");
-				else if (prop == "netattr")
-					propValue = makeNetMsgAttrStr(msgHdr[prop], "None");
-				else
-					propValue = msgHdr[prop];
-				if (typeof(propValue) === "string")
-				{
-					// Replace tabs with spaces, and strip CRLF characters
-					// TODO: Should CRLF characters actually be split into separate lines?
-					propValue = propValue.trim().replace(/\t/g, "  ").replace(/[^\x20-\x7E]/g, '');
-					propValue = propValue.replace(/\r\n/g, "");
-					propValue = propValue.replace(/\n/g, "").replace(/\r/g, "");
-				}
-
-				//msgHdrInfoLines.push(format(formatStr, propLabel, propValue));
-				msgInfoObjs.push({
-					'prop': propLabel,
-					'textArray': [ format(formatStr, propLabel, propValue) ]
-				})
-			}
-		}
-	}
-	// Sort the header lines alphabetically
-	msgInfoObjs.sort(function(pA, pB)
-	{
-		if (pA.prop < pB.prop)
-			return -1;
-		else if (pA.prop == pB.prop)
-			return 0;
-		else
-			return 1;
-	});
-	for (var i = 0; i < msgInfoObjs.length; ++i)
-	{
-		for (var j = 0; j < msgInfoObjs[i].textArray.length; ++j)
-			msgHdrInfoLines.push(msgInfoObjs[i].textArray[j]);
-	}
-	// Free some memory
-	for (var prop in msgInfoObjs)
-		delete msgInfoObjs[prop];
-
-
-	// If the caller wants to word-wrap, make sure the header lines aren't too long for the
-	// user's terminal. And leave a column for the scrollbar.
-	var hdrInfoLinesWrapped;
-	if (wordWrap)
-	{
-		hdrInfoLinesWrapped = [];
-		var maxLen = console.screen_columns - 1;
-		var colorFormatStr = "\x01n" + this.colors.hdrLineLabelColor + "%-s: \x01n" + this.colors.hdrLineValueColor + "%-s";
-		for (var i = 0; i < msgHdrInfoLines.length; ++i)
-		{
-			//var wrappedLines = word_wrap(msgHdrInfoLines[i], maxLen).split("\n");
-			var wrappedLines = lfexpand(word_wrap(msgHdrInfoLines[i], maxLen)).split("\r\n");
-			for (var wrappedI = 0; wrappedI < wrappedLines.length; ++wrappedI)
-			{
-				if (wrappedLines[wrappedI].length == 0) continue;
-				hdrInfoLinesWrapped.push(wrappedLines[wrappedI]);
-			}
-		}
-	}
-	else // No word wrapping
-		hdrInfoLinesWrapped = msgHdrInfoLines;
-
-	// If some info lines were added, then insert a header line & blank line to
-	// the beginning of the array, and remove the last empty line from the array.
-	if (hdrInfoLinesWrapped.length > 0)
-	{
-		if (prependHdrLabelLines)
-		{
-			if (kludgeOnly)
-			{
-				hdrInfoLinesWrapped.splice(0, 0, "\x01n\x01c\x01hMessage Information/Kludge Lines\x01n");
-				hdrInfoLinesWrapped.splice(1, 0, "\x01n\x01g\x01h--------------------------------\x01n");
-			}
-			else
-			{
-				hdrInfoLinesWrapped.splice(0, 0, "\x01n\x01c\x01hMessage Headers\x01n");
-				hdrInfoLinesWrapped.splice(1, 0, "\x01n\x01g\x01h---------------\x01n");
-			}
-		}
-		if (hdrInfoLinesWrapped[hdrInfoLinesWrapped.length-1].length == 0)
-			hdrInfoLinesWrapped.pop();
-	}
-
-	return hdrInfoLinesWrapped;
-}
-
-// For the DDMsgReader class: Helper for GetExtdMsgHdrInfo() - Gets
-// text lines for the field_list property in a message header
-//
-// Parameters:
-//  pHdrFieldList: The value of the field_list property in a message header
-//  pUseColors: Boolean - Whether or not to add attribute codes to the lines
-//
-// Return value: An array with text lines for the field list, with colors
-//               for displaying message header information
-function DigDistMsgReader_GetMsgHdrFieldListText(pHdrFieldList, pUseColors)
-{
-	var textLines = [];
-
-	// This function returns the number of non-blank lines in a header info array.
-	//
-	// Return value: An object with the following properties:
-	//               numNonBlankLines: The number of non-blank lines in the array
-	//               firstNonBlankLineIdx: The index of the first non-blank line
-	//               lastNonBlankLineIdx: The index of the last non-blank line
-	function findHdrFieldDataArrayNonBlankLines(pHdrArray)
-	{
-		var retObj = {
-			numNonBlankLines: 0,
-			firstNonBlankLineIdx: -1,
-			lastNonBlankLineIdx: -1
-		};
-
-		for (var lineIdx = 0; lineIdx < pHdrArray.length; ++lineIdx)
-		{
-			if (pHdrArray[lineIdx].length > 0)
-			{
-				++retObj.numNonBlankLines;
-				if (retObj.firstNonBlankLineIdx == -1)
-					retObj.firstNonBlankLineIdx = lineIdx;
-				retObj.lastNonBlankLineIdx = lineIdx;
-			}
-		}
-
-		return retObj;
-	}
-
-	// Counts the number of elements in a header field_list array with the
-	// same type, starting at a given index.
-	//
-	// Parameters:
-	//  pFieldList: The field_list array in a message header
-	//  pStartIdx: The index of the starting element to start counting at
-	//
-	// Return value: The number of elements with the same type as the start index element
-	function fieldListCountSameTypes(pFieldList, pStartIdx)
-	{
-		if (typeof(pFieldList) == "undefined")
-			return 0;
-		if (typeof(pStartIdx) != "number")
-			return 0;
-		if ((pStartIdx < 0) || (pStartIdx >= pFieldList.length))
-			return 0;
-
-		var itemCount = 1;
-		for (var idx = pStartIdx+1; idx < pFieldList.length; ++idx)
-		{
-			if (pFieldList[idx].type == pFieldList[pStartIdx].type)
-				++itemCount;
-			else
-				break;
-		}
-		return itemCount;
-	}
-
-	var fieldsAndValues = {};
-
-	var hdrFieldLabel = "";
-	var lastHdrFieldLabel = null;
-	var addBlankLineAfterIdx = -1;
-	for (var fieldI = 0; fieldI < pHdrFieldList.length; ++fieldI)
-	{
-		// TODO: Some field types can be in the array multiple times but only
-		// the last is valid.  For those, only get the last one:
-		//  32 (Reply To)
-		//  33 (Reply To agent)
-		//  34 (Reply To net type)
-		//  35 (Reply To net address)
-		//  36 (Reply To extended)
-		//  37 (Reply To position)
-		//  38 (Reply To Organization)
-		if (pUseColors)
-			hdrFieldLabel = "\x01n" + this.colors.hdrLineLabelColor + msgHdrFieldListTypeToLabel(pHdrFieldList[fieldI].type) + "\x01n";
-		else
-			hdrFieldLabel = msgHdrFieldListTypeToLabel(pHdrFieldList[fieldI].type);
-		hdrFieldLabel = hdrFieldLabel.replace(/\t/g, "  ");
-		fieldsAndValues[hdrFieldLabel] = true; // TODO: Change to the actual text
-		var infoLineWrapped = pHdrFieldList[fieldI].data;
-		var infoLineWrappedArray = lfexpand(infoLineWrapped).split("\r\n");
-		var hdrArrayNonBlankLines = findHdrFieldDataArrayNonBlankLines(infoLineWrappedArray);
-		if (hdrArrayNonBlankLines.numNonBlankLines > 0)
-		{
-			if (hdrArrayNonBlankLines.numNonBlankLines == 1)
-			{
-				var addExtraBlankLineAtEnd = false;
-				var hdrItem = "";
-				if (pUseColors)
-					hdrItem = "\x01n" + this.colors.hdrLineValueColor + infoLineWrappedArray[hdrArrayNonBlankLines.firstNonBlankLineIdx] + "\x01n";
-				else
-					hdrItem = infoLineWrappedArray[hdrArrayNonBlankLines.firstNonBlankLineIdx];
-				hdrItem = hdrItem.replace(/\t/g, "  ");
-				// If the header field label is different, then add it to the
-				// header info lines
-				if ((lastHdrFieldLabel == null) || (hdrFieldLabel != lastHdrFieldLabel))
-				{
-					var numFieldItemsWithSameType = fieldListCountSameTypes(pHdrFieldList, fieldI);
-					if (numFieldItemsWithSameType > 1)
-					{
-						textLines.push("");
-						textLines.push(hdrFieldLabel);
-						addExtraBlankLineAtEnd = true;
-						addBlankLineAfterIdx = fieldI + numFieldItemsWithSameType - 1;
-					}
-					else
-					{
-						hdrItem = hdrFieldLabel + " " + hdrItem;
-						numFieldItemsWithSameType = -1;
-					}
-				}
-				textLines.push(hdrItem);
-				/*
-				if (console.strlen((hdrItem) < this.msgAreaWidth)
-					textLines.push(hdrItem);
-				else
-				{
-					// If the header field label is different, then add a blank line
-					// to the header info lines
-					if ((lastHdrFieldLabel == null) || (hdrFieldLabel != lastHdrFieldLabel))
-						textLines.push("");
-					textLines.push(hdrFieldLabel);
-					//textLines.push(infoLineWrappedArray[hdrArrayNonBlankLines.firstNonBlankLineIdx]);
-					if ((lastHdrFieldLabel == null) || (hdrFieldLabel != lastHdrFieldLabel))
-						textLines.push("");
-				}
-				*/
-			}
-			else
-			{
-				// If the header field label is different, then add it to the
-				// header info lines
-				if ((lastHdrFieldLabel == null) || (hdrFieldLabel != lastHdrFieldLabel))
-				{
-					textLines.push("");
-					textLines.push(hdrFieldLabel);
-				}
-				var infoLineWrapped = pHdrFieldList[fieldI].data;
-				var infoLineWrappedArray = lfexpand(infoLineWrapped).split("\r\n");
-				var preAttrs = pUseColors ? "\x01n" + this.colors.hdrLineValueColor : "";
-				var postAttrs = pUseColors ? "\x01n" : "";
-				for (var lineIdx = 0; lineIdx < infoLineWrappedArray.length; ++lineIdx)
-				{
-					if (infoLineWrappedArray[lineIdx].length > 0)
-						textLines.push(preAttrs + infoLineWrappedArray[lineIdx] + postAttrs);
-				}
-				// If the header field label is different, then add a blank line to the
-				// header info lines
-				if ((lastHdrFieldLabel == null) || (hdrFieldLabel != lastHdrFieldLabel))
-					textLines.push("");
-			}
-			if (addBlankLineAfterIdx == fieldI)
-				textLines.push("");
-		}
-		lastHdrFieldLabel = hdrFieldLabel;
-	}
-
-	// For each line, replace tabs with spaces, remove any unprintable characters,
-	// and in case any line has any CRLF characters, split the lines on CRLF
-	for (var i = 0; i < textLines.length; ++i)
-	{
-		textLines[i] = textLines[i].replace(/\t/g, "  ");
-		//textLines[i] = textLines[i].replace(/\r|\n/g, "");
-		var array = textLines[i].split("\r\n");
-		if (array.length > 1)
-		{
-			textLines[i] = array[0];
-			for (var array2Idx = 1; array2Idx < array.length; ++array2Idx)
-			{
-				if (array[array2Idx].length > 0)
-					textLines.splice(i+array2Idx, 0, array[array2Idx]);
-			}
-		}
-		//textLines[i] = textLines[i].replace(/[^\x20-\x7E]/g, ''); // Remove unprintable characters
-	}
-
-	return textLines;
-}
-// Helper for GetExtdMsgHdrInfo() - For the "field_list"
-// property of a message header, this gathers the field labels & values into an
-// object (where the object properties are the labels)
-//
-// Parameters:
-//  pHdrFieldArray: The value of the field_list property in a message header
-//                  (this is normally an array of objects containing 'type' and
-//                  'data' properties)
-//
-// Return value: An object where the properties are the field labels, and the
-//               value for each is usually an array of strings
-function GetMsgHdrFieldListObj(pHdrFieldArray)
-{
-	if (!Array.isArray(pHdrFieldArray))
-		return {};
-
-	// This function returns the number of non-blank lines in a header info array.
-	//
-	// Return value: An object with the following properties:
-	//               numNonBlankLines: The number of non-blank lines in the array
-	//               firstNonBlankLineIdx: The index of the first non-blank line
-	//               lastNonBlankLineIdx: The index of the last non-blank line
-	function findHdrFieldDataArrayNonBlankLines(pHdrArray)
-	{
-		var retObj = {
-			numNonBlankLines: 0,
-			firstNonBlankLineIdx: -1,
-			lastNonBlankLineIdx: -1
-		};
-
-		for (var lineIdx = 0; lineIdx < pHdrArray.length; ++lineIdx)
-		{
-			if (pHdrArray[lineIdx].length > 0)
-			{
-				++retObj.numNonBlankLines;
-				if (retObj.firstNonBlankLineIdx == -1)
-					retObj.firstNonBlankLineIdx = lineIdx;
-				retObj.lastNonBlankLineIdx = lineIdx;
-			}
-		}
-
-		return retObj;
-	}
-
-	var fieldListObj = {};
-	for (var fieldI = 0; fieldI < pHdrFieldArray.length; ++fieldI)
-	{
-		// TODO: Some field types can be in the array multiple times but only
-		// the last is valid.  For those, only get the last one:
-		//  32 (Reply To)
-		//  33 (Reply To agent)
-		//  34 (Reply To net type)
-		//  35 (Reply To net address)
-		//  36 (Reply To extended)
-		//  37 (Reply To position)
-		//  38 (Reply To Organization)
-		var hdrFieldLabel = msgHdrFieldListTypeToLabel(pHdrFieldArray[fieldI].type, false);
-		hdrFieldLabel = hdrFieldLabel.replace(/\t/g, "  ");
-
-		// Add the data to fieldListObj, with the label as the property
-		//fieldListObj[hdrFieldLabel] = pHdrFieldArray[fieldI].data.replace(/\t/g, "  ");
-		// Make the data an array of strings, split based on CRLF characters
-		var infoLineWrappedArray = lfexpand(pHdrFieldArray[fieldI].data).replace(/\t/g, "  ").split("\r\n");
-		// If any of the strings starts with a date/time ("WhenExported" or "WhenImported"),
-		// then format it so that the date & time are more readable
-		for (var i = 0; i < infoLineWrappedArray.length; ++i)
-		{
-			if ((infoLineWrappedArray[i].indexOf("WhenExported") == 0 || infoLineWrappedArray[i].indexOf("WhenImported") == 0) && infoLineWrappedArray[i].length >= 28)
-			{
-				//system.timestr(msgHdr.when_imported_time) + " " + system.zonestr(msgHdr.when_imported_zone)
-				var firstPart = infoLineWrappedArray[i].substr(0, 14);
-				var yearStr = infoLineWrappedArray[i].substr(14, 4);
-				var monthStr = infoLineWrappedArray[i].substr(18, 2);
-				var dayStr = infoLineWrappedArray[i].substr(20, 2);
-				var hourStr = infoLineWrappedArray[i].substr(22, 2);
-				var minStr = infoLineWrappedArray[i].substr(24, 2);
-				var secStr = infoLineWrappedArray[i].substr(26, 2);
-				var remaining = infoLineWrappedArray[i].substr(28);
-				infoLineWrappedArray[i] = format("%s%s-%s-%s %s:%s:%s %s", firstPart, yearStr, monthStr, dayStr, hourStr, minStr, secStr, remaining);
-			}
-		}
-		if (!fieldListObj.hasOwnProperty(hdrFieldLabel))
-			fieldListObj[hdrFieldLabel] = infoLineWrappedArray;
-		else
-		{
-			// Append to the data already there
-			fieldListObj[hdrFieldLabel].push("");
-			for (var i = 0; i < infoLineWrappedArray.length; ++i)
-				fieldListObj[hdrFieldLabel].push(infoLineWrappedArray[i]);
-		}
-	}
-
-	return fieldListObj;
-}
-
-// Returns whether a message header property name can be considered a "kludge line"
-function MsgHdrPropIsKludgeLine(pPropName)
-{
-	if (typeof(pPropName) !== "string" || pPropName.length == 0)
-		return false;
-
-	var propNameUpper = pPropName.toUpperCase();
-	return (propNameUpper == "FTN_MSGID" || propNameUpper == "FTN_REPLY" ||
-	        propNameUpper == "FTN_AREA" || propNameUpper == "FTN_FLAGS" ||
-		propNameUpper == "FTN_PID" || propNameUpper == "FTN_TID" ||
-	        propNameUpper == "WHEN_WRITTEN_TIME" || propNameUpper == "WHEN_IMPORTED_TIME");
-}
-
-// For the DigDistMsgReader class: Gets & prepares message information for
-// the enhanced reader.
-//
-// Parameters:
-//  pMsgHdr: The message header
-//  pWordWrap: Boolean - Whether or not to word-wrap the message to fit into the
-//             display area.  This is optional and defaults to true.  This should
-//             be true for normal use; the only time this should be false is when
-//             saving the message to a file.
-//  pDetermineAttachments: Boolean - Whether or not to parse the message text to
-//                         get attachments from it.  This is optional and defaults
-//                         to true.  If false, then the message text will be left
-//                         intact with any base64-encoded attachments that may be
-//                         in the message text (for multi-part MIME messages).
-//  pGetB64Data: Boolean - Whether or not to get the Base64-encoded data for
-//               base64-encoded attachments (i.e., in multi-part MIME emails).
-//               This is optional and defaults to true.  This is only used when
-//               pDetermineAttachments is true.
-//  pMsgBody: Optional - A string containing the message body.  If this is not included
-//            or is not a string, then this method will retrieve the message body.
-//  pMsgHasANSICodes: Optional boolean - If the caller already knows whether the
-//                    message text has ANSI codes, the caller can pass this parameter.
-//
-// Return value: An object with the following properties:
-//               msgText: The unaltered message text
-//               messageLines: An array containing the message lines, wrapped to
-//                             the message area width
-//               topMsgLineIdxForLastPage: The top message line index for the last page
-//               msgFractionShown: The fraction of the message shown
-//               numSolidScrollBlocks: The number of solid scrollbar blocks
-//               numNonSolidScrollBlocks: The number of non-solid scrollbar blocks
-//               solidBlockStartRow: The starting row on the screen for the scrollbar blocks
-//               hasAttachments: Boolean - Whether or not the message has attachments
-//               attachments: An array of the attached filenames (as strings)
-//               errorMsg: An error message, if something bad happened
-function DigDistMsgReader_GetMsgInfoForEnhancedReader(pMsgHdr, pWordWrap, pDetermineAttachments,
-                                                      pGetB64Data, pMsgBody)
-{
-	var retObj = {
-		msgText: "",
-		messageLines: [],
-		topMsgLineIdxForLastPage: 0,
-		msgFractionShown: 0.0,
-		numSolidScrollBlocks: 0,
-		numNonSolidScrollBlocks: 0,
-		solidBlockStartRow: 0,
-		hasAttachments: false,
-		attachments: [],
-		errorMsg: ""
-	};
-
-	var determineAttachments = (typeof(pDetermineAttachments) == "boolean" ? pDetermineAttachments : true);
-	var getB64Data = (typeof(pGetB64Data) == "boolean" ? pGetB64Data : true);
-	var msgBody = "";
-	if (typeof(pMsgBody) == "string")
-		msgBody = pMsgBody;
-	else
-	{
-		var msgbase = new MsgBase(this.subBoardCode);
-		if (msgbase.open())
-		{
-			msgBody = msgbase.get_msg_body(false, pMsgHdr.number, false, false, true, true);
-			msgbase.close();
-		}
-		else
-		{
-			retObj.errorMsg = "Unable to open the sub-board";
-			return retObj;
-		}
-	}
-	retObj.msgText = word_wrap(msgBody, console.screen_columns - 1, true);
-
-	var msgTextAltered = retObj.msgText; // Will alter the message text, but not yet
-	// Only interpret @-codes if the user is reading personal email and the sender is a sysop.  There
-	// are many @-codes that do some action such as move the cursor, execute a
-	// script, etc., and I don't want users on message networks to do anything
-	// malicious to users on other BBSes.
-	if (this.readingPersonalEmail && msgSenderIsASysop(pMsgHdr))
-		msgTextAltered = replaceAtCodesInStr(msgTextAltered); // Or this.ParseMsgAtCodes(msgTextAltered, pMsgHdr) to replace only some @ codes
-	msgTextAltered = msgTextAltered.replace(/\t/g, this.tabReplacementText);
-	// Convert other BBS color codes to Synchronet attribute codes if the settings
-	// to do so are enabled.
-	msgTextAltered = convertAttrsToSyncPerSysCfg(msgTextAltered, false);
-	// If configured to convert Y-style MCI attribute codes to Synchronet attribute codes, then do so
-	if (this.convertYStyleMCIAttrsToSync)
-		msgTextAltered = YStyleMCIAttrsToSyncAttrs(msgTextAltered);
-
-	// If this is a message with a "By: <name> to <name>" and a date, then
-	// sometimes such a message might have enter characters (ASCII 13), which
-	// can mess up the display of the message, so remove enter characters
-	// from the beginning of the message.
-	var msgTextWithoutAttrs = strip_ctrl(msgTextAltered);
-	var fromToSearchStr = "By: " + pMsgHdr.from + " to " + pMsgHdr.to;
-	var toFromSearchStr = "By: " + pMsgHdr.to + " to " + pMsgHdr.from;
-	var fromToStrIdx = msgTextWithoutAttrs.indexOf(fromToSearchStr);
-	var toFromStrIdx = msgTextWithoutAttrs.indexOf(toFromSearchStr);
-	var strIdx = -1;
-	if (fromToStrIdx > -1)
-		strIdx = fromToStrIdx;
-	else if (toFromStrIdx > -1)
-		strIdx = toFromStrIdx;
-	if (strIdx > -1)
-	{
-		// " on Mon Feb 13 2017 01:00 pm " // 29 characters long
-		strIdx += toFromSearchStr.length + 29 + 37; // 37: Extra room for Synchronet attribute codes
-		// Remove enter characters from the beginning of the message
-		var tmpStr = msgTextAltered.substring(0, strIdx).replace(ascii(13), "");
-		msgTextAltered = tmpStr + msgTextAltered.substr(strIdx);
-		// To remove the "By: <name> to <name> on <date>" lines altogether:
-		//msgTextAltered = msgTextAltered.substr(strIdx);
-	}
-
-	// ASCII 0x8D (141 decimal) is considered a soft-CR character in FidoNet.  In
-	// echocfg, under Global Settings, the setting "Strip Incoming Soft-CRs" can
-	// be enabled to remove those characters (that are not UTF-8
-	// encoded).
-	//msgTextAltered = msgTextAltered.replace(new RegExp("\x8D", "g"), ""); // 'i' with accent; hard word wrap character
-	//msgTextAltered = msgTextAltered.replace(new RegExp("\x8D", "g"), "\r\n"); // 'i' with accent; hard word wrap character
-	// This PDF lists some characters without any description - Are these normally non-printable?
-	// https://www.utm.edu/staff/lholder/csci201/ascii_table.pdf
-	// 0x8D (141) is 'i' with accent; hard word wrap character
-	//msgTextAltered = msgTextAltered.replace(new RegExp("[\x81\x8D\x8F\x90\x9D]", "g"), "");
-
-	var wordWrapTheMsgText = true;
-	if (typeof(pWordWrap) == "boolean")
-		wordWrapTheMsgText = pWordWrap;
-	if (wordWrapTheMsgText)
-	{
-		// Wrap the text to fit into the available message area.
-		// Note: In Synchronet 3.15 (and some beta builds of 3.16), there seemed to
-		// be a bug in the word_wrap() function where the word wrap length in Linux
-		// was one less than Windows, so if the BBS is running 3.15 or earlier of
-		// Synchronet, add 1 to the word wrap length if running in Linux.
-		var textWrapLen = this.msgAreaWidth;
-		if (system.version_num <= 31500)
-			textWrapLen = gRunningInWindows ? this.msgAreaWidth : this.msgAreaWidth + 1;
-		var msgTextWrapped = word_wrap(msgTextAltered, textWrapLen);
-		retObj.messageLines = lfexpand(msgTextWrapped).split("\r\n");
-		// Go through the message lines and trim them to ensure they'll easily fit
-		// in the message display area without having to trim them later.  (Note:
-		// this is okay to do since we're only using messageLines to display the
-		// message on the screen; messageLines isn't used for quoting/replying).
-		for (var msgLnIdx = 0; msgLnIdx < retObj.messageLines.length; ++msgLnIdx)
-			retObj.messageLines[msgLnIdx] = shortenStrWithAttrCodes(retObj.messageLines[msgLnIdx], this.msgAreaWidth);
-		// Set up some variables for displaying the message
-		retObj.topMsgLineIdxForLastPage = retObj.messageLines.length - this.msgAreaHeight;
-		if (retObj.topMsgLineIdxForLastPage < 0)
-			retObj.topMsgLineIdxForLastPage = 0;
-		// Variables for the scrollbar to show the fraction of the message shown
-		retObj.msgFractionShown = this.msgAreaHeight / retObj.messageLines.length;
-		if (retObj.msgFractionShown > 1)
-			retObj.msgFractionShown = 1.0;
-		retObj.numSolidScrollBlocks = Math.floor(this.msgAreaHeight * retObj.msgFractionShown);
-		if (retObj.numSolidScrollBlocks == 0)
-			retObj.numSolidScrollBlocks = 1;
-	}
-	else
-	{
-		retObj.messageLines = [];
-		retObj.messageLines.push(msgTextAltered);
-	}
-	retObj.numNonSolidScrollBlocks = this.msgAreaHeight - retObj.numSolidScrollBlocks;
-	retObj.solidBlockStartRow = this.msgAreaTop;
-
-	return retObj;
-}
-
-// Shows a message hex dump with a scrollable interface
-//
-// Parameters:
-//  pMsgHexInfo: An object with message hex & scrollbar information, as returned by GetMsgHexInfo()
-function DigDistMsgReader_ShowMsgHex_Scrolling(pMsgHexInfo)
-{
-	if (typeof(pMsgHexInfo) !== "object" || !Array.isArray(pMsgHexInfo.msgHexArray) || pMsgHexInfo.msgHexArray.length == 0)
-		return;
-
-	var msgReaderObj = this;
-	var lastInfoSolidBlockStartRow = this.msgAreaTop;
-
-	// This is a scrollbar update function for use when viewing the header info/kludge lines.
-	function msgHexScrollbarUpdateFn(pFractionToLastPage)
-	{
-		var infoSolidBlockStartRow = msgReaderObj.msgAreaTop + Math.floor(pMsgHexInfo.numNonSolidScrollBlocks * pFractionToLastPage);
-		if (infoSolidBlockStartRow != lastInfoSolidBlockStartRow)
-			msgReaderObj.UpdateEnhancedReaderScrollbar(infoSolidBlockStartRow, lastInfoSolidBlockStartRow, pMsgHexInfo.numSolidScrollBlocks);
-		lastInfoSolidBlockStartRow = infoSolidBlockStartRow;
-		console.gotoxy(1, console.screen_rows);
-	}
-
-	if (pMsgHexInfo.msgHexArray.length > 0)
-	{
-		var msgAreaWidth = this.userSettings.useEnhReaderScrollbar ? this.msgAreaWidth : this.msgAreaWidth + 1;
-		var msgAreaHeight = this.msgAreaBottom - this.msgAreaTop + 1;
-		if (this.userSettings.useEnhReaderScrollbar)
-			this.DisplayEnhancedReaderWholeScrollbar(pMsgHexInfo.solidBlockStartRow, pMsgHexInfo.numSolidScrollBlocks);
-		scrollTextLines(pMsgHexInfo.msgHexArray, 0, this.colors.msgBodyColor, true,
-		                this.msgAreaLeft, this.msgAreaTop, msgAreaWidth, msgAreaHeight,
-		                1, console.screen_rows, this.userSettings.useEnhReaderScrollbar,
-		                msgHexScrollbarUpdateFn);
-	}
-}
-
-// Gets message hex dump information, including scrollbar information for the hex lines
-//
-// Parameters:
-//  pMessageText: The text of the message
-//  pRemoveTrailingCRLF: Optional boolean - Whether or not to remove any trailing CR or LF characters.
-//                       Defaults to false.
-//
-// Return value: An object with the following properties:
-//               msgHexArray: An array of text lines representing the hex dump of the message
-//               topLineIdxForLastPage: For scrolling, the index of the line for the top of the last page
-//               msgFractionShown: For scrolling, the fraction of the lines shown
-//               numSolidScrollBlocks: For scrolling, the number of solid srollbar blocks to use
-//               numNonSolidScrollBlocks: For scrolling, the number of non-solid scrollbar blocks to use
-//               solidBlockStartRow: For scrolling, the row on the screen to start the scrollbar at
-function DigDistMsgReader_GetMsgHexInfo(pMessageText, pRemoveTrailingCRLF)
-{
-	var retObj = {
-		msgHexArray: [],
-		topLineIdxForLastPage: 0,
-		msgFractionShown: 0.0,
-		numSolidScrollBlocks: 0,
-		numNonSolidScrollBlocks: 0,
-		solidBlockStartRow: 0
-	};
-
-	var hexArray = hexdump.generate(undefined, pMessageText, /* ASCII: */true, /* offsets: */true);
-	if (Array.isArray(hexArray) && hexArray.length > 0)
-	{
-		if (typeof(pRemoveTrailingCRLF) === "boolean" && pRemoveTrailingCRLF)
-		{
-			// Remove the trailing CR (and possibly LF) from the last Line
-			hexArray[hexArray.length-1] = hexArray[hexArray.length-1].replace(/[\r\n]+$/, "");
-		}
-
-		retObj.msgHexArray = hexArray;
-		retObj.topLineIdxForLastPage = retObj.msgHexArray.length - this.msgAreaHeight;
-		if (retObj.topLineIdxForLastPage < 0)
-			retObj.topLineIdxForLastPage = 0;
-		// Variables for the scrollbar to show the fraction of the message shown
-		retObj.msgFractionShown = this.msgAreaHeight / retObj.msgHexArray.length;
-		if (retObj.msgFractionShown > 1)
-			retObj.msgFractionShown = 1.0;
-		retObj.numSolidScrollBlocks = Math.floor(this.msgAreaHeight * retObj.msgFractionShown);
-		if (retObj.numSolidScrollBlocks == 0)
-			retObj.numSolidScrollBlocks = 1;
-		retObj.numNonSolidScrollBlocks = this.msgAreaHeight - retObj.numSolidScrollBlocks;
-		retObj.solidBlockStartRow = this.msgAreaTop;
-	}
-
-	return retObj;
-}
-
-// For the DDMsgReader class: Saves a message hex dump to a file on the BBS machine with the given filename
-//
-// Parameters:
-//  pMsgHdr: The header of the message
-//  pOutFilename: The full path & filename of the file to save the hex dump to
-//
-// Return value: An object with the following properties:
-//               saveSucceeded: Boolean - Whether or not the save succeeded
-//               errorMsg: A string containing an error on failure
-function DigDistMsgReader_SaveMsgHexDumpToFile(pMsgHdr, pOutFilename)
-{
-	var retObj = {
-		saveSucceeded: false,
-		errorMsg: ""
-	};
-
-	if (typeof(pMsgHdr) !== "object" || typeof(pOutFilename) !== "string")
-	{
-		retObj.errorMsg = "Invalid parameter given";
-		return retObj;
-	}
-
-	var msgText = "";
-	var hdrLines = null;
-	var msgbase = new MsgBase(this.subBoardCode);
-	if (msgbase.open())
-	{
-		msgText = msgbase.get_msg_body(false, pMsgHdr.number);
-		hdrLines = this.GetExtdMsgHdrInfo(msgbase, pMsgHdr.number, false, false, false, false);
-		msgbase.close();
-	}
-
-	var hexLines = hexdump.generate(undefined, msgText, /* ASCII: */true, /* offsets: */true);
-	if (Array.isArray(hexLines) && hexLines.length > 0)
-	{
-		var outFile = new File(pOutFilename);
-		if (outFile.open("w"))
-		{
-			// Write the message header lines
-			if (Array.isArray(hdrLines) && hdrLines.length > 0)
-			{
-				for (var hdrI = 0; hdrI < hdrLines.length; ++hdrI)
-					outFile.writeln(hdrLines[hdrI]);
-			}
-			else
-			{
-				outFile.writeln("From: " + pMsgHdr.from);
-				outFile.writeln("To: " + pMsgHdr.to);
-				outFile.writeln("Subject: " + pMsgHdr.subject);
-				// Message time
-				var msgWrittenLocalTime = msgWrittenTimeToLocalBBSTime(pMsgHdr);
-				var dateTimeStr = "";
-				if (msgWrittenLocalTime != -1)
-					dateTimeStr = strftime("%a, %d %b %Y %H:%M:%S", msgWrittenLocalTime);
-				else
-					dateTimeStr = pMsgHdr.date.replace(/ [-+][0-9]+$/, "");
-				outFile.writeln("Date: " + dateTimeStr);
-			}
-			outFile.writeln("=================================");
-			// Write the hex dump
-			for (var hexI = 0; hexI < hexLines.length; ++hexI)
-				outFile.writeln(hexLines[hexI]);
-			outFile.close();
-			retObj.saveSucceeded = true;
-		}
-		else
-			retObj.errorMsg = "File write failed";
-	}
-	else
-		errorMsg = "Hex dump is not available";
-
-	return retObj;
-}
-
-// For the DigDistMsgReader class: Returns the index of the last read message in
-// the current message area.  If reading personal email, this will look at the
-// search results.  Otherwise, this will use the sub-board's last_read pointer.
-// If there is no last read message or if there is a problem getting the last read
-// message index, this method will return -1.
-//
-// Parameters:
-//  pMailStartFromFirst: Optional boolean - Whether or not to start from the
-//                       first message (rather than from the last message) if
-//                       reading personal email.  Will stop looking at the first
-//                       unread message.  Defaults to false.
-//
-// Return value: An object containing the following properties:
-//               lastReadMsgIdx: The index of the last read message in the current message area
-//               lastReadMsgNum: The number of the last read message in the current message area
-function DigDistMsgReader_GetLastReadMsgIdxAndNum(pMailStartFromFirst)
-{
-	var retObj = {
-		lastReadMsgIdx: -1,
-		lastReadMsgNum: -1
-	};
-
-	if (this.readingPersonalEmail)
-	{
-		if (this.SearchingAndResultObjsDefinedForCurSub())
-		{
-			var startFromFirst = (typeof(pMailStartFromFirst) == "boolean" ? pMailStartFromFirst : false);
-			if (startFromFirst)
-			{
-				for (var idx = 0; idx < this.msgSearchHdrs[this.subBoardCode].indexed.length; ++idx)
-				{
-					if ((this.msgSearchHdrs[this.subBoardCode].indexed[idx].attr & MSG_READ) == MSG_READ)
-					{
-						retObj.lastReadMsgIdx = idx;
-						retObj.lastReadMsgNum = this.msgSearchHdrs[this.subBoardCode].indexed[idx].number;
-					}
-					else
-						break;
-				}
-			}
-			else
-			{
-				for (var idx = this.msgSearchHdrs[this.subBoardCode].indexed.length-1; idx >= 0; --idx)
-				{
-					if ((this.msgSearchHdrs[this.subBoardCode].indexed[idx].attr & MSG_READ) == MSG_READ)
-					{
-						retObj.lastReadMsgIdx = idx;
-						retObj.lastReadMsgNum = this.msgSearchHdrs[this.subBoardCode].indexed[idx].number;
-						break;
-					}
-				}
-			}
-			// Sanity checking for retObj.lastReadMsgIdx (note: this function should return -1 if
-			// there is no last read message).
-			if (retObj.lastReadMsgIdx >= this.msgSearchHdrs[this.subBoardCode].indexed.length)
-			{
-				retObj.lastReadMsgIdx = this.msgSearchHdrs[this.subBoardCode].indexed.length - 1;
-				retObj.lastReadMsgNum = this.msgSearchHdrs[this.subBoardCode].indexed[retObj.lastReadMsgIdx].number;
-			}
-		}
-	}
-	else
-	{
-		//retObj.lastReadMsgIdx = this.AbsMsgNumToIdx(msg_area.sub[this.subBoardCode].last_read);
-		retObj.lastReadMsgIdx = this.GetMsgIdx(msg_area.sub[this.subBoardCode].last_read);
-		retObj.lastReadMsgNum = msg_area.sub[this.subBoardCode].last_read;
-		/*
-		this.hdrsForCurrentSubBoard = [];
-		// hdrsForCurrentSubBoardByMsgNum is an object that maps absolute message numbers
-		// to their index to hdrsForCurrentSubBoard
-		this.msgNumToIdxMap = {};
-		*/
-		// Sanity checking for retObj.lastReadMsgIdx (note: this function should return -1 if
-		// there is no last read message).
-		var msgbase = new MsgBase(this.subBoardCode);
-		if (msgbase.open())
-		{
-			// If retObj.lastReadMsgIdx is -1, as a result of GetMsgIdx(), then see what the last read
-			// message index is according to the Synchronet message base.  If
-			// this.hdrsForCurrentSubBoard.length has been populated, then if the last
-			// message index according to Synchronet is greater than that, then set the
-			// message index to the last index in this.hdrsForCurrentSubBoard.length.
-			if (retObj.lastReadMsgIdx == -1)
-			{
-				var msgIdxAccordingToMsgbase = absMsgNumToIdxWithMsgbaseObj(msgbase, msg_area.sub[this.subBoardCode].last_read);
-				if ((this.hdrsForCurrentSubBoard.length > 0) && (msgIdxAccordingToMsgbase >= this.hdrsForCurrentSubBoard.length))
-				{
-					retObj.lastReadMsgIdx = this.hdrsForCurrentSubBoard.length - 1;
-					retObj.lastReadMsgNum = this.hdrsForCurrentSubBoard[retObj.lastReadMsgIdx].number;
-				}
-			}
-			//if (retObj.lastReadMsgIdx >= msgbase.total_msgs)
-			//	retObj.lastReadMsgIdx = msgbase.total_msgs - 1;
-			// TODO: Is this code right?  Modified 3/24/2015 to replace
-			// the above 2 commented lines.
-			if ((retObj.lastReadMsgIdx < 0) || (retObj.lastReadMsgIdx >= msgbase.total_msgs))
-			{
-				// Look for the first message not marked as deleted
-				var readableMsgIdx = this.FindNextReadableMsgIdx(0, true);
-				// If a non-deleted message was found, then set the last read
-				// pointer to it.
-				if (readableMsgIdx > -1)
-				{
-					var newLastRead = this.IdxToAbsMsgNum(readableMsgIdx);
-					if (newLastRead > -1)
-						msg_area.sub[this.subBoardCode].last_read = newLastRead;
-					else
-						msg_area.sub[this.subBoardCode].last_read = 0;
-				}
-				else
-					msg_area.sub[this.subBoardCode].last_read = 0;
-			}
-		}
-	}
-	return retObj;
-}
-
-// For the DigDistMsgReader class: Returns the index of the message pointed to
-// by the scan pointer in the current sub-board.  If reading personal email or
-// if the message base isn't open, this will return 0.  If the scan pointer is
-// 0 or if the messagebase is open and the scan pointer is invalid, this will
-// return -1.
-function DigDistMsgReader_GetScanPtrMsgIdx()
-{
-	if (this.readingPersonalEmail)
-		return 0;
-	if (msg_area.sub[this.subBoardCode].scan_ptr == 0)
-		return -1;
-
-	// If the user's scan pointer is a crazy value, that could be because
-	// the user hasn't read messages in the sub-board yet.  In that case,
-	// just use 0.  Otherwise, get the user's scan pointer message index.
-	var msgIdx = 0;
-	// If the user's scan_ptr for the sub-board isn't the 'last message'
-	// special value, then use it
-	if (!subBoardScanPtrIsLatestMsgSpecialVal(this.subBoardCode))
-		msgIdx = this.GetMsgIdx(msg_area.sub[this.subBoardCode].scan_ptr);
-	// Sanity checking for msgIdx
-	var msgbase = new MsgBase(this.subBoardCode);
-	if (msgbase.open())
-	{
-		if ((msgIdx < 0) || (msgIdx >= msgbase.total_msgs) || subBoardScanPtrIsLatestMsgSpecialVal(this.subBoardCode))
-		{
-			msgIdx = -1;
-			// Look for the first message not marked as deleted
-			var readableMsgIdx = this.FindNextReadableMsgIdx(0, true);
-			// If a non-deleted message was found, then set the scan pointer to it.
-			if (readableMsgIdx > -1)
-			{
-				var newLastRead = this.IdxToAbsMsgNum(readableMsgIdx);
-				if (newLastRead > -1)
-					msg_area.sub[this.subBoardCode].scan_ptr = newLastRead;
-				else
-					msg_area.sub[this.subBoardCode].scan_ptr = 0;
-			}
-			else
-				msg_area.sub[this.subBoardCode].scan_ptr = 0;
-		}
-		msgbase.close();
-	}
-	return msgIdx;
-}
-
-// For the DigDistMsgReader class: Returns whether there is a search specified
-// (according to this.searchType) and the search result objects are defined for
-// the current sub-board (as specified by this.subBoardCode).
-//
-// Return value: Boolean - Whether or not there is a search specified and the
-//               search result objects are defined for the current sub-board
-//               (as specified by this.subBoardCode).
-function DigDistMsgReader_SearchingAndResultObjsDefinedForCurSub()
-{
-	return (this.SearchTypePopulatesSearchResults() && (this.msgSearchHdrs != null) &&
-	         this.msgSearchHdrs.hasOwnProperty(this.subBoardCode) &&
-	         (typeof(this.msgSearchHdrs[this.subBoardCode]) == "object") &&
-	         (typeof(this.msgSearchHdrs[this.subBoardCode].indexed) != "undefined"));
-}
-
-// For the DigDistMsgReader class: Removes a message header from the search
-// results array for the current sub-board.
-//
-// Parameters:
-//  pMsgIdx: The index of the message header to remove (in the indexed messages,
-//           not necessarily the actual message offset in the messagebase)
-function DigDistMsgReader_RemoveFromSearchResults(pMsgIdx)
-{
-	if (typeof(pMsgIdx) != "number")
-		return;
-
-	if ((typeof(this.msgSearchHdrs) == "object") &&
-	    this.msgSearchHdrs.hasOwnProperty(this.subBoardCode) &&
-	    (typeof(this.msgSearchHdrs[this.subBoardCode].indexed) != "undefined"))
-	{
-		if ((pMsgIdx >= 0) && (pMsgIdx < this.msgSearchHdrs[this.subBoardCode].indexed.length))
-			this.msgSearchHdrs[this.subBoardCode].indexed.splice(pMsgIdx, 1);
-	}
-}
-
-// For the DigDistMsgReader class: Looks for the next message in the thread of
-// a given message (by its header).
-//
-// Paramters:
-//  pMsgHdr: A message header object - The next message in the thread will be
-//           searched starting from this message
-//  pThreadType: The type of threading to use.  Can be THREAD_BY_ID, THREAD_BY_TITLE,
-//               THREAD_BY_AUTHOR, or THREAD_BY_TO_USER.
-//  pPositionCursorForStatus: Optional boolean - Whether or not to move the cursor
-//                            to the bottom row before outputting status messages.
-//                            Defaults to false.
-//
-// Return value: The offset (index) of the next message thread, or -1 if none
-//               was found.
-function DigDistMsgReader_FindThreadNextOffset(pMsgHdr, pThreadType, pPositionCursorForStatus)
-{
-	if ((pMsgHdr == null) || (typeof(pMsgHdr) != "object"))
-		return -1;
-
-	var newMsgOffset = -1;
-
-	switch (pThreadType)
-	{
-		case THREAD_BY_ID:
-		default:
-			// The thread_id field was introduced in Synchronet 3.16.  So, if
-			// the Synchronet version is 3.16 or higher and the message header
-			// has a thread_id field, then look for the next message with the
-			// same thread ID.  If the Synchronet version is below 3.16 or there
-			// is no thread ID, then fall back to using the header's thread_next,
-			// if it exists.
-			if ((system.version_num >= 31600) && (typeof(pMsgHdr.thread_id) == "number"))
-			{
-				// Look for the next message with the same thread ID.
-				// Write "Searching.."  in case searching takes a while.
-				console.attributes = "N";
-				if (pPositionCursorForStatus)
-				{
-					console.gotoxy(1, console.screen_rows);
-					console.cleartoeol();
-					console.gotoxy(this.msgAreaLeft, console.screen_rows);
-				}
-				console.print("\x01h\x01ySearching\x01i...\x01n");
-				// Look for the next message in the thread
-				var nextMsgOffset = -1;
-				var numOfMessages = this.NumMessages();
-				/*
-				if (pMsgHdr.offset < numOfMessages - 1)
-				{
-					var nextMsgHdr;
-					for (var messageIdx = pMsgHdr.offset+1; (messageIdx < numOfMessages) && (nextMsgOffset == -1); ++messageIdx)
-					{
-						nextMsgHdr = this.GetMsgHdrByIdx(messageIdx);
-						if (nextMsgHdr != null && ((nextMsgHdr.attr & MSG_DELETE) == 0) && (typeof(nextMsgHdr.thread_id) == "number") && (nextMsgHdr.thread_id == pMsgHdr.thread_id))
-							nextMsgOffset = nextMsgHdr.offset;
-					}
-				}
-				*/
-				if (this.GetMsgIdx(pMsgHdr.number) < numOfMessages - 1)
-				{
-					var nextMsgHdr;
-					for (var messageIdx = this.GetMsgIdx(pMsgHdr.number)+1; (messageIdx < numOfMessages) && (nextMsgOffset == -1); ++messageIdx)
-					{
-						nextMsgHdr = this.GetMsgHdrByIdx(messageIdx);
-						if (nextMsgHdr != null && (((nextMsgHdr.attr & MSG_DELETE) == 0) || canViewDeletedMsgs()) && (typeof(nextMsgHdr.thread_id) == "number") && (nextMsgHdr.thread_id == pMsgHdr.thread_id))
-						{
-							//nextMsgOffset = nextMsgHdr.offset;
-							nextMsgOffset = this.GetMsgIdx(nextMsgHdr.number);
-						}
-					}
-				}
-				if (nextMsgOffset > -1)
-					newMsgOffset = nextMsgOffset;
-			}
-			// Fall back to thread_next if the Synchronet version is below 3.16 or there is
-			// no thread_id field in the header
-			else if ((typeof(pMsgHdr.thread_next) == "number") && (pMsgHdr.thread_next > 0))
-			{
-				//newMsgOffset = this.AbsMsgNumToIdx(pMsgHdr.thread_next);
-				newMsgOffset = this.GetMsgIdx(pMsgHdr.thread_next);
-			}
-			break;
-		case THREAD_BY_TITLE:
-		case THREAD_BY_AUTHOR:
-		case THREAD_BY_TO_USER:
-			// Title (subject) searching will look for the subject anywhere in the
-			// other messages' subjects (not a fully exact subject match), so if
-			// the message subject is blank, we won't want to do the search.
-			var doSearch = true;
-			if ((pThreadType == THREAD_BY_TITLE) && (pMsgHdr.subject.length == 0))
-				doSearch = false;
-			if (doSearch)
-			{
-				var subjUppercase = "";
-				var fromNameUppercase = "";
-				var toNameUppercase = "";
-
-				// Set up a message header matching function, depending on
-				// which field of the header we want to match
-				var msgHdrMatch;
-				if (pThreadType == THREAD_BY_TITLE)
-				{
-					subjUppercase = pMsgHdr.subject.toUpperCase();
-					// Remove any leading instances of "RE:" from the subject
-					while (/^RE:/.test(subjUppercase))
-						subjUppercase = subjUppercase.substr(3);
-					while (/^RE: /.test(subjUppercase))
-						subjUppercase = subjUppercase.substr(4);
-					// Remove any leading & trailing whitespace from the subject
-					subjUppercase = trimSpaces(subjUppercase, true, true, true);
-					msgHdrMatch = function(pMsgHdr) {
-						return ((((pMsgHdr.attr & MSG_DELETE) == 0) || canViewDeletedMsgs()) && (pMsgHdr.subject.toUpperCase().indexOf(subjUppercase, 0) > -1));
-					};
-				}
-				else if (pThreadType == THREAD_BY_AUTHOR)
-				{
-					fromNameUppercase = pMsgHdr.from.toUpperCase();
-					msgHdrMatch = function(pMsgHdr) {
-						return ((((pMsgHdr.attr & MSG_DELETE) == 0) || canViewDeletedMsgs()) && (pMsgHdr.from.toUpperCase() == fromNameUppercase));
-					};
-				}
-				else if (pThreadType == THREAD_BY_TO_USER)
-				{
-					toNameUppercase = pMsgHdr.to.toUpperCase();
-					msgHdrMatch = function(pMsgHdr) {
-						return ((((pMsgHdr.attr & MSG_DELETE) == 0) || canViewDeletedMsgs()) && (pMsgHdr.to.toUpperCase() == toNameUppercase));
-					};
-				}
-
-				// Perform the search
-				// Write "Searching.."  in case searching takes a while.
-				console.attributes = "N";
-				if (pPositionCursorForStatus)
-				{
-					console.gotoxy(1, console.screen_rows);
-					console.cleartoeol();
-					console.gotoxy(this.msgAreaLeft, console.screen_rows);
-				}
-				console.print("\x01h\x01ySearching\x01i...\x01n");
-				// Look for the next message that contains the given message's subject
-				var nextMsgOffset = -1;
-				var numOfMessages = this.NumMessages();
-				/*
-				if (pMsgHdr.offset < numOfMessages - 1)
-				{
-					var nextMsgHdr;
-					for (var messageIdx = pMsgHdr.offset+1; (messageIdx < numOfMessages) && (nextMsgOffset == -1); ++messageIdx)
-					{
-						nextMsgHdr = this.GetMsgHdrByIdx(messageIdx);
-						if (nextMsgHdr != null && msgHdrMatch(nextMsgHdr))
-							nextMsgOffset = nextMsgHdr.offset;
-					}
-				}
-				*/
-				if (this.GetMsgIdx(pMsgHdr.number) < numOfMessages - 1)
-				{
-					var nextMsgHdr;
-					for (var messageIdx = this.GetMsgIdx(pMsgHdr.number)+1; (messageIdx < numOfMessages) && (nextMsgOffset == -1); ++messageIdx)
-					{
-						nextMsgHdr = this.GetMsgHdrByIdx(messageIdx);
-						if (nextMsgHdr != null && msgHdrMatch(nextMsgHdr))
-						{
-							//nextMsgOffset = nextMsgHdr.offset;
-							nextMsgOffset = this.GetMsgIdx(nextMsgHdr.number);
-						}
-					}
-				}
-				if (nextMsgOffset > -1)
-					newMsgOffset = nextMsgOffset;
-			}
-			break;
-	}
-
-	// If no messages were found, then output a message to say so with a momentary pause.
-	if (newMsgOffset == -1)
-	{
-		if (pPositionCursorForStatus)
-		{
-			console.gotoxy(1, console.screen_rows);
-			console.cleartoeol();
-			console.gotoxy(this.msgAreaLeft, console.screen_rows);
-		}
-		else
-			console.crlf();
-		console.print("\x01n\x01h\x01yNo messages found.\x01n");
-		mswait(ERROR_PAUSE_WAIT_MS);
-	}
-
-	return newMsgOffset;
-}
-
-// For the DigDistMsgReader class: Looks for the previous message in the thread of
-// a given message (by its header).
-//
-// Paramters:
-//  pMsgHdr: A message header object - The previous message in the thread will be
-//           searched starting from this message
-//  pThreadType: The type of threading to use.  Can be THREAD_BY_ID, THREAD_BY_TITLE,
-//               THREAD_BY_AUTHOR, or THREAD_BY_TO_USER.
-//  pPositionCursorForStatus: Optional boolean - Whether or not to move the cursor
-//                            to the bottom row before outputting status messages.
-//                            Defaults to false.
-//
-// Return value: The offset (index) of the previous message thread, or -1 if
-//               none was found.
-function DigDistMsgReader_FindThreadPrevOffset(pMsgHdr, pThreadType, pPositionCursorForStatus)
-{
-	if ((pMsgHdr == null) || (typeof(pMsgHdr) != "object"))
-		return -1;
-
-	var newMsgOffset = -1;
-
-	switch (pThreadType)
-	{
-		case THREAD_BY_ID:
-		default:
-			// The thread_id field was introduced in Synchronet 3.16.  So, if
-			// the Synchronet version is 3.16 or higher and the message header
-			// has a thread_id field, then look for the previous message with the
-			// same thread ID.  If the Synchronet version is below 3.16 or there
-			// is no thread ID, then fall back to using the header's thread_next,
-			// if it exists.
-			if ((system.version_num >= 31600) && (typeof(pMsgHdr.thread_id) == "number"))
-			{
-				// Look for the previous message with the same thread ID.
-				// Write "Searching.." in case searching takes a while.
-				console.attributes = "N";
-				if (pPositionCursorForStatus)
-				{
-					console.gotoxy(1, console.screen_rows);
-					console.cleartoeol();
-					console.gotoxy(this.msgAreaLeft, console.screen_rows);
-				}
-				console.print("\x01h\x01ySearching\x01i...\x01n");
-				// Look for the previous message in the thread
-				var nextMsgOffset = -1;
-				/*
-				if (pMsgHdr.offset > 0)
-				{
-					var prevMsgHdr;
-					for (var messageIdx = pMsgHdr.offset-1; (messageIdx >= 0) && (nextMsgOffset == -1); --messageIdx)
-					{
-						prevMsgHdr = this.GetMsgHdrByIdx(messageIdx);
-						if (prevMsgHdr != null && ((prevMsgHdr.attr & MSG_DELETE) == 0) && (typeof(prevMsgHdr.thread_id) == "number") && (prevMsgHdr.thread_id == pMsgHdr.thread_id))
-							nextMsgOffset = prevMsgHdr.offset;
-					}
-				}
-				*/
-				if (this.GetMsgIdx(pMsgHdr.number) > 0)
-				{
-					var prevMsgHdr;
-					for (var messageIdx = this.GetMsgIdx(pMsgHdr.number)-1; (messageIdx >= 0) && (nextMsgOffset == -1); --messageIdx)
-					{
-						prevMsgHdr = this.GetMsgHdrByIdx(messageIdx);
-						if (prevMsgHdr != null && (((prevMsgHdr.attr & MSG_DELETE) == 0) || canViewDeletedMsgs()) && (typeof(prevMsgHdr.thread_id) == "number") && (prevMsgHdr.thread_id == pMsgHdr.thread_id))
-						{
-							//nextMsgOffset = prevMsgHdr.offset;
-							nextMsgOffset = this.GetMsgIdx(prevMsgHdr.number);
-							nextMsgOffset = 0;
-						}
-					}
-				}
-				if (nextMsgOffset > -1)
-					newMsgOffset = nextMsgOffset;
-			}
-			// Fall back to thread_next if the Synchronet version is below 3.16 or there is
-			// no thread_id field in the header
-			else if ((typeof(pMsgHdr.thread_back) == "number") && (pMsgHdr.thread_back > 0))
-			{
-				//newMsgOffset = this.AbsMsgNumToIdx(pMsgHdr.thread_back);
-				newMsgOffset = this.GetMsgIdx(pMsgHdr.thread_back);
-				if (newMsgOffset < 0)
-					newMsgOffset = 0;
-			}
-
-			/*
-			// If thread_back is valid for the message header, then use that.
-			if ((typeof(pMsgHdr.thread_back) == "number") && (pMsgHdr.thread_back > 0))
-			{
-				//newMsgOffset = this.AbsMsgNumToIdx(pMsgHdr.thread_back);
-				newMsgOffset = this.GetMsgIdx(pMsgHdr.thread_back);
-			}
-			else
-			{
-				// If thread_id is defined and the index of the first message
-				// in the thread is before the current message, then search
-				// backwards for messages with a matching thread_id.
-				//var firstThreadMsgIdx = this.AbsMsgNumToIdx(pMsgHdr.thread_first);
-				var firstThreadMsgIdx = this.GetMsgIdx(pMsgHdr.thread_first);
-				if ((typeof(pMsgHdr.thread_id) == "number") && (firstThreadMsgIdx < pMsgHdr.offset))
-				{
-					// Note (2014-10-11): Digital Man said thread_id was
-					// introduced in Synchronet version 3.16 and was still
-					// a work in progress and isn't 100% accurate for
-					// networked sub-boards.
-
-					// Look for the previous message with the same thread ID.
-					// Note: I'm not sure when thread_id was introduced in
-					// Synchronet, so I'm not sure of the minimum version where
-					// this will work.
-					// Write "Searching.." in case searching takes a while.
-					console.attributes = "N";
-					if (pPositionCursorForStatus)
-					{
-						console.gotoxy(1, console.screen_rows);
-						console.cleartoeol();
-						console.gotoxy(this.msgAreaLeft, console.screen_rows);
-					}
-					console.print("\x01h\x01ySearching\x01i...\x01n");
-					// Look for the previous message in the thread
-					var nextMsgOffset = -1;
-					if (pMsgHdr.offset > 0)
-					{
-						for (var messageIdx = pMsgHdr.offset-1; (messageIdx >= 0) && (nextMsgOffset == -1); --messageIdx)
-						{
-							var prevMsgHdr = this.GetMsgHdrByIdx(messageIdx);
-							if (prevMsgHdr != null && ((prevMsgHdr.attr & MSG_DELETE) == 0) && (typeof(prevMsgHdr.thread_id) == "number") && (prevMsgHdr.thread_id == pMsgHdr.thread_id))
-								nextMsgOffset = prevMsgHdr.offset;
-						}
-					}
-					if (nextMsgOffset > -1)
-						newMsgOffset = nextMsgOffset;
-				}
-			}
-			*/
-			break;
-		case THREAD_BY_TITLE:
-		case THREAD_BY_AUTHOR:
-		case THREAD_BY_TO_USER:
-			// Title (subject) searching will look for the subject anywhere in the
-			// other messages' subjects (not a fully exact subject match), so if
-			// the message subject is blank, we won't want to do the search.
-			var doSearch = true;
-			if ((pThreadType == THREAD_BY_TITLE) && (pMsgHdr.subject.length == 0))
-				doSearch = false;
-			if (doSearch)
-			{
-				var subjUppercase = "";
-				var fromNameUppercase = "";
-				var toNameUppercase = "";
-
-				// Set up a message header matching function, depending on
-				// which field of the header we want to match
-				var msgHdrMatch;
-				if (pThreadType == THREAD_BY_TITLE)
-				{
-					subjUppercase = pMsgHdr.subject.toUpperCase();
-					// Remove any leading instances of "RE:" from the subject
-					while (/^RE:/.test(subjUppercase))
-						subjUppercase = subjUppercase.substr(3);
-					while (/^RE: /.test(subjUppercase))
-						subjUppercase = subjUppercase.substr(4);
-					// Remove any leading & trailing whitespace from the subject
-					subjUppercase = trimSpaces(subjUppercase, true, true, true);
-					msgHdrMatch = function(pMsgHdr) {
-						return ((((pMsgHdr.attr & MSG_DELETE) == 0) || canViewDeletedMsgs()) && (pMsgHdr.subject.toUpperCase().indexOf(subjUppercase, 0) > -1));
-					};
-				}
-				else if (pThreadType == THREAD_BY_AUTHOR)
-				{
-					fromNameUppercase = pMsgHdr.from.toUpperCase();
-					msgHdrMatch = function(pMsgHdr) {
-						return ((((pMsgHdr.attr & MSG_DELETE) == 0) || canViewDeletedMsgs()) && (pMsgHdr.from.toUpperCase() == fromNameUppercase));
-					};
-				}
-				else if (pThreadType == THREAD_BY_TO_USER)
-				{
-					toNameUppercase = pMsgHdr.to.toUpperCase();
-					msgHdrMatch = function(pMsgHdr) {
-						return ((((pMsgHdr.attr & MSG_DELETE) == 0) || canViewDeletedMsgs()) && (pMsgHdr.to.toUpperCase() == toNameUppercase));
-					};
-				}
-
-				// Perform the search
-				// Write "Searching.."  in case searching takes a while.
-				console.attributes = "N";
-				if (pPositionCursorForStatus)
-				{
-					console.gotoxy(1, console.screen_rows);
-					console.cleartoeol();
-					console.gotoxy(this.msgAreaLeft, console.screen_rows);
-				}
-				console.print("\x01h\x01ySearching\x01i...\x01n");
-				// Look for the next message that contains the given message's subject
-				var nextMsgOffset = -1;
-				/*
-				if (pMsgHdr.offset > 0)
-				{
-					var nextMsgHdr;
-					for (var messageIdx = pMsgHdr.offset-1; (messageIdx >= 0) && (nextMsgOffset == -1); --messageIdx)
-					{
-						nextMsgHdr = this.GetMsgHdrByIdx(messageIdx);
-						if (prevMsgHdr != null && msgHdrMatch(nextMsgHdr))
-							nextMsgOffset = nextMsgHdr.offset;
-					}
-				}
-				*/
-				if (this.GetMsgIdx(pMsgHdr.number) > 0)
-				{
-					var nextMsgHdr;
-					for (var messageIdx = this.GetMsgIdx(pMsgHdr.number)-1; (messageIdx >= 0) && (nextMsgOffset == -1); --messageIdx)
-					{
-						nextMsgHdr = this.GetMsgHdrByIdx(messageIdx);
-						if (nextMsgHdr != null && msgHdrMatch(nextMsgHdr))
-						{
-							//nextMsgOffset = nextMsgHdr.offset;
-							nextMsgOffset = this.GetMsgIdx(nextMsgHdr.number);
-						}
-					}
-				}
-				if (nextMsgOffset > -1)
-					newMsgOffset = nextMsgOffset;
-			}
-			break;
-	}
-
-	// If no messages were found, then output a message to say so with a momentary pause.
-	if (newMsgOffset == -1)
-	{
-		if (pPositionCursorForStatus)
-		{
-			console.gotoxy(1, console.screen_rows);
-			console.cleartoeol();
-			console.gotoxy(this.msgAreaLeft, console.screen_rows);
-		}
-		else
-			console.crlf();
-		console.print("\x01n\x01h\x01yNo messages found.\x01n");
-		mswait(ERROR_PAUSE_WAIT_MS);
-	}
-
-	return newMsgOffset;
-}
-
-// For the DigDistMsgReader class: Calculates the top message index for a page,
-// for the traditional-style message list.
-//
-// Parameters:
-//  pPageNum: A page number (1-based)
-function DigDistMsgReader_CalcTraditionalMsgListTopIdx(pPageNum)
-{
-   if (this.userSettings.listMessagesInReverse)
-      this.tradListTopMsgIdx = this.NumMessages() - (this.tradMsgListNumLines * (pPageNum-1)) - 1;
-   else
-      this.tradListTopMsgIdx = (this.tradMsgListNumLines * (pPageNum-1));
-}
-
-// For the DigDistMsgReader class: Calculates the top message index for a page,
-// for the lightbar message list.
-//
-// Parameters:
-//  pPageNum: A page number (1-based)
-function DigDistMsgReader_CalcLightbarMsgListTopIdx(pPageNum)
-{
-	if (this.userSettings.listMessagesInReverse)
-		this.lightbarListTopMsgIdx = this.NumMessages() - (this.lightbarMsgListNumLines * (pPageNum-1)) - 1;
-	else
-	{
-		//this.lightbarListTopMsgIdx = (this.lightbarMsgListNumLines * (pPageNum-1));
-		var pageIdx = pPageNum - 1;
-		if (pageIdx < 0)
-			pageIdx = 0;
-		this.lightbarListTopMsgIdx = this.lightbarMsgListNumLines * pageIdx;
-	}
-}
-
-// For the DigDistMsgReader class: Given a message number (1-based), this calculates
-// the screen index veriables (stored in the object) for the message list.  This is
-// used for the enhanced reader mode when we want the message list to be in the
-// correct place for the message being read.
-//
-// Parameters:
-//  pMsgNum: The message number (1-based)
-function DigDistMsgReader_CalcMsgListScreenIdxVarsFromMsgNum(pMsgNum)
-{
-	// Calculate the message list variables
-	var numItemsPerPage = this.tradMsgListNumLines;
-	if (this.msgListUseLightbarListInterface && canDoHighASCIIAndANSI())
-		numItemsPerPage = this.lightbarMsgListNumLines;
-	var newPageNum = findPageNumOfItemNum(pMsgNum, numItemsPerPage, this.NumMessages(), this.userSettings.listMessagesInReverse);
-	this.CalcTraditionalMsgListTopIdx(newPageNum);
-	this.CalcLightbarMsgListTopIdx(newPageNum);
-	this.lightbarListSelectedMsgIdx = pMsgNum - 1;
-	if (this.lightbarListCurPos == null)
-		this.lightbarListCurPos = {};
-	this.lightbarListCurPos.x = 1;
-	this.lightbarListCurPos.y = this.lightbarMsgListStartScreenRow + ((pMsgNum-1) - this.lightbarListTopMsgIdx);
-}
-
-// For the DigDistMsgReader class: Validates a user's choice in message area.
-// Returns a status/error message for the caller to display if there's an
-// error.  This function outputs intermediate status messages (i.e.,
-// "Searching..").
-//
-// Parameters:
-//  pGrpIdx: The message group index (i.e., bbs.curgrp)
-//  pSubIdx: The message sub-board index (i.e., bbs.cursub)
-//  pCurPos: Optional - An object containing x and y properties representing
-//           the cursor position.  Used for outputting intermediate status
-//           messages, but not for outputting the error message.
-//
-// Return value: An object containing the following properties:
-//               msgAreaGood: A boolean to indicate whether the message area
-//                            can be selected
-//               errorMsg: If the message area can't be selected, this string
-//                         will contain an eror message.  Otherwise, this will
-//                         be an empty string.
-function DigDistMsgReader_ValidateMsgAreaChoice(pGrpIdx, pSubIdx, pCurPos)
-{
-	var retObj = {
-		msgAreaGood: true,
-		errorMsg: ""
-	};
-
-	// Get the internal code of the sub-board from the given group & sub-board
-	// indexes
-	var subCode = msg_area.grp_list[pGrpIdx].sub_list[pSubIdx].code;
-
-	// If a search is specified that would populate the search results, then do
-	// a search in the given sub-board.
-	if (this.SearchTypePopulatesSearchResults())
-	{
-		// See if we can use pCurPos to move the cursor before displaying messages
-		var useCurPos = (console.term_supports(USER_ANSI) && (typeof(pCurPos) == "object") &&
-		                 (typeof(pCurPos.x) == "number") && (typeof(pCurPos.y) == "number"));
-
-		// TODO: In case new messages were posted in this sub-board, it might help
-		// to check the current number of messages vs. the previous number of messages
-		// and search the new messages if there are more.
-
-		// Determine whether or not to search - If there are no search results for
-		// the given sub-board already, then do a search in the sub-board.
-		var doSearch = true;
-		if (this.msgSearchHdrs.hasOwnProperty(subCode) &&
-		    (typeof(this.msgSearchHdrs[subCode]) == "object") &&
-		    (typeof(this.msgSearchHdrs[subCode].indexed) != "undefined"))
-		{
-			doSearch = (this.msgSearchHdrs[subCode].indexed.length == 0);
-		}
-		if (doSearch)
-		{
-			if (useCurPos)
-			{
-				console.gotoxy(pCurPos);
-				console.cleartoeol("\x01n");
-				console.gotoxy(pCurPos);
-			}
-			console.print("\x01n\x01h\x01wSearching\x01i...\x01n");
-			this.msgSearchHdrs[subCode] = searchMsgbase(subCode, this.searchType, this.searchString, this.readingPersonalEmailFromUser);
-			// If there are no messages, then set the return object variables to indicate so.
-			if (this.msgSearchHdrs[subCode].indexed.length == 0)
-			{
-				retObj.msgAreaGood = false;
-				retObj.errorMsg = "No search results found";
-			}
-		}
-	}
-	else
-	{
-		// No search is specified.  Just check to see if there are any messages
-		// to read in the given sub-board.
-		var msgBase = new MsgBase(subCode);
-		if (msgBase.open())
-		{
-			if (msgBase.total_msgs == 0)
-			{
-				retObj.msgAreaGood = false;
-				retObj.errorMsg = "No messages in that message area";
-			}
-			msgBase.close();
-		}
-	}
-
-	return retObj;
-}
-
-// For the DigDistMsgReader class: Validates a message if the sub-board
-// requires message validation.
-//
-// Parameters:
-//  pSubBoardCode: The internal code of the sub-board
-//  pMsgNum: The message number
-//
-// Return value: Boolean - Whether or not validating the message was successful
-function DigDistMsgReader_ValidateMsg(pSubBoardCode, pMsgNum)
-{
-	if (!msg_area.sub[pSubBoardCode].is_moderated)
-		return true;
-
-	var validationSuccessful = false;
-	var msgbase = new MsgBase(this.subBoardCode);
-	if (msgbase.open())
-	{
-		var msgHdr = msgbase.get_msg_header(false, pMsgNum, false);
-		if (msgHdr != null)
-		{
-			if (!Boolean(msgHdr.attr & MSG_VALIDATED))
-			{
-				msgHdr.attr |= MSG_VALIDATED;
-				validationSuccessful = msgbase.put_msg_header(false, msgHdr.number, msgHdr);
-			}
-			else
-				validationSuccessful = true;
-		}
-		msgbase.close();
-	}
-
-	return validationSuccessful;
-}
-
-// For the DigDistMsgReader class: Gets the current sub-board's group name and description.
-//
-// Return value: An object with the following properties:
-//               grpName: The group name
-//               grpDesc: The group description
-function DigDistMsgReader_GetGroupNameAndDesc()
-{
-	var retObj = {
-		grpName: "",
-		grpDesc: ""
-	}
-	var msgbase = new MsgBase(this.subBoardCode);
-	if (msgbase.open())
-	{
-		retObj.grpName = msgbase.cfg.grp_name;
-		retObj.grpDesc = msgbase.cfg.description;
-		msgbase.close();
-	}
-	return retObj;
-}
-
-// For the DigDistMsgReader class:  Lets the user manage their preferences/settings (scrollable/ANSI user interface).
-//
-// Parameters:
-//  pDrawBottomhelpLineFn: A function to draw the bottom help line (it could be the message list
-//                         help line or reader help line), for refreshing after displaying an error.
-//                         This function must take the reader (this) as a parameter.
-//  pTopRowOverride: Optional - The row on the screen for the top line of the settings dialog.
-//                   If not specified, then 1 below the top of the message area (for reader mode)
-//                   will be used.
-//
-// Return value: An object containing the following properties:
-//               needWholeScreenRefresh: Boolean - Whether or not the whole screen needs to be
-//                                       refreshed (i.e., when the user has edited their twitlist)
-//               optionBoxTopLeftX: The top-left screen column of the option box
-//               optionBoxTopLeftY: The top-left screen row of the option box
-//               optionBoxWidth: The width of the option box
-//               optionBoxHeight: The height of the option box
-//               userTwitListChanged: Boolean - Whether or not the user's personal twit list changed
-function DigDistMsgReader_DoUserSettings_Scrollable(pDrawBottomhelpLineFn, pTopRowOverride)
-{
-	var retObj = {
-		needWholeScreenRefresh: false,
-		optionBoxTopLeftX: 1,
-		optionBoxTopLeftY: 1,
-		optionBoxWidth: 0,
-		optionBoxHeight: 0,
-		userTwitListChanged: false
-	};
-
-	if (!canDoHighASCIIAndANSI())
-	{
-		this.DoUserSettings_Traditional();
-		return retObj;
-	}
-
-	// Save the user's current settings so that we can check them later to see if any
-	// of them changed, in order to determine whether to save the user's settings file.
-	var originalSettings = {};
-	for (var prop in this.userSettings)
-	{
-		if (this.userSettings.hasOwnProperty(prop))
-			originalSettings[prop] = this.userSettings[prop];
-	}
-
-
-	// Create the user settings box
-	var optBoxTitle = "Setting                                      Enabled";
-	var optBoxWidth = ChoiceScrollbox_MinWidth();
-	var optBoxHeight = 14;
-	var msgBoxTopRow = 1;
-	if (typeof(pTopRowOverride) === "number" && pTopRowOverride >= 1 && pTopRowOverride <= console.screen_rows - optBoxHeight + 1)
-		msgBoxTopRow = pTopRowOverride;
-	else
-		msgBoxTopRow = this.msgAreaTop + 1;
-	var optBoxStartX = this.msgAreaLeft + Math.floor((this.msgAreaWidth/2) - (optBoxWidth/2));
-	if (optBoxStartX < this.msgAreaLeft)
-		optBoxStartX = this.msgAreaLeft;
-	var optionBox = new ChoiceScrollbox(optBoxStartX, msgBoxTopRow, optBoxWidth, optBoxHeight, optBoxTitle,
-										null/*gConfigSettings*/, false, true);
-	optionBox.addInputLoopExitKey(CTRL_U);
-	// Update the bottom help text to be more specific to the user settings box
-	var bottomBorderText = "\x01n\x01h\x01c" + UP_ARROW + "\x01b, \x01c" + DOWN_ARROW + "\x01b, \x01cEnter\x01y=\x01bSelect\x01n\x01c/\x01h\x01btoggle, "
-	                     + "\x01cESC\x01n\x01c/\x01hQ\x01n\x01c/\x01hCtrl-U\x01y=\x01bClose";
-	// This one contains the page navigation keys..  Don't really need to show those,
-	// since the settings box only has one page right now:
-	/*var bottomBorderText = "\x01n\x01h\x01c"+ UP_ARROW + "\x01b, \x01c"+ DOWN_ARROW + "\x01b, \x01cN\x01y)\x01bext, \x01cP\x01y)\x01brev, "
-						   + "\x01cF\x01y)\x01birst, \x01cL\x01y)\x01bast, \x01cEnter\x01y=\x01bSelect, "
-						   + "\x01cESC\x01n\x01c/\x01hQ\x01n\x01c/\x01hCtrl-U\x01y=\x01bClose";*/
-
-	optionBox.setBottomBorderText(bottomBorderText, true, false);
-
-	// Add the options to the option box
-	const checkIdx = 48;
-	const optionFormatStr = "%-" + (checkIdx-1) + "s[ ]";
-	const ENH_SCROLLBAR_OPT_INDEX = optionBox.addTextItem(format(optionFormatStr, "Scrollbar in reader"));
-	if (this.userSettings.useEnhReaderScrollbar)
-		optionBox.chgCharInTextItem(ENH_SCROLLBAR_OPT_INDEX, checkIdx, CHECK_CHAR);
-
-	const LIST_MESSAGES_IN_REVERSE_OPT_INDEX = optionBox.addTextItem(format(optionFormatStr, "List messages in reverse"));
-	if (this.userSettings.listMessagesInReverse)
-		optionBox.chgCharInTextItem(LIST_MESSAGES_IN_REVERSE_OPT_INDEX, checkIdx, CHECK_CHAR);
-
-	const NEWSCAN_ONLY_SHOW_NEW_MSGS_INDEX = optionBox.addTextItem(format(optionFormatStr, "Newscan: Only show new messages"));
-	if (this.userSettings.newscanOnlyShowNewMsgs)
-		optionBox.chgCharInTextItem(NEWSCAN_ONLY_SHOW_NEW_MSGS_INDEX, checkIdx, CHECK_CHAR);
-
-	const INDEXED_MODE_NEWSCAN_OPT_INDEX = optionBox.addTextItem(format(optionFormatStr, "Use indexed mode for newscan"));
-	if (this.userSettings.useIndexedModeForNewscan)
-		optionBox.chgCharInTextItem(INDEXED_MODE_NEWSCAN_OPT_INDEX, checkIdx, CHECK_CHAR);
-
-	const SHOW_INDEXED_NEWSCAN_MENU_IF_NO_NEW_MSGS_OPT_INDEX = optionBox.addTextItem(format(optionFormatStr, "Show indexed menu if there are no new messages"));
-	if (this.userSettings.displayIndexedModeMenuIfNoNewMessages)
-		optionBox.chgCharInTextItem(SHOW_INDEXED_NEWSCAN_MENU_IF_NO_NEW_MSGS_OPT_INDEX, checkIdx, CHECK_CHAR);
-
-	const INDEXED_MODE_NEWSCAN_MENU_AFTER_READING_ALL_NEW_MSGS_OPT_INDEX = optionBox.addTextItem(format(optionFormatStr, "Show indexed menu after reading all new msgs"));
-	if (this.userSettings.showIndexedNewscanMenuAfterReadingAllNewMsgs)
-		optionBox.chgCharInTextItem(INDEXED_MODE_NEWSCAN_MENU_AFTER_READING_ALL_NEW_MSGS_OPT_INDEX, checkIdx, CHECK_CHAR);
-
-	const INDEXED_MODE_MENU_SNAP_TO_NEW_MSGS_OPT_INDEX = optionBox.addTextItem(format(optionFormatStr, "Index menu: Snap to sub-boards w/ new messages"));
-	if (this.userSettings.indexedModeMenuSnapToFirstWithNew)
-		optionBox.chgCharInTextItem(INDEXED_MODE_MENU_SNAP_TO_NEW_MSGS_OPT_INDEX, checkIdx, CHECK_CHAR);
-
-	const INDEX_NEWSCAN_ENTER_SHOWS_MSG_LIST_OPT_INDEX = optionBox.addTextItem(format(optionFormatStr, "Index menu: Enter shows message list"));
-	if (this.userSettings.enterFromIndexMenuShowsMsgList)
-		optionBox.chgCharInTextItem(INDEX_NEWSCAN_ENTER_SHOWS_MSG_LIST_OPT_INDEX, checkIdx, CHECK_CHAR);
-
-	const READER_QUIT_TO_MSG_LIST_OPT_INDEX = optionBox.addTextItem(format(optionFormatStr, "Quit from reader to message list"));
-	if (this.userSettings.quitFromReaderGoesToMsgList)
-		optionBox.chgCharInTextItem(READER_QUIT_TO_MSG_LIST_OPT_INDEX, checkIdx, CHECK_CHAR);
-
-	const PROPMT_DEL_PERSONAL_MSG_AFTER_REPLY_OPT_INDEX = optionBox.addTextItem(format(optionFormatStr, "Prompt delete after reply to personal email"));
-	if (this.userSettings.promptDelPersonalEmailAfterReply)
-		optionBox.chgCharInTextItem(PROPMT_DEL_PERSONAL_MSG_AFTER_REPLY_OPT_INDEX, checkIdx, CHECK_CHAR);
-
-	const DISPLAY_PERSONAL_MAIL_REPLIED_INDICATOR_CHAR_OPT_INDEX = optionBox.addTextItem(format(optionFormatStr, "Display email 'replied' indicator"));
-	if (this.userSettings.displayMsgRepliedChar)
-		optionBox.chgCharInTextItem(DISPLAY_PERSONAL_MAIL_REPLIED_INDICATOR_CHAR_OPT_INDEX, checkIdx, CHECK_CHAR);
-
-	// Create an object containing toggle values (true/false) for each option index
-	var optionToggles = {};
-	optionToggles[ENH_SCROLLBAR_OPT_INDEX] = this.userSettings.useEnhReaderScrollbar;
-	optionToggles[LIST_MESSAGES_IN_REVERSE_OPT_INDEX] = this.userSettings.listMessagesInReverse;
-	optionToggles[NEWSCAN_ONLY_SHOW_NEW_MSGS_INDEX] = this.userSettings.newscanOnlyShowNewMsgs;
-	optionToggles[INDEXED_MODE_NEWSCAN_OPT_INDEX] = this.userSettings.useIndexedModeForNewscan;
-	optionToggles[SHOW_INDEXED_NEWSCAN_MENU_IF_NO_NEW_MSGS_OPT_INDEX] = this.userSettings.displayIndexedModeMenuIfNoNewMessages;
-	optionToggles[INDEXED_MODE_NEWSCAN_MENU_AFTER_READING_ALL_NEW_MSGS_OPT_INDEX] = this.userSettings.showIndexedNewscanMenuAfterReadingAllNewMsgs;
-	optionToggles[INDEXED_MODE_MENU_SNAP_TO_NEW_MSGS_OPT_INDEX] = this.userSettings.indexedModeMenuSnapToFirstWithNew;
-	optionToggles[INDEX_NEWSCAN_ENTER_SHOWS_MSG_LIST_OPT_INDEX] = this.userSettings.enterFromIndexMenuShowsMsgList;
-	optionToggles[READER_QUIT_TO_MSG_LIST_OPT_INDEX] = this.userSettings.quitFromReaderGoesToMsgList;
-	optionToggles[PROPMT_DEL_PERSONAL_MSG_AFTER_REPLY_OPT_INDEX] = this.userSettings.promptDelPersonalEmailAfterReply;
-	optionToggles[DISPLAY_PERSONAL_MAIL_REPLIED_INDICATOR_CHAR_OPT_INDEX] = this.userSettings.displayMsgRepliedChar;
-
-	// Other actions
-	var USER_TWITLIST_OPT_INDEX = optionBox.addTextItem("Personal twit list");
-
-	// Set up the enter key in the box to toggle the selected item.
-	optionBox.readerObj = this;
-	optionBox.setEnterKeyOverrideFn(function(pBox) {
-		var itemIndex = pBox.getChosenTextItemIndex();
-		if (itemIndex > -1)
-		{
-			// If there's an option for the chosen item, then update the text on the
-			// screen depending on whether the option is enabled or not.
-			if (optionToggles.hasOwnProperty(itemIndex))
-			{
-				// Toggle the option and refresh it on the screen
-				optionToggles[itemIndex] = !optionToggles[itemIndex];
-				if (optionToggles[itemIndex])
-					optionBox.chgCharInTextItem(itemIndex, checkIdx, CHECK_CHAR);
-				else
-					optionBox.chgCharInTextItem(itemIndex, checkIdx, " ");
-				optionBox.refreshItemCharOnScreen(itemIndex, checkIdx);
-
-				// Toggle the setting for the user in global user setting object.
-				switch (itemIndex)
-				{
-					case ENH_SCROLLBAR_OPT_INDEX:
-						this.readerObj.userSettings.useEnhReaderScrollbar = !this.readerObj.userSettings.useEnhReaderScrollbar;
-						break;
-					case LIST_MESSAGES_IN_REVERSE_OPT_INDEX:
-						this.readerObj.userSettings.listMessagesInReverse = !this.readerObj.userSettings.listMessagesInReverse;
-						break;
-					case NEWSCAN_ONLY_SHOW_NEW_MSGS_INDEX:
-						this.readerObj.userSettings.newscanOnlyShowNewMsgs = !this.readerObj.userSettings.newscanOnlyShowNewMsgs;
-						break;
-					case INDEXED_MODE_NEWSCAN_OPT_INDEX:
-						this.readerObj.userSettings.useIndexedModeForNewscan = !this.readerObj.userSettings.useIndexedModeForNewscan;
-						break;
-					case SHOW_INDEXED_NEWSCAN_MENU_IF_NO_NEW_MSGS_OPT_INDEX:
-						this.readerObj.userSettings.displayIndexedModeMenuIfNoNewMessages = !this.readerObj.userSettings.displayIndexedModeMenuIfNoNewMessages;
-						break;
-					case INDEXED_MODE_NEWSCAN_MENU_AFTER_READING_ALL_NEW_MSGS_OPT_INDEX:
-						this.readerObj.userSettings.showIndexedNewscanMenuAfterReadingAllNewMsgs = !this.readerObj.userSettings.showIndexedNewscanMenuAfterReadingAllNewMsgs;
-						break;
-					case INDEXED_MODE_MENU_SNAP_TO_NEW_MSGS_OPT_INDEX:
-						this.readerObj.userSettings.indexedModeMenuSnapToFirstWithNew = !this.readerObj.userSettings.indexedModeMenuSnapToFirstWithNew;
-						break;
-					case INDEX_NEWSCAN_ENTER_SHOWS_MSG_LIST_OPT_INDEX:
-						this.readerObj.userSettings.enterFromIndexMenuShowsMsgList = !this.readerObj.userSettings.enterFromIndexMenuShowsMsgList;
-						break;
-					case READER_QUIT_TO_MSG_LIST_OPT_INDEX:
-						this.readerObj.userSettings.quitFromReaderGoesToMsgList = !this.readerObj.userSettings.quitFromReaderGoesToMsgList;
-						break;
-					case PROPMT_DEL_PERSONAL_MSG_AFTER_REPLY_OPT_INDEX:
-						this.readerObj.userSettings.promptDelPersonalEmailAfterReply = !this.readerObj.userSettings.promptDelPersonalEmailAfterReply;
-						break;
-					case DISPLAY_PERSONAL_MAIL_REPLIED_INDICATOR_CHAR_OPT_INDEX:
-						this.readerObj.userSettings.displayMsgRepliedChar = !this.readerObj.userSettings.displayMsgRepliedChar;
-						break;
-					default:
-						break;
-				}
-			}
-			// For options that aren't on/off toggle options, take the appropriate action.
-			else
-			{
-				switch (itemIndex)
-				{
-					//case DICTIONARY_OPT_INDEX:
-					//	break;
-					case USER_TWITLIST_OPT_INDEX:
-						console.clear("\x01n", false);
-						console.editfile(gUserTwitListFilename);
-						// Re-read the user's twitlist and see if the user's twitlist changed
-						var oldUserTwitList = this.readerObj.userSettings.twitList;
-						this.readerObj.userSettings.twitList = [];
-						this.readerObj.ReadUserSettingsFile(true);
-						retObj.userTwitListChanged = !arraysHaveSameValues(this.readerObj.userSettings.twitList, oldUserTwitList);
-						optionBox.continueInputLoopOverride = false; // Exit the input loop of the option box
-						retObj.needWholeScreenRefresh = true;
-						break;
-					default:
-						break;
-				}
-			}
-		}
-	}); // Option box enter key override function
-
-	// Display the option box and have it do its input loop
-	var boxRetObj = optionBox.doInputLoop(true);
-
-	// If the user changed any of their settings, then save the user settings.
-	// If the save fails, then output an error message.
-	var settingsChanged = false;
-	for (var prop in this.userSettings)
-	{
-		if (this.userSettings.hasOwnProperty(prop))
-		{
-			settingsChanged = settingsChanged || (originalSettings[prop] != this.userSettings[prop]);
-			if (settingsChanged)
-				break;
-		}
-	}
-	if (settingsChanged)
-	{
-		if (!this.WriteUserSettingsFile())
-		{
-			writeWithPause(1, console.screen_rows, "\x01n\x01y\x01hFailed to save settings!\x01n", ERROR_PAUSE_WAIT_MS, "\x01n", true);
-			// Refresh the help line
-			pDrawBottomhelpLineFn(this);
-		}
-	}
-
-	optionBox.addInputLoopExitKey(CTRL_U);
-
-	// Prepare return object values and return
-	retObj.optionBoxTopLeftX = optionBox.dimensions.topLeftX;
-	retObj.optionBoxTopLeftY = optionBox.dimensions.topLeftY;
-	retObj.optionBoxWidth = optionBox.dimensions.width;
-	retObj.optionBoxHeight = optionBox.dimensions.height;
-	return retObj;
-}
-// For the DigDistMsgReader class:  Lets the user manage their preferences/settings (traditional user interface)
-//
-// Return value: An object containing the following properties:
-//               needWholeScreenRefresh: Boolean - Whether or not the whole screen needs to be
-//                                       refreshed (i.e., when the user has edited their twitlist)
-//               optionBoxTopLeftX: The top-left screen column of the option box
-//               optionBoxTopLeftY: The top-left screen row of the option box
-//               optionBoxWidth: The width of the option box
-//               optionBoxHeight: The height of the option box
-//               userTwitListChanged: Boolean - Whether or not the user's personal twit list changed
-function DigDistMsgReader_DoUserSettings_Traditional()
-{
-	var retObj = {
-		needWholeScreenRefresh: true,
-		optionBoxTopLeftX: 1,
-		optionBoxTopLeftY: 1,
-		optionBoxWidth: 0,
-		optionBoxHeight: 0,
-		userTwitListChanged: false
-	};
-
-	var LIST_MESSAGES_IN_REVERSE_OPT_NUM = 1;
-	var NEWSCAN_ONLY_SHOW_NEW_MSGS_OPT_NUM = 2;
-	var USE_INDEXED_MODE_FOR_NEWSCAN_OPT_NUM = 3;
-	var SHOW_INDEXED_NEWSCAN_MENU_IF_NO_NEW_MSGS_OPT_NUM = 4;
-	var INDEXED_MODE_NEWSCAN_MENU_AFTER_READING_ALL_NEW_MSGS_OPT_INDEX = 5;
-	var INDEX_NEWSCAN_ENTER_SHOWS_MSG_LIST_OPT_NUM = 6;
-	var READER_QUIT_TO_MSG_LIST_OPT_NUM = 7;
-	var PROPMT_DEL_PERSONAL_MSG_AFTER_REPLY_OPT_NUM = 8;
-	var DISPLAY_PERSONAL_MAIL_REPLIED_INDICATOR_CHAR_OPT_NUM = 9;
-	var USER_TWITLIST_OPT_NUM = 10;
-	var HIGHEST_CHOICE_NUM = USER_TWITLIST_OPT_NUM;
-
-	console.crlf();
-	var wordFirstCharAttrs = "\x01c\x01h";
-	var wordRemainingAttrs = "\x01c";
-	console.print(colorFirstCharAndRemainingCharsInWords("User Settings", wordFirstCharAttrs, wordRemainingAttrs) + "\r\n");
-	printTradUserSettingOption(LIST_MESSAGES_IN_REVERSE_OPT_NUM, "List messages in reverse", wordFirstCharAttrs, wordRemainingAttrs);
-	printTradUserSettingOption(NEWSCAN_ONLY_SHOW_NEW_MSGS_OPT_NUM, "Only show new messages for newscan", wordFirstCharAttrs, wordRemainingAttrs);
-	printTradUserSettingOption(USE_INDEXED_MODE_FOR_NEWSCAN_OPT_NUM, "Use Indexed mode for newscan", wordFirstCharAttrs, wordRemainingAttrs);
-	printTradUserSettingOption(SHOW_INDEXED_NEWSCAN_MENU_IF_NO_NEW_MSGS_OPT_NUM, "Show indexed menu if there are no new messages", wordFirstCharAttrs, wordRemainingAttrs);
-	printTradUserSettingOption(INDEXED_MODE_NEWSCAN_MENU_AFTER_READING_ALL_NEW_MSGS_OPT_INDEX, "Show indexed menu after reading all new messages", wordFirstCharAttrs, wordRemainingAttrs);
-	printTradUserSettingOption(INDEX_NEWSCAN_ENTER_SHOWS_MSG_LIST_OPT_NUM, "Index: Selection shows message list", wordFirstCharAttrs, wordRemainingAttrs);
-	printTradUserSettingOption(READER_QUIT_TO_MSG_LIST_OPT_NUM, "Quitting From reader goes to message list", wordFirstCharAttrs, wordRemainingAttrs);
-	printTradUserSettingOption(PROPMT_DEL_PERSONAL_MSG_AFTER_REPLY_OPT_NUM, "Prompt to delete personal message after replying", wordFirstCharAttrs, wordRemainingAttrs);
-	printTradUserSettingOption(DISPLAY_PERSONAL_MAIL_REPLIED_INDICATOR_CHAR_OPT_NUM, "Display email replied indicator", wordFirstCharAttrs, wordRemainingAttrs);
-	printTradUserSettingOption(USER_TWITLIST_OPT_NUM, "Personal twit list", wordFirstCharAttrs, wordRemainingAttrs);
-	console.crlf();
-	console.print("\x01cYour choice (\x01hQ\x01n\x01c: Quit)\x01h: \x01g");
-	var userChoiceNum = console.getnum(HIGHEST_CHOICE_NUM);
-	console.attributes = "N";
-	var userChoiceStr = userChoiceNum.toString().toUpperCase();
-	if (userChoiceStr.length == 0 || userChoiceStr == "Q")
-		return retObj;
-
-	var userSettingsChanged = false;
-	switch (userChoiceNum)
-	{
-		case LIST_MESSAGES_IN_REVERSE_OPT_NUM:
-			var oldListMsgsInReverseSetting = this.userSettings.listMessagesInReverse;
-			this.userSettings.listMessagesInReverse = !console.noyes("List messages in reverse");
-			userSettingsChanged = (this.userSettings.listMessagesInReverse != oldListMsgsInReverseSetting);
-			break;
-		case NEWSCAN_ONLY_SHOW_NEW_MSGS_OPT_NUM:
-			var oldOnlyShowNewMsgsSetting = this.userSettings.newscanOnlyShowNewMsgs;
-			this.userSettings.newscanOnlyShowNewMsgs = !console.noyes("Only show new messages for newscan");
-			userSettingsChanged = (this.userSettings.newscanOnlyShowNewMsgs != oldOnlyShowNewMsgsSetting);
-			break;
-		case USE_INDEXED_MODE_FOR_NEWSCAN_OPT_NUM:
-			var oldIndexedModeNewscanSetting = this.userSettings.useIndexedModeForNewscan;
-			this.userSettings.useIndexedModeForNewscan = !console.noyes("Use indexed mode for newscan-all");
-			userSettingsChanged = (this.userSettings.useIndexedModeForNewscan != oldIndexedModeNewscanSetting);
-			break;
-		case SHOW_INDEXED_NEWSCAN_MENU_IF_NO_NEW_MSGS_OPT_NUM:
-			var oldIndexedMenuIfNoMsgsSetting = this.userSettings.displayIndexedModeMenuIfNoNewMessages;
-			this.userSettings.displayIndexedModeMenuIfNoNewMessages = !console.noyes("Show indexed menu if there are no new messages");
-			userSettingsChanged = (this.userSettings.displayIndexedModeMenuIfNoNewMessages != oldIndexedMenuIfNoMsgsSetting);
-			break;
-		case INDEXED_MODE_NEWSCAN_MENU_AFTER_READING_ALL_NEW_MSGS_OPT_INDEX:
-			var oldIndexedMenuAfterReadingAllNewMsgsSetting = this.userSettings.showIndexedNewscanMenuAfterReadingAllNewMsgs;
-			this.userSettings.showIndexedNewscanMenuAfterReadingAllNewMsgs = console.yesno("Show indexed menu after reading all new messages");
-			userSettingsChanged = (this.userSettings.showIndexedNewscanMenuAfterReadingAllNewMsgs != oldIndexedMenuAfterReadingAllNewMsgsSetting);
-			break;
-		case INDEX_NEWSCAN_ENTER_SHOWS_MSG_LIST_OPT_NUM:
-			var oldIndexedModeEnterShowsMsgListSetting = this.userSettings.enterFromIndexMenuShowsMsgList;
-			this.userSettings.enterFromIndexMenuShowsMsgList = !console.noyes("Index menu: Show message list with enter");
-			userSettingsChanged = (this.userSettings.enterFromIndexMenuShowsMsgList != oldIndexedModeEnterShowsMsgListSetting);
-			break;
-		case READER_QUIT_TO_MSG_LIST_OPT_NUM:
-			var oldReaderQuitSetting = this.userSettings.quitFromReaderGoesToMsgList;
-			this.userSettings.quitFromReaderGoesToMsgList = !console.noyes("Quit key from reader: Go to the message list (rather than exit)");
-			userSettingsChanged = (this.userSettings.quitFromReaderGoesToMsgList != oldReaderQuitSetting);
-			break;
-		case PROPMT_DEL_PERSONAL_MSG_AFTER_REPLY_OPT_NUM:
-			var oldReaderQuitSetting = this.userSettings.promptDelPersonalEmailAfterReply;
-			this.userSettings.promptDelPersonalEmailAfterReply = !console.noyes("Prompt to delete personal message after replying");
-			userSettingsChanged = (this.userSettings.promptDelPersonalEmailAfterReply != oldReaderQuitSetting);
-			break;
-		case DISPLAY_PERSONAL_MAIL_REPLIED_INDICATOR_CHAR_OPT_NUM:
-			var oldDisplayRepliedCharSetting = this.userSettings.displayMsgRepliedChar;
-			this.userSettings.displayMsgRepliedChar = console.yesno("Display email 'replied' indicator");
-			userSettingsChanged = (this.userSettings.displayMsgRepliedChar != oldDisplayRepliedCharSetting);
-			break;
-		case USER_TWITLIST_OPT_NUM:
-			console.editfile(gUserTwitListFilename);
-			// Re-read the user's twitlist and see if the user's twitlist changed
-			var oldUserTwitList = this.userSettings.twitList;
-			this.userSettings.twitList = [];
-			this.ReadUserSettingsFile(true);
-			retObj.userTwitListChanged = !arraysHaveSameValues(this.userSettings.twitList, oldUserTwitList);
-			retObj.needWholeScreenRefresh = true;
-			break;
-	}
-
-	// If any user settings changed, then write them to the user settings file
-	if (userSettingsChanged)
-	{
-		if (!this.WriteUserSettingsFile())
-		{
-			console.print("\x01n\r\n\x01y\x01hFailed to save settings!\x01n");
-			console.crlf();
-			console.pause();
-		}
-	}
-
-	return retObj;
-}
-// Helper for DigDistMsgReader_DoUserSettings_Traditional: Returns a string where for each word,
-// the first letter will have one set of Synchronet attributes applied and the remainder of the word
-// will have another set of Synchronet attributes applied
-function colorFirstCharAndRemainingCharsInWords(pStr, pWordFirstCharAttrs, pWordRemainderAttrs)
-{
-	if (typeof(pStr) !== "string" || pStr.length == 0)
-		return "";
-	if (typeof(pWordFirstCharAttrs) !== "string" || typeof(pWordRemainderAttrs) !== "string")
-		return pStr;
-
-	var wordsArray = pStr.split(" ");
-	for (var i = 0; i < wordsArray.length; ++i)
-	{
-		if (wordsArray[i] != " ")
-			wordsArray[i] = "\x01n" + pWordFirstCharAttrs + wordsArray[i].substr(0, 1) + "\x01n" + pWordRemainderAttrs + wordsArray[i].substr(1);
-	}
-	return wordsArray.join(" ");
-}
-
-// Helper for DigDistMsgReader_DoUserSettings_Traditional: Returns a string where for each word,
-// the first letter will have one set of Synchronet attributes applied and the remainder of the word
-// will have another set of Synchronet attributes applied
-function printTradUserSettingOption(pOptNum, pStr, pWordFirstCharAttrs, pWordRemainderAttrs)
-{
-	printf("\x01c\x01h%d\x01g: %s\r\n", pOptNum, colorFirstCharAndRemainingCharsInWords(pStr, pWordFirstCharAttrs, pWordRemainderAttrs));
-}
-
-///////////////////////////////////////////////////////////////
-// Stuff for indexed reader mode
-
-// For the DigDistMsgReader class: Starts indexed mode. For a new scan, displays
-// a menu of sub-boards with the number of new messages etc.
-//
-// Parameters:
-//  pScanScope: Numeric - Whether to scan the current sub-board, group, or all.
-//              This would be SCAN_SCOPE_SUB_BOARD, SCAN_SCOPE_GROUP, or SCAN_SCOPE_ALL.
-//  pNewscanOnly: Boolean: Whether or not to only check sub-boards in the user's newscan configuration.
-//                Defaults to false.
-function DigDistMsgReader_DoIndexedMode(pScanScope, pNewscanOnly)
-{
-	var scanScope = (isValidScanScopeVal(pScanScope) ? pScanScope : SCAN_SCOPE_ALL);
-	var newscanOnly = (typeof(pNewscanOnly) === "boolean" ? pNewscanOnly : false);
-
-	this.indexedMode = true;
-	this.doingMsgScan = newscanOnly;
-
-	// msgHdrsCache is used to prevent long loading again when loading a sub-board
-	// a 2nd time or later. Only if this.enableIndexedModeMsgListCache is true.
-	var msgHdrsCache = {};
-
-	var clearScreenForMenu = true;
-	var drawMenu = true;
-	var writeBottomHelpLine = true;
-	var continueOn = true;
-	while (continueOn)
-	{
-		// A backup for the number of new messages so we can see if it changes (due to the user reading messages)
-		var origNumNewMessages = 0;
-		// Let the user choose a sub-board, and if their choice is valid,
-		// let them read the sub-board.
-		var indexRetObj = this.IndexedModeChooseSubBoard(clearScreenForMenu, drawMenu, writeBottomHelpLine, pScanScope, pNewscanOnly);
-		if (typeof(indexRetObj.numNewMsgs) === "number")
-			origNumNewMessages = indexRetObj.numNewMsgs;
-		var userChoseAValidSubBoard = (typeof(indexRetObj.chosenSubCode) === "string" && msg_area.sub.hasOwnProperty(indexRetObj.chosenSubCode));
-		if (userChoseAValidSubBoard)
-		{
-			// Ensure we draw the index menu & help line in the next iteration
-			drawMenu = true;
-			writeBottomHelpLine = true;
-			this.subBoardCode = indexRetObj.chosenSubCode;
-			if (!this.enableIndexedModeMsgListCache || !msgHdrsCache.hasOwnProperty(indexRetObj.chosenSubCode))
-			{
-				// Display a "Loading..." status text and populate the headers for this sub-board
-				if (console.term_supports(USER_ANSI) && this.indexedModeMenu.allowANSI) // this.msgListUseLightbarListInterface
-				{
-					console.gotoxy(1, console.screen_rows);
-					console.cleartoeol("\x01n");
-					console.print("\x01n\x01cLoading\x01h...\x01n");
-				}
-				// If the user has the option to only show new messages for a newscan and there are
-				// new messages, then populate the sub-board with only the new message from their
-				// scan pointer; otherwise, populate with all message headers for the sub-board.
-				if (this.userSettings.newscanOnlyShowNewMsgs && indexRetObj.numNewMsgs > 0)
-					this.PopulateHdrsForCurrentSubBoard(POPULATE_MSG_HDRS_FROM_SCAN_PTR);
-				else
-					this.PopulateHdrsForCurrentSubBoard(POPULATE_NEWSCAN_FORCE_GET_ALL_HDRS);
-			}
-			else
-			{
-				this.hdrsForCurrentSubBoard = msgHdrsCache[indexRetObj.chosenSubCode].hdrsForCurrentSubBoard;
-				this.msgNumToIdxMap = msgHdrsCache[indexRetObj.chosenSubCode].hdrsForCurrentSubBoardByMsgNum;
-			}
-
-			// If the user chose to view the message list, display the message list to let the user
-			// choose a message to read. Otherwise, start reader mode.
-			if (indexRetObj.viewMsgList)
-			{
-				var listRetObj = this.ListMessages(indexRetObj.chosenSubCode, false);
-				// If the user chose a message from the list, let the user read starting with that message
-				if (listRetObj.selectedMsgOffset > -1)
-				{
-					var readRetObj = this.ReadMessages(indexRetObj.chosenSubCode, listRetObj.selectedMsgOffset, false, false, true, false);
-					// Update the text for the current menu item to ensure the message numbers are up to date
-					var currentMenuItem = this.indexedModeMenu.GetItem(this.indexedModeMenu.selectedItemIdx);
-					var itemInfo = this.GetIndexedModeSubBoardMenuItemTextAndInfo(indexRetObj.chosenSubCode);
-					currentMenuItem.text = itemInfo.itemText;
-					currentMenuItem.retval.numNewMsgs = itemInfo.numNewMsgs;
-					// If enabled, store the message headers for this sub-board as a cache for performance
-					if (this.enableIndexedModeMsgListCache)
-					{
-						msgHdrsCache[indexRetObj.chosenSubCode] = {
-							hdrsForCurrentSubBoard: this.hdrsForCurrentSubBoard,
-							hdrsForCurrentSubBoardByMsgNum: this.msgNumToIdxMap
-						};
-					}
-				}
-			}
-			else
-			{
-				// Let the user read the sub-board
-				// Decide the index of the starting message: If there are no new messages, show the last
-				// messages.  Otherwise, if only showing new messages, show the first messages.
-				// Otherwise, calculate the starting index.
-				var numMessages = this.NumMessages();
-				var startIdx = 0;
-				if (newscanOnly)
-				{
-					if (indexRetObj.numNewMsgs == 0)
-						startIdx = numMessages - 1;
-					else if (!this.userSettings.newscanOnlyShowNewMsgs)
-					{
-						startIdx = numMessages > 0 ? numMessages - indexRetObj.numNewMsgs : 0;
-						if (startIdx < 0)
-							startIdx = numMessages - 1;
-					}
-				}
-				else
-				{
-					// Not a newscan - Use the last_read pointer
-					var tmpMsgbase = new MsgBase(indexRetObj.chosenSubCode);
-					if (tmpMsgbase.open())
-					{
-						var lastReadMsgHdr = tmpMsgbase.get_msg_index(false, msg_area.sub[indexRetObj.chosenSubCode].last_read, false);
-						if (lastReadMsgHdr != null)
-						{
-							//startIdx = absMsgNumToIdxWithMsgbaseObj(tmpMsgbase, lastReadMsgHdr.number);
-
-							//this.PopulateHdrsForCurrentSubBoard();
-							this.subBoardCode = indexRetObj.chosenSubCode;
-							if (this.hdrsForCurrentSubBoard.length > 0)
-							{
-								startIdx = this.GetMsgIdx(GetScanPtrOrLastMsgNum(this.subBoardCode)) + 1;
-								if (startIdx < 0)
-									startIdx = 0;
-								else if (startIdx >= this.hdrsForCurrentSubBoard.length)
-									startIdx = this.hdrsForCurrentSubBoard.length - 1;
-							}
-						}
-						tmpMsgbase.close();
-					}
-				}
-
-				// pSubBoardCode, pStartingMsgOffset, pReturnOnMessageList, pAllowChgArea, pReturnOnNextAreaNav,
-				// pPromptToGoToNextAreaIfNoSearchResults
-				var readRetObj = this.ReadMessages(indexRetObj.chosenSubCode, startIdx, false, false, true, false);
-				// Even if not doing a newscan, still update the scan pointer to the user's last_read pointer
-				if (!newscanOnly)
-				{
-					if (typeof(msg_area.sub[indexRetObj.chosenSubCode].scan_ptr) === "number")
-					{
-						if (!msgNumIsLatestMsgSpecialVal(msg_area.sub[indexRetObj.chosenSubCode].scan_ptr) && msg_area.sub[indexRetObj.chosenSubCode].scan_ptr < msg_area.sub[indexRetObj.chosenSubCode].last_read)
-							msg_area.sub[indexRetObj.chosenSubCode].scan_ptr = msg_area.sub[indexRetObj.chosenSubCode].last_read;
-					}
-					else
-						msg_area.sub[indexRetObj.chosenSubCode].scan_ptr = msg_area.sub[indexRetObj.chosenSubCode].last_read;
-				}
-
-				// Update the text for the current menu item to ensure the message numbers are up to date
-				var currentMenuItem = this.indexedModeMenu.GetItem(this.indexedModeMenu.selectedItemIdx);
-				var itemInfo = this.GetIndexedModeSubBoardMenuItemTextAndInfo(indexRetObj.chosenSubCode);
-				currentMenuItem.text = itemInfo.itemText;
-				currentMenuItem.retval.numNewMsgs = itemInfo.numNewMsgs;
-				// If enabled, store the message headers for this sub-board as a cache for performance
-				if (this.enableIndexedModeMsgListCache)
-				{
-					msgHdrsCache[indexRetObj.chosenSubCode] = {
-						hdrsForCurrentSubBoard: this.hdrsForCurrentSubBoard,
-						hdrsForCurrentSubBoardByMsgNum: this.msgNumToIdxMap
-					};
-				}
-				if (!readRetObj.stoppedReading && (readRetObj.lastAction == ACTION_GO_NEXT_MSG_AREA || readRetObj.lastAction == ACTION_GO_NEXT_MSG))
-					this.indexedModeSetIdxMnuIdxOneMore = true;
-				else
-					this.indexedModeSetIdxMnuIdxOneMore = false;
-
-				/*
-				switch (readRetObj.lastAction)
-				{
-					case ACTION_QUIT:
-						//continueOn = false;
-						break;
-					default:
-						break;
-				}
-				*/
-			}
-			
-			// If the number of new messages has changed (due to reading the sub-board),
-			// then empty the header caches so that we'll fully populate them next time
-			// the user chooses the same sub-board
-			var latestPostInfo = getLatestPostTimestampAndNumNewMsgs(indexRetObj.chosenSubCode);
-			if (latestPostInfo.numNewMsgs != origNumNewMessages)
-			{
-				this.hdrsForCurrentSubBoard = [];
-				this.msgNumToIdxMap = {};
-				if (msgHdrsCache.hasOwnProperty(indexRetObj.chosenSubCode))
-					delete msgHdrsCache[indexRetObj.chosenSubCode];
-			}
-		}
-		else
-		{
-			// On ? keypress, show the help screen. Otherwise, quit.
-			if (indexRetObj.lastUserInput == this.indexedModeMenuKeys.help)
-			{
-				this.ShowIndexedListHelp();
-				drawMenu = true;
-				clearScreenForMenu = true;
-				writeBottomHelpLine = true;
-			}
-			// Ctrl-U: User settings
-			else if (indexRetObj.lastUserInput == this.indexedModeMenuKeys.userSettings)
-			{
-				drawMenu = false;
-				clearScreenForMenu = false;
-				writeBottomHelpLine = false;
-				if (console.term_supports(USER_ANSI) && this.indexedModeMenu.allowANSI) // pReader.msgListUseLightbarListInterface
-				{
-					var userSettingsRetObj = this.DoUserSettings_Scrollable(function(pReader) {
-						var usingANSI = console.term_supports(USER_ANSI) && pReader.indexedModeMenu.allowANSI; // pReader.msgListUseLightbarListInterface
-						if (usingANSI)
-						{
-							// Make sure the help line is built, if not already
-							pReader.MakeIndexedModeHelpLine();
-							// Display the help line at the bottom of the screen
-							console.gotoxy(1, console.screen_rows);
-							console.attributes = "N";
-							console.putmsg(pReader.indexedModeHelpLine); // console.putmsg() can process @-codes, which we use for mouse click tracking
-							console.attributes = "N";
-						}
-					}, 2);
-					if (userSettingsRetObj.needWholeScreenRefresh)
-					{
-						drawMenu = true;
-						clearScreenForMenu = true;
-						writeBottomHelpLine = true;
-					}
-					else
-					{
-						this.indexedModeMenu.DrawPartialAbs(userSettingsRetObj.optionBoxTopLeftX,
-						                                    userSettingsRetObj.optionBoxTopLeftY,
-						                                    userSettingsRetObj.optionBoxWidth,
-						                                    userSettingsRetObj.optionBoxHeight);
-					}
-				}
-				else
-					this.DoUserSettings_Traditional();
-			}
-			else
-				continueOn = false;
-		}
-	}
-
-	this.indexedMode = false;
-	this.doingMsgScan = false;
-}
-
-// For indexed mode: Displays any sub-boards with new messages and lets the user choose one
-//
-// Parameters:
-//  pClearScreen: Whether or not to clear the screen. Defaults to true.
-//  pDrawMenu: Whether or not to draw the menu. Defaults to true.
-//  pDisplayHelpLine: Whether or not to draw the help line at the bottom of the screen. Defaults to true.
-//  pScanScope: Numeric - Whether to scan the current sub-board, group, or all.
-//              This would be SCAN_SCOPE_SUB_BOARD, SCAN_SCOPE_GROUP, or SCAN_SCOPE_ALL.
-//  pNewscanOnly: Boolean: Whether or not to only check sub-boards in the user's newscan configuration.
-//                   Defaults to false.
-//
-// Return value: An object containing the following values:
-//               chosenSubCode: The user's chosen sub-board code; if none selected, this will be null
-//               numNewMsgs: The number of new messages in the chosen sub-board
-//               viewMsgList: Whether or not to view the message list instead of going to reader mode
-//               lastUserInput: The last keypress entered by the user
-function DigDistMsgReader_IndexedModeChooseSubBoard(pClearScreen, pDrawMenu, pDisplayHelpLine, pScanScope, pNewscanOnly)
-{
-	var retObj = {
-		chosenSubCode: null,
-		numNewMsgs: 0,
-		viewMsgList: false, // To view the message list instead of reader mode
-		lastUserInput: ""
-	};
-
-	var clearScreen = (typeof(pClearScreen) === "boolean" ? pClearScreen : true);
-	var displayHelpLine = (typeof(pDisplayHelpLine) === "boolean" ? pDisplayHelpLine : true);
-
-	var scanScope = (isValidScanScopeVal(pScanScope) ? pScanScope : SCAN_SCOPE_ALL);
-	var newScanOnly = (typeof(pNewscanOnly) === "boolean" ? pNewscanOnly : false);
-
-	// Note: DDlightbarMenu supports non-ANSI terminals with a more traditional UI
-	// of listing the items and letting the user choose one by typing its number.
-	// If we are to use the traditional interface, the menu will be in numbered mode,
-	// so we'll need to account for the width of the number of items in the menu.
-	var usingTradInterface = !this.msgListUseLightbarListInterface || !console.term_supports(USER_ANSI);
-	var numberedModeItemNumWidth = 0;
-	if (usingTradInterface)
-	{
-		// Count the number of items that we'll add to the menu
-		var numItems = 0;
-		for (var grpIdx = 0; grpIdx < msg_area.grp_list.length; ++grpIdx)
-		{
-			// If scanning the user's current group or sub-board and this is the wrong group, then skip this group.
-			if ((scanScope == SCAN_SCOPE_GROUP || scanScope == SCAN_SCOPE_SUB_BOARD) && bbs.curgrp != grpIdx)
-				continue;
-
-			var grpNameItemAddedToMenu = false;
-			for (var subIdx = 0; subIdx < msg_area.grp_list[grpIdx].sub_list.length; ++subIdx)
-			{
-				// Skip sub-boards that the user can't read or doesn't have configured for newscans
-				if (!msg_area.grp_list[grpIdx].sub_list[subIdx].can_read)
-					continue;
-				if ((msg_area.grp_list[grpIdx].sub_list[subIdx].scan_cfg & SCAN_CFG_NEW) == 0)
-					continue;
-				// If scanning the user's current sub-board and this is the wrong sub-board, then
-				// skip this sub-board (the other groups should have been skipped in the outer loop).
-				if (scanScope == SCAN_SCOPE_SUB_BOARD && bbs.cursub != subIdx)
-					continue;
-				// Count the item for the group separator (if not added), as well as the item itself
-				if (!grpNameItemAddedToMenu)
-				{
-					++numItems;
-					grpNameItemAddedToMenu = true;
-				}
-				++numItems;
-			}
-		}
-		if (numItems > 0)
-			numberedModeItemNumWidth = numItems.toString().length;
-	}
-
-	// Set text widths for the menu items
-	var newMsgWidthObj = findWidestNumMsgsAndNumNewMsgs(scanScope, newScanOnly);
-	var numMsgsWidth = newMsgWidthObj.widestNumMsgs;
-	var numNewMsgsWidth = newMsgWidthObj.widestNumNewMsgs;
-	// Ensure the column widths for the last few columns (after description) are wide enough
-	// to fit the labels
-	if (numMsgsWidth < 5) // "Total"
-		numMsgsWidth = 5;
-	if (numNewMsgsWidth < 3) // "New"
-		numNewMsgsWidth = 3;
-	var lastPostDateWidth = 10;
-	this.indexedModeItemDescWidth = console.screen_columns - numMsgsWidth - numNewMsgsWidth - lastPostDateWidth - 4;
-	if (usingTradInterface)
-		this.indexedModeItemDescWidth -= (numberedModeItemNumWidth+1);
-	this.indexedModeSubBoardMenuSubBoardFormatStr = "%-" + this.indexedModeItemDescWidth + "s %" + numMsgsWidth + "d %" + numNewMsgsWidth + "d %" + lastPostDateWidth + "s";
-	var thisFunctionFirstCall = false; // Whether or not this is the first call of this function
-	if (typeof(this.indexedModeMenu) !== "object")
-	{
-		thisFunctionFirstCall = true;
-		this.indexedModeMenu = this.CreateLightbarIndexedModeMenu(numMsgsWidth, numNewMsgsWidth, lastPostDateWidth, this.indexedModeItemDescWidth, this.indexedModeSubBoardMenuSubBoardFormatStr);
-	}
-	else
-	{
-		DigDistMsgReader_IndexedModeChooseSubBoard.selectedItemIdx = this.indexedModeMenu.selectedItemIdx;
-		/*
-		// Temporary
-		if (user.is_sysop)
-		{
-			console.print("\x01n\r\n");
-			console.print("Indexed menu item index: " + DigDistMsgReader_IndexedModeChooseSubBoard.selectedItemIdx + "\r\n");
-			console.pause();
-		}
-		// End Temporary
-		*/
-	}
-	// Ensure the menu is clear, and (re-)populate the menu with sub-board information w/ # of new messages in each, etc.
-	// Also, build an array of sub-board codes for each menu item.
-	this.indexedModeMenu.RemoveAllItems();
-	var numSubBoards = 0;
-	var totalNewMsgs = 0;
-	for (var grpIdx = 0; grpIdx < msg_area.grp_list.length; ++grpIdx)
-	{
-		// If scanning the user's current group or sub-board and this is the wrong group, then skip this group.
-		if ((scanScope == SCAN_SCOPE_GROUP || scanScope == SCAN_SCOPE_SUB_BOARD) && bbs.curgrp != grpIdx)
-			continue;
-
-		var grpNameItemAddedToMenu = false;
-		for (var subIdx = 0; subIdx < msg_area.grp_list[grpIdx].sub_list.length; ++subIdx)
-		{
-			// Skip sub-boards that the user can't read or doesn't have configured for newscans
-			if (!msg_area.grp_list[grpIdx].sub_list[subIdx].can_read)
-				continue;
-			if (newScanOnly && !Boolean(msg_area.grp_list[grpIdx].sub_list[subIdx].scan_cfg & SCAN_CFG_NEW))
-				continue;
-			// If scanning the user's current sub-board and this is the wrong sub-board, then
-			// skip this sub-board (the other groups should have been skipped in the outer loop).
-			if (scanScope == SCAN_SCOPE_SUB_BOARD && bbs.cursub != subIdx)
-				continue;
-
-			++numSubBoards;
-
-			if (!grpNameItemAddedToMenu)
-			{
-				//var grpDesc = msg_area.grp_list[grpIdx].name + " - " + msg_area.grp_list[grpIdx].description;
-				var grpDesc = msg_area.grp_list[grpIdx].name;
-				if (msg_area.grp_list[grpIdx].name != msg_area.grp_list[grpIdx].description)
-					grpDesc += " - " + msg_area.grp_list[grpIdx].description;
-				var menuItemText = "\x01n" + this.colors.indexMenuSeparatorLine + charStr(HORIZONTAL_SINGLE, 5);
-				menuItemText += "\x01n" + this.colors.indexMenuSeparatorText + " ";
-				menuItemText += grpDesc;
-				var menuItemLen = console.strlen(menuItemText);
-				if (menuItemLen < this.indexedModeMenu.size.width)
-				{
-					menuItemText += " \x01n" + this.colors.indexMenuSeparatorLine;
-					var numChars = this.indexedModeMenu.size.width - menuItemLen - 1;
-					menuItemText += charStr(HORIZONTAL_SINGLE, numChars);
-				}
-				menuItemText = skipsp(truncsp(menuItemText)); // Trim leading & trailing whitespace
-				this.indexedModeMenu.Add(menuItemText, null, null, false); // Not selectable
-				grpNameItemAddedToMenu = true;
-			}
-
-			var itemInfo = this.GetIndexedModeSubBoardMenuItemTextAndInfo(msg_area.grp_list[grpIdx].sub_list[subIdx].code);
-			this.indexedModeMenu.Add(itemInfo.itemText, {
-				subCode: msg_area.grp_list[grpIdx].sub_list[subIdx].code,
-				numNewMsgs: itemInfo.numNewMsgs
-			});
-
-			totalNewMsgs += itemInfo.numNewMsgs;
-		}
-	}
-	// If there are no items on the menu, then show a message and return
-	if (this.indexedModeMenu.NumItems() == 0)
-	{
-		if (newScanOnly)
-		{
-			console.print("\x01n" + this.text.msgScanCompleteText + "\x01n");
-			console.crlf();
-			console.pause();
-		}
-		return retObj;
-	}
-	// For a newscan, if there are no new messages and the user setting to show the indexed menu when there are no new messages
-	// is disabled, then say so and return.
-	else if (thisFunctionFirstCall && totalNewMsgs == 0 && (!this.userSettings.displayIndexedModeMenuIfNoNewMessages || !this.userSettings.showIndexedNewscanMenuAfterReadingAllNewMsgs))
-	{
-		if (newScanOnly)
-		{
-			console.attributes = "N";
-			console.putmsg(bbs.text(QWKNoNewMessages));
-			//console.crlf();
-			console.pause();
-			return retObj;
-		}
-	}
-	// For a newscan, if this is not the first time the indexed newscan menu is being displayed and there are no more new
-	// messages, and the user has the setting to show the indexed newscan menu now is disabled, then return.
-	else if (!thisFunctionFirstCall && totalNewMsgs == 0 && !this.userSettings.showIndexedNewscanMenuAfterReadingAllNewMsgs)
-	{
-		if (newScanOnly)
-		{
-			console.attributes = "N";
-			console.crlf();
-			printf(bbs.text(MessageScanComplete), numSubBoards);
-			console.pause();
-			return retObj;
-		}
-	}
-
-	// If we've saved the index of the selected item in the menu, then set it back in the menu, if it's
-	// valid.  This is done because the list of items is cleared each time this function is called.
-	if (!thisFunctionFirstCall && typeof(DigDistMsgReader_IndexedModeChooseSubBoard.selectedItemIdx) === "number")
-	{
-		var savedItemIdx = DigDistMsgReader_IndexedModeChooseSubBoard.selectedItemIdx;
-		if (this.indexedModeSetIdxMnuIdxOneMore)
-			++savedItemIdx;
-		if (savedItemIdx >= 0 && savedItemIdx < this.indexedModeMenu.NumItems())
-			setIndexedSubBoardMenuSelectedItemIdx(this.indexedModeMenu, savedItemIdx);
-		else
-			DigDistMsgReader_IndexedModeChooseSubBoard.selectedItemIdx = 0;
-	}
-
-	// For a newscan, if the user setting to "snap" to the first sub-board with new messages is enabled,
-	// then set that as the selected item index.
-	if (newScanOnly && this.userSettings.indexedModeMenuSnapToFirstWithNew)
-	{
-		var foundMenuItem = indexedSubMenuSetSelectedNextWithnNewMsgs(this.indexedModeMenu, this.indexedModeMenu.selectedItemIdx, this.indexedModeMenu.NumItems());
-		// If we haven't found a sub-board with new messages and we didn't start at the
-		// first, then wrap around
-		if (!foundMenuItem && this.indexedModeMenu.selectedItemIdx > 0)
-			indexedSubMenuSetSelectedNextWithnNewMsgs(this.indexedModeMenu, 0, this.indexedModeMenu.selectedItemIdx);
-	}
-
-	// Clear the screen, if desired
-	if (clearScreen)
-		console.clear("\x01n");
-
-	// If using ANSI, then display the help line at the bottom of the scren
-	var usingANSI = console.term_supports(USER_ANSI) && this.indexedModeMenu.allowANSI; // this.msgListUseLightbarListInterface
-	if (usingANSI && displayHelpLine)
-	{
-		// Make sure the help line is built, if not already
-		this.MakeIndexedModeHelpLine();
-		// Display the help line at the bottom of the screen
-		console.gotoxy(1, console.screen_rows);
-		console.attributes = "N";
-		console.putmsg(this.indexedModeHelpLine); // console.putmsg() can process @-codes, which we use for mouse click tracking
-		console.attributes = "N";
-	}
-
-	// Output a header above the menu
-	if (usingANSI)
-		console.gotoxy(1, 1);
-	var descWidthForHdr = this.indexedModeItemDescWidth;
-	var maxScreenWidth = console.screen_columns - 3; // 3 spaces
-	var currentTotalColWidth = descWidthForHdr + numMsgsWidth + numNewMsgsWidth + lastPostDateWidth;
-	if (currentTotalColWidth < maxScreenWidth)
-		descWidthForHdr += (maxScreenWidth - currentTotalColWidth - 1);
-	var indexMenuHdrFormatStr = "\x01n" + this.colors.indexMenuHeader;
-	indexMenuHdrFormatStr += "%-" + descWidthForHdr + "s %" + numMsgsWidth + "s %" + numNewMsgsWidth + "s %" + lastPostDateWidth + "s";
-	printf(indexMenuHdrFormatStr, "Description", "Total", "New", "Last Post");
-	console.attributes = "N";
-	if (!usingANSI)
-		console.crlf();
-
-	// Indexed mode menu input loop
-	var continueOn = true;
-	var drawMenu = (typeof(pDrawMenu) === "boolean" ? pDrawMenu : true);
-	while (continueOn)
-	{
-		var menuRetval = this.indexedModeMenu.GetVal(drawMenu);
-		// Show the menu and get the user's choice
-		retObj.lastUserInput = this.indexedModeMenu.lastUserInput;
-		var lastUserInputUpper = "";
-		if (typeof(this.indexedModeMenu.lastUserInput) === "string")
-			lastUserInputUpper = this.indexedModeMenu.lastUserInput.toUpperCase();
-		if (menuRetval != null)
-		{
-			retObj.chosenSubCode = menuRetval.subCode;
-			retObj.numNewMsgs = menuRetval.numNewMsgs;
-			// If the user has the option enabled to view the message list when pressing enter here,
-			// then allow that.
-			if (this.userSettings.enterFromIndexMenuShowsMsgList)
-				retObj.viewMsgList = true;
-			continueOn = false;
-		}
-		else if (lastUserInputUpper == this.indexedModeMenuKeys.quit || retObj.lastUserInput == KEY_ESC)
-			continueOn = false;
-		else if (lastUserInputUpper == this.indexedModeMenuKeys.showMsgList)
-		{
-			// Message list for the highlighted sub-board
-			var highlightedItem = this.indexedModeMenu.GetItem(this.indexedModeMenu.selectedItemIdx);
-			retObj.chosenSubCode = highlightedItem.retval.subCode;
-			retObj.numNewMsgs = highlightedItem.retval.numNewMsgs;
-			retObj.viewMsgList = true;
-			continueOn = false;
-		}
-		else if (lastUserInputUpper == this.indexedModeMenuKeys.markAllRead)
-		{
-			// Mark all read in the sub-board
-			var highlightedItem = this.indexedModeMenu.GetItem(this.indexedModeMenu.selectedItemIdx);
-			if (subBoardNewscanAllRead(highlightedItem.retval.subCode))
-			{
-				// Update the item in the indexed mode menu
-				var itemInfo = this.GetIndexedModeSubBoardMenuItemTextAndInfo(highlightedItem.retval.subCode);
-				var menuItem = this.indexedModeMenu.MakeItemWithRetval({
-					subCode: highlightedItem.retval.subCode,
-					numNewMsgs: itemInfo.numNewMsgs
-				});
-				menuItem.text = itemInfo.itemText;
-				this.indexedModeMenu.items[this.indexedModeMenu.selectedItemIdx] = menuItem;
-				this.indexedModeMenu.WriteItemAtItsLocation(this.indexedModeMenu.selectedItemIdx, true, false);
-			}
-			drawMenu = false; // No need to re-draw the whole menu
-		}
-		else if (lastUserInputUpper == this.indexedModeMenuKeys.userSettings)
-		{
-			// The calling function will do the user settings dialog
-			continueOn = false;
-			retObj.lastUserInput = this.indexedModeMenuKeys.userSettings;
-		}
-		else if (lastUserInputUpper == this.indexedModeMenuKeys.help)
-		{
-			// The calling function will show the help screen and re-drawe
-			// the bottom help line below the menu
-			continueOn = false;
-			retObj.lastUserInput = this.indexedModeMenuKeys.help;
-		}
-	}
-	console.attributes = "N";
-	return retObj;
-}
-
-// Helper for DigDistMsgReader_IndexedModeChooseSubBoard(): Sets the selected item in the
-// indexed mode sub-board menu and adjusts the menu items to be in a good location
-//
-// Parameters:
-//  pIndexSubBoardMenu: The indexed sub-board menu
-//  pSelectedItemIdx: The index of the item to set as the selected item
-function setIndexedSubBoardMenuSelectedItemIdx(pIndexSubBoardMenu, pSelectedItemIdx)
-{
-	if (pSelectedItemIdx >= 0 && pSelectedItemIdx < pIndexSubBoardMenu.NumItems())
-	{
-		pIndexSubBoardMenu.SetSelectedItemIdx(pSelectedItemIdx);
-		// If the indexed menu has more items than will fit on the screen & isn't on the last
-		// page, then set the top item index to one before the selected index (if >0) or the
-		// same as the selected item index.
-		var selectedItemIsFirst = pIndexSubBoardMenu.selectedItemIdx == pIndexSubBoardMenu.topItemIdx;
-		var selectedItemOnLastPage = pSelectedItemIdx >= pIndexSubBoardMenu.GetTopItemIdxOfLastPage();
-		var moreThanOneScreenfulOfItems = pIndexSubBoardMenu.NumItems() > console.screen_columns - 2;
-		// Checking if the selected item index is on the first page
-		var numItems = pIndexSubBoardMenu.NumItems();
-		var numItemsPerPage = pIndexSubBoardMenu.GetNumItemsPerPage();
-		var lastItemIdxForFirstPage = (numItems > numItemsPerPage ? numItemsPerPage - 1 : numItems - 1);
-		var selectedItemIsOnFirstPage = (pIndexSubBoardMenu.selectedItemIdx >= 0 && pIndexSubBoardMenu.selectedItemIdx <= lastItemIdxForFirstPage);
-		if (!selectedItemIsFirst && !selectedItemOnLastPage && moreThanOneScreenfulOfItems && !selectedItemIsOnFirstPage)
-		{
-			if (pIndexSubBoardMenu.selectedItemIdx > 0)
-				pIndexSubBoardMenu.topItemIdx = pIndexSubBoardMenu.selectedItemIdx - 1;
-			else
-				pIndexSubBoardMenu.topItemIdx = pIndexSubBoardMenu.selectedItemIdx;
-		}
-	}
-}
-
-// Helper for DigDistMsgReader_IndexedModeChooseSubBoard(): Sets the selected item in the
-// indexed mode sub-board menu to the next sub-board with new messages, including the
-// one at the given starting index.
-//
-// Parameters:
-//  pIndexSubBoardMenu: The indexed sub-board menu
-//  pStartIdx: The index of the sub-board to start at
-//  pOnePastLastIdx: One past the index of the last sub-board to check
-//
-// Return value: Boolean - Whether or not a sub-board with new messages was found in the menu
-function indexedSubMenuSetSelectedNextWithnNewMsgs(pIndexSubBoardMenu, pStartIdx, pOnePastLastIdx)
-{
-	var foundMenuItem = false;
-	for (var i = pStartIdx; i < pOnePastLastIdx; ++i)
-	{
-		var menuItem = pIndexSubBoardMenu.GetItem(i);
-		if (menuItem == null || typeof(menuItem) !== "object" || !menuItem.hasOwnProperty("retval"))
-			continue;
-		if (menuItem.retval != null && menuItem.retval.numNewMsgs > 0)
-		{
-			if (menuItem.retval.numNewMsgs > 0)
-			{
-				setIndexedSubBoardMenuSelectedItemIdx(pIndexSubBoardMenu, i);
-				DigDistMsgReader_IndexedModeChooseSubBoard.selectedItemIdx = i;
-				foundMenuItem = true;
-				break;
-			}
-		}
-	}
-	return foundMenuItem;
-}
-
-// Returns a string to use for a sub-board for the indexed mode sub-board menu
-//
-// Parameters:
-//  pSubCode: The internal code of the sub-board
-//
-// Return value: An object containing the following properties:
-//               itemText: A string for the indexed mode menu item for the sub-board
-//               numNewMsgs: The number of new messages in the sub-board
-function DigDistMsgReader_GetIndexedModeSubBoardMenuItemTextAndInfo(pSubCode)
-{
-	var retObj = {
-		itemText: "",
-		numNewMsgs: 0
-	};
-
-	if (typeof(this.indexedModeSubBoardMenuSubBoardFormatStr) !== "string" || typeof(this.indexedModeItemDescWidth) !== "number")
-		return retObj;
-
-	// posts: number of messages currently posted to this sub-board (introduced in v3.18c)
-	var totalNumMsgsInSub = msg_area.sub[pSubCode].posts;
-	var latestPostInfo = getLatestPostTimestampAndNumNewMsgs(pSubCode);
-	var lastPostDate = strftime("%Y-%m-%d", latestPostInfo.latestMsgTimestamp);
-	var subDesc = (latestPostInfo.numNewMsgs > 0 ? "NEW " : "    ");
-	subDesc += msg_area.sub[pSubCode].name;
-	if (msg_area.sub[pSubCode].name !== msg_area.sub[pSubCode].description)
-		subDesc += " - " + msg_area.sub[pSubCode].description;
-	subDesc = subDesc.substr(0, this.indexedModeItemDescWidth);
-	retObj.itemText = format(this.indexedModeSubBoardMenuSubBoardFormatStr, subDesc, totalNumMsgsInSub, latestPostInfo.numNewMsgs, lastPostDate);
-	retObj.numNewMsgs = latestPostInfo.numNewMsgs;
-	return retObj;
-}
-
-// Builds the indexed mode help line (for the bottom of the screen) if it's not already
-// built yet
-function DigDistMsgReader_MakeIndexedModeHelpLine()
-{
-	// If it's already built, then just return now
-	if (typeof(this.indexedModeHelpLine) === "string")
-		return;
-
-	this.indexedModeHelpLine = this.colors.lightbarIndexedModeHelpLineHotkeyColor + "@CLEAR_HOT@@`" + UP_ARROW + "`" + KEY_UP + "@"
-	                         + this.colors.lightbarIndexedModeHelpLineGeneralColor + ", "
-	                         + this.colors.lightbarIndexedModeHelpLineHotkeyColor + "@`" + DOWN_ARROW + "`" + KEY_DOWN + "@"
-	                         + this.colors.lightbarIndexedModeHelpLineGeneralColor + ", "
-	                         + this.colors.lightbarIndexedModeHelpLineHotkeyColor + "@`PgUp`" + "\x1b[V" + "@"
-	                         + this.colors.lightbarIndexedModeHelpLineGeneralColor + "/"
-	                         + this.colors.lightbarIndexedModeHelpLineHotkeyColor + "@`P`" + "\x1b[V" + "@"
-	                         + this.colors.lightbarIndexedModeHelpLineGeneralColor + ", "
-	                         + this.colors.lightbarIndexedModeHelpLineHotkeyColor + "@`PgDn`" + KEY_PAGEDN + "@"
-	                         + this.colors.lightbarIndexedModeHelpLineGeneralColor + "/"
-	                         + this.colors.lightbarIndexedModeHelpLineHotkeyColor + "@`N`" + "\x1b[V" + "@"
-	                         + this.colors.lightbarIndexedModeHelpLineGeneralColor + ", "
-	                         + this.colors.lightbarIndexedModeHelpLineHotkeyColor + "@`ENTER`" + KEY_ENTER + "@"
-	                         + this.colors.lightbarIndexedModeHelpLineGeneralColor + ", "
-	                         + this.colors.lightbarIndexedModeHelpLineHotkeyColor + "@`HOME`" + KEY_HOME + "@"
-	                         + this.colors.lightbarIndexedModeHelpLineGeneralColor + "/"
-	                         + this.colors.lightbarIndexedModeHelpLineHotkeyColor + "@`F`" + "\x1b[V" + "@"
-	                         + this.colors.lightbarIndexedModeHelpLineGeneralColor + ", "
-	                         + this.colors.lightbarIndexedModeHelpLineHotkeyColor + "@`END`" + KEY_END + "@"
-	                         + this.colors.lightbarIndexedModeHelpLineGeneralColor + "/"
-	                         + this.colors.lightbarIndexedModeHelpLineHotkeyColor + "@`L`" + "\x1b[V" + "@"
-	                         + this.colors.lightbarIndexedModeHelpLineGeneralColor + ", "
-	                         + this.colors.lightbarIndexedModeHelpLineHotkeyColor + "@`Ctrl-U`" + CTRL_U + "@"
-	                         + this.colors.lightbarIndexedModeHelpLineGeneralColor + ", "
-	                         + this.colors.lightbarIndexedModeHelpLineHotkeyColor + "@`Q`Q@"
-	                         + this.colors.lightbarIndexedModeHelpLineParenColor + ")"
-	                         + this.colors.lightbarIndexedModeHelpLineGeneralColor + "uit, "
-	                         + this.colors.lightbarIndexedModeHelpLineHotkeyColor + "@`?`?@";
-	// Add spaces so that the text is centered on the screen
-	var helpLineLen = 60;
-	var leftSideNumChars = Math.floor(console.screen_columns / 2) - Math.floor(helpLineLen / 2);
-	var rightSideNumChars = leftSideNumChars;
-	var totalNumChars = leftSideNumChars + rightSideNumChars + helpLineLen;
-	var maxLen = console.screen_columns - 1;
-	if (totalNumChars > maxLen)
-		rightSideNumChars -= (totalNumChars - maxLen);
-	else if (totalNumChars < maxLen)
-		rightSideNumChars += (maxLen - totalNumChars);
-	this.indexedModeHelpLine = "\x01n" + this.colors.lightbarIndexedModeHelpLineBkgColor + format("%*s", leftSideNumChars, "")
-	                         + this.indexedModeHelpLine + format("%*s", rightSideNumChars, "");
-}
-
-// Shows help for the indexed mode list
-function DigDistMsgReader_ShowIndexedListHelp()
-{
-	console.clear("\x01n");
-	DisplayProgramInfo();
-	console.attributes = "N";
-	console.print(this.colors.tradInterfaceHelpScreenColor);
-	console.print("The current mode is Indexed Mode, which shows total and new messages for any\r\n");
-	console.print("sub-boards in your newscan configuration. You may choose a sub-board from this\r\n");
-	console.print("list to read messages in that sub-board.\r\n");
-	console.crlf();
-	if (console.term_supports(USER_ANSI) && this.msgListUseLightbarListInterface)
-	{
-		var formatStr = "\x01n\x01h\x01c%15s" + this.colors.tradInterfaceHelpScreenColor + ": %s\r\n";
-		var formatStr2 = "\x01n\x01h\x01c%10s \x01n\x01cor \x01h%s" + this.colors.tradInterfaceHelpScreenColor + ": %s\r\n";
-		console.crlf();
-		displayTextWithLineBelow("Summary of the keyboard commands:", false, this.colors.tradInterfaceHelpScreenColor, "\x01k\x01h");
-		console.print(this.colors.tradInterfaceHelpScreenColor);
-		printf(formatStr, "Down arrow", "Move the cursor down/select the next sub-board");
-		printf(formatStr, "Up arrow", "Move the cursor up/select the previous sub-board");
-		printf(formatStr2, "PageDown", "N", "Go to the next page");
-		printf(formatStr2, "PageUp", "P", "Go to the previous page");
-		printf(formatStr2, "HOME", "F", "Go to the first item");
-		printf(formatStr2, "END", "L", "Go to the last item");
-		printf(formatStr, "ENTER", "Read the sub-board");
-		printf(formatStr, "R", "Mark all read");
-		printf(formatStr, "M", "Show message list for the sub-board");
-		printf(formatStr, "Ctrl-U", "User settings");
-		printf(formatStr, "Q", "Quit");
-		//printf(formatStr, "?", "Show this help screen");
-	}
-	console.pause();
-	console.aborted = false;
-}
-
-// Returns an object with the widest text length of the number of new messsages and
-// number of new-to-you messages in all readable sub-boards in the user's newscan configuration
-//
-// Parameters:
-//  pScanScope: Numeric - Whether to scan the current sub-board, group, or all.
-//              This would be SCAN_SCOPE_SUB_BOARD, SCAN_SCOPE_GROUP, or SCAN_SCOPE_ALL.
-//  pForNewscanOnly: Boolean: Whether or not to only check sub-boards in the user's newscan configuration.
-//                   Defaults to false.
-//
-// Return value: An object with the following properties:
-//               widestNumMsgs: The biggest length of the number of messages in the sub-boards
-//               widestNumNewMsgs: The biggest length of the number of new (unread) messages in the sub-boards
-function findWidestNumMsgsAndNumNewMsgs(pScanScope, pForNewscanOnly)
-{
-	var retObj = {
-		widestNumMsgs: 0,
-		widestNumNewMsgs: 0
-	};
-
-	var scanScope = (isValidScanScopeVal(pScanScope) ? pScanScope : SCAN_SCOPE_ALL);
-	var onlyNewscanCfg = (typeof(pForNewscanOnly) === "boolean" ? pForNewscanOnly : false);
-
-	for (var grpIdx = 0; grpIdx < msg_area.grp_list.length; ++grpIdx)
-	{
-		// If scanning the user's current group or sub-board and this is the wrong group, then skip this group.
-		if ((scanScope == SCAN_SCOPE_GROUP || scanScope == SCAN_SCOPE_SUB_BOARD) && bbs.curgrp != grpIdx)
-			continue;
-
-		for (var subIdx = 0; subIdx < msg_area.grp_list[grpIdx].sub_list.length; ++subIdx)
-		{
-			if (!msg_area.grp_list[grpIdx].sub_list[subIdx].can_read)
-				continue;
-			if (onlyNewscanCfg && !Boolean(msg_area.grp_list[grpIdx].sub_list[subIdx].scan_cfg & SCAN_CFG_NEW))
-				continue;
-			// If scanning the user's current sub-board and this is the wrong sub-board, then
-			// skip this sub-board (the other groups should have been skipped in the outer loop).
-			if (scanScope == SCAN_SCOPE_SUB_BOARD && bbs.cursub != subIdx)
-				continue;
-
-			++retObj.numSubBoards;
-			var totalNumMsgsInSub = msg_area.grp_list[grpIdx].sub_list[subIdx].posts;
-			var totalNumMsgsInSubLen = totalNumMsgsInSub.toString().length;
-			if (totalNumMsgsInSubLen > retObj.widestNumMsgs)
-				retObj.widestNumMsgs = totalNumMsgsInSubLen;
-			var latestPostInfo = getLatestPostTimestampAndNumNewMsgs(msg_area.grp_list[grpIdx].sub_list[subIdx].code);
-			var numNewMessagesInSubLen = latestPostInfo.numNewMsgs.toString().length;
-			if (numNewMessagesInSubLen > retObj.widestNumNewMsgs)
-				retObj.widestNumNewMsgs = numNewMessagesInSubLen;
-		}
-	}
-	return retObj;
-}
-
-// Gets the timestamp of the latest post in a sub-board and number of new messages (unread
-// to the user) in a sub-board (based on the user's scan_ptr).
-//
-// Parameters:
-//  pSubCode: The internal code of a sub-board to check
-//
-// Return value: An object with the following properties:
-//               latestMsgTimestamp: The timestamp of the latest post in the sub-board
-//               numnewMsgs: The number of new messages (unread to the user) in the sub-board
-function getLatestPostTimestampAndNumNewMsgs(pSubCode)
-{
-	var retObj = {
-		latestMsgTimestamp: 0,
-		numNewMsgs: 0
-	};
-
-	var msgbase = new MsgBase(pSubCode);
-	if (msgbase.open())
-	{
-		retObj.latestMsgTimestamp = getLatestPostTimeWithMsgbase(msgbase, pSubCode);
-		var totalNumMsgs = msgbase.total_msgs;
-		// scan_ptr: user's current new message scan pointer (highest-read message number)
-		if (typeof(msg_area.sub[pSubCode].scan_ptr) === "number")
-		{
-			var lastReadableMsgHdr = getLastReadableMsgHdrInSubBoard(pSubCode);
-			if (lastReadableMsgHdr != null)
-			{
-				// If the user's scan_ptr for the sub-board isn't the 'last message'
-				// special value, then use it
-				if (!subBoardScanPtrIsLatestMsgSpecialVal(pSubCode))
-				{
-					// Count the number of readable messages after scan_ptr up to the last read message.
-					// If both index objects are null, then calculate this via the last readable message
-					// number and the scan pointer.
-					var scanPtrMsgIndex = msgbase.get_msg_index(false, msg_area.sub[pSubCode].scan_ptr, false);
-					//var lastMsgIndex = msgbase.get_msg_index(false, msgbase.last_msg, false);
-					//if (scanPtrMsgIndex != null && lastMsgIndex != null)
-					if (scanPtrMsgIndex != null)
-					{
-						//for (var i = scanPtrMsgIndex.offset; i < lastMsgIndex.offset; ++i)
-						for (var i = scanPtrMsgIndex.offset; i < lastReadableMsgHdr.offset; ++i)
-						{
-							var msgIndex = msgbase.get_msg_index(true, i, false);
-							if (msgIndex != null && isReadableMsgHdr(msgIndex, pSubCode))
-								++retObj.numNewMsgs;
-						}
-					}
-					else
-					{
-						retObj.numNewMsgs = lastReadableMsgHdr.number - msg_area.sub[pSubCode].scan_ptr;
-						// Calculating the number of new messages in the above way seems to
-						// sometimes (though rarely) be incorrect (returning more than the actual
-						// number of new messages).  Another way might be to start from scan_ptr
-						// scan_ptr and count the number of readable messages.
-						//retObj.numNewMsgs = numReadableMsgsFromAbsMsgNumWithMsgbase(msgbase, pSubCode, msg_area.sub[pSubCode].scan_ptr);
-					}
-				}
-			}
-		}
-		else if (typeof(msg_area.sub[pSubCode].last_read) === "number")
-		{
-			var lastReadableMsgHdr = getLastReadableMsgHdrInSubBoard(pSubCode);
-			if (lastReadableMsgHdr != null)
-			{
-				retObj.numNewMsgs = lastReadableMsgHdr.number - msg_area.sub[pSubCode].last_read;
-				// Calculating the number of new messages in the above way seems to
-				// sometimes (though rarely) be incorrect (returning more than the actual
-				// number of new messages).  Another way might be to start from scan_ptr
-				// scan_ptr and count the number of readable messages.
-				//retObj.numNewMsgs = numReadableMsgsFromAbsMsgNumWithMsgbase(msgbase, pSubCode, msg_area.sub[pSubCode].last_read);
-			}
-			else // Count the number of new readable messages.
-				retObj.numNewMsgs = numReadableMsgsFromAbsMsgNumWithMsgbase(msgbase, pSubCode, msg_area.sub[pSubCode].last_read);
-		}
-		else
-			retObj.numNewMsgs = msg_area.sub[pSubCode].posts;
-		msgbase.close();
-		if (retObj.numNewMsgs < 0)
-			retObj.numNewMsgs = 0;
-	}
-	return retObj;
-}
-
-// Creates the DDLightbarMenu object for indexed reader mode
-function DigDistMsgReader_CreateLightbarIndexedModeMenu(pNumMsgsWidth, pNumNewMsgsWidth, pLastPostDateWidth, pDescWidth)
-{
-	// Start & end indexes for the selectable items
-	var indexMenuIdxes = {
-		newStatusStart: 0,
-		newStatusEnd: 4,
-		descStart: 4,
-		descEnd: pDescWidth+1,
-		totalStart: pDescWidth+1,
-		totalEnd: pDescWidth+pNumMsgsWidth+2,
-		newMsgsStart: pDescWidth+1+pNumMsgsWidth+1,
-		newMsgsEnd: pDescWidth+1+pNumMsgsWidth+pNumNewMsgsWidth+2,
-		lastPostDateStart: pDescWidth+1+pNumMsgsWidth+pNumNewMsgsWidth+2,
-		lastPostDateEnd: pDescWidth+1+pNumMsgsWidth+pNumNewMsgsWidth+pLastPostDateWidth+3
-	};
-
-	//var menuHeight = 12;
-	// For the menu height, -2 gives one row at the top for the column headers and one row
-	// at the bottom for the help line
-	var menuHeight = console.screen_rows - 2;
-
-	var indexedModeMenu = new DDLightbarMenu(1, 2, console.screen_columns, menuHeight);
-	indexedModeMenu.allowUnselectableItems = true;
-	indexedModeMenu.scrollbarEnabled = true;
-	indexedModeMenu.borderEnabled = false;
-	// Colors:
-	var newStatusHigh = "\x01n" + this.colors.indexMenuHighlightBkg + this.colors.indexMenuNewIndicatorHighlight;
-	var descHigh = "\x01n" + this.colors.indexMenuHighlightBkg + this.colors.indexMenuDescHighlight;
-	var totalMsgsHi = "\x01n" + this.colors.indexMenuHighlightBkg + this.colors.indexMenuTotalMsgsHighlight;
-	var numNewMsgsHi = "\x01n" + this.colors.indexMenuHighlightBkg + this.colors.indexMenuNumNewMsgsHighlight;
-	var lastPostDateHi = "\x01n" + this.colors.indexMenuHighlightBkg + this.colors.indexMenuLastPostDateHighlight;
-	indexedModeMenu.SetColors({
-		itemColor: [{start: indexMenuIdxes.newStatusStart, end: indexMenuIdxes.newStatusEnd, attrs: "\x01n" + this.colors.indexMenuNewIndicator},
-		            {start: indexMenuIdxes.descStart, end: indexMenuIdxes.descEnd, attrs: "\x01n" + this.colors.indexMenuDesc},
-		            {start: indexMenuIdxes.totalStart, end: indexMenuIdxes.totalEnd, attrs: "\x01n" + this.colors.indexMenuTotalMsgs},
-		            {start: indexMenuIdxes.newMsgsStart, end: indexMenuIdxes.newMsgsEnd, attrs: "\x01n" + this.colors.indexMenuNumNewMsgs},
-		            {start: indexMenuIdxes.lastPostDateStart, end: indexMenuIdxes.lastPostDateEnd, attrs: "\x01n" + this.colors.indexMenuLastPostDate}],
-		selectedItemColor: [{start: indexMenuIdxes.newStatusStart, end: indexMenuIdxes.newStatusEnd, attrs: newStatusHigh},
-		                    {start: indexMenuIdxes.descStart, end: indexMenuIdxes.descEnd, attrs: descHigh},
-		                    {start: indexMenuIdxes.totalStart, end: indexMenuIdxes.totalEnd, attrs: totalMsgsHi},
-		                    {start: indexMenuIdxes.newMsgsStart, end: indexMenuIdxes.newMsgsEnd, attrs: numNewMsgsHi},
-		                    {start: indexMenuIdxes.lastPostDateStart, end: indexMenuIdxes.lastPostDateEnd, attrs: lastPostDateHi}],
-		unselectableItemColor: ""
-	});
-
-	indexedModeMenu.multiSelect = false;
-	indexedModeMenu.ampersandHotkeysInItems = false;
-	indexedModeMenu.wrapNavigation = false;
-	indexedModeMenu.allowANSI = this.msgListUseLightbarListInterface && console.term_supports(USER_ANSI);
-
-	// Add additional keypresses for quitting the menu's input loop so we can
-	// respond to these keys
-	// TODO: Include Mm to allow the user to view the message list instead of read it from the indexed menu
-	//indexedModeMenu.AddAdditionalQuitKeys();
-	for (var key in this.indexedModeMenuKeys)
-	{
-		if (/[a-zA-Z]/.test(this.indexedModeMenuKeys[key]))
-		{
-			indexedModeMenu.AddAdditionalQuitKeys(this.indexedModeMenuKeys[key].toLowerCase());
-			indexedModeMenu.AddAdditionalQuitKeys(this.indexedModeMenuKeys[key].toUpperCase());
-		}
-		else
-			indexedModeMenu.AddAdditionalQuitKeys(this.indexedModeMenuKeys[key]);
-	}
-
-	// Add additional keypresses for PageUp, PageDown, HOME (first page), and END (last page)
-	indexedModeMenu.AddAdditionalPageUpKeys("Pp"); // Previous page
-	indexedModeMenu.AddAdditionalPageDownKeys("Nn"); // Next page
-	indexedModeMenu.AddAdditionalFirstPageKeys("Ff"); // First page
-	indexedModeMenu.AddAdditionalLastPageKeys("Ll"); // Last page
-
-	return indexedModeMenu;
-}
-
-// For the DigDistMsgReader class: Writes message lines to a file on the BBS
-// machine.
-//
-// Parameters:
-//  pMsgHdr: The header object for the message
-//  pFilename: The name of the file to write the message to
-//  pPromptPos: Optional - An object containing x & y coordinates for the prompot position,
-//              if using the ANSI interfce. If this is a valid object with x & y, this
-//              will be used for cursor positioning before prompting to save all message
-//              headers (if applicable).
-//
-// Return value: An object containing the following properties:
-//               succeeded: Boolean - Whether or not the file was successfully written
-//               errorMsg: String - On failure, will contain the reason it failed
-function DigDistMsgReader_SaveMsgToFile(pMsgHdr, pFilename, pPromptPos)
-{
-	// Sanity checking
-	if (typeof(pMsgHdr) !== "object")
-		return({ succeeded: false, errorMsg: "Header object not given"});
-	if (typeof(pFilename) != "string")
-		return({ succeeded: false, errorMsg: "Filename parameter not a string"});
-	if (pFilename.length == 0)
-		return({ succeeded: false, errorMsg: "Empty filename given"});
-
-	var retObj = {
-		succeeded: true,
-		errorMsg: ""
-	};
-
-	// Get the message text and save it
-	// Note: GetMsgInfoForEnhancedReader() can expand @-codes in the message,
-	// but for now we're saving the message basically as-is.
-	//var msgInfo = this.GetMsgInfoForEnhancedReader(pMsgHdr, false, false, false);
-	var msgbase = new MsgBase(this.subBoardCode);
-	if (msgbase.open())
-	{
-		var msgBody = msgbase.get_msg_body(false, pMsgHdr.number, false, false, true, true);
-		var hdrLines = this.GetExtdMsgHdrInfo(msgbase, pMsgHdr.number, false, false, false, false);
-		msgbase.close();
-
-		var messageSaveFile = new File(pFilename);
-		if (messageSaveFile.open("w"))
-		{
-			var writeAllHeaders = false;
-			if (typeof(this.saveAllHdrsWhenSavingMsgToBBSPC) === "boolean")
-				writeAllHeaders = this.saveAllHdrsWhenSavingMsgToBBSPC;
-			else if (typeof(this.saveAllHdrsWhenSavingMsgToBBSPC) === "string" && this.saveAllHdrsWhenSavingMsgToBBSPC.toUpperCase() == "ASK")
-			{
-				console.attributes = "N";
-				if (typeof(pPromptPos) === "object" && pPromptPos.hasOwnProperty("x") && pPromptPos.hasOwnProperty("y"))
-				{
-					console.gotoxy(pPromptPos);
-					console.cleartoeol("\x01n");
-					console.gotoxy(pPromptPos);
-				}
-				writeAllHeaders = !console.noyes("Write all headers to saved message");
-			}
-
-			if (writeAllHeaders)
-			{
-				// Write all header information to the file
-				for (var i = 0; i < hdrLines.length; ++i)
-					messageSaveFile.writeln(hdrLines[i]);
-			}
-			else
-			{
-				// Write to, from, subjetc, etc. to the file
-				if (this.subBoardCode == "mail")
-				{
-					if (!msgIsToCurrentUserByName(pMsgHdr))
-					{
-						messageSaveFile.writeln("From " +  pMsgHdr.to + "'s personal email");
-						messageSaveFile.writeln("=======================");
-					}
-				}
-				else
-				{
-					var line = format("From sub-board: %s, %s",
-					                  msg_area.grp_list[msg_area.sub[this.subBoardCode].grp_index].description,
-					                  msg_area.sub[this.subBoardCode].description);
-					messageSaveFile.writeln(line);
-				}
-				messageSaveFile.writeln("From: " + pMsgHdr.from);
-				messageSaveFile.writeln("To: " + pMsgHdr.to);
-				messageSaveFile.writeln("Subject: " + pMsgHdr.subject);
-				// Message time
-				var msgWrittenLocalTime = msgWrittenTimeToLocalBBSTime(pMsgHdr);
-				var dateTimeStr = "";
-				if (msgWrittenLocalTime != -1)
-					dateTimeStr = strftime("%a, %d %b %Y %H:%M:%S", msgWrittenLocalTime);
-				else
-					dateTimeStr = pMsgHdr.date.replace(/ [-+][0-9]+$/, "");
-				messageSaveFile.writeln("Date: " + dateTimeStr);
-
-			}
-			messageSaveFile.writeln("===============================");
-
-			// If the message body has ANSI, then use the Graphic object to strip it
-			// of any cursor movement codes
-			var msgHasANSICodes = msgBody.indexOf("\x1b[") >= 0;
-			if (msgHasANSICodes)
-			{
-				if (textHasDrawingChars(msgBody))
-				{
-					//var graphic = new Graphic(this.msgAreaWidth, this.msgAreaHeight);
-					// To help ensure ANSI messages look good, it seems the Graphic object should have
-					// its with later set to 1 less than the width used to create it.
-					var graphicWidth = (msgAreaWidth < console.screen_columns ? msgAreaWidth+1 : console.screen_columns);
-					var graphic = new Graphic(graphicWidth, this.msgAreaHeight);
-					graphic.auto_extend = true;
-					graphic.ANSI = ansiterm.expand_ctrl_a(msgBody);
-					graphic.width = graphicWidth - 1;
-					msgBody = syncAttrCodesToANSI(graphic.MSG);
-				}
-				else
-					msgBody = syncAttrCodesToANSI(msgBody);
-			}
-
-			// Write the message body to the file
-			messageSaveFile.write(msgBody);
-			messageSaveFile.close();
-		}
-		else
-		{
-			retObj.succeeded = false;
-			retObj.errorMsg = "Failed to open the file for writing";
-		}
-	}
-	else
-	{
-		retObj.succeeded = false;
-		retObj.errorMsg = "Unable to open the messagebase";
-	}
-
-	return retObj;
-}
-
-// For the DigDistMsgReader class: Toggles whether a message has been 'selected'
-// (i.e., for things like batch delete, etc.)
-//
-// Parameters:
-//  pSubCode: The internal sub-board code of the message sub-board where the
-//            message resides
-//  pMsgIdx: The index of the message to toggle
-//  pSelected: Optional boolean to explictly specify whether the message should
-//             be selected.  If this is not provided (or is null), then this
-//             message will simply toggle the selection state of the message.
-function DigDistMsgReader_ToggleSelectedMessage(pSubCode, pMsgIdx, pSelected)
-{
-	// Sanity checking
-	if (typeof(pSubCode) !== "string") return;
-	if (typeof(pMsgIdx) !== "number") return;
-
-	// If the 'selected message' object doesn't have the sub code index,
-	// then add it.
-	if (!this.selectedMessages.hasOwnProperty(pSubCode))
-		this.selectedMessages[pSubCode] = {};
-
-	// If pSelected is a boolean, then it specifies the specific selection
-	// state of the message (true = selected, false = not selected).
-	if (typeof(pSelected) == "boolean")
-	{
-		if (pSelected)
-		{
-			if (!this.selectedMessages[pSubCode].hasOwnProperty(pMsgIdx))
-				this.selectedMessages[pSubCode][pMsgIdx] = true;
-		}
-		else
-		{
-			if (this.selectedMessages[pSubCode].hasOwnProperty(pMsgIdx))
-				delete this.selectedMessages[pSubCode][pMsgIdx];
-		}
-	}
-	else
-	{
-		// pSelected is not a boolean, so simply toggle the selection state of
-		// the message.
-		// If the object for the given sub-board code contains the message
-		// index, then remove it.  Otherwise, add it.
-		if (this.selectedMessages[pSubCode].hasOwnProperty(pMsgIdx))
-			delete this.selectedMessages[pSubCode][pMsgIdx];
-		else
-			this.selectedMessages[pSubCode][pMsgIdx] = true;
-	}
-}
-
-// For the DigDistMsgReader class: Returns whether a message (by sub-board code & index)
-// is selected (i.e., for batch delete, etc.).
-//
-// Parameters:
-//  pSubCode: The internal sub-board code of the message sub-board where the
-//            message resides
-//  pMsgIdx: The index of the message to toggle
-//
-// Return value: Boolean - Whether or not the given message has been selected
-function DigDistMsgReader_MessageIsSelected(pSubCode, pMsgIdx)
-{
-	return (this.selectedMessages.hasOwnProperty(pSubCode) && this.selectedMessages[pSubCode].hasOwnProperty(pMsgIdx));
-}
-
-// For the DigDistMsgReader class: Checks to see if all selected messages can
-// be deleted (i.e., whether the user has permission to delete all of them).
-function DigDistMsgReader_AllSelectedMessagesCanBeDeleted()
-{
-	// If the user has sysop access, then they should be able to delete messages.
-	if (user.is_sysop)
-		return true;
-
-	var userCanDeleteAllSelectedMessages = true;
-
-	var msgBase = null;
-	for (var subBoardCode in this.selectedMessages)
-	{
-		// If the current sub-board is personal mail, then the user can delete
-		// those messages.  Otherwise, check the sub-board configuration to see
-		// if the user can delete messages in the sub-board.
-		if (subBoardCode != "mail")
-		{
-			msgBase = new MsgBase(subBoardCode);
-			if (msgBase.open())
-			{
-				userCanDeleteAllSelectedMessages = userCanDeleteAllSelectedMessages && ((msgBase.cfg.settings & SUB_DEL) == SUB_DEL);
-				msgBase.close();
-			}
-		}
-		if (!userCanDeleteAllSelectedMessages)
-			break;
-	}
-	
-	return userCanDeleteAllSelectedMessages;
-}
-
-// For the DigDistMsgReader class: Marks the 'selected messages' (in
-// this.selecteMessages) as deleted, or not deleted.
-//
-// Parameters:
-//  pDelete: Boolean - Whether or not the message should be marked deleted.  Defaults to true.
-//           If false, the message will be marked not deleted (if it is marked as deleted).
-//
-// Return value: An object with the following
-// properties:
-//  opSuccessful: Boolean - Whether or not all messages were successfully marked
-//              for deletion
-//  failureList: An object containing indexes of messages that failed to get
-//               marked for deletion, indexed by internal sub-board code, then
-//               containing messages indexes as properties.  Reasons for failing
-//               to mark messages deleted can include the user not having permission
-//               to delete in a sub-board, failure to open the sub-board, etc.
-function DigDistMsgReader_DeleteOrUndeleteSelectedMessages(pDelete)
-{
-	var retObj = {
-		opSuccessful: false,
-		failureList: {}
-	};
-
-	var markAsDeleted = (typeof(pDelete) === "boolean" ? pDelete : true);
-
-	var msgBase = null;
-	var msgHdr = null;
-	for (var subBoardCode in this.selectedMessages)
-	{
-		msgBase = new MsgBase(subBoardCode);
-		if (msgBase.open())
-		{
-			// If deleting messages, then check whether the user is the sysop, they're
-			// reading their personal mail, or the sub-board allows deleting messages.
-			var canContinue = true;
-			if (markAsDeleted)
-				canContinue = (user.is_sysop || (subBoardCode == "mail") || ((msgBase.cfg.settings & SUB_DEL) == SUB_DEL));
-			if (canContinue)
-			{
-				for (var msgIdx in this.selectedMessages[subBoardCode])
-				{
-					// It seems that msgIdx is a string, so make sure we have a
-					// numeric version.
-					var msgIdxNumber = +msgIdx;
-					// If doing a search (this.msgSearchHdrs has the sub-board code),
-					// then get the message header by index from there.  Otherwise,
-					// use the message base object to get the message by index.
-					if (this.msgSearchHdrs.hasOwnProperty(subBoardCode) &&
-					    (this.msgSearchHdrs[subBoardCode].indexed.length > 0))
-					{
-						if ((msgIdxNumber >= 0) && (msgIdxNumber < this.msgSearchHdrs[subBoardCode].indexed.length))
-							msgHdr = this.msgSearchHdrs[subBoardCode].indexed[msgIdxNumber];
-					}
-					else if (this.hdrsForCurrentSubBoard.length > 0)
-					{
-						if ((msgIdxNumber >= 0) && (msgIdxNumber < this.hdrsForCurrentSubBoard.length))
-							msgHdr = this.hdrsForCurrentSubBoard[msgIdxNumber];
-					}
-					else
-					{
-						if ((msgIdxNumber >= 0) && (msgIdxNumber < msgBase.total_msgs))
-							msgHdr = msgBase.get_msg_header(true, msgIdxNumber, false);
-					}
-					// If we got the message header, then mark it for deletion.
-					// If the message header wasn't marked as deleted, then add
-					// the message index to the return object.
-					if (msgHdr != null)
-					{
-						if (markAsDeleted)
-						{
-							// remove_msg() just marks a message for deletion
-							//retObj.opSuccessful = msgBase.remove_msg(true, msgHdr.offset);
-							retObj.opSuccessful = msgBase.remove_msg(false, msgHdr.number);
-						}
-						else
-						{
-							// If the message is marked deleted, unmark it.
-							if ((msgHdr.attr & MSG_DELETE) == MSG_DELETE)
-							{
-								var tmpMsgHdr = msgBase.get_msg_header(false, msgHdr.number, false);
-								if (tmpMsgHdr != null)
-								{
-									tmpMsgHdr.attr = tmpMsgHdr.attr ^ MSG_DELETE;
-									retObj.opSuccessful = msgBase.put_msg_header(false, msgHdr.number, tmpMsgHdr);
-								}
-								else
-									retObj.opSuccessful = false;
-							}
-							else
-								retObj.opSuccessful = true; // No change necessary
-						}
-					}
-					else
-						retObj.opSuccessful = false;
-					if (retObj.opSuccessful)
-					{
-						// Refresh the message header in the header arrays (if it exists there) and
-						// remove the message index from the selectedMessages object.  Also, delete
-						// or undelete any vote response messages that may exist for this message.
-						this.RefreshHdrInSavedArrays(msgIdxNumber, MSG_DELETE, markAsDeleted, subBoardCode, false);
-						var voteDelRetObj = toggleVoteMsgsDeleted(msgBase, msgHdr.number, msgHdr.id, markAsDeleted, (subBoardCode == "mail"));
-						if (!voteDelRetObj.allVoteMsgsAffected)
-						{
-							retObj.opSuccessful = false;
-							if (!retObj.failureList.hasOwnProperty(subBoardCode))
-								retObj.failureList[subBoardCode] = [];
-							retObj.failureList[subBoardCode].push(msgIdxNumber);
-						}
-						// If deleting and the user can't view deleted messages, then remove the message from this.selectedMessages
-						if (markAsDeleted && !canViewDeletedMsgs())
-							delete this.selectedMessages[subBoardCode][msgIdx];
-					}
-					else
-					{
-						retObj.opSuccessful = false;
-						if (!retObj.failureList.hasOwnProperty(subBoardCode))
-							retObj.failureList[subBoardCode] = [];
-						retObj.failureList[subBoardCode].push(msgIdxNumber);
-					}
-				}
-				if (markAsDeleted)
-				{
-					// If the sub-board index array no longer has any properties (i.e.,
-					// all messages in the sub-board were marked as deleted) and the user
-					// can't view deleted messages, then remove the sub-board property from
-					// this.selectedMessages
-					if (Object.keys(this.selectedMessages[subBoardCode]).length == 0 && !canViewDeletedMsgs())
-						delete this.selectedMessages[subBoardCode];
-				}
-			}
-			else
-			{
-				if (markAsDeleted && !canContinue)
-				{
-					// The user doesn't have permission to delete messages
-					// in this sub-board.
-					// Create an entry in retObj.failureList indexed by the
-					// sub-board code to indicate failure to delete all
-					// messages in the sub-board.
-					retObj.opSuccessful = false;
-					retObj.failureList[subBoardCode] = [];
-				}
-			}
-
-			msgBase.close();
-		}
-		else
-		{
-			// Failure to open the sub-board.
-			// Create an entry in retObj.failureList indexed by the
-			// sub-board code to indicate failure to delete all messages
-			// in the sub-board.
-			retObj.opSuccessful = false;
-			retObj.failureList[subBoardCode] = [];
-		}
-	}
-
-	return retObj;
-}
-
-// For the DigDistMsgReader class: Returns the number of selected messages
-function DigDistMsgReader_NumSelectedMessages()
-{
-	var numSelectedMsgs = 0;
-
-	for (var subBoardCode in this.selectedMessages)
-		numSelectedMsgs += Object.keys(this.selectedMessages[subBoardCode]).length;
-
-	return numSelectedMsgs;
-}
-
-// Allows the user to forward a message to an email address or
-// another user.  This function is interactive with the user.
-//
-// Parameters:
-//  pMsgHeader: The header of the message being forwarded
-//  pMsgBody: The body text of the message
-//
-// Return value: A blank string on success or a string containing a
-//               message upon failure.
-function DigDistMsgReader_ForwardMessage(pMsgHdr, pMsgBody)
-{
-	if (typeof(pMsgHdr) != "object")
-		return "Invalid message header given";
-
-	var retStr = "";
-
-	console.attributes = "N";
-	console.crlf();
-	console.print("\x01cUser name/number/email address\x01h:\x01n");
-	console.crlf();
-	var msgDest = console.getstr(console.screen_columns - 1, K_LINE);
-	console.attributes = "N";
-	console.crlf();
-	if (msgDest.length > 0)
-	{
-		// Let the user change the subject if they want (prepend it with "Fwd: " for forwarded
-		// and let the user edit the subject
-		var subjPromptText = bbs.text(SubjectPrompt);
-		console.putmsg(subjPromptText);
-		var initialMsgSubject = (this.prependFowardMsgSubject ? "Fwd: " + pMsgHdr.subject : pMsgHdr.subject);
-		var msgSubject = console.getstr(initialMsgSubject, console.screen_columns - console.strlen(subjPromptText) - 1, K_LINE | K_EDIT);
-
-		var tmpMsgbase = new MsgBase("mail");
-		if (tmpMsgbase.open())
-		{
-			// If the given message body is not a string, then get the
-			// message body from the messagebase.
-			if (typeof(pMsgBody) != "string")
-			{
-				var msgbase = new MsgBase(this.subBoardCode);
-				if (msgbase.open())
-				{
-					pMsgBody = msgbase.get_msg_body(false, pMsgHdr.number, false, false, true, true);
-					msgbase.close();
-				}
-				else
-					return "Unable to open the sub-board to get the message body";
-			}
-
-			// Prepend some lines to the message body to describe where
-			// the message came from originally.
-			var newMsgBody = "This is a forwarded message from " + system.name + "\n";
-			newMsgBody += "Forwarded by: " + user.alias;
-			if (user.alias != user.name)
-				newMsgBody += " (" + user.name + ")";
-			newMsgBody += "\n";
-			if (this.subBoardCode == "mail")
-				newMsgBody += "From " + user.name + "'s personal email\n";
-			else
-			{
-				newMsgBody += "From sub-board: "
-				           + msg_area.grp_list[msg_area.sub[this.subBoardCode].grp_index].description
-				           + ", " + msg_area.sub[this.subBoardCode].description + "\n";
-			}
-			newMsgBody += "From: " + pMsgHdr.from + "\n";
-			newMsgBody += "To: " + pMsgHdr.to + "\n";
-			if (msgSubject == pMsgHdr.subject)
-				newMsgBody += "Subject: " + pMsgHdr.subject + "\n";
-			else
-			{
-				newMsgBody += "Subject: " + msgSubject + "\n";
-				newMsgBody += "(Original subject: " + pMsgHdr.subject + ")\n";
-			}
-			newMsgBody += "==================================\n\n";
-			newMsgBody += pMsgBody;
-
-			// Ask whether to edit the message before forwarding it,
-			// and use console.editfile(filename) to edit it.
-			if (!console.noyes("Edit the message before sending"))
-			{
-				var baseWorkDir = system.node_dir + "DDMsgReader_Temp";
-				deltree(baseWorkDir + "/");
-				if (mkdir(baseWorkDir))
-				{
-					// TODO: Let the user edit the message, then read it
-					// and set newMsgBody to it
-					var tmpMsgFilename = baseWorkDir + "/message.txt";
-					// Write the current message to the file
-					var wroteMsgToTmpFile = false;
-					var outFile = new File(tmpMsgFilename);
-					if (outFile.open("w"))
-					{
-						wroteMsgToTmpFile = outFile.write(newMsgBody, newMsgBody.length);
-						outFile.close();
-					}
-					if (wroteMsgToTmpFile)
-					{
-						// Let the user edit the file, and if successful,
-						// read it in to newMsgBody
-						if (console.editfile(tmpMsgFilename))
-						{
-							var inFile = new File(tmpMsgFilename);
-							if (inFile.open("r"))
-							{
-								newMsgBody = inFile.read(inFile.length);
-								inFile.close();
-							}
-						}
-					}
-					else
-					{
-						console.print("\x01n\x01cFailed to write message to a file for editing\x01n");
-						console.crlf();
-						console.pause();
-					}
-				}
-				else
-				{
-					console.print("\x01n\x01cCouldn't create temporary directory\x01n");
-					console.crlf();
-					console.pause();
-				}
-			}
-			// End New (editing message)
-
-			// Create part of a header object which will be used when saving/sending
-			// the message.  The destination ("to" informatoin) will be filled in
-			// according to the destination type.
-			var destMsgHdr = { to_net_type: NET_NONE, from: user.name,
-							   replyto: user.name, subject: msgSubject }; // pMsgHdr.subject
-			if (user.netmail.length > 0)
-			{
-				destMsgHdr.replyto_net_addr = user.netmail;
-			}
-			else
-			{
-				destMsgHdr.replyto_net_addr = user.email;
-			}
-			//destMsgHdr.when_written_time = 
-			//destMsgHdr.when_written_zone = system.timezone;
-			//destMsgHdr.when_written_zone_offset = 
-
-			var confirmedForwardMsg = true;
-
-			// If the destination is in the format anything@anything, then
-			// accept it as the message destination.  It could be an Internet
-			// address (someone@somewhere.com), FidoNet address (sysop@1:327/4),
-			// or a QWK address (someone@HOST).
-			// We could specifically use gEmailRegex and gFTNEmailRegex to test
-			// msgDest, but just using those would be too restrictive.
-			if (/^.*@.*$/.test(msgDest))
-			{
-				confirmedForwardMsg = console.yesno("Forward via email to " + msgDest);
-				if (confirmedForwardMsg)
-				{
-					console.print("\x01n\x01cForwarding via email to " + msgDest + "\x01n");
-					console.crlf();
-					destMsgHdr.to = msgDest;
-					destMsgHdr.to_net_addr = msgDest;
-					destMsgHdr.to_net_type = netaddr_type(msgDest);
-				}
-			}
-			else
-			{
-				// See if what the user entered is a user number/name/alias
-				var userNum = 0;
-				if (/^[0-9]+$/.test(msgDest))
-				{
-					userNum = +msgDest;
-					// Determine if the user entered a valid user number
-					var lastUserNum = (system.lastuser == undefined ? system.stats.total_users : system.lastuser + 1);
-					if ((userNum < 1) || (userNum >= lastUserNum))
-					{
-						userNum = 0;
-						console.print("\x01h\x01y* Invalid user number (" + msgDest + ")\x01n");
-						console.crlf();
-					}
-				}
-				else // Try to get a user number assuming msgDest is a username/alias
-					userNum = system.matchuser(msgDest, true);
-				// If we have a valid user number, then we can forward the message.
-				if (userNum > 0)
-				{
-					var destUser = new User(userNum);
-					confirmedForwardMsg = console.yesno("Forward to " + destUser.alias + " (user " + destUser.number + ")");
-					if (confirmedForwardMsg)
-					{
-						destMsgHdr.to = destUser.alias;
-						// If the destination user has an Internet email address,
-						// ask the user if they want to send to the destination
-						// user's Internet email address
-						var sendToNetEmail = false;
-						if (destUser.netmail.length > 0)
-						{
-							sendToNetEmail = !console.noyes("Send to the user's Internet email (" + destUser.netmail + ")");
-							if (sendToNetEmail)
-							{
-								console.print("\x01n\x01cForwarding to " + destUser.netmail + "\x01n");
-								console.crlf();
-								destMsgHdr.to = destUser.name;
-								destMsgHdr.to_net_addr = destUser.netmail;
-								destMsgHdr.to_net_type = NET_INTERNET;
-							}
-						}
-						if (!sendToNetEmail)
-						{
-							console.print("\x01n\x01cForwarding to " + destUser.alias + "\x01n");
-							console.crlf();
-							destMsgHdr.to_ext = destUser.number;
-							destMsgHdr.to_net_type = NET_NONE;
-						}
-					}
-				}
-				else
-				{
-					confirmedForwardMsg = false;
-					console.print("\x01h\x01y* Unknown destination\x01n");
-					console.crlf();
-				}
-			}
-			var savedMsg = true;
-			if (confirmedForwardMsg)
-				savedMsg = tmpMsgbase.save_msg(destMsgHdr, newMsgBody);
-			else
-			{
-				console.print("\x01n\x01cCanceled\x01n");
-				console.crlf();
-			}
-			tmpMsgbase.close();
-
-			if (!savedMsg)
-			{
-				console.print("\x01h\x01y* Failed to send the message!\x01n");
-				console.crlf();
-			}
-
-			// Pause for user input so the user can see the messages written
-			console.pause();
-		}
-		else
-			retStr = "Failed to open email messagebase";
-	}
-	else
-	{
-		console.print("\x01n\x01cCanceled\x01n");
-		console.crlf();
-		console.pause();
-	}
-
-	return retStr;
-}
-
-function printMsgHdrInfo(pMsgHdr)
-{
-	if (typeof(pMsgHdr) != "object")
-		return;
-
-	for (var prop in pMsgHdr)
-	{
-		if (prop == "to_net_type")
-			print(prop + ": " + toNetTypeToStr(pMsgHdr[prop]));
-		else
-			console.print(prop + ": " + pMsgHdr[prop]);
-		console.crlf();
-	}
-}
-
-function toNetTypeToStr(toNetType)
-{
-	var toNetTypeStr = "Unknown";
-	if (typeof(toNetType) == "number")
-	{
-		switch (toNetType)
-		{
-			case NET_NONE:
-				toNetTypeStr = "Local";
-				break;
-			case NET_UNKNOWN:
-				toNetTypeStr = "Unknown networked";
-				break;
-			case NET_FIDO:
-				toNetTypeStr = "FidoNet";
-				break;
-			case NET_POSTLINK:
-				toNetTypeStr = "PostLink";
-				break;
-			case NET_QWK:
-				toNetTypeStr = "QWK";
-				break;
-			case NET_INTERNET:
-				toNetTypeStr = "Internet";
-				break;
-			default:
-				toNetTypeStr = "Unknown";
-				break;
-		}
-	}
-	return toNetTypeStr;
-}
-
-// For the DigDistMsgReader class: Lets the user vote on a message
-//
-// Parameters:
-//  pMsgHdr: The header of the mesasge being voted on
-//  pRemoveNLsFromVoteText: Optional boolean - Whether or not to remove newlines
-//                          (and carriage returns) from the voting text from
-//                          text.dat.  Defaults to false.
-//
-// Return value: An object with the following properties:
-//               BBSHasVoteFunction: Boolean - Whether or not the system has
-//                                   the vote_msg function
-//               savedVote: Boolean - Whether or not the vote was saved
-//               userQuit: Boolean - Whether or not the user quit and didn't vote
-//               errorMsg: String - An error message, if something went wrong
-//               mnemonicsRequiredForErrorMsg: Boolean - Whether or not mnemonics is required to print the error message
-//               updatedHdr: The updated message header containing vote information.
-//                           If something went wrong, this will be null.
-function DigDistMsgReader_VoteOnMessage(pMsgHdr, pRemoveNLsFromVoteText)
-{
-	var retObj = {
-		BBSHasVoteFunction: false,
-		savedVote: false,
-		userQuit: false,
-		errorMsg: "",
-		mnemonicsRequiredForErrorMsg: false,
-		updatedHdr: null
-	};
-
-	// Don't allow voting for personal email
-	if (this.subBoardCode == "mail")
-	{
-		retObj.errorMsg = "Can not vote on personal email";
-		return retObj;
-	}
-	
-	// Check whether the user has the voting restiction
-	if ((user.security.restrictions & UFLAG_V) == UFLAG_V)
-	{
-		// Use the line from text.dat that says the user is not allowed to vote,
-		// and remove newlines from it.
-		retObj.errorMsg = "\x01n" + bbs.text(typeof(R_Voting) != "undefined" ? R_Voting : 781).replace("\r\n", "").replace("\n", "").replace("\N", "").replace("\r", "").replace("\R", "").replace("\R\n", "").replace("\r\N", "").replace("\R\N", "");
-		return retObj;
-	}
-
-	var msgbase = new MsgBase(this.subBoardCode);
-	if (!msgbase.open())
-	{
-		return retObj;
-	}
-
-	// If the message vote function is not defined in the running verison of Synchronet,
-	// then just return.
-	retObj.BBSHasVoteFunction = (typeof(msgbase.vote_msg) == "function");
-	if (!retObj.BBSHasVoteFunction)
-	{
-		msgbase.close();
-		return retObj;
-	}
-
-	var removeNLsFromVoteText = (typeof(pRemoveNLsFromVoteText) === "boolean" ? pRemoveNLsFromVoteText : false)
-
-	// See if voting is allowed in the current sub-board
-	if ((msg_area.sub[this.subBoardCode].settings & SUB_NOVOTING) == SUB_NOVOTING)
-	{
-		retObj.errorMsg = bbs.text(typeof(VotingNotAllowed) != "undefined" ? VotingNotAllowed : 779);
-		if (removeNLsFromVoteText)
-			retObj.errorMsg = retObj.errorMsg.replace("\r\n", "").replace("\n", "").replace("\N", "").replace("\r", "").replace("\R", "").replace("\R\n", "").replace("\r\N", "").replace("\R\N", "");
-		retObj.mnemonicsRequiredForErrorMsg = true;
-		msgbase.close();
-		return retObj;
-	}
-
-	// If the message is a poll question and has the maximum number of votes
-	// already or is closed for voting, then don't let the user vote on it.
-	if ((pMsgHdr.attr & MSG_POLL) == MSG_POLL)
-	{
-		var userVotedMaxVotes = false;
-		var numVotes = (pMsgHdr.hasOwnProperty("votes") ? pMsgHdr.votes : 0);
-		if (typeof(msgbase.how_user_voted) === "function")
-		{
-			var votes = msgbase.how_user_voted(pMsgHdr.number, (msgbase.cfg.settings & SUB_NAME) == SUB_NAME ? user.name : user.alias);
-			// TODO: I'm not sure if this 'if' section is correct anymore for
-			// the latest 3.17 build of Synchronet (August 14, 2017)
-			// Digital Man said:
-			// In a poll message, the "votes" property specifies the maximum number of
-			// answers/votes per ballot (0 is the equivalent of 1).
-			// Max votes testing? :
-			// userVotedMaxVotes = (votes == pMsgHdr.votes);
-			if (votes >= 0)
-			{
-				if ((votes == 0) || (votes == 1))
-					userVotedMaxVotes = (votes == 3); // (1 << 0) | (1 << 1);
-				else
-				{
-					userVotedMaxVotes = true;
-					for (var voteIdx = 0; voteIdx <= numVotes; ++voteIdx)
-					{
-						if (votes && (1 << voteIdx) == 0)
-						{
-							userVotedMaxVotes = false;
-							break;
-						}
-					}
-				}
-			}
-		}
-		var pollIsClosed = ((pMsgHdr.auxattr & POLL_CLOSED) == POLL_CLOSED);
-		if (pollIsClosed)
-		{
-			retObj.errorMsg = "This poll is closed";
-			msgbase.close();
-			return retObj;
-		}
-		else if (userVotedMaxVotes)
-		{
-			retObj.errorMsg = bbs.text(typeof(VotedAlready) != "undefined" ? VotedAlready : 780);
-			if (removeNLsFromVoteText)
-				retObj.errorMsg = retObj.errorMsg.replace("\r\n", "").replace("\n", "").replace("\N", "").replace("\r", "").replace("\R", "").replace("\R\n", "").replace("\r\N", "").replace("\R\N", "");
-			retObj.mnemonicsRequiredForErrorMsg = true;
-			msgbase.close();
-			return retObj;
-		}
-	}
-
-	// If the user has voted on this message already, then set an error message and return.
-	if (this.HasUserVotedOnMsg(pMsgHdr.number))
-	{
-		retObj.errorMsg = bbs.text(typeof(VotedAlready) != "undefined" ? VotedAlready : 780);
-		if (removeNLsFromVoteText)
-			retObj.errorMsg = retObj.errorMsg.replace("\r\n", "").replace("\n", "").replace("\N", "").replace("\r", "").replace("\R", "").replace("\R\n", "").replace("\r\N", "").replace("\R\N", "");
-		retObj.mnemonicsRequiredForErrorMsg = true;
-		msgbase.close();
-		return retObj;
-	}
-
-	// New MsgBase method: vote_msg(). it takes a message header object
-	// (like save_msg), except you only need a few properties, in order of
-	// importarnce:
-	// attr: you need to have this set to MSG_UPVOTE, MSG_DOWNVOTE, or MSG_VOTE
-	// thread_back or reply_id: either of these must be set to indicate msg to vote on
-	// from: name of voter
-	// from_net_type and from_net_addr: if applicable
-
-	// Do some initial setup of the header for the vote message to be
-	// saved to the messagebase
-	var voteMsgHdr = {
-		thread_back: pMsgHdr.number,
-		reply_id: pMsgHdr.id,
-		from: (msgbase.cfg.settings & SUB_NAME) == SUB_NAME ? user.name : user.alias
-	};
-	if (pMsgHdr.from.hasOwnProperty("from_net_type"))
-	{
-		voteMsgHdr.from_net_type = pMsgHdr.from_net_type;
-		if (pMsgHdr.from_net_type != NET_NONE)
-			voteMsgHdr.from_net_addr = user.email;
-	}
-
-	// Input vote options from the user differently depending on whether
-	// the message is a poll or not
-	if ((pMsgHdr.attr & MSG_POLL) == MSG_POLL)
-	{
-		if (pMsgHdr.hasOwnProperty("field_list"))
-		{
-			console.clear("\x01n");
-			var selectHdr = bbs.text(typeof(BallotHdr) != "undefined" ? BallotHdr : 791);
-			printf("\x01n" + selectHdr + "\x01n", pMsgHdr.subject);
-			var optionFormatStr = "\x01n\x01c\x01h%2d\x01n\x01c: \x01h%s\x01n";
-			var optionNum = 1;
-			for (var fieldI = 0; fieldI < pMsgHdr.field_list.length; ++fieldI)
-			{
-				if (pMsgHdr.field_list[fieldI].type == SMB_POLL_ANSWER)
-				{
-					printf(optionFormatStr, optionNum++, pMsgHdr.field_list[fieldI].data);
-					console.crlf();
-				}
-			}
-			console.crlf();
-			// Get & process the selection from the user
-			var voteResponse = 0;
-			if (pMsgHdr.votes > 1)
-			{
-				// Support multiple answers from the user
-				console.print("\x01n\x01gYour vote numbers, separated by commas, up to \x01h" + pMsgHdr.votes + "\x01n\x01g (Blank/Q=Quit):");
-				console.crlf();
-				//console.print("\x01c\x01h");
-				console.attributes = "CH";
-				var userInput = consoleGetStrWithValidKeys("0123456789,Q", null, false);
-				if ((userInput.length > 0) && (userInput.toUpperCase() != "Q"))
-				{
-					var userAnswers = userInput.split(",");
-					if (userAnswers.length > 0)
-					{
-						// Generate confirmation text and an array of numbers
-						// representing the user's choices, up to the number
-						// of responses allowed
-						var confirmText = "Vote ";
-						var voteNumbers = [];
-						for (var i = 0; (i < userAnswers.length) && (i < pMsgHdr.votes); ++i)
-						{
-							// Trim any whitespace from the user's response
-							userAnswers[i] = trimSpaces(userAnswers[i], true, true, true);
-							if (/^[0-9]+$/.test(userAnswers[i]))
-							{
-								voteNumbers.push(+userAnswers[i]);
-								confirmText += userAnswers[i] + ",";
-							}
-						}
-						// If the confirmation text has a trailing comma, remove it
-						if (/,$/.test(confirmText))
-							confirmText = confirmText.substr(0, confirmText.length-1);
-						// Confirm from the user and submit their vote if they say yes
-						if (voteNumbers.length > 0)
-						{
-							if (console.yesno(confirmText))
-							{
-								voteResponse = 0;
-								for (var i = 0; i < voteNumbers.length; ++i)
-									voteResponse |= (1 << (voteNumbers[i]-1));
-							}
-							else
-								retObj.userQuit = true;
-						}
-					}
-				}
-				else
-					retObj.userQuit = true;
-			}
-			else
-			{
-				// Get the selection prompt text from text.dat and replace the %u or %d with
-				// the number 1 (default option)
-				var selectPromptText = bbs.text(SelectItemWhich);
-				selectPromptText = selectPromptText.replace(/%[uU]/, 1).replace(/%[dD]/, 1);
-				console.mnemonics(selectPromptText);
-				var maxNum = optionNum - 1;
-				var userInputNum = console.getnum(maxNum);
-				if (userInputNum == -1) // The user chose Q to quit
-					retObj.userQuit = true;
-				else
-					voteResponse = (1 << (userInputNum-1));
-				console.attributes = "N";
-			}
-			if (!retObj.userQuit)
-			{
-				voteMsgHdr.attr = MSG_VOTE;
-				voteMsgHdr.votes = voteResponse;
-			}
-		}
-	}
-	else
-	{
-		// The message is not a poll - Prompt for up/downvote
-		if ((typeof(MSG_UPVOTE) != "undefined") && (typeof(MSG_DOWNVOTE) != "undefined"))
-		{
-			var voteAttr = 0;
-			// Get text line 783 to prompt for voting
-			var textDatText = bbs.text(typeof(VoteMsgUpDownOrQuit) != "undefined" ? VoteMsgUpDownOrQuit : 783);
-			if (removeNLsFromVoteText)
-				textDatText = textDatText.replace("\r\n", "").replace("\n", "").replace("\N", "").replace("\r", "").replace("\R", "").replace("\R\n", "").replace("\r\N", "").replace("\R\N", "");
-			console.attributes = "N";
-			console.mnemonics(textDatText);
-			console.attributes = "N";
-			// Using getAllowedKeyWithMode() instead of console.getkeys() so we
-			// can control the input mode better, so it doesn't output a CRLF
-			switch (getAllowedKeyWithMode("UDQ" + KEY_UP + KEY_DOWN, K_NOCRLF|K_NOSPIN))
-			{
-				case "U":
-				case KEY_UP:
-					voteAttr = MSG_UPVOTE;
-					break;
-				case "D":
-				case KEY_DOWN:
-					voteAttr = MSG_DOWNVOTE;
-					break;
-				case "Q":
-				default:
-					retObj.userQuit = true;
-					break;
-			}
-			// If the user voted, then save the user's vote in the attr property
-			// in the header
-			if (voteAttr != 0)
-				voteMsgHdr.attr = voteAttr;
-		}
-		else
-			retObj.errorMsg = "MSG_UPVOTE & MSG_DOWNVOTE are not defined";
-	}
-
-	// If the user hasn't quit and there is no error message, then save the vote
-	// message header
-	if (!retObj.userQuit && (retObj.errorMsg.length == 0))
-	{
-		console.print("\x01n  Submitting..");
-		retObj.savedVote = msgbase.vote_msg(voteMsgHdr);
-		// If the save was successful, then update
-		// this.hdrsForCurrentSubBoard with the updated
-		// message header (for the message that was read)
-		if (retObj.savedVote)
-		{
-			if (this.msgNumToIdxMap.hasOwnProperty(pMsgHdr.number))
-			{
-				var originalMsgIdx = this.msgNumToIdxMap[pMsgHdr.number];
-				// Calling get_all_msg_headers() to include vote information:
-				var tmpHdrs = msgbase.get_all_msg_headers(true);
-				if (tmpHdrs.hasOwnProperty(pMsgHdr.number))
-				{
-					this.hdrsForCurrentSubBoard[originalMsgIdx] = tmpHdrs[pMsgHdr.number];
-					// Originally, this script assigned retObj.updatedHdr as follows:
-					//retObj.updatedHdr = pMsgHdr;
-					// However, after an update, there were a couple errors that total_votes and upvotes
-					// were read-only, so it wuldn't assign to them, so now we copy pMsgHdr this way:
-					retObj.updatedHdr = {};
-					for (var prop in pMsgHdr)
-						retObj.updatedHdr[prop] = pMsgHdr[prop];
-					if (this.hdrsForCurrentSubBoard[originalMsgIdx].hasOwnProperty("total_votes"))
-						retObj.updatedHdr.total_votes = this.hdrsForCurrentSubBoard[originalMsgIdx].total_votes;
-					if (this.hdrsForCurrentSubBoard[originalMsgIdx].hasOwnProperty("upvotes"))
-						retObj.updatedHdr.upvotes = this.hdrsForCurrentSubBoard[originalMsgIdx].upvotes;
-					if (this.hdrsForCurrentSubBoard[originalMsgIdx].hasOwnProperty("tally"))
-						retObj.updatedHdr.tally = this.hdrsForCurrentSubBoard[originalMsgIdx].tally;
-				}
-				// I thought we should be able to call get_msg_header() and get valid vote information,
-				// but that doesn't seem to be the case:
-				/*
-				var hdrWithVotes = msgbase.get_msg_header(false, pMsgHdr.number, true, true);
-				if (hdrWithVotes != null)
-				{
-					this.hdrsForCurrentSubBoard[originalMsgIdx] = hdrWithVotes;
-					// Originally, this script assigned retObj.updatedHdr as follows:
-					//retObj.updatedHdr = pMsgHdr;
-					// However, after an update, there were a couple errors that total_votes and upvotes
-					// were read-only, so it wuldn't assign to them, so now we copy pMsgHdr this way:
-					retObj.updatedHdr = {};
-					for (var prop in pMsgHdr)
-						retObj.updatedHdr[prop] = pMsgHdr[prop];
-					if (this.hdrsForCurrentSubBoard[originalMsgIdx].hasOwnProperty("total_votes"))
-						retObj.updatedHdr.total_votes = this.hdrsForCurrentSubBoard[originalMsgIdx].total_votes;
-					if (this.hdrsForCurrentSubBoard[originalMsgIdx].hasOwnProperty("upvotes"))
-						retObj.updatedHdr.upvotes = this.hdrsForCurrentSubBoard[originalMsgIdx].upvotes;
-					if (this.hdrsForCurrentSubBoard[originalMsgIdx].hasOwnProperty("tally"))
-						retObj.updatedHdr.tally = this.hdrsForCurrentSubBoard[originalMsgIdx].tally;
-				}
-				*/
-			}
-		}
-		else
-		{
-			// Failed to save the vote
-			retObj.errorMsg = "Failed to save your vote";
-		}
-	}
-
-	msgbase.close();
-
-	return retObj;
-}
-
-// For the DigDistMsgReader class: Checks to see whether a user has voted on a message.
-// The message must belong to the currently-open sub-board.
-//
-// Parameters:
-//  pMsgNum: The message number
-//  pUser: Optional - A user account to check.  If omitted, the current logged-in
-//         user will be used.
-function DigDistMsgReader_HasUserVotedOnMsg(pMsgNum, pUser)
-{
-	// Don't do this for personal email
-	if (this.subBoardCode == "mail")
-		return false;
-
-	// Thanks to echicken for explaining how to check this.  To check a user's
-	// vote, use MsgBase.how_user_voted().
-	/*
-	The return value will be:
-	0 - user hasn't voted
-	1 - upvoted
-	2 - downvoted
-	Or, if the message was a poll, it's a bitfield:
-	if (votes&(1<<2)) {
-	 // User selected answer 2
-	}
-	*/
-	var userHasVotedOnMsg = false;
-	var msgbase = new MsgBase(this.subBoardCode);
-	if (msgbase.open())
-	{
-		if (typeof(msgbase.how_user_voted) === "function")
-		{
-			var votes = 0;
-			if (typeof(pUser) == "object")
-				votes = msgbase.how_user_voted(pMsgNum, (msgbase.cfg.settings & SUB_NAME) == SUB_NAME ? pUser.name : pUser.alias);
-			else
-				votes = msgbase.how_user_voted(pMsgNum, (msgbase.cfg.settings & SUB_NAME) == SUB_NAME ? user.name : user.alias);
-			userHasVotedOnMsg = (votes > 0);
-		}
-		msgbase.close();
-	}
-	return userHasVotedOnMsg;
-}
-
-// Gets information about the upvotes and downvotes for a message.
-// If the user is a sysop, this will also get who voted on the message.
-//
-// Parameters:
-//  pMsgHdr: A header of a message that has upvotes & downvotes
-//
-// Return value: An array of strings containing information about the upvotes,
-//               downvotes, tally, and (if the user is a sysop) who submitted
-//               votes on the message.
-function DigDistMsgReader_GetUpvoteAndDownvoteInfo(pMsgHdr)
-{
-	// If the message header doesn't have the "total_votes" or "upvotes" properties,
-	// then there's no vote information, so just return an empty array.
-	if (!pMsgHdr.hasOwnProperty("total_votes") || !pMsgHdr.hasOwnProperty("upvotes"))
-		return [];
-
-	var msgVoteInfo = getMsgUpDownvotesAndScore(pMsgHdr);
-	var voteInfo = [];
-	voteInfo.push("Upvotes: " + msgVoteInfo.upvotes);
-	voteInfo.push("Downvotes: " + msgVoteInfo.downvotes);
-	voteInfo.push("Score: " + msgVoteInfo.voteScore);
-	if (pMsgHdr.hasOwnProperty("tally"))
-		voteInfo.push("Tally: " + pMsgHdr.tally);
-
-	// If the user is the sysop, then also add the names of people who
-	// voted on the message.
-	if (user.is_sysop)
-	{
-		// Check all the messages in the messagebase after the current one
-		// to find response messages
-		var msgbase = new MsgBase(this.subBoardCode);
-		if (msgbase.open())
-		{
-			// Pass true to get_all_msg_headers() to tell it to return vote messages
-			// (the parameter was introduced in Synchronet 3.17+)
-			var tmpHdrs = msgbase.get_all_msg_headers(true);
-			for (var tmpProp in tmpHdrs)
-			{
-				if (tmpHdrs[tmpProp] == null)
-					continue;
-				// If this header's thread_back or reply_id matches the poll message
-				// number, then append the 'user voted' string to the message body.
-				if ((tmpHdrs[tmpProp].thread_back == pMsgHdr.number) || (tmpHdrs[tmpProp].reply_id == pMsgHdr.id))
-				{
-					var tmpMessageBody = msgbase.get_msg_body(false, tmpHdrs[tmpProp].number, false, false, true, true);
-					if ((tmpHdrs[tmpProp].field_list.length == 0) && (tmpMessageBody.length == 0))
-					{
-						var msgWrittenLocalTime = msgWrittenTimeToLocalBBSTime(tmpHdrs[tmpProp]);
-						var voteDate = strftime("%a %b %d %Y %H:%M:%S", msgWrittenLocalTime);
-						voteInfo.push("\x01n\x01c\x01h" + tmpHdrs[tmpProp].from + "\x01n\x01c voted on this message on " + voteDate + "\x01n");
-					}
-				}
-			}
-			msgbase.close();
-		}
-	}
-
-	return voteInfo;
-}
-
-// For the DigDistMsgReader class: Gets the body (text) of a message.  If it's
-// a poll, this method will format the message body with poll results.  Otherwise,
-// this method will simply get the message body.
-//
-// Parameters:
-//  pMsgHeader: The message header
-//
-// Return value: An object with the following properties:
-//               msgBody: The message body
-//               pmode: The mode flags to be used when printing the message body
-function DigDistMsgReader_GetMsgBody(pMsgHdr)
-{
-	var retObj = {
-		msgBody: "",
-		pmode: 0
-	};
-
-	var msgbase = new MsgBase(this.subBoardCode);
-	if (!msgbase.open())
-		return retObj;
-
-	retObj.pmode = msg_pmode(msgbase, pMsgHdr);
-
-	if ((typeof(MSG_TYPE_POLL) != "undefined") && (pMsgHdr.type & MSG_TYPE_POLL) == MSG_TYPE_POLL)
-	{
-		// A poll is intended to be parsed (and displayed) using on the header data. The
-		// (optional) comments are stored in the hdr.field_list[] with type values of
-		// SMB_COMMENT (now defined in sbbsdefs.js) and the available answers are in the
-		// field_list[] with type values of SMB_POLL_ANSWER.
-
-		// The 'comments' and 'answers' are also a part of the message header, so you can
-		// grab them separately, then format and display them however you want.  You can
-		// find them in the header.field_list array; each element in that array should be
-		// an object with a 'type' and a 'data' property.  Relevant types here are
-		// SMB_COMMENT and SMB_POLL_ANSWER.  (This is what I'm doing on the web, and I
-		// just ignore the message body for poll messages.)
-
-		if (pMsgHdr.hasOwnProperty("field_list"))
-		{
-			// Figure out the longest length of the poll choices, with
-			// a maximum of 22 characters less than the terminal width.
-			// Use a minimum of 27 characters.
-			// That length will be used for the formatting strings for
-			// the poll results.
-			var voteOptDescLen = 0;
-			for (var fieldI = 0; fieldI < pMsgHdr.field_list.length; ++fieldI)
-			{
-				if (pMsgHdr.field_list[fieldI].type == SMB_POLL_ANSWER)
-				{
-					if (pMsgHdr.field_list[fieldI].data.length > voteOptDescLen)
-						voteOptDescLen = pMsgHdr.field_list[fieldI].data.length;
-				}
-			}
-			if (voteOptDescLen > console.screen_columns - 22)
-				voteOptDescLen = console.screen_columns - 22;
-			else if (voteOptDescLen < 27)
-				voteOptDescLen = 27;
-
-			// Format strings for outputting the voting option lines
-			var unvotedOptionFormatStr = "\x01n\x01c\x01h%2d\x01n\x01c: \x01w\x01h%-" + voteOptDescLen + "s [%-4d %6.2f%]\x01n";
-			var votedOptionFormatStr = "\x01n\x01c\x01h%2d\x01n\x01c: \x01" + "5\x01w\x01h%-" + voteOptDescLen + "s [%-4d %6.2f%]\x01n";
-			// Add up the total number of votes so that we can
-			// calculate vote percentages.
-			var totalNumVotes = 0;
-			if (pMsgHdr.hasOwnProperty("tally"))
-			{
-				for (var tallyI = 0; tallyI < pMsgHdr.tally.length; ++tallyI)
-					totalNumVotes += pMsgHdr.tally[tallyI];
-			}
-			// Go through field_list and append the voting options and stats to
-			// retObj.msgBody
-			var pollComment = "";
-			var optionNum = 1;
-			var numVotes = 0;
-			var votePercentage = 0;
-			var tallyIdx = 0;
-			for (var fieldI = 0; fieldI < pMsgHdr.field_list.length; ++fieldI)
-			{
-				if (pMsgHdr.field_list[fieldI].type == SMB_COMMENT)
-					pollComment += pMsgHdr.field_list[fieldI].data + "\r\n";
-				else if (pMsgHdr.field_list[fieldI].type == SMB_POLL_ANSWER)
-				{
-					// Figure out the number of votes on this option and the
-					// vote percentage
-					if (pMsgHdr.hasOwnProperty("tally"))
-					{
-						if (tallyIdx < pMsgHdr.tally.length)
-						{
-							numVotes = pMsgHdr.tally[tallyIdx];
-							votePercentage = (numVotes / totalNumVotes) * 100;
-						}
-					}
-					// Append to the message text
-					retObj.msgBody += format(numVotes == 0 ? unvotedOptionFormatStr : votedOptionFormatStr,
-					                  optionNum++, pMsgHdr.field_list[fieldI].data.substr(0, voteOptDescLen),
-					                  numVotes, votePercentage);
-					if (numVotes > 0)
-						retObj.msgBody += " " + CHECK_CHAR;
-					retObj.msgBody += "\r\n";
-					++tallyIdx;
-				}
-			}
-			if (pollComment.length > 0)
-				retObj.msgBody = pollComment + "\r\n" + retObj.msgBody;
-
-			// If voting is allowed in this sub-board and the current logged-in
-			// user has not voted on this message, then append some text saying
-			// how to vote.
-			var votingAllowed = ((this.subBoardCode != "mail") && (((msg_area.sub[this.subBoardCode].settings & SUB_NOVOTING) == 0)));
-			if (votingAllowed && !this.HasUserVotedOnMsg(pMsgHdr.number))
-				retObj.msgBody += "\x01n\r\n\x01gTo vote in this poll, press \x01w\x01h" + this.enhReaderKeys.vote + "\x01n\x01g now.\r\n";
-
-			// If the current logged-in user created this poll, then show the
-			// users who have voted on it so far.
-			var msgFromUpper = pMsgHdr.from.toUpperCase();
-			if ((msgFromUpper == user.name.toUpperCase()) || (msgFromUpper == user.handle.toUpperCase()))
-			{
-				// Check all the messages in the messagebase after the current one
-				// to find poll response messages
-				// Get the line from text.dat for writing who voted & when.  It
-				// is a format string and should look something like this:
-				//"\r\n\x01n\x01hOn %s, in \x01c%s \x01n\x01c%s\r\n\x01h\x01m%s voted in your poll: \x01n\x01h%s\r\n" 787 PollVoteNotice
-				var userVotedInYourPollText = bbs.text(typeof(PollVoteNotice) != "undefined" ? PollVoteNotice : 787);
-
-				// Pass true to get_all_msg_headers() to tell it to return vote messages
-				// (the parameter was introduced in Synchronet 3.17+)
-				var tmpHdrs = msgbase.get_all_msg_headers(true);
-				for (var tmpProp in tmpHdrs)
-				{
-					if (tmpHdrs[tmpProp] == null)
-						continue;
-					// If this header's thread_back or reply_id matches the poll message
-					// number, then append the 'user voted' string to the message body.
-					if ((tmpHdrs[tmpProp].thread_back == pMsgHdr.number) || (tmpHdrs[tmpProp].reply_id == pMsgHdr.id))
-					{
-						var msgWrittenLocalTime = msgWrittenTimeToLocalBBSTime(tmpHdrs[tmpProp]);
-						var voteDate = strftime("%a %b %d %Y %H:%M:%S", msgWrittenLocalTime);
-						var grpName = "";
-						var msgbaseCfgName = "";
-						var msgbase = new MsgBase(this.subBoardCode);
-						if (msgbase.open())
-						{
-							grpName = msgbase.cfg.grp_name;
-							msgbaseCfgName = msgbase.cfg.name;
-							msgbase.close();
-						}
-						retObj.msgBody += format(userVotedInYourPollText, voteDate, grpName, msgbaseCfgName, tmpHdrs[tmpProp].from, pMsgHdr.subject);
-					}
-				}
-			}
-		}
-	}
-	else
-	{
-		// If the message is UTF8 and the terminal is not UTF8-capable, then convert
-		// the text to cp437.
-		retObj.msgBody = msgbase.get_msg_body(false, pMsgHdr.number, false, false, true, true);
-		if (pMsgHdr.hasOwnProperty("is_utf8") && pMsgHdr.is_utf8)
-		{
-			var userConsoleSupportsUTF8 = false;
-			if (typeof(USER_UTF8) != "undefined")
-				userConsoleSupportsUTF8 = console.term_supports(USER_UTF8);
-			if (!userConsoleSupportsUTF8)
-				retObj.msgBody = utf8_cp437(retObj.msgBody);
-		}
-		// Remove any initial coloring from the message body, which can color the whole message
-		retObj.msgBody = removeInitialColorFromMsgBody(retObj.msgBody);
-		// For HTML-formatted messages, convert HTML entities
-		if (pMsgHdr.hasOwnProperty("text_subtype") && pMsgHdr.text_subtype.toLowerCase() == "html")
-		{
-			retObj.msgBody = html2asc(retObj.msgBody);
-			// Remove excessive blank lines after HTML-translation
-			retObj.msgBody = retObj.msgBody.replace(/\r\n\r\n\r\n/g, '\r\n\r\n');
-		}
-	}
-	msgbase.close();
-
-	// Remove any Synchronet pause codes that might exist in the message
-	retObj.msgBody = retObj.msgBody.replace("\x01p", "").replace("\x01P", "");
-	
-	// If the user is a sysop, this is a moderated message area, and the message
-	// hasn't been validated, then prepend the message with a message to let the
-	// sysop now know to validate it.
-	if (this.subBoardCode != "mail")
-	{
-		if (user.is_sysop && msg_area.sub[this.subBoardCode].is_moderated && ((pMsgHdr.attr & MSG_VALIDATED) == 0))
-		{
-			var validateNotice = "\x01n\x01h\x01yThis is an unvalidated message in a moderated area.  Press "
-							   + this.enhReaderKeys.validateMsg + " to validate it.\r\n\x01g";
-			for (var i = 0; i < 79; ++i)
-				validateNotice += HORIZONTAL_SINGLE;
-			validateNotice += "\x01n\r\n";
-			retObj.msgBody = validateNotice + retObj.msgBody;
-		}
-	}
-
-
-	// If this message has been marked for deletion, prepend a couple lines saying so
-	if ((pMsgHdr.attr & MSG_DELETE) == MSG_DELETE)
-	{
-		var deletedNotice = "\x01n\x01h\x01yThis message has been marked for deletion.";
-		if (user.is_sysop)
-			deletedNotice += " To un-mark, return to the message list and press U to un-mark this message.";
-		deletedNotice += "\x01n\r\n\r\n";
-		retObj.msgBody = deletedNotice + retObj.msgBody;
-	}
-
-	return retObj;
-}
-
-// For the DigDistMsgReader class: Refreshes a message header in one of the
-// internal message arrays.
-//
-// Parameters:
-//  pMsgNum: The number of the message to replace
-function DigDistMsgReader_RefreshMsgHdrInArrays(pMsgNum)
-{
-	var msgbase = new MsgBase(this.subBoardCode);
-	if (!msgbase.open())
-		return;
-	if (this.msgSearchHdrs.hasOwnProperty(this.subBoardCode) &&
-	    (this.msgSearchHdrs[this.subBoardCode].indexed.length > 0))
-	{
-		for (var i = 0; i < this.msgSearchHdrs[this.subBoardCode].indexed.length; ++i)
-		{
-			if (this.msgSearchHdrs[this.subBoardCode].indexed[i].number == pMsgNum)
-			{
-				var newMsgHdr = msgbase.get_msg_header(false, pMsgNum, true, true);
-				if (newMsgHdr != null)
-					this.msgSearchHdrs[this.subBoardCode].indexed[i] = newMsgHdr;
-				break;
-			}
-		}
-	}
-	else if (this.hdrsForCurrentSubBoard.length > 0)
-	{
-		if (this.msgNumToIdxMap.hasOwnProperty(pMsgNum))
-		{
-			// Calling get_all_msg_headers() to include vote information:
-			var msgHdrs = msgbase.get_all_msg_headers(true);
-			if (msgHdrs.hasOwnProperty(pMsgNum))
-			{
-				var msgIdx = this.msgNumToIdxMap[pMsgNum];
-				this.hdrsForCurrentSubBoard[msgIdx] = msgHdrs[pMsgNum];
-			}
-			// I thought we should be able to call get_msg_header() and get valid vote information,
-			// but that doesn't seem to be the case:
-			/*
-			var updatedMsgHdr = msgbase.get_msg_header(false, pMsgNum, true, true);
-			if (updatedMsgHdr != null)
-			{
-				var msgIdx = this.msgNumToIdxMap[pMsgNum];
-				this.hdrsForCurrentSubBoard[msgIdx] = updatedMsgHdr;
-			}
-			*/
-		}
-	}
-	msgbase.close();
-}
-
-// For the DigDistMessageReader class: Re-calculates the message list widths and
-// format strings
-//
-// Parameters:
-//  pMsgNumLen: Optional - Length to use for the message number field.  If not specified,
-//              then this will get the number of messages in the sub-board and use that
-//              length.
-function DigDistMsgReader_RecalcMsgListWidthsAndFormatStrs(pMsgNumLen)
-{
-	// Note: Constructing these strings must be done after reading the configuration
-	// file in order for the configured colors to be used
-
-	// TODO: Having the separate printf strings for regular, to-user, and from-user
-	// are a bit pointless now that coloring & alternate coloring is done via
-	// DDLightbarMenu
-	this.sMsgListHdrFormatStr = "";
-	this.sMsgInfoFormatStr = "";
-	this.sMsgInfoToUserFormatStr = "";
-	this.sMsgInfoFromUserFormatStr = "";
-	this.sMsgInfoFormatHighlightStr = "";
-
-	this.MSGNUM_LEN = (typeof(pMsgNumLen) == "number" ? pMsgNumLen : this.NumMessages().toString().length);
-	if (this.MSGNUM_LEN < 4)
-		this.MSGNUM_LEN = 4;
-	this.DATE_LEN = 10; // i.e., YYYY-MM-DD
-	this.TIME_LEN = 8;  // i.e., HH:MM:SS
-	// Variable field widths: From, to, and subject
-	this.FROM_LEN = (console.screen_columns * (15/console.screen_columns)).toFixed(0);
-	this.TO_LEN = (console.screen_columns * (15/console.screen_columns)).toFixed(0);
-	//var colsLeftForSubject = console.screen_columns-this.MSGNUM_LEN-this.DATE_LEN-this.TIME_LEN-this.FROM_LEN-this.TO_LEN-6; // 6 to account for the spaces
-	//this.SUBJ_LEN = (console.screen_columns * (colsLeftForSubject/console.screen_columns)).toFixed(0);
-	this.SUBJ_LEN = console.screen_columns-this.MSGNUM_LEN-this.DATE_LEN-this.TIME_LEN-this.FROM_LEN-this.TO_LEN-8; // 8 to account for the spaces
-
-	if (this.showScoresInMsgList)
-	{
-		this.SUBJ_LEN -= (this.SCORE_LEN + 1);
-		this.sMsgListHdrFormatStr = "%" + this.MSGNUM_LEN + "s   %-" + this.FROM_LEN + "s %-"
-		                          + this.TO_LEN + "s %-" + this.SUBJ_LEN + "s %"
-		                          + this.SCORE_LEN + "s %-" + this.DATE_LEN + "s %-"
-		                          + this.TIME_LEN + "s";
-
-		this.sMsgInfoFormatStr = this.colors.msgListMsgNumColor + "%" + this.MSGNUM_LEN + "d %s "
-		                       + this.colors.msgListFromColor + "%-" + this.FROM_LEN + "s "
-		                       + this.colors.msgListToColor + "%-" + this.TO_LEN + "s "
-		                       + this.colors.msgListSubjectColor + "%-" + this.SUBJ_LEN + "s "
-		                       + this.colors.msgListScoreColor + "%" + this.SCORE_LEN + "d "
-		                       + this.colors.msgListDateColor + "%-" + this.DATE_LEN + "s "
-		                       + this.colors.msgListTimeColor + "%-" + this.TIME_LEN + "s";
-		// Message information format string with colors to use when the message is
-		// written to the user.
-		this.sMsgInfoToUserFormatStr = this.colors.msgListToUserMsgNumColor + "%" + this.MSGNUM_LEN + "d %s "
-		                             + this.colors.msgListToUserFromColor
-		                             + "%-" + this.FROM_LEN + "s " + this.colors.msgListToUserToColor + "%-"
-		                             + this.TO_LEN + "s " + this.colors.msgListToUserSubjectColor + "%-"
-		                             + this.SUBJ_LEN + "s " + this.colors.msgListToUserScoreColor + "%"
-		                             + this.SCORE_LEN + "d " + this.colors.msgListToUserDateColor
-		                             + "%-" + this.DATE_LEN + "s " + this.colors.msgListToUserTimeColor
-		                             + "%-" + this.TIME_LEN + "s";
-		// Message information format string with colors to use when the message is
-		// from the user.
-		this.sMsgInfoFromUserFormatStr = this.colors.msgListFromUserMsgNumColor + "%" + this.MSGNUM_LEN + "d %s "
-		                               + this.colors.msgListFromUserFromColor
-		                               + "%-" + this.FROM_LEN + "s " + this.colors.msgListFromUserToColor + "%-"
-		                               + this.TO_LEN + "s " + this.colors.msgListFromUserSubjectColor + "%-"
-		                               + this.SUBJ_LEN + "s " + this.colors.msgListFromUserScoreColor + "%"
-		                               + this.SCORE_LEN + "d " + this.colors.msgListFromUserDateColor
-		                               + "%-" + this.DATE_LEN + "s " + this.colors.msgListFromUserTimeColor
-		                               + "%-" + this.TIME_LEN + "s";
-		// Highlighted message information line for the message list (used for the
-		// lightbar interface)
-		this.sMsgInfoFormatHighlightStr = this.colors.msgListMsgNumHighlightColor
-		                                + "%" + this.MSGNUM_LEN + "d %s "
-		                                + this.colors.msgListFromHighlightColor + "%-" + this.FROM_LEN
-		                                + "s " + this.colors.msgListToHighlightColor + "%-" + this.TO_LEN + "s "
-		                                + this.colors.msgListSubjHighlightColor + "%-" + this.SUBJ_LEN + "s "
-		                                + this.colors.msgListScoreHighlightColor + "%" + this.SCORE_LEN + "d "
-		                                + this.colors.msgListDateHighlightColor + "%-" + this.DATE_LEN + "s "
-		                                + this.colors.msgListTimeHighlightColor + "%-" + this.TIME_LEN + "s";
-	}
-	else
-	{
-		this.sMsgListHdrFormatStr = "%" + this.MSGNUM_LEN + "s   %-" + this.FROM_LEN + "s %-"
-		                          + this.TO_LEN + "s %-" + this.SUBJ_LEN + "s %-"
-		                          + this.DATE_LEN + "s %-" + this.TIME_LEN + "s";
-
-		this.sMsgInfoFormatStr = this.colors.msgListMsgNumColor + "%" + this.MSGNUM_LEN + "d %s "
-		                       + this.colors.msgListFromColor + "%-" + this.FROM_LEN + "s "
-		                       + this.colors.msgListToColor + "%-" + this.TO_LEN + "s "
-		                       + this.colors.msgListSubjectColor + "%-" + this.SUBJ_LEN + "s "
-		                       + this.colors.msgListDateColor + "%-" + this.DATE_LEN + "s "
-		                       + this.colors.msgListTimeColor + "%-" + this.TIME_LEN + "s";
-		// Message information format string with colors to use when the message is
-		// written to the user.
-		this.sMsgInfoToUserFormatStr = this.colors.msgListToUserMsgNumColor + "%" + this.MSGNUM_LEN + "d %s "
-		                             + this.colors.msgListToUserFromColor
-		                             + "%-" + this.FROM_LEN + "s " + this.colors.msgListToUserToColor + "%-"
-		                             + this.TO_LEN + "s " + this.colors.msgListToUserSubjectColor + "%-"
-		                             + this.SUBJ_LEN + "s " + this.colors.msgListToUserDateColor
-		                             + "%-" + this.DATE_LEN + "s " + this.colors.msgListToUserTimeColor
-		                             + "%-" + this.TIME_LEN + "s";
-		// Message information format string with colors to use when the message is
-		// from the user.
-		this.sMsgInfoFromUserFormatStr = this.colors.msgListFromUserMsgNumColor + "%" + this.MSGNUM_LEN + "d %s "
-		                               + this.colors.msgListFromUserFromColor
-		                               + "%-" + this.FROM_LEN + "s " + this.colors.msgListFromUserToColor + "%-"
-		                               + this.TO_LEN + "s " + this.colors.msgListFromUserSubjectColor + "%-"
-		                               + this.SUBJ_LEN + "s " + this.colors.msgListFromUserDateColor
-		                               + "%-" + this.DATE_LEN + "s " + this.colors.msgListFromUserTimeColor
-		                               + "%-" + this.TIME_LEN + "s";
-		// Highlighted message information line for the message list (used for the
-		// lightbar interface)
-		this.sMsgInfoFormatHighlightStr = this.colors.msgListMsgNumHighlightColor
-		                                + "%" + this.MSGNUM_LEN + "d %s "
-		                                + this.colors.msgListFromHighlightColor + "%-" + this.FROM_LEN
-		                                + "s " + this.colors.msgListToHighlightColor + "%-" + this.TO_LEN + "s "
-		                                + this.colors.msgListSubjHighlightColor + "%-" + this.SUBJ_LEN + "s "
-		                                + this.colors.msgListDateHighlightColor + "%-" + this.DATE_LEN + "s "
-		                                + this.colors.msgListTimeHighlightColor + "%-" + this.TIME_LEN + "s";
-	}
-
-	// If the user's terminal doesn't support ANSI, then append a newline to
-	// the end of the header format string (we won't be able to move the cursor).
-	if (!canDoHighASCIIAndANSI())
-		this.sMsgListHdrFormatStr += "\r\n";
-}
-
-// For the DigDistMessageReader class: Writes a temporary error message at the key help line
-// for lightbar mode.
-//
-// Parameters:
-//  pErrorMsg: The error message to write
-//  pHelpLineRefreshDef: Optional - Specifies which help line to refresh on the screen
-//                       (i.e., REFRESH_MSG_AREA_CHG_LIGHTBAR_HELP_LINE)
-function DigDistMsgReader_WriteLightbarKeyHelpErrorMsg(pErrorMsg, pLineRefreshDef)
-{
-	console.gotoxy(1, console.screen_rows);
-	console.cleartoeol("\x01n");
-	console.gotoxy(1, console.screen_rows);
-	console.print("\x01y\x01h" + pErrorMsg + "\x01n");
-	mswait(ERROR_WAIT_MS);
-	var helpLineRefreshDef = (typeof(pHelpLineRefreshDef) == "number" ? pHelpLineRefreshDef : -1);
-	if (helpLineRefreshDef == REFRESH_MSG_AREA_CHG_LIGHTBAR_HELP_LINE)
-		this.WriteChgMsgAreaKeysHelpLine();
-}
-
-// For the scrollable reader interface: Refreshes a rectangular region on the screen
-// by printing part of the message text.
-//
-//  pTxtLines: The array of text lines of the message being displayed
-//  pTopLineIdx: The index of the text line currently at the top row in the reader area
-//  pTopLeftX: The upper-left corner column of the rectangle to be refreshed (1-based) - Absolute screen coordinate
-//  pTopLeftY: The upper-left corner row of the rectangle to be refreshed (1-based) - Absolute screen coordinate
-//  pWidth: The width of the rectangle to be refreshed
-//  pHeight: The height of the rectangle to be refreshed
-function DigDistMsgReader_RefreshMsgAreaRectangle(pTxtLines, pTopLineIdx, pTopLeftX, pTopLeftY, pWidth, pHeight)
-{
-	if (typeof(pTxtLines) !== "object")
-		return;
-	if (typeof(pTopLineIdx) !== "number" || pTopLineIdx < 0 || pTopLineIdx >= pTxtLines.length)
-		return;
-	if (typeof(pTopLeftX) !== "number" || pTopLeftX < 1 || pTopLeftX > console.screen_columns)
-		return;
-	if (typeof(pTopLeftY) !== "number" || pTopLeftY < 1 || pTopLeftY > console.screen_rows)
-		return;
-	if (typeof(pWidth) !== "number" || pWidth <= 0 || typeof(pHeight) !== "number" || pHeight <= 0)
-		return;
-
-	var firstTxtLineIdx = pTopLeftY - pTopLineIdx - 1;
-	if (firstTxtLineIdx < 0) firstTxtLineIdx = 0; // Shouldn't happen, but just in case
-	//this.msgAreaLeft = 1;
-	//this.msgAreaRight = console.screen_columns - 1;
-	//this.msgAreaWidth = this.msgAreaRight - this.msgAreaLeft + 1;
-	//this.msgAreaHeight = this.msgAreaBottom - this.msgAreaTop + 1;
-	// Sanity checking
-	if (pTopLeftY < this.msgAreaTop)
-	{
-		var diff = this.msgAreaTop - pTopLeftY;
-		pTopLeftY = this.msgAreaTop;
-		pTopLineIdx += diff;
-	}
-	var lastScreenRow = pTopLeftY + pHeight - 1; // Inclusive, for loop
-	if (lastScreenRow > this.msgAreaBottom)
-		lastScreenRow = this.msgAreaBottom;
-	if (pTopLeftX + pWidth > this.msgAreaRight)
-		pWidth = this.msgAreaRight - pTopLeftX + 1;
-
-	// Print the parts of the text lines that make up the rectangle
-	var txtLineIdx = pTopLineIdx + (pTopLeftY-this.msgAreaTop);
-	if (txtLineIdx < 0) txtLineIdx = 0; // Just in case, but shouldn't happen
-	var txtLineStartIdx = pTopLeftX - this.msgAreaLeft; // Within each text line (it seemed right to subtract 1 but it wasn't)
-	if (txtLineStartIdx < 0) txtLineStartIdx = 0; // Just in case, but shouldn't happen
-	var emptyFormatStr = "\x01n%" + pWidth + "s"; // For printing empty strings after printing all text lines
-	console.attributes = "N";
-	for (var screenRow = pTopLeftY; screenRow <= lastScreenRow; ++screenRow)
-	{
-		console.gotoxy(pTopLeftX, screenRow);
-		// If the current text line index is within the array of text lines, then output the section of the
-		// text line.  Otherwise, output an empty string.
-		if (txtLineIdx < pTxtLines.length)
-		{
-			if (txtLineStartIdx < console.strlen(pTxtLines[txtLineIdx]))
-			{
-				// Get the text attributes up to the current point and output them
-				//console.print(getAllEditLineAttrsUntilLineIdx(pTxtLines, txtLineIdx, true, txtLineStartIdx));
-				// Get the section of line (and make sure it can fill the needed width), and print it
-				// Note: substrWithAttrCodes() is defined in dd_lightbar_menu.js
-				var lineText = "\x01n" + substrWithAttrCodes(pTxtLines[txtLineIdx].replace(/[\r\n]+/g, ""), txtLineStartIdx, pWidth);
-				var printableTxtLen = console.strlen(lineText);
-				if (printableTxtLen < pWidth)
-					lineText += format("\x01n%*s", pWidth - printableTxtLen, "");
-				console.print(lineText);
-			}
-			else // The start index is beyond the length of the string, so print an empty string
-				printf(emptyFormatStr, "");
-		}
-		else // We've printed all the remaining text lines, so now print an empty string.
-			printf(emptyFormatStr, "");
-
-		++txtLineIdx;
-	}
-}
-
-// For the scrollable reader interface: Returns whether a 'from' or 'to' name in a message header
-// is in the user's personal twitlist.
-//
-// Parameters:
-//  pMsgHdr: A message header to check
-//
-// Return value: Boolean - Whether or not the header 'from' or 'to' name is in the user's personal twitlist
-function DigDistMsgReader_MsgHdrFromOrToInUserTwitlist(pMsgHdr)
-{
-	if (pMsgHdr == null || typeof(pMsgHdr) !== "object")
-		return false;
-	if (!pMsgHdr.hasOwnProperty("from") && !pMsgHdr.hasOwnProperty("to"))
-		return false;
-
-	// The names in the user's twitlist have been converted to lowercase for case-insensitive matching.
-	var fromLower = pMsgHdr.from.toLowerCase();
-	var toLower = pMsgHdr.to.toLowerCase();
-	var hdrNamesInTwitlist = false;
-	for (var i = 0; i < this.userSettings.twitList.length && !hdrNamesInTwitlist; ++i)
-		hdrNamesInTwitlist = (this.userSettings.twitList[i] == fromLower || this.userSettings.twitList[i] == toLower);
-	return hdrNamesInTwitlist;
-}
-
-///////////////////////////////////////////////////////////////////////////////////
-// Helper functions
-
-// Displays the program information.
-function DisplayProgramInfo()
-{
-	displayTextWithLineBelow("Digital Distortion Message Reader", true, "\x01n\x01c\x01h", "\x01k\x01h")
-	console.center("\x01n\x01cVersion \x01g" + READER_VERSION + " \x01w\x01h(\x01b" + READER_DATE + "\x01w)");
-	console.crlf();
-}
-
-// This function returns an array of default colors used in the
-// DigDistMessageReader class.
-function getDefaultColors()
-{
-	return {
-		// Colors for the message header displayed above messages in the scrollable reader mode
-		msgHdrMsgNumColor: "\x01n\x01b\x01h", // Message #
-		msgHdrFromColor: "\x01n\x01b\x01h",   // From username
-		msgHdrToColor: "\x01n\x01b\x01h",     // To username
-		msgHdrToUserColor: "\x01n\x01g\x01h", // To username when it's to the current user
-		msgHdrSubjColor: "\x01n\x01b\x01h",   // Message subject
-		msgHdrDateColor: "\x01n\x01b\x01h",   // Message date
-
-		// Message list header line: "Current msg group:"
-		msgListHeaderMsgGroupTextColor: "\x01n\x01" + "4\x01c", 	// Normal cyan on blue background
-		//	msgListHeaderMsgGroupTextColor: "\x01n\x01" + "4\x01w", 	// Normal white on blue background
-
-		// Message list header line: Message group name
-		msgListHeaderMsgGroupNameColor: "\x01h\x01c", 	// High cyan
-		//	msgListHeaderMsgGroupNameColor: "\x01h\x01w", 	// High white
-
-		// Message list header line: "Current sub-board:"
-		msgListHeaderSubBoardTextColor: "\x01n\x01" + "4\x01c", 	// Normal cyan on blue background
-		//	msgListHeaderSubBoardTextColor: "\x01n\x01" + "4\x01w", 	// Normal white on blue background
-
-		// Message list header line: Message sub-board name
-		msgListHeaderMsgSubBoardName: "\x01h\x01c", 	// High cyan
-		//	msgListHeaderMsgSubBoardName: "\x01h\x01w", 	// High white
-		// Line with column headers
-		//	msgListColHeader: "\x01h\x01w", 	// High white (keep blue background)
-		msgListColHeader: "\x01n\x01h\x01w", 	// High white on black background
-		//	msgListColHeader: "\x01h\x01c", 	// High cyan (keep blue background)
-		//	msgListColHeader: "\x01" + "4\x01h\x01y", 	// High yellow (keep blue background)
-
-		// Message list information
-		msgListMsgNumColor: "\x01n\x01h\x01y",
-		msgListFromColor: "\x01n\x01c",
-		msgListToColor: "\x01n\x01c",
-		msgListSubjectColor: "\x01n\x01c",
-		msgListScoreColor: "\x01n\x01c",
-		msgListDateColor: "\x01h\x01b",
-		msgListTimeColor: "\x01h\x01b",
-		// Message information for messages written to the user
-		msgListToUserMsgNumColor: "\x01n\x01h\x01y",
-		msgListToUserFromColor: "\x01h\x01g",
-		msgListToUserToColor: "\x01h\x01g",
-		msgListToUserSubjectColor: "\x01h\x01g",
-		msgListToUserScoreColor: "\x01h\x01g",
-		msgListToUserDateColor: "\x01h\x01b",
-		msgListToUserTimeColor: "\x01h\x01b",
-		// Message information for messages from the user
-		msgListFromUserMsgNumColor: "\x01n\x01h\x01y",
-		msgListFromUserFromColor: "\x01n\x01c",
-		msgListFromUserToColor: "\x01n\x01c",
-		msgListFromUserSubjectColor: "\x01n\x01c",
-		msgListFromUserScoreColor: "\x01n\x01c",
-		msgListFromUserDateColor: "\x01h\x01b",
-		msgListFromUserTimeColor: "\x01h\x01b",
-
-		// Message list highlight colors
-		msgListHighlightBkgColor: "\x014", 	// Background
-		msgListMsgNumHighlightColor: "\x01h\x01y",
-		msgListFromHighlightColor: "\x01h\x01c",
-		msgListToHighlightColor: "\x01h\x01c",
-		msgListSubjHighlightColor: "\x01h\x01c",
-		msgListScoreHighlightColor: "\x01h\x01c",
-		msgListDateHighlightColor: "\x01h\x01w",
-		msgListTimeHighlightColor: "\x01h\x01w",
-
-		// Lightbar message list help line colors
-		lightbarMsgListHelpLineBkgColor: "\x017", 	// Background
-		lightbarMsgListHelpLineGeneralColor: "\x01b",
-		lightbarMsgListHelpLineHotkeyColor: "\x01r",
-		lightbarMsgListHelpLineParenColor: "\x01m",
-
-		// Continue prompt colors
-		tradInterfaceContPromptMainColor: "\x01n\x01g", 	// Main text color
-		tradInterfaceContPromptHotkeyColor: "\x01h\x01c", 	// Hotkey color
-		tradInterfaceContPromptUserInputColor: "\x01h\x01g", 	// User input color
-
-		// Message body color
-		msgBodyColor: "\x01n\x01w",
-
-		// Read message confirmation colors
-		readMsgConfirmColor: "\x01n\x01c",
-		readMsgConfirmNumberColor: "\x01h\x01c",
-		// Prompt for continuing to list messages after reading a message
-		afterReadMsg_ListMorePromptColor: "\x01n\x01c",
-
-		// Help screen text color
-		tradInterfaceHelpScreenColor: "\x01n\x01h\x01w",
-
-		// Colors for choosing a message group & sub-board
-		areaChooserMsgAreaNumColor: "\x01n\x01w\x01h",
-		areaChooserMsgAreaDescColor: "\x01n\x01c",
-		areaChooserMsgAreaNumItemsColor: "\x01b\x01h",
-		areaChooserMsgAreaHeaderColor: "\x01n\x01y\x01h",
-		areaChooserSubBoardHeaderColor: "\x01n\x01g",
-		areaChooserMsgAreaMarkColor: "\x01g\x01h",
-		areaChooserMsgAreaLatestDateColor: "\x01n\x01g",
-		areaChooserMsgAreaLatestTimeColor: "\x01n\x01m",
-		// Highlighted colors (for lightbar mode)
-		areaChooserMsgAreaBkgHighlightColor: "\x014", 	// Blue background
-		areaChooserMsgAreaNumHighlightColor: "\x01w\x01h",
-		areaChooserMsgAreaDescHighlightColor: "\x01c",
-		areaChooserMsgAreaDateHighlightColor: "\x01w\x01h",
-		areaChooserMsgAreaTimeHighlightColor: "\x01w\x01h",
-		areaChooserMsgAreaNumItemsHighlightColor: "\x01w\x01h",
-		// Lightbar area chooser help line
-		lightbarAreaChooserHelpLineBkgColor: "\x017", 	// Background
-		lightbarAreaChooserHelpLineGeneralColor: "\x01b",
-		lightbarAreaChooserHelpLineHotkeyColor: "\x01r",
-		lightbarAreaChooserHelpLineParenColor: "\x01m",
-
-		// Scrollbar background and scroll block colors (for the enhanced
-		// message reader interface)
-		scrollbarBGColor: "\x01n\x01h\x01k",
-		scrollbarScrollBlockColor: "\x01n\x01h\x01w",
-		// Color for the line drawn in the 2nd to last line of the message
-		// area in the enhanced reader mode before a prompt
-		enhReaderPromptSepLineColor: "\x01n\x01h\x01g",
-		// Colors for the enhanced reader help line
-		enhReaderHelpLineBkgColor: "\x017",
-		enhReaderHelpLineGeneralColor: "\x01b",
-		enhReaderHelpLineHotkeyColor: "\x01r",
-		enhReaderHelpLineParenColor: "\x01m",
-
-		// Message header line colors
-		hdrLineLabelColor: "\x01n\x01c",
-		hdrLineValueColor: "\x01n\x01b\x01h",
-
-		// Selected message marker color
-		selectedMsgMarkColor: "\x01n\x01w\x01h",
-
-		// Unread personal email message marker color
-		unreadMsgMarkColor: "\x01n\x01w\x01h\x01i",
-
-		// Colors for the indexed mode sub-board menu:
-		indexMenuHeader: "\x01n\x01w",
-		indexMenuNewIndicator: "\x01n\x01w",
-		indexMenuDesc: "\x01n\x01w",
-		indexMenuTotalMsgs: "\x01n\x01w",
-		indexMenuNumNewMsgs: "\x01n\x01w",
-		indexMenuLastPostDate: "\x01b\x01h",
-		// Highlighted/selected:
-		indexMenuHighlightBkg: "\x014",
-		indexMenuNewIndicatorHighlight: "\x01w\x01h",
-		indexMenuDescHighlight: "\x01w\x01h",
-		indexMenuTotalMsgsHighlight: "\x01w\x01h",
-		indexMenuNumNewMsgsHighlight: "\x01w\x01h",
-		indexMenuLastPostDateHighlight: "\x01w\x01h",
-		indexMenuSeparatorLine: "\x01b",
-		indexMenuSeparatorText: "\x01y\x01h",
-
-		// Colors for the indexed mode help line text:
-		// Background
-		lightbarIndexedModeHelpLineBkgColor: "\x017",
-		// Hotkey color
-		lightbarIndexedModeHelpLineHotkeyColor: "\x01r",
-		// General text
-		lightbarIndexedModeHelpLineGeneralColor: "\x01b",
-		// For ) separating the hotkeys from general text
-		lightbarIndexedModeHelpLineParenColor: "\x01m"
-	};
-}
-
-// This function returns the month number (1-based) from a capitalized
-// month name.
-//
-// Parameters:
-//  pMonthName: The name of the month
-//
-// Return value: The number of the month (1-12).
-function getMonthNum(pMonthName)
-{
-	var monthNum = 1;
-
-	if (pMonthName.substr(0, 3) == "Jan")
-		monthNum = 1;
-	else if (pMonthName.substr(0, 3) == "Feb")
-		monthNum = 2;
-	else if (pMonthName.substr(0, 3) == "Mar")
-		monthNum = 3;
-	else if (pMonthName.substr(0, 3) == "Apr")
-		monthNum = 4;
-	else if (pMonthName.substr(0, 3) == "May")
-		monthNum = 5;
-	else if (pMonthName.substr(0, 3) == "Jun")
-		monthNum = 6;
-	else if (pMonthName.substr(0, 3) == "Jul")
-		monthNum = 7;
-	else if (pMonthName.substr(0, 3) == "Aug")
-		monthNum = 8;
-	else if (pMonthName.substr(0, 3) == "Sep")
-		monthNum = 9;
-	else if (pMonthName.substr(0, 3) == "Oct")
-		monthNum = 10;
-	else if (pMonthName.substr(0, 3) == "Nov")
-		monthNum = 11;
-	else if (pMonthName.substr(0, 3) == "Dec")
-		monthNum = 12;
-
-	return monthNum;
-}
-
-// Clears each line from a given line to the end of the screen.
-//
-// Parameters:
-//  pStartLineNum: The line number to start at (1-based)
-function clearToEOS(pStartLineNum)
-{
-	if (typeof(pStartLineNum) == "undefined")
-		return;
-	if (pStartLineNum == null)
-		return;
-
-	for (var lineNum = pStartLineNum; lineNum <= console.screen_rows; ++lineNum)
-	{
-		console.gotoxy(1, lineNum);
-		console.clearline();
-	}
-}
-
-// Returns the number of messages in a sub-board.
-//
-// Parameters:
-//  pSubBoardCode: The sub-board code (i.e., from bbs.cursub_code)
-//
-// Return value: The number of messages in the sub-board, or 0
-//               if the sub-board could not be opened.
-function numMessages(pSubBoardCode)
-{
-   var messageCount = 0;
-
-   var myMsgbase = new MsgBase(pSubBoardCode);
-	if (myMsgbase.open())
-		messageCount = myMsgbase.total_msgs;
-	myMsgbase.close();
-	myMsgbase = null;
-
-	return messageCount;
-}
-
-// Removes multiple, leading, and/or trailing spaces
-// The search & replace regular expressions used in this
-// function came from the following URL:
-//  http://qodo.co.uk/blog/javascript-trim-leading-and-trailing-spaces
-//
-// Parameters:
-//  pString: The string to trim
-//  pLeading: Whether or not to trim leading spaces (optional, defaults to true)
-//  pMultiple: Whether or not to trim multiple spaces (optional, defaults to true)
-//  pTrailing: Whether or not to trim trailing spaces (optional, defaults to true)
-//
-// Return value: The string with whitespace trimmed
-function trimSpaces(pString, pLeading, pMultiple, pTrailing)
-{
-	var leading = true;
-	var multiple = true;
-	var trailing = true;
-	if (typeof(pLeading) != "undefined")
-		leading = pLeading;
-	if (typeof(pMultiple) != "undefined")
-		multiple = pMultiple;
-	if (typeof(pTrailing) != "undefined")
-		trailing = pTrailing;
-
-	// To remove both leading & trailing spaces:
-	//pString = pString.replace(/(^\s*)|(\s*$)/gi,"");
-
-	if (leading)
-		pString = skipsp(pString); //pString.replace(/(^\s*)/gi,"");
-	if (multiple)
-		pString = pString.replace(/[ ]{2,}/gi," ");
-	if (trailing)
-		pString = truncsp(pString); //pString.replace(/(\s*$)/gi,"");
-
-	return pString;
-}
-
-// Returns whether an internal sub-board code is valid.
-//
-// Parameters:
-//  pSubBoardCode: The internal sub-board code to test
-//
-// Return value: Boolean - Whether or not the given internal code is a valid
-//               sub-board code
-function subBoardCodeIsValid(pSubBoardCode)
-{
-   return ((pSubBoardCode == "mail") || (typeof(msg_area.sub[pSubBoardCode]) == "object"))
-}
-
-// Displays some text with a solid horizontal line on the next line.
-//
-// Parameters:
-//  pText: The text to display
-//  pCenter: Whether or not to center the text.  Optional; defaults
-//           to false.
-//  pTextColor: The color to use for the text.  Optional; by default,
-//              normal white will be used.
-//  pLineColor: The color to use for the line underneath the text.
-//              Optional; by default, bright black will be used.
-function displayTextWithLineBelow(pText, pCenter, pTextColor, pLineColor)
-{
-	var centerText = (typeof(pCenter) == "boolean" ? pCenter : false);
-	var textColor = (typeof(pTextColor) == "string" ? pTextColor : "\x01n\x01w");
-	var lineColor = (typeof(pLineColor) == "string" ? pLineColor : "\x01n\x01k\x01h");
-
-	// Output the text and a solid line on the next line.
-	if (centerText)
-	{
-		console.center(textColor + pText);
-		var solidLine = "";
-		var textLength = console.strlen(pText);
-		for (var i = 0; i < textLength; ++i)
-			solidLine += HORIZONTAL_SINGLE;
-		console.center(lineColor + solidLine);
-	}
-	else
-	{
-		console.print(textColor + pText);
-		console.crlf();
-		console.print(lineColor);
-		var textLength = console.strlen(pText);
-		for (var i = 0; i < textLength; ++i)
-			console.print(HORIZONTAL_SINGLE);
-		console.crlf();
-	}
-}
-
-// Removes multiple, leading, and/or trailing spaces.
-// The search & replace regular expressions used in this
-// function came from the following URL:
-//  http://qodo.co.uk/blog/javascript-trim-leading-and-trailing-spaces
-//
-// Parameters:
-//  pString: The string to trim
-//  pLeading: Whether or not to trim leading spaces (optional, defaults to true)
-//  pMultiple: Whether or not to trim multiple spaces (optional, defaults to true)
-//  pTrailing: Whether or not to trim trailing spaces (optional, defaults to true)
-function trimSpaces(pString, pLeading, pMultiple, pTrailing)
-{
-	var leading = true;
-	var multiple = true;
-	var trailing = true;
-	if(typeof(pLeading) != "undefined")
-		leading = pLeading;
-	if(typeof(pMultiple) != "undefined")
-		multiple = pMultiple;
-	if(typeof(pTrailing) != "undefined")
-		trailing = pTrailing;
-		
-	// To remove both leading & trailing spaces:
-	//pString = pString.replace(/(^\s*)|(\s*$)/gi,"");
-
-	if (leading)
-		pString = pString.replace(/(^\s*)/gi,"");
-	if (multiple)
-		pString = pString.replace(/[ ]{2,}/gi," ");
-	if (trailing)
-		pString = pString.replace(/(\s*$)/gi,"");
-
-	return pString;
-}
-
-// Calculates & returns a page number.
-//
-// Parameters:
-//  pTopIndex: The index (0-based) of the topmost item on the page
-//  pNumPerPage: The number of items per page
-//
-// Return value: The page number
-function calcPageNum(pTopIndex, pNumPerPage)
-{
-  return ((pTopIndex / pNumPerPage) + 1);
-}
-
-// Returns the greatest number of messages of all sub-boards within
-// a message group.
-//
-// Parameters:
-//  pGrpIndex: The index of the message group
-//
-// Returns: The greatest number of messages of all sub-boards within
-//          the message group
-function getGreatestNumMsgs(pGrpIndex)
-{
-  // Sanity checking
-  if (typeof(pGrpIndex) != "number")
-    return 0;
-  if (typeof(msg_area.grp_list[pGrpIndex]) == "undefined")
-    return 0;
-
-  var greatestNumMsgs = 0;
-  var msgBase = null;
-  for (var subIndex = 0; subIndex < msg_area.grp_list[pGrpIndex].sub_list.length; ++subIndex)
-  {
-    msgBase = new MsgBase(msg_area.grp_list[pGrpIndex].sub_list[subIndex].code);
-    if (msgBase == null) continue;
-    if (msgBase.open())
-    {
-      if (msgBase.total_msgs > greatestNumMsgs)
-        greatestNumMsgs = msgBase.total_msgs;
-      msgBase.close();
-    }
-  }
-  return greatestNumMsgs;
-}
-
-// Inputs a keypress from the user and handles some ESC-based
-// characters such as PageUp, PageDown, and ESC.  If PageUp
-// or PageDown are pressed, this function will return the
-// string defined by KEY_PAGE_UP or KEY_PAGE_DOWN,
-// respectively.  Also, F1-F5 will be returned as "\x01F1"
-// through "\x01F5", respectively.
-// Thanks goes to Psi-Jack for the original impementation
-// of this function.
-//
-// Parameters:
-//  pGetKeyMode: Optional - The mode bits for console.getkey().
-//               If not specified, K_NONE will be used.
-//
-// Return value: The user's keypress
-function getKeyWithESCChars(pGetKeyMode)
-{
-	var getKeyMode = K_NONE;
-	if (typeof(pGetKeyMode) == "number")
-		getKeyMode = pGetKeyMode;
-
-	var userInput = console.getkey(getKeyMode);
-	if (userInput == KEY_ESC) {
-		switch (console.inkey(K_NOECHO|K_NOSPIN, 2)) {
-			case '[':
-				switch (console.inkey(K_NOECHO|K_NOSPIN, 2)) {
-					case 'V':
-						userInput = KEY_PAGE_UP;
-						break;
-					case 'U':
-						userInput = KEY_PAGE_DOWN;
-						break;
-				}
-				break;
-			case 'O':
-				switch (console.inkey(K_NOECHO|K_NOSPIN, 2)) {
-					case 'P':
-						userInput = "\x01F1";
-						break;
-					case 'Q':
-						userInput = "\x01F2";
-						break;
-					case 'R':
-						userInput = "\x01F3";
-						break;
-					case 'S':
-						userInput = "\x01F4";
-						break;
-					case 't':
-						userInput = "\x01F5";
-						break;
-				}
-			default:
-				break;
-		}
-	}
-
-	return userInput;
-}
-
-// Finds the next or previous non-empty message sub-board.  Returns an
-// object containing the message group & sub-board indexes.  If all of
-// the next/previous sub-boards are empty, then the given current indexes
-// will be returned.
-//
-// Parameters:
-//  pStartGrpIdx: The index of the message group to start from
-//  pStartSubIdx: The index of the sub-board in the message group to start from
-//  pForward: Boolean - Whether or not to search forward (true) or backward (false).
-//            Optional; defaults to true, to search forward.
-//
-// Return value: An object with the following properties:
-//               foundSubBoard: Boolean - Whether or not a different sub-board was found
-//               grpIdx: The message group index of the found sub-board
-//               subIdx: The sub-board index in the group of the found sub-board
-//               subCode: The internal code of the sub-board
-//               subChanged: Boolean - Whether or not the found sub-board is
-//                           different from the one that was passed in
-//               paramsValid: Boolean - Whether or not all the passed-in parameters
-//                            were valid.
-function findNextOrPrevNonEmptySubBoard(pStartGrpIdx, pStartSubIdx, pForward)
-{
-   var retObj = {
-	   grpIdx: pStartGrpIdx,
-	   subIdx: pStartSubIdx,
-	   subCode: msg_area.grp_list[pStartGrpIdx].sub_list[pStartSubIdx].code,
-	   foundSubBoard: false
-   };
-
-   // Sanity checking
-   retObj.paramsValid = ((pStartGrpIdx >= 0) && (pStartGrpIdx < msg_area.grp_list.length) &&
-                         (pStartSubIdx >= 0) &&
-                         (pStartSubIdx < msg_area.grp_list[pStartGrpIdx].sub_list.length));
-   if (!retObj.paramsValid)
-      return retObj;
-
-   var grpIdx = pStartGrpIdx;
-   var subIdx = pStartSubIdx;
-   var searchForward = (typeof(pForward) == "boolean" ? pForward : true);
-   if (searchForward)
-   {
-      // Advance the sub-board (and group) index, and determine whether or not
-      // to do the search (i.e., we might not want to if the starting sub-board
-      // is the last sub-board in the last group).
-      var searchForSubBoard = true;
-      if (subIdx <  msg_area.grp_list[grpIdx].sub_list.length - 1)
-         ++subIdx;
-      else
-      {
-         if ((grpIdx < msg_area.grp_list.length - 1) && (msg_area.grp_list[grpIdx+1].sub_list.length > 0))
-         {
-            subIdx = 0;
-            ++grpIdx;
-         }
-         else
-            searchForSubBoard = false;
-      }
-      // If we can search, then do it.
-      if (searchForSubBoard)
-      {
-         while (numMsgsInSubBoard(msg_area.grp_list[grpIdx].sub_list[subIdx].code) == 0)
-         {
-            if (subIdx < msg_area.grp_list[grpIdx].sub_list.length - 1)
-               ++subIdx;
-            else
-            {
-               if ((grpIdx < msg_area.grp_list.length - 1) && (msg_area.grp_list[grpIdx+1].sub_list.length > 0))
-               {
-                  subIdx = 0;
-                  ++grpIdx;
-               }
-               else
-                  break; // Stop searching
-            }
-         }
-      }
-   }
-   else
-   {
-      // Search the sub-boards in reverse
-      // Decrement the sub-board (and group) index, and determine whether or not
-      // to do the search (i.e., we might not want to if the starting sub-board
-      // is the first sub-board in the first group).
-      var searchForSubBoard = true;
-      if (subIdx > 0)
-         --subIdx;
-      else
-      {
-         if ((grpIdx > 0) && (msg_area.grp_list[grpIdx-1].sub_list.length > 0))
-         {
-            --grpIdx;
-            subIdx = msg_area.grp_list[grpIdx].sub_list.length - 1;
-         }
-         else
-            searchForSubBoard = false;
-      }
-      // If we can search, then do it.
-      if (searchForSubBoard)
-      {
-         while (numMsgsInSubBoard(msg_area.grp_list[grpIdx].sub_list[subIdx].code) == 0)
-         {
-            if (subIdx > 0)
-               --subIdx;
-            else
-            {
-               if ((grpIdx > 0) && (msg_area.grp_list[grpIdx-1].sub_list.length > 0))
-               {
-                  --grpIdx;
-                  subIdx = msg_area.grp_list[grpIdx].sub_list.length - 1;
-               }
-               else
-                  break; // Stop searching
-            }
-         }
-      }
-   }
-   // If we found a sub-board with messages in it, then set the variables
-   // in the return object
-   if (numMsgsInSubBoard(msg_area.grp_list[grpIdx].sub_list[subIdx].code) > 0)
-   {
-      retObj.grpIdx = grpIdx;
-      retObj.subIdx = subIdx;
-      retObj.subCode = msg_area.grp_list[grpIdx].sub_list[subIdx].code;
-      retObj.foundSubBoard = true;
-      retObj.subChanged = ((grpIdx != pStartGrpIdx) || (subIdx != pStartSubIdx));
-   }
-
-   return retObj;
-}
-
-// Returns the number of messages in a sub-board.
-//
-// Parameters:
-//  pSubBoardCode: The internal code of the sub-board to check
-//  pIncludeDeleted: Optional boolean - Whether or not to include deleted
-//                   messages in the count.  Defaults to false.
-//
-// Return value: The number of messages in the sub-board
-function numMsgsInSubBoard(pSubBoardCode, pIncludeDeleted)
-{
-   var numMessages = 0;
-   var msgbase = new MsgBase(pSubBoardCode);
-   if (msgbase.open())
-   {
-      var includeDeleted = (typeof(pIncludeDeleted) == "boolean" ? pIncludeDeleted : false);
-      if (includeDeleted)
-         numMessages = msgbase.total_msgs;
-      else
-      {
-         // Don't include deleted messages.  Go through each message
-         // in the sub-board and count the ones that aren't marked
-         // as deleted.
-         for (var msgIdx = 0; msgIdx < msgbase.total_msgs; ++msgIdx)
-         {
-            var msgHdr = msgbase.get_msg_index(true, msgIdx, false);
-            if ((msgHdr != null) && (((msgHdr.attr & MSG_DELETE) == 0) || canViewDeletedMsgs()))
-               ++numMessages;
-         }
-      }
-      msgbase.close();
-   }
-   return numMessages;
-}
-
-// Replaces @-codes in a string and returns the new string.
-//
-// Parameters:
-//  pStr: A string in which to replace @-codes
-//
-// Return value: A version of the string with @-codes interpreted
-function replaceAtCodesInStr(pStr)
-{
-	if (typeof(pStr) != "string")
-		return "";
-
-	// This code was originally written by Deuce.  I updated it to check whether
-	// the string returned by bbs.atcode() is null, and if so, just return
-	// the original string.
-	return pStr.replace(/@([^@]+)@/g, function(m, code) {
-		var decoded = bbs.atcode(code);
-		return (decoded != null ? decoded : "@" + code + "@");
-	});
-}
-
-// Shortens a string, accounting for control/attribute codes.  Returns a new
-// (shortened) copy of the string.
-//
-// Parameters:
-//  pStr: The string to shorten
-//  pNewLength: The new (shorter) length of the string
-//  pFromLeft: Optional boolean - Whether to start from the left (default) or
-//             from the right.  Defaults to true.
-//
-// Return value: The shortened version of the string
-function shortenStrWithAttrCodes(pStr, pNewLength, pFromLeft)
-{
-	if (typeof(pStr) != "string")
-		return "";
-	if (typeof(pNewLength) != "number")
-		return pStr;
-	if (pNewLength >= console.strlen(pStr))
-		return pStr;
-
-	var fromLeft = (typeof(pFromLeft) == "boolean" ? pFromLeft : true);
-	var strCopy = "";
-	var tmpStr = "";
-	var strIdx = 0;
-	var lengthGood = true;
-	if (fromLeft)
-	{
-		while (lengthGood && (strIdx < pStr.length))
-		{
-			tmpStr = strCopy + pStr.charAt(strIdx++);
-			if (console.strlen(tmpStr) <= pNewLength)
-				strCopy = tmpStr;
-			else
-				lengthGood = false;
-		}
-	}
-	else
-	{
-		strIdx = pStr.length - 1;
-		while (lengthGood && (strIdx >= 0))
-		{
-			tmpStr = pStr.charAt(strIdx--) + strCopy;
-			if (console.strlen(tmpStr) <= pNewLength)
-				strCopy = tmpStr;
-			else
-				lengthGood = false;
-		}
-	}
-	return strCopy;
-}
-
-// Returns whether a given name or CRC16 value matches the logged-in user's
-// handle, alias, or name.
-//
-// Parameters:
-//  pNameOrCRC16: A name (string) to match against the logged-in user, or a CRC16 (number)
-//                to match against the logged-in user
-//
-// Return value: Boolean - Whether or not the given name matches the logged-in
-//               user's handle, alias, or name
-function userHandleAliasNameMatch(pNameOrCRC16)
-{
-	var checkByCRC16 = (typeof(pNameOrCRC16) === "number");
-	if (!checkByCRC16 && typeof(pNameOrCRC16) !== "string")
-		return false;
-
-	var userMatch = false;
-	if (checkByCRC16)
-	{
-		if (user.handle.length > 0)
-		{
-			if (userHandleAliasNameMatch.userHandleCRC16 === undefined)
-				userHandleAliasNameMatch.userHandleCRC16 = crc16_calc(user.handle.toLowerCase());
-			userMatch = (userHandleAliasNameMatch.userHandleCRC16 == pNameOrCRC16);
-		}
-		if (!userMatch && (user.alias.length > 0))
-		{
-			if (userHandleAliasNameMatch.userAliasCRC16 === undefined)
-				userHandleAliasNameMatch.userAliasCRC16 = crc16_calc(user.alias.toLowerCase());
-			userMatch = (userHandleAliasNameMatch.userAliasCRC16 == pNameOrCRC16);
-		}
-		if (!userMatch && (user.name.length > 0))
-		{
-			if (userHandleAliasNameMatch.userNameCRC16 === undefined)
-				userHandleAliasNameMatch.userNameCRC16 = crc16_calc(user.name.toLowerCase());
-			userMatch = (userHandleAliasNameMatch.userNameCRC16 == pNameOrCRC16);
-		}
-	}
-	else
-	{
-		if (pNameOrCRC16 != "")
-		{
-			var nameUpper = pNameOrCRC16.toUpperCase();
-			// If the name starts & ends with the same quote character, then remove the
-			// quote characters.
-			var firstChar = nameUpper.charAt(0);
-			var lastChar = nameUpper.charAt(nameUpper.length-1);
-			if ((firstChar == "\"" && lastChar == "\"") ||(firstChar == "'" && lastChar == "'"))
-				nameUpper = nameUpper.substring(1, nameUpper.length-1);
-			
-			if (user.handle.length > 0)
-			{
-				if (userHandleAliasNameMatch.userHandleUpper === undefined)
-					userHandleAliasNameMatch.userHandleUpper = user.handle.toUpperCase();
-				userMatch = (nameUpper.indexOf(userHandleAliasNameMatch.userHandleUpper) > -1);
-			}
-			if (!userMatch && (user.alias.length > 0))
-			{
-				if (userHandleAliasNameMatch.userAliasUpper === undefined)
-					userHandleAliasNameMatch.userAliasUpper = user.alias.toUpperCase();
-				userMatch = (nameUpper.indexOf(userHandleAliasNameMatch.userAliasUpper) > -1);
-			}
-			if (!userMatch && (user.name.length > 0))
-			{
-				if (userHandleAliasNameMatch.userNameUpper === undefined)
-					userHandleAliasNameMatch.userNameUpper = user.name.toUpperCase();
-				userMatch = (nameUpper.indexOf(userHandleAliasNameMatch.userNameUpper) > -1);
-			}
-		}
-	}
-	return userMatch;
-}
-
-// Displays a range of text lines on the screen and allows scrolling through them
-// with the up & down arrow keys, PageUp, PageDown, HOME, and END.  It is assumed
-// that the array of text lines are already truncated to fit in the width of the
-// text area, as a speed optimization.
-//
-// Parameters:
-//  pTxtLines: The array of text lines to allow scrolling for
-//  pTopLineIdx: The index of the text line to display at the top
-//  pTxtAttrib: The attribute(s) to apply to the text lines
-//  pWriteTxtLines: Boolean - Whether or not to write the text lines (in addition
-//                  to doing the message loop).  If false, this will only do the
-//                  the message loop.  This parameter is intended as a screen
-//                  refresh optimization.
-//  pTopLeftX: The upper-left corner column for the text area
-//  pTopLeftY: The upper-left corner row for the text area
-//  pWidth: The width of the text area
-//  pHeight: The height of the text area
-//  pPostWriteCurX: The X location for the cursor after writing the message
-//                  lines
-//  pPostWriteCurY: The Y location for the cursor after writing the message
-//                  lines
-//  pUseScrollbar: Boolean - Whether or not to display the scrollbar.  If false,
-//                 this will display a scroll status line at the bottom instead.
-//  pScrollUpdateFn: A function that the caller can provide for updating the
-//                   scroll position.  This function has one parameter:
-//                   - fractionToLastPage: The fraction of the top index divided
-//                     by the top index for the last page (basically, the progress
-//                     to the last page).
-//  pmode: Optional - Print mode (important for UTF8 info)
-//
-// Return value: An object with the following properties:
-//               lastKeypress: The last key pressed by the user (a string)
-//               topLineIdx: The new top line index of the text lines, in case of scrolling
-function scrollTextLines(pTxtLines, pTopLineIdx, pTxtAttrib, pWriteTxtLines, pTopLeftX, pTopLeftY,
-                         pWidth, pHeight, pPostWriteCurX, pPostWriteCurY, pUseScrollbar, pScrollUpdateFn,
-                         pmode)
-{
-	// Variables for the top line index for the last page, scrolling, etc.
-	var topLineIdxForLastPage = pTxtLines.length - pHeight;
-	if (topLineIdxForLastPage < 0)
-		topLineIdxForLastPage = 0;
-	var msgFractionShown = pHeight / pTxtLines.length;
-	if (msgFractionShown > 1)
-		msgFractionShown = 1.0;
-	var fractionToLastPage = 0;
-	var lastTxtRow = pTopLeftY + pHeight - 1;
-	var txtLineFormatStr = "%-" + pWidth + "s";
-
-	var retObj = {
-		lastKeypress: "",
-		topLineIdx: pTopLineIdx
-	};
-
-	// Create an array of color/attribute codes for each line of
-	// text, in case there are any such codes in the text lines,
-	// so that the colors in the message are displayed properly.
-	// First, get the last color/attribute codes from first text
-	// line and apply them to the next line, and so on.
-	var attrCodes = getAttrsBeforeStrIdx(pTxtLines[0], pTxtLines[0].length-1);
-	for (var lineIdx = 1; lineIdx < pTxtLines.length; ++lineIdx)
-	{
-		pTxtLines[lineIdx] = attrCodes + pTxtLines[lineIdx];
-		attrCodes = getAttrsBeforeStrIdx(pTxtLines[lineIdx], pTxtLines[lineIdx].length-1);
-	}
-
-	var pMode = (typeof(pmode) === "number" ? pmode|P_NOATCODES : P_NOATCODES);
-	var writeTxtLines = pWriteTxtLines;
-	var continueOn = true;
-	var mouseInputOnly_continue = false;
-	while (continueOn)
-	{
-		mouseInputOnly_continue = false;
-
-		// If we are to write the text lines, then write each of them and also
-		// clear out the rest of the row on the screen
-		if (writeTxtLines)
-		{
-			// If the scroll update function parameter is a function, then calculate
-			// the fraction to the last page and call the scroll update function.
-			if (pUseScrollbar && typeof(pScrollUpdateFn) == "function")
-			{
-				if (topLineIdxForLastPage != 0)
-					fractionToLastPage = retObj.topLineIdx / topLineIdxForLastPage;
-				pScrollUpdateFn(fractionToLastPage);
-			}
-			var screenY = pTopLeftY;
-			for (var lineIdx = retObj.topLineIdx; (lineIdx < pTxtLines.length) && (screenY <= lastTxtRow); ++lineIdx)
-			{
-				console.gotoxy(pTopLeftX, screenY++);
-				// Print the text line, then clear the rest of the line
-				console.print(pTxtAttrib + pTxtLines[lineIdx], pMode);
-				printf("\x01n%*s", pWidth-console.strlen(pTxtLines[lineIdx]), "");
-			}
-			// If there are still some lines left in the message reading area, then
-			// clear the lines.
-			console.print("\x01n" + pTxtAttrib);
-			while (screenY <= lastTxtRow)
-			{
-				console.gotoxy(pTopLeftX, screenY++);
-				printf(txtLineFormatStr, "");
-			}
-		}
-
-		writeTxtLines = false;
-
-		// Get a keypress from the user and take action based on it
-		console.gotoxy(pPostWriteCurX, pPostWriteCurY);
-		retObj.lastKeypress = getKeyWithESCChars(K_UPPER|K_NOCRLF|K_NOECHO|K_NOSPIN);
-		if (!continueOn)
-			break;
-
-		switch (retObj.lastKeypress)
-		{
-			case KEY_UP:
-				if (retObj.topLineIdx > 0)
-				{
-					--retObj.topLineIdx;
-					writeTxtLines = true;
-				}
-				break;
-			case KEY_DOWN:
-				if (retObj.topLineIdx < topLineIdxForLastPage)
-				{
-					++retObj.topLineIdx;
-					writeTxtLines = true;
-				}
-				break;
-			case KEY_PAGE_UP: // Previous page
-				if (retObj.topLineIdx > 0)
-				{
-					retObj.topLineIdx -= pHeight;
-					if (retObj.topLineIdx < 0)
-						retObj.topLineIdx = 0;
-					writeTxtLines = true;
-				}
-				break;
-			case KEY_PAGE_DOWN: // Next page
-				if (retObj.topLineIdx < topLineIdxForLastPage)
-				{
-					retObj.topLineIdx += pHeight;
-					if (retObj.topLineIdx > topLineIdxForLastPage)
-						retObj.topLineIdx = topLineIdxForLastPage;
-					writeTxtLines = true;
-				}
-				break;
-			case KEY_HOME: // First page
-				if (retObj.topLineIdx > 0)
-				{
-					retObj.topLineIdx = 0;
-					writeTxtLines = true;
-				}
-				break;
-			case KEY_END: // Last page
-				if (retObj.topLineIdx < topLineIdxForLastPage)
-				{
-					retObj.topLineIdx = topLineIdxForLastPage;
-					writeTxtLines = true;
-				}
-				break;
-			default:
-				continueOn = false;
-				break;
-		}
-	}
-	return retObj;
-}
-
-// Gets all the attribute codes from an array of text lines until a certain index.
-//
-// Parameters:
-//  pTxtLines: The array of text lines of the message being displayed
-//  pEndArrayIdx: One past the last edit line index to get attributes for
-//  pIncludeEndArrayIdxAttrs: Optional boolean: Whether or not to include the attributes for the line at the end array index.
-//                            Defaults to false.
-//  pLastLineTextEndIdx: Optional - Only used when pIncludeEndArrayIdxAttrs is true, this parameter specifies the
-//                    end index (non-inclusive) in the last text line to include attributes for.  If not specified,
-//                    the entire end line will be used to include its attributes.
-//
-// Return value: A string containing the relevant attribute codes to apply up to the given line index (non-inclusive).
-function getAllEditLineAttrsUntilLineIdx(pTxtLines, pEndArrayIdx, pIncludeEndArrayIdxAttrs, pLastLineTextEndIdx)
-{
-	if (typeof(pTxtLines) !== "object" || typeof(pEndArrayIdx) !== "number" || pEndArrayIdx < 0)
-		return "";
-
-	var includeEndArrayIdxAttrs = (typeof(pIncludeEndArrayIdxAttrs) === "boolean" ? pIncludeEndArrayIdxAttrs : false);
-
-	var syncAttrRegex = /\x01[krgybmcw01234567hinpq,;\.dtl<>\[\]asz]/gi;
-	var attributesStr = "";
-	var onePastLastIdx = (includeEndArrayIdxAttrs ? pEndArrayIdx + 1 : pEndArrayIdx);
-	for (var i = 0; i < onePastLastIdx; ++i)
-	{
-		if (typeof(pTxtLines[i]) !== "string") continue;
-		var attrCodes;
-		if (typeof(pLastLineTextEndIdx) === "number" && i === (onePastLastIdx-1))
-		{
-			// Note: dd_lightbar_menu.js defines substrWithAttrCodes(pStr, pStartIdx, pLen)
-			var textLine = substrWithAttrCodes(pTxtLines[i], 0, pLastLineTextEndIdx);
-			attrCodes = textLine.match(syncAttrRegex);
-		}
-		else
-			attrCodes = pTxtLines[i].match(syncAttrRegex);
-		if (attrCodes != null)
-		{
-			for (var attrMatchI = 0; attrMatchI < attrCodes.length; ++attrMatchI)
-				attributesStr += attrCodes[attrMatchI];
-		}
-	}
-	// If there is a normal attribute code in the middle of the string, remove anything before it
-	var normalAttrIdx = attributesStr.lastIndexOf("\x01n");
-	if (normalAttrIdx < 0)
-		normalAttrIdx = attributesStr.lastIndexOf("\x01N");
-	if (normalAttrIdx > -1)
-		attributesStr = attributesStr.substr(normalAttrIdx/*+2*/);
-	return attributesStr;
-}
-
-// Finds the (1-based) page number of an item by number (1-based).  If no page
-// is found, then the return value will be 0.
-//
-// Parameters:
-//  pItemNum: The item number (1-based)
-//  pNumPerPage: The number of items per page
-//  pTotoalNum: The total number of items in the list
-//  pReverseOrder: Boolean - Whether or not the list is in reverse order.  If not specified,
-//                 this will default to false.
-//
-// Return value: The page number (1-based) of the item number.  If no page is found,
-//               the return value will be 0.
-function findPageNumOfItemNum(pItemNum, pNumPerPage, pTotalNum, pReverseOrder)
-{
-   if ((typeof(pItemNum) != "number") || (typeof(pNumPerPage) != "number") || (typeof(pTotalNum) != "number"))
-      return 0;
-   if ((pItemNum < 1) || (pItemNum > pTotalNum))
-      return 0;
-
-   var reverseOrder = (typeof(pReverseOrder) == "boolean" ? pReverseOrder : false);
-   var itemPageNum = 0;
-   if (reverseOrder)
-   {
-      var pageNum = 1;
-      for (var topNum = pTotalNum; ((topNum > 0) && (itemPageNum == 0)); topNum -= pNumPerPage)
-      {
-         if ((pItemNum <= topNum) && (pItemNum >= topNum-pNumPerPage+1))
-            itemPageNum = pageNum;
-         ++pageNum;
-      }
-   }
-   else // Forward order
-      itemPageNum = Math.ceil(pItemNum / pNumPerPage);
-
-   return itemPageNum;
-}
-
-// This function converts a search mode string to one of the defined search value
-// constants.  If the passed-in mode string is unknown, then the return value will
-// be SEARCH_NONE (-1).
-//
-// Parameters:
-//  pSearchTypeStr: A string describing a search mode ("keyword_search", "from_name_search",
-//                  "to_name_search", "to_user_search", "new_msg_scan", "new_msg_scan_cur_sub",
-//                  "new_msg_scan_cur_grp", "new_msg_scan_all", "to_user_new_scan",
-//                  "to_user_all_scan")
-//
-// Return value: An integer representing the search value (SEARCH_KEYWORD,
-//               SEARCH_FROM_NAME, SEARCH_TO_NAME_CUR_MSG_AREA,
-//               SEARCH_TO_USER_CUR_MSG_AREA), or SEARCH_NONE (-1) if the passed-in
-//               search type string is unknown.
-function searchTypeStrToVal(pSearchTypeStr)
-{
-	if (typeof(pSearchTypeStr) != "string")
-		return SEARCH_NONE;
-
-	var searchTypeInt = SEARCH_NONE;
-	var modeStr = pSearchTypeStr.toLowerCase();
-	if (modeStr == "keyword_search")
-		searchTypeInt = SEARCH_KEYWORD;
-	else if (modeStr == "from_name_search")
-		searchTypeInt = SEARCH_FROM_NAME;
-	else if (modeStr == "to_name_search")
-		searchTypeInt = SEARCH_TO_NAME_CUR_MSG_AREA;
-	else if (modeStr == "to_user_search")
-		searchTypeInt = SEARCH_TO_USER_CUR_MSG_AREA;
-	else if (modeStr == "new_msg_scan")
-		searchTypeInt = SEARCH_MSG_NEWSCAN;
-	else if (modeStr == "new_msg_scan_cur_sub")
-		searchTypeInt = SEARCH_MSG_NEWSCAN_CUR_SUB;
-	else if (modeStr == "new_msg_scan_cur_grp")
-		searchTypeInt = SEARCH_MSG_NEWSCAN_CUR_GRP;
-	else if (modeStr == "new_msg_scan_all")
-		searchTypeInt = SEARCH_MSG_NEWSCAN_ALL;
-	else if (modeStr == "to_user_new_scan")
-		searchTypeInt = SEARCH_TO_USER_NEW_SCAN;
-	else if (modeStr == "to_user_new_scan_cur_sub")
-		searchTypeInt = SEARCH_TO_USER_NEW_SCAN_CUR_SUB;
-	else if (modeStr == "to_user_new_scan_cur_grp")
-		searchTypeInt = SEARCH_TO_USER_NEW_SCAN_CUR_GRP;
-	else if (modeStr == "to_user_new_scan_all")
-		searchTypeInt = SEARCH_TO_USER_NEW_SCAN_ALL;
-	else if (modeStr == "to_user_all_scan")
-		searchTypeInt = SEARCH_ALL_TO_USER_SCAN;
-	return searchTypeInt;
-}
-
-// This function converts a search type value to a string description.
-//
-// Parameters:
-//  pSearchType: The search type value to convert
-//
-// Return value: A string describing the search type value
-function searchTypeValToStr(pSearchType)
-{
-	if (typeof(pSearchType) != "number")
-		return "Unknown (not a number)";
-
-	var searchTypeStr = "";
-	switch (pSearchType)
-	{
-		case SEARCH_NONE:
-			searchTypeStr = "None (SEARCH_NONE)";
-			break;
-		case SEARCH_KEYWORD:
-			searchTypeStr = "Keyword (SEARCH_KEYWORD)";
-			break;
-		case SEARCH_FROM_NAME:
-			searchTypeStr = "'From' name (SEARCH_FROM_NAME)";
-			break;
-		case SEARCH_TO_NAME_CUR_MSG_AREA:
-			searchTypeStr = "'To' name (SEARCH_TO_NAME_CUR_MSG_AREA)";
-			break;
-		case SEARCH_TO_USER_CUR_MSG_AREA:
-			searchTypeStr = "To you (SEARCH_TO_USER_CUR_MSG_AREA)";
-			break;
-		case SEARCH_MSG_NEWSCAN:
-			searchTypeStr = "New message scan (SEARCH_MSG_NEWSCAN)";
-			break;
-		case SEARCH_MSG_NEWSCAN_CUR_SUB:
-			searchTypeStr = "New in current message area (SEARCH_MSG_NEWSCAN_CUR_SUB)";
-			break;
-		case SEARCH_MSG_NEWSCAN_CUR_GRP:
-			searchTypeStr = "New in current message group (SEARCH_MSG_NEWSCAN_CUR_GRP)";
-			break;
-		case SEARCH_MSG_NEWSCAN_ALL:
-			searchTypeStr = "Newscan - All (SEARCH_MSG_NEWSCAN_ALL)";
-			break;
-		case SEARCH_TO_USER_NEW_SCAN:
-			searchTypeStr = "To You new scan (SEARCH_TO_USER_NEW_SCAN)";
-			break;
-		case SEARCH_TO_USER_NEW_SCAN_CUR_SUB:
-			searchTypeStr = "To You new scan, current sub-board (SEARCH_TO_USER_NEW_SCAN_CUR_SUB)";
-			break;
-		case SEARCH_TO_USER_NEW_SCAN_CUR_GRP:
-			searchTypeStr = "To You new scan, current group (SEARCH_TO_USER_NEW_SCAN_CUR_GRP)";
-			break;
-		case SEARCH_TO_USER_NEW_SCAN_ALL:
-			searchTypeStr = "To You new scan, all sub-boards (SEARCH_TO_USER_NEW_SCAN_ALL)";
-			break;
-		case SEARCH_ALL_TO_USER_SCAN:
-			searchTypeStr = "All To You scan (SEARCH_ALL_TO_USER_SCAN)";
-			break;
-		default:
-			searchTypeStr = "Unknown (" + pSearchType + ")";
-			break;
-	}
-	return searchTypeStr;
-}
-
-// This function converts a reader mode string to one of the defined reader mode
-// value constants.  If the passed-in mode string is unknown, then the return value
-// will be -1.
-//
-// Parameters:
-//  pModeStr: A string describing a reader mode ("read", "reader", "list", "lister")
-//
-// Return value: An integer representing the reader mode value (READER_MODE_READ,
-//               READER_MODE_LIST), or -1 if the passed-in mode string is unknown.
-function readerModeStrToVal(pModeStr)
-{
-   if (typeof(pModeStr) != "string")
-      return -1;
-
-   var readerModeInt = -1;
-   var modeStr = pModeStr.toLowerCase();
-   if ((modeStr == "read") || (modeStr == "reader"))
-      readerModeInt = READER_MODE_READ;
-   else if ((modeStr == "list") || (modeStr == "lister"))
-      readerModeInt = READER_MODE_LIST;
-   return readerModeInt;
-}
-
-// This function returns a boolean to signify whether or not the user's
-// terminal supports both high-ASCII characters and ANSI codes.
-function canDoHighASCIIAndANSI()
-{
-	//return (console.term_supports(USER_ANSI) && (user.settings & USER_NO_EXASCII == 0));
-	return (console.term_supports(USER_ANSI));
-}
-
-// Searches a given range in an open message base and returns an object with arrays
-// containing the message headers (0-based indexed and indexed by message number)
-// with the message headers of any found messages.
-//
-// Parameters:
-//  pSubCode: The internal code of the message sub-board
-//  pSearchType: The type of search to do (one of the SEARCH_ values)
-//  pSearchString: The string to search for.
-//  pListingPersonalEmailFromUser: Optional boolean - Whether or not we're listing
-//                                 personal email sent by the user.  This defaults
-//                                 to false.
-//  pStartIndex: Optional: The starting message index (0-based).  Defaults to 0.
-//  pEndIndex: Optional: One past the last message index.  Defaults to the total number
-//             of messages.
-//
-//  pUserNum: Optional: The user number (for reading personal email)
-//
-// Return value: An object with the following arrays:
-//               indexed: A 0-based indexed array of message headers
-function searchMsgbase(pSubCode, pSearchType, pSearchString, pListingPersonalEmailFromUser, pStartIndex, pEndIndex, pUserNum)
-{
-	var msgHeaders = {
-		indexed: []
-	};
-	if ((pSubCode != "mail") && ((typeof(pSearchString) != "string") || !searchTypePopulatesSearchResults(pSearchType)))
-		return msgHeaders;
-
-	var msgbase = new MsgBase(pSubCode);
-	if (!msgbase.open())
-		return msgHeaders;
-
-	var startMsgIndex = 0;
-	var endMsgIndex = msgbase.total_msgs;
-	if (typeof(pStartIndex) == "number")
-	{
-		if ((pStartIndex >= 0) && (pStartIndex < msgbase.total_msgs))
-			startMsgIndex = pStartIndex;
-	}
-	if (typeof(pEndIndex) == "number")
-	{
-		if ((pEndIndex >= 0) && (pEndIndex > startMsgIndex) && (pEndIndex <= msgbase.total_msgs))
-			endMsgIndex = pEndIndex;
-	}
-
-	// Define a search function for the message field we're going to search
-	var readingPersonalEmailFromUser = (typeof(pListingPersonalEmailFromUser) == "boolean" ? pListingPersonalEmailFromUser : false);
-	var matchFn = null;
-	var getAllMsgHdrs = false;
-	switch (pSearchType)
-	{
-		// It might seem odd to have SEARCH_NONE in here, but it's here because
-		// when reading personal email, we need to search for messages only to
-		// the current user.
-		case SEARCH_NONE:
-			if (pSubCode == "mail")
-			{
-				// Set up the match function slightly differently depending on whether
-				// we're looking for mail from the current user or to the current user.
-				if (readingPersonalEmailFromUser)
-				{
-					matchFn = function(pSearchStr, pMsgHdr, pMsgBase, pSubBoardCode) {
-						var msgText = strip_ctrl(pMsgBase.get_msg_body(false, pMsgHdr.number, false, false, true, true));
-						return gAllPersonalEmailOptSpecified || msgIsFromUser(pMsgHdr, pUserNum);
-					}
-				}
-				else
-				{
-					// We're reading mail to the user
-					matchFn = function(pSearchStr, pMsgHdr, pMsgBase, pSubBoardCode) {
-						var msgText = strip_ctrl(pMsgBase.get_msg_body(false, pMsgHdr.number, false, false, true, true));
-						var msgMatchesCriteria = (gAllPersonalEmailOptSpecified || msgIsToUserByNum(pMsgHdr, pUserNum));
-						// If only new/unread personal email is to be displayed, then check
-						// that the message has not been read.
-						if (gCmdLineArgVals.onlynewpersonalemail)
-							msgMatchesCriteria = (msgMatchesCriteria && ((pMsgHdr.attr & MSG_READ) == 0));
-						return msgMatchesCriteria;
-					}
-				}
-			}
-			break;
-		case SEARCH_KEYWORD:
-			getAllMsgHdrs = true;
-			matchFn = function(pSearchStr, pMsgHdr, pMsgBase, pSubBoardCode) {
-				var msgText = strip_ctrl(pMsgBase.get_msg_body(false, pMsgHdr.number, false, false, true, true));
-				var keywordFound = ((pMsgHdr.subject.toUpperCase().indexOf(pSearchStr) > -1) || (msgText.toUpperCase().indexOf(pSearchStr) > -1));
-				if (pSubBoardCode == "mail")
-					return keywordFound && msgIsToUserByNum(pMsgHdr);
-				else
-					return keywordFound;
-			}
-			break;
-		case SEARCH_FROM_NAME:
-			getAllMsgHdrs = true;
-			matchFn = function(pSearchStr, pMsgHdr, pMsgBase, pSubBoardCode) {
-				var fromNameFound = (pMsgHdr.from.toUpperCase() == pSearchStr.toUpperCase());
-				if (pSubBoardCode == "mail")
-					return fromNameFound && (gAllPersonalEmailOptSpecified || msgIsToUserByNum(pMsgHdr));
-				else
-					return fromNameFound;
-			}
-			break;
-		case SEARCH_TO_NAME_CUR_MSG_AREA:
-			getAllMsgHdrs = true;
-			matchFn = function(pSearchStr, pMsgHdr, pMsgBase, pSubBoardCode) {
-				return (pMsgHdr.to.toUpperCase() == pSearchStr);
-			}
-			break;
-		case SEARCH_TO_USER_CUR_MSG_AREA:
-			getAllMsgHdrs = true;
-		case SEARCH_ALL_TO_USER_SCAN:
-			matchFn = function(pSearchStr, pMsgHdr, pMsgBase, pSubBoardCode) {
-				// See if the message is not marked as deleted and the 'To' name
-				// matches the user's handle, alias, and/or username.
-				return ((((pMsgHdr.attr & MSG_DELETE) == 0) || canViewDeletedMsgs()) && userHandleAliasNameMatch(pMsgHdr.to));
-			}
-			break;
-		case SEARCH_TO_USER_NEW_SCAN:
-		case SEARCH_TO_USER_NEW_SCAN_CUR_SUB:
-		case SEARCH_TO_USER_NEW_SCAN_CUR_GRP:
-		case SEARCH_TO_USER_NEW_SCAN_ALL:
-			if (pSubCode != "mail")
-			{
-				/*
-				// If pStartIndex or pEndIndex aren't specified, then set
-				// startMsgIndex to the scan pointer and endMsgIndex to one
-				// past the index of the last message in the sub-board
-				if (typeof(pStartIndex) != "number")
-				{
-					// First, write some messages to the log if verbose logging is enabled
-					if (gCmdLineArgVals.verboselogging)
-					{
-						writeToSysAndNodeLog("New-to-user scan for " +
-						                     subBoardGrpAndName(pSubCode) + " -- Scan pointer: " +
-						                     msg_area.sub[pSubCode].scan_ptr);
-					}
-					//startMsgIndex = absMsgNumToIdxWithMsgbaseObj(msgbase, msg_area.sub[pSubCode].last_read);
-					startMsgIndex = absMsgNumToIdxWithMsgbaseObj(msgbase, GetScanPtrOrLastMsgNum(pSubCode));
-					if (startMsgIndex == -1)
-					{
-						msg_area.sub[pSubCode].scan_ptr = 0;
-						startMsgIndex = 0;
-					}
-					else
-					{
-						// If this message has been read, then start at the next message.
-						var startMsgHeader = msgbase.get_msg_index(true, startMsgIndex, false);
-						if (startMsgHeader == null)
-							++startMsgIndex;
-						else
-						{
-							if ((startMsgHeader.attr & MSG_READ) == MSG_READ)
-								++startMsgIndex;
-						}
-					}
-				}
-				if (typeof(pEndIndex) != "number")
-					endMsgIndex = (msgbase.total_msgs > 0 ? msgbase.total_msgs : 0);
-				*/
-				// For the new-to-you scan faster, check messages to the user from the user's new scan pointer to
-				// messagebase.last_msg
-				// scan_ptr (not last_read?)
-				if (typeof(msg_area.sub[pSubCode].scan_ptr) === "number")
-					startMsgIndex = absMsgNumToIdxWithMsgbaseObj(msgbase, msg_area.sub[pSubCode].scan_ptr);
-				else if (typeof(pStartIndex) === "number")
-					startMsgIndex = pStartIndex;
-				else
-					startMsgIndex = 0;
-				endMsgIndex = absMsgNumToIdxWithMsgbaseObj(msgbase, msgbase.last_msg) + 1;
-				if (endMsgIndex == 0) // Not valid
-					endMsgIndex = msgbase.total_msgs;
-			}
-			matchFn = function(pSearchStr, pMsgHdr, pMsgBase, pSubBoardCode) {
-				// Note: This assumes pSubBoardCode is not "mail" (personal mail).
-				// See if the message 'To' name matches the user's handle, alias,
-				// and/or username and is not marked as deleted and is unread.
-				return ((((pMsgHdr.attr & MSG_DELETE) == 0) || canViewDeletedMsgs()) && ((pMsgHdr.attr & MSG_READ) == 0) && userHandleAliasNameMatch(pMsgHdr.to));
-			}
-			break;
-		case SEARCH_MSG_NEWSCAN:
-		case SEARCH_MSG_NEWSCAN_CUR_SUB:
-		case SEARCH_MSG_NEWSCAN_CUR_GRP:
-		case SEARCH_MSG_NEWSCAN_ALL:
-			/*
-			matchFn = function(pSearchStr, pMsgHdr, pMsgBase, pSubBoardCode) {
-				// Note: This assumes pSubBoardCode is not "mail" (personal mail).
-				// Get the offset of the last read message and compare it with the
-				// offset of the given message header
-				var lastReadMsgHdr = pMsgBase.get_msg_index(false, msg_area.sub[pSubBoardCode].last_read, false);
-				var lastReadMsgOffset = 0;
-				if (lastReadMsgHdr != null)
-					lastReadMsgOffset = absMsgNumToIdx(pSubBoardCode, lastReadMsgHdr.number);
-				if (lastReadMsgOffset < 0)
-					lastReadMsgOffset = 0;
-				return (absMsgNumToIdx(pSubBoardCode, pMsgHdr.number) > lastReadMsgOffset);
-			}
-			*/
-			if (typeof(msg_area.sub[pSubCode].scan_ptr) === "number")
-				startMsgIndex = absMsgNumToIdxWithMsgbaseObj(msgbase, msg_area.sub[pSubCode].scan_ptr);
-			else if (typeof(pStartIndex) === "number")
-				startMsgIndex = pStartIndex;
-			else
-				startMsgIndex = 0;
-			endMsgIndex = absMsgNumToIdxWithMsgbaseObj(msgbase, msgbase.last_msg) + 1;
-			// TODO: Is this matchFn really needed?
-			matchFn = function(pSearchStr, pMsgHdr, pMsgBase, pSubBoardCode) {
-				// Note: This assumes pSubBoardCode is not "mail" (personal mail).
-				// Get the offset of the last read message and compare it with the
-				// offset of the given message header
-				var lastReadMsgHdr = pMsgBase.get_msg_index(false, msg_area.sub[pSubBoardCode].last_read, false);
-				var lastReadMsgOffset = 0;
-				if (lastReadMsgHdr != null)
-					lastReadMsgOffset = absMsgNumToIdx(pSubBoardCode, lastReadMsgHdr.number);
-				if (lastReadMsgOffset < 0)
-					lastReadMsgOffset = 0;
-				return (absMsgNumToIdx(pSubBoardCode, pMsgHdr.number) > lastReadMsgOffset);
-			}
-			break;
-	}
-	// Search the messages
-	if (matchFn != null)
-	{
-		// If we want to use get_all_msg_headers, then use it.  Otherwise,
-		// iterate through all message offsets and get the headers.  We want to
-		// use get_all_msg_hdrs() if possible because that will include information
-		// about how many votes each message got (up/downvotes for regular
-		// messages or who voted for which options for poll messages).
-		if (getAllMsgHdrs)
-		{
-			// Pass false to get_all_msg_headers() to tell it not to include vote messages
-			// (the parameter was introduced in Synchronet 3.17+)
-			var tmpHdrs = msgbase.get_all_msg_headers(false);
-			// Re-do startMsgIndex and endMsgIndex based on the message headers we got
-			startMsgIndex = 0;
-			endMsgIndex = msgbase.total_msgs;
-			if (typeof(pStartIndex) == "number")
-			{
-				if ((pStartIndex >= 0) && (pStartIndex < tmpHdrs.length))
-					startMsgIndex = pStartIndex;
-			}
-			if (typeof(pEndIndex) == "number")
-			{
-				if ((pEndIndex >= 0) && (pEndIndex > startMsgIndex) && (pEndIndex <= tmpHdrs.length))
-					endMsgIndex = pEndIndex;
-			}
-			// Search the message headers
-			var msgIdx = 0;
-			for (var prop in tmpHdrs)
-			{
-				// Only add the message header if the message is readable to the user
-				// and msgIdx is within bounds
-				if ((msgIdx >= startMsgIndex) && (msgIdx < endMsgIndex) && isReadableMsgHdr(tmpHdrs[prop], pSubCode))
-				{
-					if (tmpHdrs[prop] != null)
-					{
-						if (matchFn(pSearchString, tmpHdrs[prop], msgbase, pSubCode))
-							msgHeaders.indexed.push(tmpHdrs[prop]);
-					}
-				}
-				++msgIdx;
-			}
-		}
-		else
-		{
-			for (var msgIdx = startMsgIndex; msgIdx < endMsgIndex; ++msgIdx)
-			{
-				var msgHeader = msgbase.get_msg_header(true, msgIdx, false);
-				if (msgHeader != null)
-				{
-					if (matchFn(pSearchString, msgHeader, msgbase, pSubCode))
-						msgHeaders.indexed.push(msgHeader);
-				}
-			}
-		}
-	}
-	else
-	{
-		// There is no match function - Get all message headers between the start & end indexes
-	}
-	msgbase.close();
-	return msgHeaders;
-}
-
-// Tries to look up a message index (offset) with a given message number.
-// On error, returns -1.
-//
-// Parameters:
-//  pMsgbase: The messagebase object from which to retrieve the message header
-//  pMsgNum: The absolute message number
-//
-// Return value: The message's index, or -1 on error.
-function absMsgNumToIdxWithMsgbaseObj(pMsgbase, pMsgNum)
-{
-	if ((pMsgbase == null) || (typeof(pMsgbase) != "object"))
-		return -1;
-	if (!pMsgbase.is_open)
-		return -1;
-
-	if (typeof(pMsgNum) != "number")
-		return -1;
-	
-	var messageIdx = 0;
-	// If pMsgNum is the 'last number' special value, then use the last message
-	// number in the messagebase.
-	if (msgNumIsLatestMsgSpecialVal(pMsgNum))
-		messageIdx = pMsgbase.total_msgs - 1; // Or this.NumMessages() - 1 but can't because this isn't a class member function
-	else
-	{
-		var msgHdr = pMsgbase.get_msg_index(false, pMsgNum, false);
-		if (msgHdr == null && gCmdLineArgVals.verboselogging)
-		{
-			writeToSysAndNodeLog("Message area " + pMsgbase.cfg.code + ": Tried to get message header for absolute message number " +
-			                     pMsgNum + " but got a null header object.");
-		}
-		messageIdx = (msgHdr != null ? msgHdr.offset : -1);
-	}
-	return messageIdx;
-}
-// Tries to look up a message index (offset) with a given message number.
-// On error, returns -1.
-//
-// Parameters:
-//  pSubCodeOrMsgbase: The internal sub-board code of the messagebase to check, or
-//                     an already-open messagebase object
-//  pMsgNum: The absolute message number
-//
-// Return value: The message's index, or -1 on error.
-function absMsgNumToIdx(pSubCodeOrMsgbase, pMsgNum)
-{
-	var typename = typeof(pSubCodeOrMsgbase);
-	if (typename === "string")
-	{
-		var msgOffset = -1;
-		var msgbase = new MsgBase(pSubCodeOrMsgbase);
-		if (msgbase.open())
-		{
-			msgOffset = absMsgNumToIdxWithMsgbaseObj(msgbase, pMsgNum);
-			msgbase.close();
-		}
-		return msgOffset;
-	}
-	else if (typename === "object")
-		return absMsgNumToIdxWithMsgbaseObj(msgbase, pMsgNum);
-}
-
-// Takes a message index and returns the message's absolute message number.
-//
-// Parameters:
-//  pMsgbase: The messagebase object from which to retrieve the message header
-//  pMsgIdx: The message index
-//
-// Return value: The absolue message number, or -1 on error.
-function idxToAbsMsgNum(pMsgbase, pMsgIdx)
-{
-	if ((pMsgbase == null) || (typeof(pMsgbase) != "object"))
-		return -1;
-	if (!pMsgbase.is_open)
-		return -1;
-
-	var msgHdr = pMsgbase.get_msg_index(true, pMsgIdx, false);
-	if (msgHdr == null && gCmdLineArgVals.verboselogging)
-	{
-		writeToSysAndNodeLog("Tried to get message header for message offset " +
-		                     pMsgIdx + " but got a null header object.");
-	}
-	return (msgHdr != null ? msgHdr.number : -1);
-}
-
-// Returns whether or not a message is to the current user (either the current
-// logged-in user or the user specified by the userNum command-line argument)
-// and is not deleted.
-//
-// Parameters:
-//  pMsgHdr: A message header object
-//  pUserNum: Optional - A user number to match with.  If not specified, this will default to
-//            the current logged-in user number.
-//
-// Return value: Boolean - Whether or not the message is to the user and is not
-//               deleted.
-function msgIsToUserByNum(pMsgHdr, pUserNum)
-{
-	if (typeof(pMsgHdr) !== "object")
-		return false;
-	// Return false if the message is marked as deleted and the user can't read deleted messages
-	if (((pMsgHdr.attr & MSG_DELETE) == MSG_DELETE) && !canViewDeletedMsgs())
-		return false;
-
-	var userNum = user.number;
-	if (user.is_sysop && typeof(pUserNum) === "number" && pUserNum > 0 && pUserNum <= system.lastuser)
-		userNum = pUserNum;
-
-	var msgIsToUser = false;
-	// If an alternate user number was specified on the command line, then use that
-	// user information.  Otherwise, use the current logged-in user.
-	if (gCmdLineArgVals.hasOwnProperty("altUserNum"))
-		msgIsToUser = (pMsgHdr.to_ext == gCmdLineArgVals.altUserNum);
-	else
-		msgIsToUser = (pMsgHdr.to_ext == userNum);
-	return msgIsToUser;
-}
-
-// Returns whether or not a message header is to the current logged-in user by name, alias, or handle
-function msgIsToCurrentUserByName(pMsgHdrOrIdx)
-{
-	if (typeof(pMsgHdrOrIdx) !== "object" || !pMsgHdrOrIdx.hasOwnProperty("to"))
-		return false;
-	// Return false if the message is marked as deleted and the user can't read deleted messages
-	if (((pMsgHdrOrIdx.attr & MSG_DELETE) == MSG_DELETE) && !canViewDeletedMsgs())
-		return false;
-
-	return userHandleAliasNameMatch(pMsgHdrOrIdx.to);
-}
-
-// Checks to see if there are any unread messages to the current
-// logged-in user in a sub-board
-//
-// Parameters:
-//  pMsgbase: An opened MessageBase object for the sub-board
-//  pSubCode: The internal code of the sub-board of the opened messagebase
-//
-// Return value: Boolean - Whether or not there are any unread messages to the current logged-in user
-function anyUnreadMsgsToUserWithMsgbase(pMsgbase, pSubCode)
-{
-	if (typeof(pMsgbase) !== "object")
-		return false;
-
-	var unreadMsgsToUserFound = false;
-	if (typeof(msg_area.sub[pSubCode].scan_ptr) === "number" && !msgNumIsLatestMsgSpecialVal(msg_area.sub[pSubCode].scan_ptr))
-	{
-		var startMsgIdx = absMsgNumToIdxWithMsgbaseObj(pMsgbase, msg_area.sub[pSubCode].scan_ptr);
-		var endMsgIdx = absMsgNumToIdxWithMsgbaseObj(pMsgbase, pMsgbase.last_msg) + 1;
-		if (endMsgIdx == 0) // Not valid
-			endMsgIdx = pMsgbase.total_msgs;
-		// Check for any messages to the user between the start & end indexes
-		for (var i = startMsgIdx; i < endMsgIdx; ++i)
-		{
-			var msgHeader = pMsgbase.get_msg_index(true, i, false);
-			if (msgHeader != null && (msgHeader.attr & MSG_READ) == 0 && msgIsToCurrentUserByName(msgHeader))
-			{
-				unreadMsgsToUserFound = true;
-				break;
-			}
-		}
-	}
-	return unreadMsgsToUserFound;
-}
-// Checks to see if there are any unread messages to the current
-// logged-in user in a sub-board
-//
-// Parameters:
-//  pSuBCode: The internal code of the sub-board to check
-//
-// Return value: Boolean - Whether or not there are any unread messages to the current logged-in user
-function anyUnreadMsgsToUser(pSubCode)
-{
-	var unreadMsgsToUserFound = false;
-	var msgbase = new MsgBase(pSubCode);
-	if (msgbase.open())
-	{
-		unreadMsgsToUserFound = anyUnreadMsgsToUserWithMsgbase(msgbase, pSubCode);
-		msgbase.close();
-	}
-	return unreadMsgsToUserFound;
-}
-
-// Returns whether or not a message is from the current user (either the current
-// logged-in user or the user specified by the userNum command-line argument)
-// and is not deleted.
-//
-// Parameters:
-//  pMsgHdr: A message header object
-//  pUserNum: Optional - A user number to match with.  If not specified, this will default to
-//            the current logged-in user number.
-//
-// Return value: Boolean - Whether or not the message is from the logged-in user
-//               and is not deleted.
-function msgIsFromUser(pMsgHdr, pUserNum)
-{
-	if (typeof(pMsgHdr) != "object")
-		return false;
-	// Return false if  the message is marked as deleted and the user can't read deleted messages
-	if (Boolean(pMsgHdr.attr & MSG_DELETE) && !canViewDeletedMsgs())
-		return false;
-
-	var pUserNumIsValid = (typeof(pUserNum) === "number" && pUserNum > 0 && pUserNum <= system.lastuser);
-
-	var isFromUser = false;
-
-	// If an alternate user number was specified on the command line, then use that
-	// user information.  Otherwise, use the current logged-in user.
-
-	if (pMsgHdr.hasOwnProperty("from_ext"))
-	{
-		if (gCmdLineArgVals.hasOwnProperty("altUserNum"))
-			isFromUser = (pMsgHdr.from_ext == gCmdLineArgVals.altUserNum);
-		else
-			isFromUser = (pMsgHdr.from_ext == (pUserNumIsValid && user.is_sysop ? pUserNum : user.number));
-	}
-	else
-	{
-		var hdrFromUpper = pMsgHdr.from.toUpperCase();
-		if (gCmdLineArgVals.hasOwnProperty("altUserName") && gCmdLineArgVals.hasOwnProperty("altUserAlias"))
-			isFromUser = ((hdrFromUpper == gCmdLineArgVals.altUserAlias.toUpperCase()) || (hdrFromUpper == gCmdLineArgVals.altUserName.toUpperCase()));
-		else
-		{
-			if (pUserNumIsValid)
-			{
-				var theUser = new User(pUserNum);
-				isFromUser = ((hdrFromUpper == theUser.alias.toUpperCase()) || (hdrFromUpper == theUser.name.toUpperCase()));
-			}
-			else
-				isFromUser = ((hdrFromUpper == user.alias.toUpperCase()) || (hdrFromUpper == user.name.toUpperCase()));
-		}
-	}
-
-	return isFromUser;
-}
-
-// Returns whether a given message group index & sub-board index (or the current ones,
-// based on bbs.curgrp and bbs.cursub) are for the last message sub-board on the system.
-//
-// Parameters:
-//  pGrpIdx: Optional - The index of the message group.  If not specified, this will
-//           default to bbs.curgrp.  If bbs.curgrp is not defined in that case,
-//           then this method will return false.
-//  pSubIdx: Optional - The index of the message sub-board.  If not specified, this will
-//           default to bbs.cursub.  If bbs.cursub is not defined in that case,
-//           then this method will return false.
-//
-// Return value: Boolean - Whether or not the current/given message group index & sub-board
-//               index are for the last message sub-board on the system.  If there
-//               are any issues with any of the values (including bbs.curgrp or
-//               bbs.cursub), this method will return false.
-function curMsgSubBoardIsLast(pGrpIdx, pSubIdx)
-{
-	var curGrp = 0;
-	if (typeof(pGrpIdx) == "number")
-		curGrp = pGrpIdx;
-	else if (typeof(bbs.curgrp) == "number")
-		curGrp = bbs.curgrp;
-	else
-		return false;
-	var curSub = 0;
-	if (typeof(pSubIdx) == "number")
-		curSub = pSubIdx;
-	else if (typeof(bbs.cursub) == "number")
-		curSub = bbs.cursub;
-	else
-		return false;
-
-	return (curGrp == msg_area.grp_list.length-1) && (curSub == msg_area.grp_list[msg_area.grp_list.length-1].sub_list.length-1);
-}
-
-// With a MsgBase object, counts the number of readable messages starting
-// with (and including) a message number
-//
-// Parameters:
-//  pMsgbase: A MsgBase object (should be open)
-//  pSubCode: The internal code of the sub-board
-//  pMsgNum: A message number to start at
-//
-// Return value: The number of readable messages starting at the given message number
-function numReadableMsgsFromAbsMsgNumWithMsgbase(pMsgbase, pSubCode, pMsgNum)
-{
-	var numReadableMsgs = 0;
-	if (pMsgbase.is_open)
-	{
-		var totalNumMsgs = pMsgbase.total_msgs;
-		var msgIdx = absMsgNumToIdx(pMsgbase, pMsgNum);
-		for (; i < totalNumMsgs; ++msgIdx)
-		{
-			if (isReadableMsgHdr(pMsgbase.get_msg_index(true, msgIdx), pSubCode))
-				++numReadableMsgs;
-		}
-	}
-	return numReadableMsgs;
-}
-
-// Parses arguments, where each argument in the given array is in the format
-// -arg=val.  If the value is the string "true" or "false", then the value will
-// be a boolean.  Otherwise, the value will be a string.
-//
-// Parameters:
-//  argv: An array of strings containing values in the format -arg=val
-//
-// Return value: An object containing the argument values.  The index will be
-//               the argument names, converted to lowercase.  The values will
-//               be either the string argument values or boolean values, depending
-//               on the formats of the arguments passed in.
-function parseArgs(argv)
-{
-	var argVals = getDefaultArgParseObj();
-
-	// Sanity checking for argv - Make sure it's an array
-	if (!Array.isArray(argv))
-		return argVals;
-
-	// First, test the arguments to see if they're in a format as called by
-	// Synchronet for loadable modules
-	argVals = parseLoadableModuleArgs(argv);
-	if (argVals.loadableModule || argVals.continuousNewScan || argVals.newScanBack)
-		return argVals;
-
-	// Go through argv looking for strings in the format -arg=val and parse them
-	// into objects in the argVals array.
-	var equalsIdx = 0;
-	var argName = "";
-	var argVal = "";
-	var argValLower = ""; // For case-insensitive "true"/"false" matching
-	var argValIsTrue = false;
-	for (var i = 0; i < argv.length; ++i)
-	{
-		// We're looking for strings that start with "-", except strings that are
-		// only "-".
-		if ((typeof(argv[i]) != "string") || (argv[i].length == 0) ||
-		    (argv[i].charAt(0) != "-") || (argv[i] == "-"))
-		{
-			continue;
-		}
-
-		// Look for an = and if found, split the string on the =
-		equalsIdx = argv[i].indexOf("=");
-		// If a = is found, then split on it and add the argument name & value
-		// to the array.  Otherwise (if the = is not found), then treat the
-		// argument as a boolean and set it to true (to enable an option).
-		if (equalsIdx > -1)
-		{
-			argName = argv[i].substring(1, equalsIdx).toLowerCase();
-			argVal = argv[i].substr(equalsIdx+1);
-			argValLower = argVal.toLowerCase();
-			// If the argument value is the word "true" or "false", then add it as a
-			// boolean.  Otherwise, add it as a string.
-			argValIsTrue = (argValLower == "true");
-			if (argValIsTrue || (argValLower == "false"))
-				argVals[argName] = argValIsTrue;
-			else
-				argVals[argName] = argVal;
-		}
-		else // An equals sign (=) was not found.  Add as a boolean set to true to enable the option.
-		{
-			argName = argv[i].substr(1).toLowerCase();
-			if ((argName == "chooseareafirst") || (argName == "personalemail") ||
-			    (argName == "personalemailsent") || (argName == "allpersonalemail") ||
-			    (argName == "verboselogging") || (argName == "suppresssearchtypetext") ||
-			    (argName == "onlynewpersonalemail") || (argName == "indexedmode"))
-			{
-				argVals[argName] = true;
-			}
-		}
-	}
-
-	// Sanity checking
-	// If the arguments include personalEmail and personalEmail is enabled,
-	// then check to see if a search type was specified - If so, only allow
-	// keyword search and from name search.
-	if (argVals.hasOwnProperty("personalemail") && argVals.personalemail)
-	{
-		argVals.subboard = "mail";
-		// If a search type is specified, only allow keyword search & from name
-		// search
-		if (argVals.hasOwnProperty("search"))
-		{
-			var searchValLower = argVals.search.toLowerCase();
-			if ((searchValLower != "keyword_search") && (searchValLower != "from_name_search"))
-				delete argVals.search;
-		}
-	}
-	// If the arguments include userNum, make sure the value is all digits.  If so,
-	// add altUserNum to the arguments as a number type for user matching when looking
-	// for personal email to the user.
-	if (argVals.hasOwnProperty("usernum"))
-	{
-		if (/^[0-9]+$/.test(argVals.usernum))
-		{
-			var specifiedUserNum = Number(argVals.usernum);
-			// If the specified number is different than the current logged-in
-			// user, then load the other user account and read their name and
-			// alias and also store their user number in the arg vals as a
-			// number.
-			if (specifiedUserNum != user.number)
-			{
-				var theUser = new User(specifiedUserNum);
-				argVals.altUserNum = theUser.number;
-				argVals.altUserName = theUser.name;
-				argVals.altUserAlias = theUser.alias;
-			}
-			else
-				delete argVals.usernum;
-		}
-		else
-			delete argVals.usernum;
-	}
-
-	return argVals;
-}
-// Helper for parseArgs() - If we get loadable module arguments from Synchronet, this parses them.
-//
-// Parameters:
-//  argv: An array of strings containing values in the format -arg=val
-//
-// Return value: An object containing the argument values.  The property "loadableModule"
-//               in this object will be a boolean that specifies whether or not loadable
-//               module arguments were specified.
-function parseLoadableModuleArgs(argv)
-{
-	// TODO: Allow indexed reader mode?
-	//argVals.indexedmode = true;
-
-	var argVals = getDefaultArgParseObj();
-
-	var allDigitsRegex = /^[0-9]+$/; // To check if a string consists only of digits
-	var arg1Lower = argv[0].toLowerCase();
-	// 2 args, and the 1st arg is a sub-board code & the 2nd arg is numeric & is
-	// the value of SCAN_INDEX: List messages in the specified sub-board (List Msgs module)
-	if (argv.length == 2 && subBoardCodeIsValid(arg1Lower) && allDigitsRegex.test(argv[1]) && +(argv[1]) === SCAN_INDEX)
-	{
-		argVals.loadableModule = true;
-		argVals.subboard = arg1Lower;
-		argVals.startmode = "list";
-	}
-	// 2 parameters: Whether or not all subs are being scanned (0 or 1), and the scan mode (numeric)
-	// (Scan Subs module)
-	else if (argv.length == 2 && /^[0-1]$/.test(argv[0]) && allDigitsRegex.test(argv[1]))
-	{
-		argVals.loadableModule = true;
-		var scanAllSubs = (argv[0] == "1");
-		var scanMode = +(argv[1]);
-		//if ((scanMode & SCAN_NEW) == SCAN_NEW)
-		if (Boolean(scanMode & SCAN_NEW))
-		{
-			// Newscan
-			argVals.search = "new_msg_scan";
-			argVals.suppresssearchtypetext = true;
-			if (scanAllSubs)
-				argVals.search = "new_msg_scan_all";
-			// TODO: SCAN_CONT and SCAN_BACK could be used along with SCAN_NEW
-			// SCAN_CONT: Continuous message scanning
-			// SCAN_BACK: Display most recent message if none new
-			if (Boolean(scanMode & SCAN_CONT) || Boolean(scanMode & SCAN_BACK))
-			{
-				// Stock Synchronet functionality for continuous & back newscan
-				bbs.scan_subs(scanMode, scanAllSubs);
-				argVals.exitNow = true;
-			}
-		}
-		//else if (((scanMode & SCAN_TOYOU) == SCAN_TOYOU) || ((scanMode & SCAN_UNREAD) == SCAN_UNREAD))
-		else if (Boolean(scanMode & SCAN_TOYOU) || Boolean(scanMode & SCAN_UNREAD))
-		{
-			// Scan for messages posted to you/new messages posted to you
-			argVals.startmode = "read";
-			argVals.search = "to_user_new_scan";
-			argVals.suppresssearchtypetext = true;
-			if (scanAllSubs)
-				argVals.search = "to_user_new_scan_all";
-		}
-		else if (Boolean(scanMode & SCAN_FIND))
-		{
-			argVals.search = "keyword_search";
-			argVals.startmode = "list";
-		}
-		else
-		{
-			// Stock Synchronet functionality.  Includes SCAN_CONT and SCAN_BACK.
-			bbs.scan_subs(scanMode, scanAllSubs);
-			argVals.exitNow = true;
-		}
-	}
-	// Scan Msgs loadable module support:
-	// 1. The sub-board internal code
-	// 2. The scan mode (numeric)
-	// 3. Optional: Search text (if any)
-	else if ((argv.length == 2 || argv.length == 3) && subBoardCodeIsValid(arg1Lower) && allDigitsRegex.test(argv[1]))
-	{
-		argVals.loadableModule = true;
-		var scanMode = +(argv[1]);
-		if (scanMode == SCAN_READ)
-		{
-			argVals.subboard = arg1Lower;
-			argVals.startmode = "read";
-			// If a search string is specified (as the 3rd command-line argument),
-			// then use it for a search scan.
-			if (argv.length == 3 && argv[2] != "")
-			{
-				argVals.search = "keyword_search";
-				argVals.searchtext = argv[2];
-			}
-		}
-		else if (scanMode == SCAN_FIND)
-		{
-			argVals.subboard = arg1Lower;
-			argVals.search = "keyword_search";
-			argVals.startmode = "list";
-			if (argv.length == 3 && argv[2] != "")
-				argVals.searchtext = argv[2];
-		}
-		// Some modes that the Digital Distortion Message Reader doesn't handle yet: Use
-		// Synchronet's stock behavior.
-		else
-		{
-			if (argv.length == 3)
-				bbs.scan_msgs(arg1Lower, scanMode, argv[2]);
-			else
-				bbs.scan_msgs(arg1Lower, scanMode);
-			argVals.exitNow = true;
-		}
-	}
-	// Reading personal email: 'Which' mailbox & user number (both numeric) (Read Mail module)
-	else if ((argv.length == 2 || argv.length == 3) && allDigitsRegex.test(argv[0]) && allDigitsRegex.test(argv[1]) && isValidUserNum(+(argv[1])))
-	{
-		argVals.loadableModule = true;
-		var whichMailbox = +(argv[0]);
-		var userNum = +(argv[1]);
-		// The optional 3rd argument in this case is mode bits.  See if we should only display
-		// new (unread) personal email.
-		var newMailOnly = false;
-		if (argv.length >= 3)
-		{
-			var modeVal = +(argv[2]);
-			newMailOnly = (((modeVal & SCAN_FIND) == SCAN_FIND) && ((modeVal & LM_UNREAD) == LM_UNREAD));
-		}
-		// Start in list mode
-		argVals.startmode = "list"; // "read"
-		// Note: MAIL_ANY won't be passed to this script.
-		switch (whichMailbox)
-		{
-			case MAIL_YOUR: // Mail sent to you
-				argVals.personalemail = true;
-				argVals.usernum = +(argv[1]);
-				if (newMailOnly)
-					argVals.onlynewpersonalemail = true;
-				break;
-			case MAIL_SENT: // Mail you have sent
-				argVals.personalemailsent = true;
-				argVals.usernum = +(argv[1]);
-				break;
-			case MAIL_ALL:
-				argVals.allpersonalemail = true;
-				break;
-			default:
-				bbs.read_mail(whichMailbox);
-				argVals.exitNow = true;
-				break;
-		}
-	}
-	return argVals;
-}
-// Returns an object with default settings for argument parsing
-function getDefaultArgParseObj()
-{
-	return {
-		chooseareafirst: false,
-		personalemail: false,
-		onlynewpersonalemail: false,
-		personalemailsent: false,
-		verboselogging: false,
-		suppresssearchtypetext: false,
-		indexedmode: false,
-		loadableModule: false,
-		exitNow: false
-	};
-}
-
-// Returns a string describing all message attributes (main, auxiliary, and net).
-//
-// Parameters:
-//  pMsgHdr: A message header object.  
-//
-// Return value: A string describing all of the message attributes
-function makeAllMsgAttrStr(pMsgHdr)
-{
-	if ((pMsgHdr == null) || (typeof(pMsgHdr) != "object"))
-		return "";
-
-	var msgAttrStr = makeMainMsgAttrStr(pMsgHdr.attr);
-	var auxAttrStr = makeAuxMsgAttrStr(pMsgHdr.auxattr);
-	if (auxAttrStr.length > 0)
-	{
-		if (msgAttrStr.length > 0)
-			msgAttrStr += ", ";
-		msgAttrStr += auxAttrStr;
-	}
-	var netAttrStr = makeNetMsgAttrStr(pMsgHdr.netattr);
-	if (netAttrStr.length > 0)
-	{
-		if (msgAttrStr.length > 0)
-			msgAttrStr += ", ";
-		msgAttrStr += netAttrStr;
-	}
-	return msgAttrStr;
-}
-
-// Returns a string describing the main message attributes.
-//
-// Parameters:
-//  pMainMsgAttrs: The bit field for the main message attributes
-//                 (normally, the 'attr' property of a header object)
-//  pIfEmptyString: Optional - A string to use if there are no attributes set
-//
-// Return value: A string describing the main message attributes
-function makeMainMsgAttrStr(pMainMsgAttrs, pIfEmptyString)
-{
-	if (makeMainMsgAttrStr.attrStrs === undefined)
-	{
-		makeMainMsgAttrStr.attrStrs = [
-			{ attr: MSG_DELETE, str: "Del" },
-			{ attr: MSG_PRIVATE, str: "Priv" },
-			{ attr: MSG_READ, str: "Read" },
-			{ attr: MSG_PERMANENT, str: "Perm" },
-			{ attr: MSG_LOCKED, str: "Lock" },
-			{ attr: MSG_ANONYMOUS, str: "Anon" },
-			{ attr: MSG_KILLREAD, str: "Killread" },
-			{ attr: MSG_MODERATED, str: "Mod" },
-			{ attr: MSG_VALIDATED, str: "Valid" },
-			{ attr: MSG_REPLIED, str: "Repl" },
-			{ attr: MSG_NOREPLY, str: "NoRepl" }
-		];
-	}
-
-	var msgAttrStr = "";
-	if (typeof(pMainMsgAttrs) == "number")
-	{
-		for (var i = 0; i < makeMainMsgAttrStr.attrStrs.length; ++i)
-		{
-			if (Boolean(pMainMsgAttrs & makeMainMsgAttrStr.attrStrs[i].attr))
-			{
-				if (msgAttrStr.length > 0)
-					msgAttrStr += ", ";
-				msgAttrStr += makeMainMsgAttrStr.attrStrs[i].str;
-			}
-		}
-	}
-	if ((msgAttrStr.length == 0) && (typeof(pIfEmptyString) == "string"))
-		msgAttrStr = pIfEmptyString;
-	return msgAttrStr;
-}
-
-// Returns a string describing auxiliary message attributes.
-//
-// Parameters:
-//  pAuxMsgAttrs: The bit field for the auxiliary message attributes
-//                (normally, the 'auxattr' property of a header object)
-//  pIfEmptyString: Optional - A string to use if there are no attributes set
-//
-// Return value: A string describing the auxiliary message attributes
-function makeAuxMsgAttrStr(pAuxMsgAttrs, pIfEmptyString)
-{
-	if (makeAuxMsgAttrStr.attrStrs === undefined)
-	{
-		makeAuxMsgAttrStr.attrStrs = [
-			{ attr: MSG_FILEREQUEST, str: "Freq" },
-			{ attr: MSG_FILEATTACH, str: "Attach" },
-			{ attr: MSG_KILLFILE, str: "KillFile" },
-			{ attr: MSG_RECEIPTREQ, str: "RctReq" },
-			{ attr: MSG_CONFIRMREQ, str: "ConfReq" },
-			{ attr: MSG_NODISP, str: "NoDisp" }
-		];
-		if (typeof(MSG_TRUNCFILE) === "number")
-			makeAuxMsgAttrStr.attrStrs.push({ attr: MSG_TRUNCFILE, str: "TruncFile" });
-	}
-
-	var msgAttrStr = "";
-	if (typeof(pAuxMsgAttrs) == "number")
-	{
-		for (var i = 0; i < makeAuxMsgAttrStr.attrStrs.length; ++i)
-		{
-			if (Boolean(pAuxMsgAttrs & makeAuxMsgAttrStr.attrStrs[i].attr))
-			{
-				if (msgAttrStr.length > 0)
-					msgAttrStr += ", ";
-				msgAttrStr += makeAuxMsgAttrStr.attrStrs[i].str;
-			}
-		}
-	}
-	if ((msgAttrStr.length == 0) && (typeof(pIfEmptyString) == "string"))
-		msgAttrStr = pIfEmptyString;
-	return msgAttrStr;
-}
-
-// Returns a string describing network message attributes.
-//
-// Parameters:
-//  pNetMsgAttrs: The bit field for the network message attributes
-//                (normally, the 'netattr' property of a header object)
-//  pIfEmptyString: Optional - A string to use if there are no attributes set
-//
-// Return value: A string describing the network message attributes
-function makeNetMsgAttrStr(pNetMsgAttrs, pIfEmptyString)
-{
-	if (makeNetMsgAttrStr.attrStrs === undefined)
-	{
-		makeNetMsgAttrStr.attrStrs = [
-			{ attr: MSG_LOCAL, str: "FromLocal" },
-			{ attr: MSG_INTRANSIT, str: "Transit" },
-			{ attr: MSG_SENT, str: "Sent" },
-			{ attr: MSG_KILLSENT, str: "KillSent" },
-			{ attr: MSG_ARCHIVESENT, str: "ArcSent" },
-			{ attr: MSG_HOLD, str: "Hold" },
-			{ attr: MSG_CRASH, str: "Crash" },
-			{ attr: MSG_IMMEDIATE, str: "Now" },
-			{ attr: MSG_DIRECT, str: "Direct" }
-		];
-		if (typeof(MSG_GATE) === "number")
-			makeNetMsgAttrStr.attrStrs.push({ attr: MSG_GATE, str: "Gate" });
-		if (typeof(MSG_ORPHAN) === "number")
-			makeNetMsgAttrStr.attrStrs.push({ attr: MSG_ORPHAN, str: "Orphan" });
-		if (typeof(MSG_FPU) === "number")
-			makeNetMsgAttrStr.attrStrs.push({ attr: MSG_FPU, str: "FPU" });
-		if (typeof(MSG_TYPELOCAL) === "number")
-			makeNetMsgAttrStr.attrStrs.push({ attr: MSG_TYPELOCAL, str: "ForLocal" });
-		if (typeof(MSG_TYPEECHO) === "number")
-			makeNetMsgAttrStr.attrStrs.push({ attr: MSG_TYPEECHO, str: "ForEcho" });
-		if (typeof(MSG_TYPENET) === "number")
-			makeNetMsgAttrStr.attrStrs.push({ attr: MSG_TYPENET, str: "ForNetmail" });
-		if (typeof(MSG_MIMEATTACH) === "number")
-			makeNetMsgAttrStr.attrStrs.push({ attr: MSG_MIMEATTACH, str: "MimeAttach" });
-	}
-
-	var msgAttrStr = "";
-	if (typeof(pNetMsgAttrs) == "number")
-	{
-		for (var i = 0; i < makeNetMsgAttrStr.attrStrs.length; ++i)
-		{
-			if (Boolean(pNetMsgAttrs & makeNetMsgAttrStr.attrStrs[i].attr))
-			{
-				if (msgAttrStr.length > 0)
-					msgAttrStr += ", ";
-				msgAttrStr += makeNetMsgAttrStr.attrStrs[i].str;
-			}
-		}
-	}
-	if ((msgAttrStr.length == 0) && (typeof(pIfEmptyString) == "string"))
-		msgAttrStr = pIfEmptyString;
-	return msgAttrStr;
-}
-
-// Given a sub-board code, this function returns a sub-board's group and name.
-// If the given sub-board code is "mail", then this will return "Personal mail".
-//
-// Parameters:
-//  pSubBoardCode: An internal sub-board code
-//
-// Return value: A string containing the sub-board code group & name, or
-//               "Personal email" if it's the personal email sub-board
-function subBoardGrpAndName(pSubBoardCode)
-{
-	if (typeof(pSubBoardCode) != "string")
-		return "";
-
-	var subBoardGrpAndName = "";
-	if (pSubBoardCode == "mail")
-		subBoardGrpAndName = "Personal mail";
-	else
-	{
-		subBoardGrpAndName = msg_area.sub[pSubBoardCode].grp_name + " - "
-                         + msg_area.sub[pSubBoardCode].name;
-	}
-
-	return subBoardGrpAndName;
-}
-
-// Writes a log message to the system log (using LOG_INFO log level) and to the
-// node log.  This will prepend the text "Digital Distortion Message Reader ("
-// + user.alias + "): " to the log message.
-// 
-// Parameters:
-//  pMessage: The message to log
-//  pLogLevel: The log level.  Optional - Defaults to LOG_INFO.
-function writeToSysAndNodeLog(pMessage, pLogLevel)
-{
-	if (typeof(pMessage) != "string")
-		return;
-
-	var logMessage = "Digital Distortion Message Reader (" +  user.alias + "): " + pMessage;
-	var logLevel = (typeof(pLogLevel) == "number" ? pLogLevel : LOG_INFO);
-	log(logLevel, logMessage);
-	bbs.log_str(logMessage);
-}
-
-// This function looks up and returns a sub-board code from the sub-board number.
-// If no matching sub-board is found, this will return an empty string.
-//
-// Parameters:
-//  pSubBoardNum: A sub-board number
-//
-// Return value: The sub-board code.  If no matching sub-board is found, an empty
-//               string will be returned.
-function getSubBoardCodeFromNum(pSubBoardNum)
-{
-	// Ensure we're using a numeric type for the sub-board number
-	// (in case pSubBoardNum is a string rather than a number)
-	var subNum = Number(pSubBoardNum);
-
-	var subBoardCode = "";
-	for (var subCode in msg_area.sub)
-	{
-		if (msg_area.sub[subCode].number == subNum)
-		{
-			subBoardCode = subCode;
-			break;
-		}
-	}
-	return subBoardCode;
-}
-
-// This function recursively removes a directory and all of its contents.  Returns
-// whether or not the directory was removed.
-//
-// Parameters:
-//  pDir: The directory to remove (with trailing slash).
-//
-// Return value: Boolean - Whether or not the directory was removed.
-function deltree(pDir)
-{
-	if ((pDir == null) || (pDir == undefined))
-		return false;
-	if (typeof(pDir) != "string")
-		return false;
-	if (pDir.length == 0)
-		return false;
-	// Make sure pDir actually specifies a directory.
-	if (!file_isdir(pDir))
-		return false;
-	// Don't wipe out a root directory.
-	if ((pDir == "/") || (pDir == "\\") || (/:\\$/.test(pDir)) || (/:\/$/.test(pDir)) || (/:$/.test(pDir)))
-		return false;
-
-	// If we're on Windows, then use the "RD /S /Q" command to delete
-	// the directory.  Otherwise, assume *nix and use "rm -rf" to
-	// delete the directory.
-	if (deltree.inWindows == undefined)
-		deltree.inWindows = (/^WIN/.test(system.platform.toUpperCase()));
-	if (deltree.inWindows)
-		system.exec("RD " + withoutTrailingSlash(pDir) + " /s /q");
-	else
-		system.exec("rm -rf " + withoutTrailingSlash(pDir));
-	// The directory should be gone, so we should return true.  I'd like to verify that the
-	// directory really is gone, but file_exists() seems to return false for directories,
-	// even if the directory does exist.  So I test to make sure no files are seen in the dir.
-	return (directory(pDir + "*").length == 0);
-
-	/*
-	// Recursively deleting each file & dir using JavaScript:
-	var retval = true;
-
-	// Open the directory and delete each entry.
-	var files = directory(pDir + "*");
-	for (var i = 0; i < files.length; ++i)
-	{
-		// If the entry is a directory, then deltree it (Note: The entry
-		// should have a trailing slash).  Otherwise, delete the file.
-		// If the directory/file couldn't be removed, then break out
-		// of the loop.
-		if (file_isdir(files[i]))
-		{
-			retval = deltree(files[i]);
-			if (!retval)
-				break;
-		}
-		else
-		{
-			retval = file_remove(files[i]);
-			if (!retval)
-				break;
-		}
-	}
-
-	// Delete the directory specified by pDir.
-	if (retval)
-		retval = rmdir(pDir);
-
-	return retval;
-*/
-}
-
-// Removes a trailing (back)slash from a path.
-//
-// Parameters:
-//  pPath: A directory path
-//
-// Return value: The path without a trailing (back)slash.
-function withoutTrailingSlash(pPath)
-{
-	if ((pPath == null) || (pPath == undefined))
-		return "";
-
-	var retval = pPath;
-	if (retval.length > 0)
-	{
-		var lastIndex = retval.length - 1;
-		var lastChar = retval.charAt(lastIndex);
-		if ((lastChar == "\\") || (lastChar == "/"))
-			retval = retval.substr(0, lastIndex);
-	}
-	return retval;
-}
-
-// Adds double-quotes around a string if the string contains spaces.
-//
-// Parameters:
-//  pStr: A string to add double-quotes around if it has spaces
-//
-// Return value: The string with double-quotes if it contains spaces.  If the
-//               string doesn't contain spaces, then the same string will be
-//               returned.
-function quoteStrWithSpaces(pStr)
-{
-	if (typeof(pStr) != "string")
-		return "";
-	var strCopy = pStr;
-	if (pStr.indexOf(" ") > -1)
-		strCopy = "\"" + pStr + "\"";
-	return strCopy;
-}
-
-// Given a message header field list type number (i.e., the 'type' property for an
-// entry in the field_list array in a message header), this returns a text label
-// to be used for outputting the field.
-//
-// Parameters:
-//  pFieldListType: A field_list entry type (numeric)
-//  pIncludeTrailingColon: Optional boolean - Whether or not to include a trailing ":"
-//                         at the end of the returned string.  Defaults to true.
-//
-// Return value: A text label for the field (a string)
-function msgHdrFieldListTypeToLabel(pFieldListType, pIncludeTrailingColon)
-{
-	var fieldTypeLabel = "";
-	switch (pFieldListType)
-	{
-		case SMB_COMMENT:
-			fieldTypeLabel = "Comment";
-			break
-		case SMB_POLL_ANSWER:
-			fieldTypeLabel = "Poll answer";
-			break;
-		case 0x64: // SMB_GROUP
-			fieldTypeLabel = "Group";
-			break;			
-		case FIDOCTRL:
-			fieldTypeLabel = "FIDO control";
-			break;
-		case FIDOSEENBY:
-			fieldTypeLabel = "Seen-by";
-			break;
-		case FIDOPATH:
-			fieldTypeLabel = "FIDO Path";
-			break;
-		case RFC822HEADER:
-			fieldTypeLabel = "RFCC822 Header";
-			break;
-		case 0xB3: // RFC822TO
-			fieldTypeLabel = "RFC822 To";
-			break;
-		case 0xB6: // RFC822CC
-			fieldTypeLabel = "RFC822 CC";
-			break;
-		case 0xB7: // RFC822ORG
-			fieldTypeLabel = "RFC822 Org";
-			break;
-		case 0xB4: // RFC822FROM
-			fieldTypeLabel = "RFC822 From";
-			break;
-		case 0xB5: // RFC822REPLYTO
-			fieldTypeLabel = "RFC822 Reply To";
-			break;
-		case 0xB8: // RFC822SUBJECT
-			fieldTypeLabel = "RFC822 Subject";
-			break;
-		case SMTPRECEIVED:
-			fieldTypeLabel = "SMTP Received";
-			break;
-		case 0xF1: // UNKNOWNASCII
-			fieldTypeLabel = "UNKNOWN (ASCII)";
-			break;
-		default:
-			fieldTypeLabel = "Unknown (" + pFieldListType.toString() + ")";
-			break;
-	}
-
-	var includeTrailingColon = (typeof(pIncludeTrailingColon) == "boolean" ? pIncludeTrailingColon : true);
-	if (includeTrailingColon)
-		fieldTypeLabel += ":";
-
-	return fieldTypeLabel;
-}
-
-// Capitalizes the first character of a string.
-//
-// Parameters:
-//  pStr: The string to capitalize
-//
-// Return value: A version of the sting with the first character capitalized
-function capitalizeFirstChar(pStr)
-{
-	var retStr = "";
-	if (typeof(pStr) == "string")
-	{
-		if (pStr.length > 0)
-			retStr = pStr.charAt(0).toUpperCase() + pStr.slice(1);
-	}
-	return retStr;
-}
-
-// Parses a list of numbers (separated by commas or spaces), which may contain
-// ranges separated by dashes.  Returns an array of the individual numbers.
-//
-// Parameters:
-//  pList: A comma-separated list of numbers, some which may contain
-//         2 numbers separated by a dash denoting a range of numbers.
-//
-// Return value: An array of the individual numbers from the list
-function parseNumberList(pList)
-{
-	if (typeof(pList) != "string")
-		return [];
-
-	var numberList = [];
-
-	// Split pList on commas or spaces
-	var commaOrSpaceSepArray = pList.split(/[\s,]+/);
-	if (commaOrSpaceSepArray.length > 0)
-	{
-		// Go through the comma-separated array - If the element is a
-		// single number, then append it to the number list to be returned.
-		// If there is a range (2 numbers separated by a dash), then
-		// append each number in the range individually to the array to be
-		// returned.
-		for (var i = 0; i < commaOrSpaceSepArray.length; ++i)
-		{
-			// If it's a single number, append it to numberList.
-			if (/^[0-9]+$/.test(commaOrSpaceSepArray[i]))
-				numberList.push(+commaOrSpaceSepArray[i]);
-			// If there are 2 numbers separated by a dash, then split it on the
-			// dash and generate the intermediate numbers.
-			else if (/^[0-9]+-[0-9]+$/.test(commaOrSpaceSepArray[i]))
-			{
-				var twoNumbers = commaOrSpaceSepArray[i].split("-");
-				if (twoNumbers.length == 2)
-				{
-					var num1 = +twoNumbers[0];
-					var num2 = +twoNumbers[1];
-					// If the 1st number is bigger than the 2nd, then swap them.
-					if (num1 > num2)
-					{
-						var temp = num1;
-						num1 = num2;
-						num2 = temp;
-					}
-					// Append each individual number in the range to numberList.
-					for (var number = num1; number <= num2; ++number)
-						numberList.push(number);
-				}
-			}
-		}
-	}
-
-	return numberList;
-}
-
-// Inputs a single keypress from the user from a list of valid keys, allowing
-// input modes (see K_* in sbbsdefs.js for mode bits).  This is similar to
-// console.getkeys(), except that this allows mode bits (such as K_NOCRLF, etc.).
-//
-// Parameters:
-//  pAllowedKeys: A list of allowed keys (string)
-//  pMode: Mode bits (see K_* in sbbsdefs.js)
-//
-// Return value: The user's inputted keypress
-function getAllowedKeyWithMode(pAllowedKeys, pMode)
-{
-	var userInput = "";
-
-	var keypress = "";
-	var i = 0;
-	var matchedKeypress = false;
-	while (!matchedKeypress)
-	{
-		keypress = console.getkey(K_NOECHO|pMode);
-		// Check to see if the keypress is one of the allowed keys
-		for (i = 0; i < pAllowedKeys.length; ++i)
-		{
-			if (keypress == pAllowedKeys[i])
-				userInput = keypress;
-			else if (keypress.toUpperCase() == pAllowedKeys[i])
-				userInput = keypress.toUpperCase();
-			else if (keypress.toLowerCase() == pAllowedKeys[i])
-				userInput = keypress.toLowerCase();
-			if (userInput.length > 0)
-			{
-				matchedKeypress = true;
-				// If K_NOECHO is not in pMode, then output the user's keypress
-				if ((pMode & K_NOECHO) == 0)
-					console.print(userInput);
-				// If K_NOCRLF is not in pMode, then output a CRLF
-				if ((pMode & K_NOCRLF) == 0)
-					console.crlf();
-				break;
-			}
-		}
-	}
-
-	return userInput;
-}
-
-// Loads a text file (an .ans or .asc) into an array.  This will first look for
-// an .ans version, and if exists, convert to Synchronet colors before loading
-// it.  If an .ans doesn't exist, this will look for an .asc version.
-//
-// Parameters:
-//  pFilenameBase: The filename without the extension
-//  pMaxNumLines: Optional - The maximum number of lines to load from the text file
-//
-// Return value: An array containing the lines from the text file
-function loadTextFileIntoArray(pFilenameBase, pMaxNumLines)
-{
-	if (typeof(pFilenameBase) != "string")
-		return [];
-
-	var maxNumLines = (typeof(pMaxNumLines) == "number" ? pMaxNumLines : -1);
-
-	var txtFileLines = [];
-	// See if there is a header file that is made for the user's terminal
-	// width (areaChgHeader-<width>.ans/asc).  If not, then just go with
-	// msgHeader.ans/asc.
-	var txtFileExists = true;
-	var txtFilenameFullPath = gStartupPath + pFilenameBase;
-	var txtFileFilename = "";
-	if (file_exists(txtFilenameFullPath + "-" + console.screen_columns + ".ans"))
-		txtFileFilename = txtFilenameFullPath + "-" + console.screen_columns + ".ans";
-	else if (file_exists(txtFilenameFullPath + "-" + console.screen_columns + ".asc"))
-		txtFileFilename = txtFilenameFullPath + "-" + console.screen_columns + ".asc";
-	else if (file_exists(txtFilenameFullPath + ".ans"))
-		txtFileFilename = txtFilenameFullPath + ".ans";
-	else if (file_exists(txtFilenameFullPath + ".asc"))
-		txtFileFilename = txtFilenameFullPath + ".asc";
-	else
-		txtFileExists = false;
-	if (txtFileExists)
-	{
-		var syncConvertedHdrFilename = txtFileFilename;
-		// If the user's console doesn't support ANSI and the header file is ANSI,
-		// then convert it to Synchronet attribute codes and read that file instead.
-		if (!console.term_supports(USER_ANSI) && (getStrAfterPeriod(txtFileFilename).toUpperCase() == "ANS"))
-		{
-			syncConvertedHdrFilename = txtFilenameFullPath + "_converted.asc";
-			if (!file_exists(syncConvertedHdrFilename))
-			{
-				if (getStrAfterPeriod(txtFileFilename).toUpperCase() == "ANS")
-				{
-					var dotIdx = txtFileFilename.lastIndexOf(".");
-					if (dotIdx >= 0)
-					{
-						var filenameBase = txtFileFilename.substr(0, dotIdx);
-						var cmdLine = system.exec_dir + "ans2asc \"" + txtFileFilename + "\" \""
-									+ syncConvertedHdrFilename + "\"";
-						// Note: Both system.exec(cmdLine) and
-						// bbs.exec(cmdLine, EX_NATIVE, gStartupPath) could be used to
-						// execute the command, but system.exec() seems noticeably faster.
-						system.exec(cmdLine);
-					}
-				}
-				else
-					syncConvertedHdrFilename = txtFileFilename;
-			}
-		}
-		/*
-		// If the header file is ANSI, then convert it to Synchronet attribute
-		// codes and read that file instead.  This is done so that this script can
-		// accurately get the file line lengths using console.strlen().
-		var syncConvertedHdrFilename = txtFilenameFullPath + "_converted.asc";
-		if (!file_exists(syncConvertedHdrFilename))
-		{
-			if (getStrAfterPeriod(txtFileFilename).toUpperCase() == "ANS")
-			{
-				var filenameBase = txtFileFilename.substr(0, dotIdx);
-				var cmdLine = system.exec_dir + "ans2asc \"" + txtFileFilename + "\" \""
-				            + syncConvertedHdrFilename + "\"";
-				// Note: Both system.exec(cmdLine) and
-				// bbs.exec(cmdLine, EX_NATIVE, gStartupPath) could be used to
-				// execute the command, but system.exec() seems noticeably faster.
-				system.exec(cmdLine);
-			}
-			else
-				syncConvertedHdrFilename = txtFileFilename;
-		}
-		*/
-		// Read the header file into txtFileLines
-		var hdrFile = new File(syncConvertedHdrFilename);
-		if (hdrFile.open("r"))
-		{
-			var fileLine = null;
-			while (!hdrFile.eof)
-			{
-				// Read the next line from the header file.
-				fileLine = hdrFile.readln(2048);
-				// fileLine should be a string, but I've seen some cases
-				// where it isn't, so check its type.
-				if (typeof(fileLine) != "string")
-					continue;
-
-				// Make sure the line isn't longer than the user's terminal
-				//if (fileLine.length > console.screen_columns)
-				//   fileLine = fileLine.substr(0, console.screen_columns);
-				txtFileLines.push(fileLine);
-
-				// If the header array now has the maximum number of lines, then
-				// stop reading the header file.
-				if (txtFileLines.length == maxNumLines)
-					break;
-			}
-			hdrFile.close();
-		}
-	}
-	return txtFileLines;
-}
-
-// Returns the portion (if any) of a string after the period.
-//
-// Parameters:
-//  pStr: The string to extract from
-//
-// Return value: The portion of the string after the dot, if there is one.  If
-//               not, then an empty string will be returned.
-function getStrAfterPeriod(pStr)
-{
-	var strAfterPeriod = "";
-	var dotIdx = pStr.lastIndexOf(".");
-	if (dotIdx > -1)
-		strAfterPeriod = pStr.substr(dotIdx+1);
-	return strAfterPeriod;
-}
-
-// Adjusts a message's when-written time to the BBS's local time.
-//
-// Parameters:
-//  pMsgHdr: A message header object
-//
-// Return value: The message's when_written_time adjusted to the BBS's local time.
-//               If the message header doesn't have a when_written_time or
-//               when_written_zone property, then this function will return -1.
-function msgWrittenTimeToLocalBBSTime(pMsgHdr)
-{
-	if (!pMsgHdr.hasOwnProperty("when_written_time") || !pMsgHdr.hasOwnProperty("when_written_zone_offset") || !pMsgHdr.hasOwnProperty("when_imported_zone_offset"))
-		return -1;
-
-	var timeZoneDiffMinutes = pMsgHdr.when_imported_zone_offset - pMsgHdr.when_written_zone_offset;
-	//var timeZoneDiffMinutes = pMsgHdr.when_written_zone - system.timezone;
-	var timeZoneDiffSeconds = timeZoneDiffMinutes * 60;
-	var msgWrittenTimeAdjusted = pMsgHdr.when_written_time + timeZoneDiffSeconds;
-	return msgWrittenTimeAdjusted;
-}
-
-// Returns a string containing the message group & sub-board numbers and
-// descriptions.
-//
-// Parameters:
-//  pMsgbase: A MsgBase object
-//
-// Return value: A string containing the message group & sub-board numbers and
-// descriptions
-function getMsgAreaDescStr(pMsgbase)
-{
-	if (typeof(pMsgbase) != "object")
-		return "";
-	if (!pMsgbase.is_open)
-		return "";
-
-	var descStr = "";
-	if (pMsgbase.cfg != null)
-	{
-		descStr = format("Group/sub-board num: %d, %d; %s - %s", pMsgbase.cfg.grp_number,
-		                 pMsgbase.subnum, msg_area.grp_list[pMsgbase.cfg.grp_number].description,
-		                 pMsgbase.cfg.description);
-	}
-	else
-	{
-		if ((pMsgbase.subnum == -1) || (pMsgbase.subnum == 65535))
-			descStr = "Electronic Mail";
-		else
-			descStr = "Unspecified";
-	}
-	return descStr;
-}
-
-// Lets the sysop edit a user.
-//
-// Parameters:
-//  pUsername: The name of the user to edit
-//
-// Return value: A function containing the following properties:
-//               errorMsg: An error message on failure, or a blank string on success
-function editUser(pUsername)
-{
-	var retObj = {
-		errorMsg: ""
-	};
-
-	if (typeof(pUsername) != "string")
-	{
-		retObj.errorMsg = "Given username is not a string";
-		return retObj;
-	}
-
-	// If the logged-in user is not a sysop, then just return.
-	if (!user.is_sysop)
-	{
-		retObj.errorMsg = "Only a sysop can edit a user";
-		return retObj;
-	}
-
-	// If the user exists, then let the sysop edit the user.
-	var userNum = system.matchuser(pUsername);
-	if (userNum != 0)
-		bbs.exec("*str_cmds uedit " + userNum);
-	else
-		retObj.errorMsg = "User \"" + pUsername + "\" not found";
-	
-	return retObj;
-}
-
-// Returns an object containing bare minimum properties necessary to
-// display an invalid message header.  Additionally, an object returned
-// by this function will have an extra property, isBogus, that will be
-// a boolean set to true.
-//
-// Parameters:
-//  pSubject: Optional - A string to use as the subject in the bogus message
-//            header object
-function getBogusMsgHdr(pSubject)
-{
-	var msgHdr = {
-		subject: (typeof(pSubject) == "string" ? pSubject : ""),
-		when_imported_time: 0,
-		when_written_time: 0,
-		when_written_zone: 0,
-		date: "Fri, 1 Jan 1960 00:00:00 -0000",
-		attr: 0,
-		to: "Nobody",
-		from: "Nobody",
-		number: 0,
-		offset: 0,
-		isBogus: true
-	};
-	return msgHdr;
-}
-
-// Returns whether a message is readable to the user, based on its
-// header and the sub-board code. This also checks pMsgHdrOrIdx for
-// null; if it's null, this function will return false.
-//
-// Parameters:
-//  pMsgHdrOrIdx: The header or index object for the message
-//  pSubBoardCode: The internal code for the sub-board the message is in
-//
-// Return value: Boolean - Whether or not the message is readable for the user
-function isReadableMsgHdr(pMsgHdrOrIdx, pSubBoardCode)
-{
-	if (pMsgHdrOrIdx === null)
-		return false;
-	// Let the sysop see unvalidated messages and private messages but not other users.
-	if (!user.is_sysop)
-	{
-		if (pSubBoardCode != "mail")
-		{
-			if ((msg_area.sub[pSubBoardCode].is_moderated && ((pMsgHdrOrIdx.attr & MSG_VALIDATED) == 0)) ||
-			    (((pMsgHdrOrIdx.attr & MSG_PRIVATE) == MSG_PRIVATE) && !userHandleAliasNameMatch(pMsgHdrOrIdx.to)))
-			{
-				return false;
-			}
-		}
-	}
-	// If the message is deleted, determine whether it should be viewable, based
-	// on the system settings.
-	if (((pMsgHdrOrIdx.attr & MSG_DELETE) == MSG_DELETE) && !canViewDeletedMsgs())
-		return false;
-	// The message voting and poll variables were added in sbbsdefs.js for
-	// Synchronet 3.17.  Make sure they're defined before referencing them.
-	if (typeof(MSG_UPVOTE) != "undefined")
-	{
-		if ((pMsgHdrOrIdx.attr & MSG_UPVOTE) == MSG_UPVOTE)
-			return false;
-	}
-	if (typeof(MSG_DOWNVOTE) != "undefined")
-	{
-		if ((pMsgHdrOrIdx.attr & MSG_DOWNVOTE) == MSG_DOWNVOTE)
-			return false;
-	}
-	// Don't include polls as being unreadable messages - They just need to have
-	// their answer selections read from the header instead of the message body
-	/*
-	if (typeof(MSG_POLL) != "undefined")
-	{
-		if ((pMsgHdrOrIdx.attr & MSG_POLL) == MSG_POLL)
-			return false;
-	}
-	*/
-	return true;
-}
-
-// Returns the header of the last readable message in a messagbase.  If none is found,
-// this will return null.
-//
-// Parameters:
-//  pMsgbase: An open MessageBase object
-//  pSubBoardCode: The internal code of the messagebase sub-board
-//
-// Return value: The header of the last readable message in the messagebase. If none is
-//               found, this will be null.
-function getLastReadableMsgHdrInMsgbase(pMsgbase, pSubBoardCode)
-{
-	var hdrOfLastReadableMsg = null;
-	if (pMsgbase.is_open)
-	{
-		var numMsgs = pMsgbase.total_msgs;
-		for (var i = numMsgs - 1; i >= 0; --i)
-		{
-			if (isReadableMsgHdr(pMsgbase.get_msg_index(true, i, false), pSubBoardCode))
-			{
-				hdrOfLastReadableMsg = pMsgbase.get_msg_header(true, i, false, false);
-				break;
-			}
-		}
-	}
-	return hdrOfLastReadableMsg;
-}
-// Returns the header of the last readable message in a messagbase (temporarily opens
-// the sub-board).  If none is found, this will return null.
-//
-// Parameters:
-//  pSubBoardCode: The internal code of the messagebase sub-board
-//
-// Return value: The header of the last readable message in the messagebase. If none is
-//               found, this will be null.
-function getLastReadableMsgHdrInSubBoard(pSubBoardCode)
-{
-	var msgHdr = null;
-	var msgbase = new MsgBase(pSubBoardCode);
-	if (msgbase.open())
-	{
-		msgHdr = getLastReadableMsgHdrInMsgbase(msgbase, pSubBoardCode);
-		msgbase.close();
-	}
-	return msgHdr;
-}
-
-// Returns the number of readable messages in a sub-board.
-//
-// Parameters:
-//  pMsgbase: The MsgBase object representing the sub-board
-//  pSubBoardCode: The internal code of the sub-board
-//
-// Return value: The number of readable messages in the sub-board
-function numReadableMsgs(pMsgbase, pSubBoardCode)
-{
-	// The posts property in msg_area.sub[sub_code] and msg_area.grp_list.sub_list is the number
-	// of posts excluding vote posts
-	if (typeof(msg_area.sub[pSubBoardCode].posts) === "number")
-		return msg_area.sub[pSubBoardCode].posts;
-	else if ((pMsgbase !== null) && pMsgbase.is_open)
-	{
-		// Just return the total number of messages..  This isn't accurate, but it's fast.
-		return pMsgbase.total_msgs;
-	}
-	else if (pMsgbase === null)
-	{
-		var numMsgs = 0;
-		var msgBase = new MsgBase(pSubBoardCode);
-		if (msgBase.open())
-		{
-			// Just return the total number of messages..  This isn't accurate, but it's fast.
-			numMsgs = msgBase.total_msgs;
-			msgBase.close();
-		}
-		return numMsgs;
-	}
-	else
-		return 0;
-}
-
-// Marks or unmarks vote messages as deleted (messages that have voting response data for a message with
-// a given message number).
-//
-// Parameters:
-//  pMsgbase: A MessageBase object containing the messages to be deleted
-//  pMsgNum: The number of the message for which vote messages should be deleted
-//  pMsgID: The ID of the message for which vote messages should be deleted
-//  pDoDelete: Boolean: If true, then mark for deletion.  If false, then remove the deleted attribute.
-//  pIsMailSub: Boolean - Whether or not it's the personal email area
-//
-// Return value: An object containing the following properties:
-//               numVoteMsgs: The number of vote messages for the given message number
-//               toggleVoteMsgsAffected: The number of vote messages that were deleted/undeleted
-//               allVoteMsgsAffected: Boolean - Whether or not all vote messages were deleted/undeleted
-function toggleVoteMsgsDeleted(pMsgbase, pMsgNum, pMsgID, pDoDelete, pIsEmailSub)
-{
-	var retObj = {
-		numVoteMsgs: 0,
-		toggleVoteMsgsAffected: 0,
-		allVoteMsgsAffected: true
-	};
-
-	if ((pMsgbase === null) || !pMsgbase.is_open)
-		return retObj;
-	if (typeof(pMsgNum) != "number")
-		return retObj;
-	if (pIsEmailSub)
-		return retObj;
-
-	// This relies on get_all_msg_headers() returning vote messages.
-	var msgHdrs = pMsgbase.get_all_msg_headers(true);
-	for (var msgHdrsProp in msgHdrs)
-	{
-		if (msgHdrs[msgHdrsProp] == null)
-			continue;
-		// If this header is a vote header and its thread_back or reply_id matches the given message,
-		// then we can delete this message.
-		if (isVoteHdr(msgHdrs[msgHdrsProp]) && (msgHdrs[msgHdrsProp].thread_back == pMsgNum) || (msgHdrs[msgHdrsProp].reply_id == pMsgID))
-		{
-			++retObj.numVoteMsgs;
-			var msgWasAffected = false;
-			if (pDoDelete)
-				msgWasAffected = pMsgbase.remove_msg(false, msgHdrs[msgHdrsProp].number);
-			else
-			{
-				var tmpMsgHdr = pMsgbase.get_msg_header(false, msgHdrs[msgHdrsProp].number, false);
-				if (tmpMsgHdr != null)
-				{
-					if ((tmpMsgHdr.attr & MSG_DELETE) == MSG_DELETE)
-					{
-						tmpMsgHdr.attr = tmpMsgHdr.attr ^ MSG_DELETE;
-						msgWasAffected = pMsgbase.put_msg_header(false, msgHdrs[msgHdrsProp].number, tmpMsgHdr);
-					}
-					else
-						msgWasAffected = true; // No action needed
-				}
-			}
-			retObj.allVoteMsgsAffected = (retObj.allVoteMsgsAffected && msgWasAffected);
-			if (msgWasAffected)
-				++retObj.toggleVoteMsgsAffected;
-		}
-	}
-
-	return retObj;
-}
-
-// Returns whether the user's scan_ptr for a sub-board is 4294967295
-// (0xffffffff, or ~0). That is a special value for the user's scan_ptr
-// meaning it should point to the latest message in the messagebase.
-//
-// Parameters:
-//  pSubCode: The internal code of a sub-board
-//
-// Return value: Whether or not the user's scan_ptr for the sub-board is that special value
-function subBoardScanPtrIsLatestMsgSpecialVal(pSubCode)
-{
-	if (typeof(msg_area.sub[pSubCode].scan_ptr) === "number")
-		return msgNumIsLatestMsgSpecialVal(msg_area.sub[pSubCode].scan_ptr);
-	else
-		return false;
-}
-// Returns whether a message number is  4294967295 (0xffffffff, or ~0). That is
-// a special value for a message number meaning it should point to the latest
-// message in the messagebase.
-//
-// Parameters:
-//  pMsgNum: A message number
-//
-// Return value: Whether or not the given message number is the special value
-function msgNumIsLatestMsgSpecialVal(pMsgNum)
-{
-	return (pMsgNum == 0xffffffff);
-}
-
-/////////////////////////////////////////////////////////////////////////
-// Debug helper & error output functions
-
-// Prints information from a message header on the screen, for debugging purpurposes.
-//
-// Parameters:
-//  pMsgHdr: A message header object
-function printMsgHdr(pMsgHdr)
-{
-	for (var prop in pMsgHdr)
-	{
-		if ((prop == "field_list") && (typeof(pMsgHdr[prop]) == "object"))
-		{
-			console.print(prop + ":\r\n");
-			for (var objI = 0; objI < pMsgHdr[prop].length; ++objI)
-			{
-				console.print(" " + objI + ":\r\n");
-				for (var innerProp in pMsgHdr[prop][objI])
-					console.print("  " + innerProp + ": " + pMsgHdr[prop][objI][innerProp] + "\r\n");
-			}
-		}
-		else
-			console.print(prop + ": " + pMsgHdr[prop] + "\r\n");
-	}
-	console.pause();
-}
-
-// Closes a poll, using an existing MessageBase object.
-//
-// Parameters:
-//  pMsgbase: A MessageBase object representing the current sub-board.  It
-//            must be open.
-//  pMsgNum: The message number (not the index)
-//
-// Return value: Boolean - Whether or not closing the poll succeeded
-function closePollWithOpenMsgbase(pMsgbase, pMsgNum)
-{
-	var pollClosed = false;
-	if ((pMsgbase !== null) && pMsgbase.is_open)
-	{
-		var userNameOrAlias = user.alias;
-		// See if the poll was posted using the user's real name instead of
-		// their alias
-		var msgHdr = pMsgbase.get_msg_header(false, pMsgNum, false);
-		if ((msgHdr != null) && ((msgHdr.attr & MSG_POLL) == MSG_POLL))
-		{
-			if (msgHdr.from.toUpperCase() == user.name.toUpperCase())
-				userNameOrAlias = msgHdr.from;
-		}
-		// Close the poll (the close_poll() method was added in the Synchronet
-		// 3.17 build on August 19, 2017)
-		pollClosed = pMsgbase.close_poll(pMsgNum, userNameOrAlias);
-	}
-	return pollClosed;
-}
-
-// Closes a poll.
-//
-// Parameters:
-//  pSubBoardCode: The internal code of the sub-board
-//  pMsgNum: The message number (not the index)
-//
-// Return value: Boolean - Whether or not closing the poll succeeded
-function closePoll(pSubBoardCode, pMsgNum)
-{
-	var pollClosed = false;
-	var msgbase = new MsgBase(pSubBoardCode);
-	if (msgbase.open())
-	{
-		pollClosed = closePollWithOpenMsgbase(msgbase, pMsgNum);
-		msgbase.close();
-	}
-	return pollClosed;
-}
-
-// Gets a message header from the messagebase, either by index (offset) or number.
-//
-// Parameters:
-//  pMsgbase: Optional messagebase object.  If this is provided, then pSubBoardCode is not used.
-//  pSubBoardCode: The messagebase sub-board code
-//  pByIdx: Boolean - Whether or not to get the message header by index (if false, then by number)
-//  pMsgIdxOrNum: The message index or number of the message header to retrieve
-//  pExpandFields: Boolean - Whether or not to expand fields for the message header
-function getHdrFromMsgbase(pMsgbase, pSubBoardCode, pByIdx, pMsgIdxOrNum, pExpandFields)
-{
-	var msgbaseIsOpen = false;
-	var msgbase = null;
-	var msgHdr = null;
-	if (pMsgbase == null)
-	{
-		msgbase = new MsgBase(pSubBoardCode);
-		msgbaseIsOpen = msgbase.open();
-	}
-	else
-	{
-		msgbase = pMsgbase;
-		msgbaseIsOpen = pMsgbase.is_open;
-	}
-	if (msgbaseIsOpen)
-	{
-		var getMsgHdr = true;
-		if (pByIdx)
-			getMsgHdr = ((pMsgIdxOrNum >= 0) && (pMsgIdxOrNum < msgbase.total_msgs))
-		if (getMsgHdr)
-		{
-			// TODO: I think we should be able to call get_msg_header() and get valid vote information,
-			// but that doesn't seem to be the case:
-			msgHdr = msgbase.get_msg_header(pByIdx, pMsgIdxOrNum, pExpandFields, true); // Last true: Include votes
-		}
-		if (pMsgbase == null)
-			msgbase.close();
-	}
-	return msgHdr;
-}
-
-// Inputs a string from the user, restricting their input to certain keys (optionally).
-//
-// Parameters:
-//  pKeys: A string containing valid characters for input.  Optional
-//  pMaxNumChars: The maximum number of characters to input.  Optional
-//  pCaseSensitive: Boolean - Whether or not the input should be case-sensitive.  Optional.
-//                  Defaults to true.  If false, then the user input will be uppercased.
-//
-// Return value: A string containing the user's input
-function consoleGetStrWithValidKeys(pKeys, pMaxNumChars, pCaseSensitive)
-{
-	var maxNumChars = 0;
-	if ((typeof(pMaxNumChars) == "number") && (pMaxNumChars > 0))
-		maxNumChars = pMaxNumChars;
-
-	var regexPattern = (typeof(pKeys) == "string" ? "[" + pKeys + "]" : ".");
-	var caseSensitive = (typeof(pCaseSensitive) == "boolean" ? pCaseSensitive : true);
-	var regex = new RegExp(regexPattern, (caseSensitive ? "" : "i"));
-
-	var CTRL_H = "\x08";
-	var BACKSPACE = CTRL_H;
-	var CTRL_M = "\x0d";
-	var KEY_ENTER = CTRL_M;
-
-	var modeBits = (caseSensitive ? K_NONE : K_UPPER);
-	var userInput = "";
-	var continueOn = true;
-	while (continueOn)
-	{
-		var userChar = console.getkey(K_NOECHO|modeBits);
-		if (regex.test(userChar) && isPrintableChar(userChar))
-		{
-			var appendChar = true;
-			if ((maxNumChars > 0) && (userInput.length >= maxNumChars))
-				appendChar = false;
-			if (appendChar)
-			{
-				userInput += userChar;
-				if ((modeBits & K_NOECHO) == 0)
-					console.print(userChar);
-			}
-		}
-		else if (userChar == BACKSPACE)
-		{
-			if (userInput.length > 0)
-			{
-				if ((modeBits & K_NOECHO) == 0)
-				{
-					console.print(BACKSPACE);
-					console.print(" ");
-					console.print(BACKSPACE);
-				}
-				userInput = userInput.substr(0, userInput.length-1);
-			}
-		}
-		else if (userChar == KEY_ENTER)
-		{
-			continueOn = false;
-			if ((modeBits & K_NOCRLF) == 0)
-				console.crlf();
-		}
-	}
-	return userInput;
-}
-
-// Returns whether or not a character is printable.
-//
-// Parameters:
-//  pChar: A character to test
-//
-// Return value: Boolean - Whether or not the character is printable
-function isPrintableChar(pChar)
-{
-	// Make sure pChar is valid and is a string.
-	if (typeof(pChar) != "string")
-		return false;
-	if (pChar.length == 0)
-		return false;
-
-	// Make sure the character is a printable ASCII character in the range of 32 to 254,
-	// except for 127 (delete).
-	var charCode = pChar.charCodeAt(0);
-	return ((charCode > 31) && (charCode < 255) && (charCode != 127));
-}
-
-// Adds message attributes to a message header and saves it in the messagebase.
-// To do that, this function first loads the messag header from the messagebase
-// without expanded fields, applies the attributes, and then saves the header
-// back to the messagebase.
-//
-// Parameters:
-//  pMsgbaseOrSubCode: An open MessageBase object or a sub-board code (string)
-//  pMsgNum: The number of the message to update
-//  pMsgAttrs: The message attributes to apply to the message (numeric bitfield)
-//
-// Return value: An object containing the following properties:
-//               saveSucceeded: Boolean - Whether or not the message header was successfully saved
-//               msgAttrs: A numeric bitfield containing the updated attributes of the message header
-function applyAttrsInMsgHdrInMessagbase(pMsgbaseOrSubCode, pMsgNum, pMsgAttrs)
-{
-	var retObj = {
-		saveSucceeded: false,
-		msgAttrs: 0
-	};
-
-	var msgbaseOpen = false;
-	var msgbase = null;
-	if (typeof(pMsgbaseOrSubCode) == "object")
-	{
-		msgbase = pMsgbaseOrSubCode;
-		msgbaseOpen = msgbase.is_open;
-	}
-	else if (typeof(pMsgbaseOrSubCode) == "string")
-	{
-		msgbase = new MsgBase(pMsgbaseOrSubCode);
-		msgbaseOpen = msgbase.open();
-	}
-	else
-		return retObj;
-
-	if (msgbaseOpen)
-	{
-		// Get the message header without expanded fields (we can't save it with
-		// expanded fields), then add the 'read' attribute and save it back to the messagebase.
-		var msgHdr = msgbase.get_msg_header(false, pMsgNum, false);
-		if (msgHdr != null)
-		{
-			msgHdr.attr |= pMsgAttrs;
-			// TODO: Occasional when going to next message area:
-			// Error: Error -110 adding SENDERNETADDR field to message header
-			retObj.saveSucceeded = msgbase.put_msg_header(false, pMsgNum, msgHdr);
-			if (retObj.saveSucceeded)
-				retObj.msgAttrs = msgHdr.attr;
-			else
-			{
-				writeToSysAndNodeLog("Failed to save message header with the following attributes: " + msgAttrsToString(pMsgAttrs), LOG_ERR);
-				writeToSysAndNodeLog(getMsgAreaDescStr(msgbase), LOG_ERR);
-				writeToSysAndNodeLog(format("Message offset: %d, number: %d", msgHdr.offset, msgHdr.number), LOG_ERR);
-				writeToSysAndNodeLog("Status: " + msgbase.status, LOG_ERR);
-				writeToSysAndNodeLog("Error: " + msgbase.error, LOG_ERR);
-				/*
-				// For sysops, output a debug message
-				if (user.is_sysop)
-				{
-					console.attributes = "N";
-					console.crlf();
-					console.print("* Failed to save msg header the with the following attributes: " + msgAttrsToString(pMsgAttrs));
-					console.crlf();
-					console.print("Status: " + msgbase.status);
-					console.crlf();
-					console.print("Error: " + msgbase.error);
-					console.crlf();
-					console.crlf();
-					//console.print("put_msg_header params: false, " + msgHdr.number + ", header:\r\n");
-					//console.print("put_msg_header params: true, " + msgHdr.offset + ", header:\r\n");
-					//console.print("put_msg_header params: " + msgHdr.number + ", header:\r\n");
-					printMsgHdr(msgHdr);
-				}
-				*/
-			}
-		}
-
-		// If a sub-board code was passed in, then close the messagebase object
-		// that we created here.
-		if (typeof(pMsgbaseOrSubCode) == "string")
-			msgbase.close();
-	}
-
-	return retObj;
-}
-
-// Converts a message attributes bitfield to a string.
-//
-// Parameters:
-//  pMsgAttrs: A numeric type with message attribute bits
-//
-// Return value: A string containing a list of the message attributes
-function msgAttrsToString(pMsgAttrs)
-{
-	if (typeof(pMsgAttrs) != "number")
-		return "";
-
-	var attrsStr = "";
-	if ((pMsgAttrs & MSG_PRIVATE) == MSG_PRIVATE)
-	{
-		if (attrsStr.length > 0)
-			attrsStr += ", ";
-		attrsStr += "MSG_PRIVATE";
-	}
-	if ((pMsgAttrs & MSG_READ) == MSG_READ)
-	{
-		if (attrsStr.length > 0)
-			attrsStr += ", ";
-		attrsStr += "MSG_READ";
-	}
-	if ((pMsgAttrs & MSG_PERMANENT) == MSG_PERMANENT)
-	{
-		if (attrsStr.length > 0)
-			attrsStr += ", ";
-		attrsStr += "MSG_PERMANENT";
-	}
-	if ((pMsgAttrs & MSG_LOCKED) == MSG_LOCKED)
-	{
-		if (attrsStr.length > 0)
-			attrsStr += ", ";
-		attrsStr += "MSG_LOCKED";
-	}
-	if ((pMsgAttrs & MSG_DELETE) == MSG_DELETE)
-	{
-		if (attrsStr.length > 0)
-			attrsStr += ", ";
-		attrsStr += "MSG_DELETE";
-	}
-	if ((pMsgAttrs & MSG_ANONYMOUS) == MSG_ANONYMOUS)
-	{
-		if (attrsStr.length > 0)
-			attrsStr += ", ";
-		attrsStr += "MSG_ANONYMOUS";
-	}
-	if ((pMsgAttrs & MSG_KILLREAD) == MSG_KILLREAD)
-	{
-		if (attrsStr.length > 0)
-			attrsStr += ", ";
-		attrsStr += "MSG_KILLREAD";
-	}
-	if ((pMsgAttrs & MSG_MODERATED) == MSG_MODERATED)
-	{
-		if (attrsStr.length > 0)
-			attrsStr += ", ";
-		attrsStr += "MSG_MODERATED";
-	}
-	if ((pMsgAttrs & MSG_VALIDATED) == MSG_VALIDATED)
-	{
-		if (attrsStr.length > 0)
-			attrsStr += ", ";
-		attrsStr += "MSG_VALIDATED";
-	}
-	if ((pMsgAttrs & MSG_REPLIED) == MSG_REPLIED)
-	{
-		if (attrsStr.length > 0)
-			attrsStr += ", ";
-		attrsStr += "MSG_REPLIED";
-	}
-	if ((pMsgAttrs & MSG_NOREPLY) == MSG_NOREPLY)
-	{
-		if (attrsStr.length > 0)
-			attrsStr += ", ";
-		attrsStr += "MSG_NOREPLY";
-	}
-	if ((pMsgAttrs & MSG_UPVOTE) == MSG_UPVOTE)
-	{
-		if (attrsStr.length > 0)
-			attrsStr += ", ";
-		attrsStr += "MSG_UPVOTE";
-	}
-	if ((pMsgAttrs & MSG_DOWNVOTE) == MSG_DOWNVOTE)
-	{
-		if (attrsStr.length > 0)
-			attrsStr += ", ";
-		attrsStr += "MSG_DOWNVOTE";
-	}
-	if ((pMsgAttrs & MSG_POLL) == MSG_POLL)
-	{
-		if (attrsStr.length > 0)
-			attrsStr += ", ";
-		attrsStr += "MSG_POLL";
-	}
-	return attrsStr;
-}
-
-// Returns the index of the first Synchronet attribute code before a given index
-// in a string.
-//
-// Parameters:
-//  pStr: The string to search in
-//  pIdx: The index to search back from
-//  pSeriesOfAttrs: Optional boolean - Whether or not to look for a series of
-//                  attributes.  Defaults to false (look for just one attribute).
-//  pOnlyInWord: Optional boolean - Whether or not to look only in the current word
-//               (with words separated by whitespace).  Defaults to false.
-//
-// Return value: The index of the first Synchronet attribute code before the given
-//               index in the string, or -1 if there is none or if the parameters
-//               are invalid
-function strIdxOfSyncAttrBefore(pStr, pIdx, pSeriesOfAttrs, pOnlyInWord)
-{
-	if (typeof(pStr) != "string")
-		return -1;
-	if (typeof(pIdx) != "number")
-		return -1;
-	if ((pIdx < 0) || (pIdx >= pStr.length))
-		return -1;
-
-	var seriesOfAttrs = (typeof(pSeriesOfAttrs) == "boolean" ? pSeriesOfAttrs : false);
-	var onlyInWord = (typeof(pOnlyInWord) == "boolean" ? pOnlyInWord : false);
-
-	var attrCodeIdx = pStr.lastIndexOf("\x01", pIdx-1);
-	if (attrCodeIdx > -1)
-	{
-		// If we are to only check the current word, then continue only if
-		// there isn't a space between the attribute code and the given index.
-		if (onlyInWord)
-		{
-			if (pStr.lastIndexOf(" ", pIdx-1) >= attrCodeIdx)
-				attrCodeIdx = -1;
-		}
-	}
-	if (attrCodeIdx > -1)
-	{
-		var syncAttrRegexWholeWord = /^\x01[krgybmcw01234567hinpq,;\.dtl<>\[\]asz]$/i;
-		if (syncAttrRegexWholeWord.test(pStr.substr(attrCodeIdx, 2)))
-		{
-			if (seriesOfAttrs)
-			{
-				for (var i = attrCodeIdx - 2; i >= 0; i -= 2)
-				{
-					if (syncAttrRegexWholeWord.test(pStr.substr(i, 2)))
-						attrCodeIdx = i;
-					else
-						break;
-				}
-			}
-		}
-		else
-			attrCodeIdx = -1;
-	}
-	return attrCodeIdx;
-}
-
-// Returns a string with any Synchronet color/attribute codes found in a string
-// before a given index.
-//
-// Parameters:
-//  pStr: The string to search in
-//  pIdx: The index in the string to search before
-//
-// Return value: A string containing any Synchronet attribute codes found before
-//               the given index in the given string
-function getAttrsBeforeStrIdx(pStr, pIdx)
-{
-	if (typeof(pStr) != "string")
-		return "";
-	if (typeof(pIdx) != "number")
-		return "";
-	if (pIdx < 0)
-		return "";
-
-	var idx = (pIdx < pStr.length ? pIdx : pStr.length-1);
-	var attrStartIdx = strIdxOfSyncAttrBefore(pStr, idx, true, false);
-	var attrEndIdx = strIdxOfSyncAttrBefore(pStr, idx, false, false); // Start of 2-character code
-	var attrsStr = "";
-	if ((attrStartIdx > -1) && (attrEndIdx > -1))
-		attrsStr = pStr.substring(attrStartIdx, attrEndIdx+2);
-	return attrsStr;
-}
-
-// Given a message header, this function gets/calculates the message's
-// upvotes, downvotes, and vote score, if that information is present.
-//
-// Parameters:
-//  pMsgHdr: A message header object
-//
-// Return value: An object containign the following properties:
-//               foundVoteInfo: Boolean - Whether the vote information exited in the header
-//               upvotes: The number of upvotes
-//               downvotes: The number of downvotes
-//               voteScore: The overall vote score
-function getMsgUpDownvotesAndScore(pMsgHdr, pVerbose)
-{
-	var retObj = {
-		foundVoteInfo: false,
-		upvotes: 0,
-		downvotes: 0,
-		voteScore: 0
-	};
-
- 	if ((pMsgHdr.hasOwnProperty("total_votes") || pMsgHdr.hasOwnProperty("downvotes")) && pMsgHdr.hasOwnProperty("upvotes"))
-	{
-		retObj.foundVoteInfo = true;
-		retObj.upvotes = pMsgHdr.upvotes;
-		if (pMsgHdr.hasOwnProperty("downvotes"))
-			retObj.downvotes = pMsgHdr.downvotes;
-		else
-			retObj.downvotes = pMsgHdr.total_votes - pMsgHdr.upvotes;
-		retObj.voteScore = pMsgHdr.upvotes - retObj.downvotes;
-		if (pVerbose && user.is_sysop)
-		{
-			console.print("\x01n\r\n");
-			console.print("Vote information from header:\r\n");
-			console.print("Upvotes: " + pMsgHdr.upvotes + "\r\n");
-			console.print("Downvotes: " + retObj.downvotes + "\r\n");
-			console.print("Score: " + retObj.voteScore + "\r\n");
-			console.pause();
-		}
-	}
-	else
-	{
-		if (pVerbose && user.is_sysop)
-			console.print("\x01n\r\nMsg header does NOT have needed vote info\r\n\x01p");
-	}
-
-	return retObj;
-}
-
-// Removes any initial Synchronet attribute(s) from a message body,
-// which can sometimes color the whole message.
-//
-// Parameters:
-//  pMsgBody: The original message body
-//
-// Return value: The message body, with any initial color removed
-function removeInitialColorFromMsgBody(pMsgBody)
-{
-	if (pMsgBody == null)
-		return "";
-
-	var msgBody = pMsgBody;
-
-	var msgBodyLines = pMsgBody.split("\r\n", 3);
-	if (msgBodyLines.length == 3)
-	{
-		// A regex for attribute settings - Note that this doesn't contain some of the control codes
-		var onlySyncAttrsRegexWholeWord = new RegExp("^[\x01krgybmcw01234567hinq,;\.dtlasz]+$", 'i');;
-		var line1Match = /^  Re: .*/.test(strip_ctrl(msgBodyLines[0]));
-		var line2Match = /^  By: .* on .*/.test(strip_ctrl(msgBodyLines[1]));
-		var line3OnlySyncAttrs = onlySyncAttrsRegexWholeWord.test(msgBodyLines[2]);
-		if (line1Match && line2Match)
-		{
-			msgBodyLines = pMsgBody.split("\r\n");
-			msgBodyLines[0] = strip_ctrl(msgBodyLines[0]);
-			msgBodyLines[1] = strip_ctrl(msgBodyLines[1]);
-			if (line3OnlySyncAttrs)
-			{
-				var originalLine3SyncAttrs = msgBodyLines[2];
-				msgBodyLines[2] = strip_ctrl(msgBodyLines[2]);
-				// If the first word of the 4th line is only Synchronet attribute codes,
-				// and they're the same as the codes on the 3rd line, then remove them.
-				if (msgBodyLines.length >= 4)
-				{
-					var line4Words = msgBodyLines[3].split(" ");
-					if ((line4Words.length > 0) && onlySyncAttrsRegexWholeWord.test(line4Words[0]) && (line4Words[0] == originalLine3SyncAttrs))
-						msgBodyLines[3] = msgBodyLines[3].substr(line4Words[0].length);
-				}
-			}
-			msgBody = "";
-			for (var i = 0; i < msgBodyLines.length; ++i)
-				msgBody += msgBodyLines[i] + "\r\n";
-			// Remove the trailing \r\n characters from msgBody
-			msgBody = msgBody.substr(0, msgBody.length-2);
-		}
-	}
-
-	return msgBody;
-}
-
-// Finds a user with a name, alias, or handle matching a given string.
-// If system.matchuser() can't find it, this will iterate through all users
-// to find the first user with a name, alias, or handle matching the given
-// name.
-function findUserNumWithName(pName)
-{
-	if (typeof(pName) !== "string" || pName.length == 0)
-		return 0;
-
-	var userNum = system.matchuser(pName);
-	if (userNum == 0)
-	{
-		try
-		{
-			userNum = system.matchuserdata(U_NAME, pName);
-		}
-		catch (error) {}
-	}
-	if (userNum == 0)
-	{
-		try
-		{
-			userNum = system.matchuserdata(U_ALIAS, pName);
-		}
-		catch (error) {}
-	}
-	if (userNum == 0)
-	{
-		try
-		{
-			userNum = system.matchuserdata(U_HANDLE, pName);
-		}
-		catch (error) {}
-	}
-	return userNum;
-}
-
-// Inputs a string from the user, with a timeout
-//
-// Parameters:
-//  pMode: The mode bits to use for the input (i.e., defined in sbbsdefs.js)
-//  pMaxLength: The maximum length of the string (0 or less for no limit)
-//  pTimeout: The timeout (in milliseconds).  When the timeout is reached,
-//            input stops and the user's input is returned.
-//
-// Return value: The user's input (string)
-function getStrWithTimeout(pMode, pMaxLength, pTimeout)
-{
-	var inputStr = "";
-
-	var mode = K_NONE;
-	if (typeof(pMode) == "number")
-		mode = pMode;
-	var maxWidth = 0;
-	if (typeof(pMaxLength) == "number")
-			maxWidth = pMaxLength;
-	var timeout = 0;
-	if (typeof(pTimeout) == "number")
-		timeout = pTimeout;
-
-	var setNormalAttrAtEnd = false;
-	if (((mode & K_LINE) == K_LINE) && (maxWidth > 0) && console.term_supports(USER_ANSI))
-	{
-		var curPos = console.getxy();
-		printf("\x01n\x01w\x01h\x01" + "4%" + maxWidth + "s", "");
-		console.gotoxy(curPos);
-		setNormalAttrAtEnd = true;
-	}
-
-	var curPos = console.getxy();
-	var userKey = "";
-	do
-	{
-		userKey = console.inkey(mode, timeout);
-		if ((userKey.length > 0) && isPrintableChar(userKey))
-		{
-			var allowAppendChar = true;
-			if ((maxWidth > 0) && (inputStr.length >= maxWidth))
-				allowAppendChar = false;
-			if (allowAppendChar)
-			{
-				inputStr += userKey;
-				console.print(userKey);
-				++curPos.x;
-			}
-		}
-		else if (userKey == BACKSPACE)
-		{
-			if (inputStr.length > 0)
-			{
-				inputStr = inputStr.substr(0, inputStr.length-1);
-				console.gotoxy(curPos.x-1, curPos.y);
-				console.print(" ");
-				console.gotoxy(curPos.x-1, curPos.y);
-				--curPos.x;
-			}
-		}
-		else if (userKey == KEY_ENTER)
-			userKey = "";
-	} while(userKey.length > 0);
-
-	if (setNormalAttrAtEnd)
-		console.attributes = "N";
-
-	return inputStr;
-}
-
-// Calculates the page number (1-based) and top index for the page (0-based),
-// given an item index.
-//
-// Parameters:
-//  pItemIdx: The index of the item
-//  pNumItemsPerPage: The number of items per page
-//
-// Return value: An object containing the following properties:
-//               pageNum: The page number of the item (1-based; will be 0 if not found)
-//               pageTopIdx: The index of the top item on the page (or -1 if not found)
-function calcPageNumAndTopPageIdx(pItemIdx, pNumItemsPerPage)
-{
-	var retObj = {
-		pageNum: 0,
-		pageTopIdx: -1
-	};
-
-	var pageNum = 1;
-	var topIdx = 0;
-	var continueOn = true;
-	do
-	{
-		var endIdx = topIdx + pNumItemsPerPage;
-		if ((pItemIdx >= topIdx) && (pItemIdx < endIdx))
-		{
-			continueOn = false;
-			retObj.pageNum = pageNum;
-			retObj.pageTopIdx = topIdx;
-		}
-		else
-		{
-			++pageNum;
-			topIdx = endIdx;
-		}
-	} while (continueOn);
-
-	return retObj;
-}
-
-// Finds the page number of a message group or sub-board, given some text to
-// search for.
-//
-// Parameters:
-//  pText: The text to search for in the items
-//  pNumItemsPerPage: The number of items per page
-//  pSubBoard: Boolean - If true, search the sub-board list for the given group index.
-//             If false, search the group list.
-//  pStartItemIdx: The item index to start at
-//  pGrpIdx: The index of the group to search in (only for doing a sub-board search)
-//
-// Return value: An object containing the following properties:
-//               pageNum: The page number of the item (1-based; will be 0 if not found)
-//               pageTopIdx: The index of the top item on the page (or -1 if not found)
-//               itemIdx: The index of the item (or -1 if not found)
-function getMsgAreaPageNumFromSearch(pText, pNumItemsPerPage, pSubBoard, pStartItemIdx, pGrpIdx)
-{
-	var retObj = {
-		pageNum: 0,
-		pageTopIdx: -1,
-		itemIdx: -1
-	};
-
-	// Sanity checking
-	if ((typeof(pText) != "string") || (typeof(pNumItemsPerPage) != "number") || (typeof(pSubBoard) != "boolean"))
-		return retObj;
-
-	// Convert the text to uppercase for case-insensitive searching
-	var srchText = pText.toUpperCase();
-	if (pSubBoard)
-	{
-		if ((typeof(pGrpIdx) == "number") && (pGrpIdx >= 0) && (pGrpIdx < msg_area.grp_list.length))
-		{
-			// Go through the sub-board list of the given group and
-			// search for text in the descriptions
-			for (var i = pStartItemIdx; i < msg_area.grp_list[pGrpIdx].sub_list.length; ++i)
-			{
-				if ((msg_area.grp_list[pGrpIdx].sub_list[i].description.toUpperCase().indexOf(srchText) > -1) ||
-				    (msg_area.grp_list[pGrpIdx].sub_list[i].name.toUpperCase().indexOf(srchText) > -1))
-				{
-					retObj.itemIdx = i;
-					// Figure out the page number and top index for the page
-					var pageObj = calcPageNumAndTopPageIdx(i, pNumItemsPerPage);
-					if ((pageObj.pageNum > 0) && (pageObj.pageTopIdx > -1))
-					{
-						retObj.pageNum = pageObj.pageNum;
-						retObj.pageTopIdx = pageObj.pageTopIdx;
-					}
-					break;
-				}
-			}
-		}
-	}
-	else
-	{
-		// Go through the message group list and look for a match
-		for (var i = pStartItemIdx; i < msg_area.grp_list.length; ++i)
-		{
-			if ((msg_area.grp_list[i].name.toUpperCase().indexOf(srchText) > -1) ||
-			    (msg_area.grp_list[i].description.toUpperCase().indexOf(srchText) > -1))
-			{
-				retObj.itemIdx = i;
-				// Figure out the page number and top index for the page
-				var pageObj = calcPageNumAndTopPageIdx(i, pNumItemsPerPage);
-				if ((pageObj.pageNum > 0) && (pageObj.pageTopIdx > -1))
-				{
-					retObj.pageNum = pageObj.pageNum;
-					retObj.pageTopIdx = pageObj.pageTopIdx;
-				}
-				break;
-			}
-		}
-	}
-
-	return retObj;
-}
-
-// Finds a message group index with search text, matching either the name or
-// description, case-insensitive.
-//
-// Parameters:
-//  pSearchText: The name/description text to look for
-//  pStartItemIdx: The item index to start at.  Defaults to 0
-//
-// Return value: The index of the message group, or -1 if not found
-function findMsgGrpIdxFromText(pSearchText, pStartItemIdx)
-{
-	if (typeof(pSearchText) != "string")
-		return -1;
-
-	var grpIdx = -1;
-
-	var startIdx = (typeof(pStartItemIdx) == "number" ? pStartItemIdx : 0);
-	if ((startIdx < 0) || (startIdx > msg_area.grp_list.length))
-		startIdx = 0;
-
-	// Go through the message group list and look for a match
-	var searchTextUpper = pSearchText.toUpperCase();
-	for (var i = startIdx; i < msg_area.grp_list.length; ++i)
-	{
-		if ((msg_area.grp_list[i].name.toUpperCase().indexOf(searchTextUpper) > -1) ||
-		    (msg_area.grp_list[i].description.toUpperCase().indexOf(searchTextUpper) > -1))
-		{
-			grpIdx = i;
-			break;
-		}
-	}
-
-	return grpIdx;
-}
-
-// Finds a message group index with search text, matching either the name or
-// description, case-insensitive.
-//
-// Parameters:
-//  pGrpIdx: The index of the message group
-//  pSearchText: The name/description text to look for
-//  pStartItemIdx: The item index to start at.  Defaults to 0
-//
-// Return value: The index of the message group, or -1 if not found
-function findSubBoardIdxFromText(pGrpIdx, pSearchText, pStartItemIdx)
-{
-	if (typeof(pGrpIdx) != "number")
-		return -1;
-	if (typeof(pSearchText) != "string")
-		return -1;
-
-	var subBoardIdx = -1;
-
-	var startIdx = (typeof(pStartItemIdx) == "number" ? pStartItemIdx : 0);
-	if ((startIdx < 0) || (startIdx > msg_area.grp_list[pGrpIdx].sub_list.length))
-		startIdx = 0;
-
-	// Go through the message group list and look for a match
-	var searchTextUpper = pSearchText.toUpperCase();
-	for (var i = startIdx; i < msg_area.grp_list[pGrpIdx].sub_list.length; ++i)
-	{
-		if ((msg_area.grp_list[pGrpIdx].sub_list[i].name.toUpperCase().indexOf(searchTextUpper) > -1) ||
-		    (msg_area.grp_list[pGrpIdx].sub_list[i].description.toUpperCase().indexOf(searchTextUpper) > -1))
-		{
-			subBoardIdx = i;
-			break;
-		}
-	}
-
-	return subBoardIdx;
-}
-
-// Searches for a @MSG_TO @-code in a string and inserts a color/attribute code
-// before the @-code in the string.
-//
-// Parameters:
-//  pStr: The string to look in
-//  pToUserColor: The color/attribute code to insert before the @MSG_TO @-code
-//
-// Return value: A string with the given color/attribute code inserted before the
-//               @MSG_TO @-code
-function strWithToUserColor(pStr, pToUserColor)
-{
-	if ((typeof(pStr) != "string") || (typeof(pToUserColor) != "string"))
-		return "";
-	if (pToUserColor.length == 0)
-		return pStr;
-
-	// Find start & end indexes of a @MSG_TO* @-code, i.e.,
-	// @MSG_TO, @MSG_TO_NAME, @MSG_TO_EXT, @MSG_TO_NET
-	var toCodeStartIdx = pStr.indexOf("@MSG_TO");
-	if (toCodeStartIdx < 0)
-		return pStr;
-	// Insert the color in the right position and return the line
-	return pStr.substr(0, toCodeStartIdx) + "\x01n" + pToUserColor + pStr.substr(toCodeStartIdx) + "\x01n";
-	/*
-	// Insert the color in the right position, and
-	// put a \x01n right after the end of the @-code
-	var str = "";
-	var toCodeEndIdx = pStr.indexOf("@", toCodeStartIdx+1);
-	if (toCodeEndIdx >= 0)
-	{
-		str = pStr.substr(0, toCodeStartIdx) + "\x01n" + pToUserColor + pStr.substr(toCodeStartIdx, toCodeEndIdx-toCodeStartIdx+1)
-		    + "\x01n" + pStr.substr(toCodeEndIdx);
-	}
-	else
-		str = pStr.substr(0, toCodeStartIdx) + "\x01n" + pToUserColor + pStr.substr(toCodeStartIdx) + "\x01n";
-	return str;
-	*/
-}
-
-// Gets the value of the user's current scan_ptr in a sub-board, or if it's
-// the 'last message' special value, returns the message number of the last
-// readable message in the sub-board (this is the message number, not the index).
-//
-// Parameters:
-//  pSubCode: A sub-board internal code
-//
-// Return value: The user's scan_ptr value or the message number of the
-//               last readable message in the sub-board
-function GetScanPtrOrLastMsgNum(pSubCode)
-{
-	var msgNumToReturn = 0;
-	// If the user's scan_ptr for the sub-board isn't the 'last message'
-	// special value, then use it; otherwise, use the latest readable
-	// message number.
-	if (!subBoardScanPtrIsLatestMsgSpecialVal(pSubCode))
-		msgNumToReturn = msg_area.sub[pSubCode].scan_ptr;
-	else
-	{
-		var lastReadableMsgHdr = getLastReadableMsgHdrInSubBoard(pSubCode);
-		if (lastReadableMsgHdr != null)
-			msgNumToReturn = lastReadableMsgHdr.number;
-	}
-
-	return msgNumToReturn;
-}
-
-// Returns whether a message header has one of the attachment flags
-// enabled (for Synchtonet 3.17 or newer).
-//
-// Parameters:
-//  pMsgHdr: A message header (returned from MsgBase.get_msg_header())
-//
-// Return value: Boolean - Whether or not the message has one of the attachment flags
-function msgHdrHasAttachmentFlag(pMsgHdr)
-{
-	if (typeof(pMsgHdr) !== "object" || typeof(pMsgHdr.auxattr) === "undefined")
-		return false;
-
-	var attachmentFlag = false;
-	if (typeof(MSG_FILEATTACH) !== "undefined" && typeof(MSG_MIMEATTACH) !== "undefined")
-		attachmentFlag = (pMsgHdr.auxattr & (MSG_FILEATTACH|MSG_MIMEATTACH)) > 0;
-	return attachmentFlag;
-}
-
-// Allows the user to download a message and its attachments, using the newer
-// Synchronet interface (the function bbs.download_msg_attachments() must exist).
-//
-// Parameters:
-//  pMsgHdr: The message header
-//  pSubCode: The sub-board code that the message is in
-function allowUserToDownloadMessage_NewInterface(pMsgHdr, pSubCode)
-{
-	if (typeof(bbs.download_msg_attachments) !== "function")
-		return;
-	if (typeof(pSubCode) !== "string")
-		return;
-	if (typeof(pMsgHdr) !== "object" || typeof(pMsgHdr.number) == "undefined")
-		return;
-
-	var msgBase = new MsgBase(pSubCode);
-	if (msgBase.open())
-	{
-		// bbs.download_msg_attachments() requires a message header returned
-		// by MsgBase.get_msg_header()
-		var msgHdrForDownloading = msgBase.get_msg_header(false, pMsgHdr.number, false);
-		// Allow the user to download the message
-		if (!console.noyes("Download message", P_NOCRLF))
-		{
-			if (!download_msg(msgHdrForDownloading, msgBase, console.yesno("Plain-text only")))
-				console.print("\x01n\r\nFailed\r\n");
-		}
-		// Allow the user to download the attachments
-		console.creturn();
-		bbs.download_msg_attachments(msgHdrForDownloading);
-		msgBase.close();
-	}
-}
-
-// From msglist.js - Prompts the user if they want to download the message text
-function download_msg(msg, msgbase, plain_text)
-{
-	var fname = system.temp_dir + "msg_" + msg.number + ".txt";
-	var f = new File(fname);
-	if(!f.open("wb"))
-		return false;
-	var text = msgbase.get_msg_body(msg
-				,/* strip ctrl-a */false
-				,/* dot-stuffing */false
-				,/* tails */true
-				,plain_text);
-	f.write(msg.get_rfc822_header(/* force_update: */false, /* unfold: */false
-		,/* default_content_type */!plain_text));
-	f.writeln(text);
-	f.close();
-	return bbs.send_file(fname);
-}
-
-
-////////// Message list sort functions
-
-// For sorting message headers by date & time
-//
-// Parameters:
-//  msgHdrA: The first message header
-//  msgHdrB: The second message header
-//
-// Return value: -1, 0, or 1, depending on whether header A comes before,
-//               is equal to, or comes after header B
-function sortMessageHdrsByDateTime(msgHdrA, msgHdrB)
-{
-	// Return -1, 0, or 1, depending on whether msgHdrA's date & time comes
-	// before, is equal to, or comes after msgHdrB's date & time
-	// Convert when_written_time to local time before comparing the times
-	var localWrittenTimeA = msgWrittenTimeToLocalBBSTime(msgHdrA);
-	var localWrittenTimeB = msgWrittenTimeToLocalBBSTime(msgHdrB);
-	var yearA = +strftime("%Y", localWrittenTimeA);
-	var monthA = +strftime("%m", localWrittenTimeA);
-	var dayA = +strftime("%d", localWrittenTimeA);
-	var hourA = +strftime("%H", localWrittenTimeA);
-	var minuteA = +strftime("%M", localWrittenTimeA);
-	var secondA = +strftime("%S", localWrittenTimeA);
-	var yearB = +strftime("%Y", localWrittenTimeB);
-	var monthB = +strftime("%m", localWrittenTimeB);
-	var dayB = +strftime("%d", localWrittenTimeB);
-	var hourB = +strftime("%H", localWrittenTimeB);
-	var minuteB = +strftime("%M", localWrittenTimeB);
-	var secondB = +strftime("%S", localWrittenTimeB);
-	if (yearA < yearB)
-		return -1;
-	else if (yearA > yearB)
-		return 1;
-	else
-	{
-		if (monthA < monthB)
-			return -1;
-		else if (monthA > monthB)
-			return 1;
-		else
-		{
-			if (dayA < dayB)
-				return -1;
-			else if (dayA > dayB)
-				return 1;
-			else
-			{
-				if (hourA < hourB)
-					return -1;
-				else if (hourA > hourB)
-					return 1;
-				else
-				{
-					if (minuteA < minuteB)
-						return -1;
-					else if (minuteA > minuteB)
-						return 1;
-					else
-					{
-						if (secondA < secondB)
-							return -1;
-						else if (secondA > secondB)
-							return 1;
-						else
-							return 0;
-					}
-				}
-			}
-		}
-	}
-}
-
-// Returns an array of internal sub-board codes to scan for a given scan scope.
-//
-// Parameters:
-//  pScanScopeChar: A string specifying "A" for all sub-boards, "G" for current
-//                  message group sub-boards, or "S" for the current sub-board
-//
-// Return value: An array of internal sub-board codes for sub-boards to scan
-function getSubBoardsToScanArray(pScanScopeChar)
-{
-	var subBoardsToScan = [];
-	if (pScanScopeChar == "A") // All sub-board scan
-	{
-		for (var grpIndex = 0; grpIndex < msg_area.grp_list.length; ++grpIndex)
-		{
-			for (var subIndex = 0; subIndex < msg_area.grp_list[grpIndex].sub_list.length; ++subIndex)
-				subBoardsToScan.push(msg_area.grp_list[grpIndex].sub_list[subIndex].code);
-		}
-	}
-	else if (pScanScopeChar == "G") // Group scan
-	{
-		for (var subIndex = 0; subIndex < msg_area.grp_list[bbs.curgrp].sub_list.length; ++subIndex)
-			subBoardsToScan.push(msg_area.grp_list[bbs.curgrp].sub_list[subIndex].code);
-	}
-	else if (pScanScopeChar == "S") // Current sub-board scan
-		subBoardsToScan.push(bbs.cursub_code);
-	return subBoardsToScan;
-}
-
-// Returns whether a user number is valid (only an actual, active user)
-//
-// Parameters:
-//  pUserNum: A user number
-//
-// Return value: Boolean - Whether or not the given user number is valid
-function isValidUserNum(pUserNum)
-{
-	if (typeof(pUserNum) !== "number")
-		return false;
-	if (pUserNum < 1 || pUserNum > system.lastuser)
-		return false;
-
-	var userIsValid = false;
-	var theUser = new User(pUserNum);
-	if (theUser != null && (theUser.settings & USER_DELETED) == 0 && (theUser.settings & USER_INACTIVE) == 0)
-		userIsValid = true;
-	return userIsValid;
-}
-
-// Returns the index of the last ANSI code in a string.
-//
-// Parameters:
-//  pStr: The string to search in
-//  pANSIRegexes: An array of regular expressions to use for searching for ANSI codes
-//
-// Return value: The index of the last ANSI code in the string, or -1 if not found
-function idxOfLastANSICode(pStr, pANSIRegexes)
-{
-	var lastANSIIdx = -1;
-	for (var i = 0; i < pANSIRegexes.length; ++i)
-	{
-		var lastANSIIdxTmp = regexLastIndexOf(pStr, pANSIRegexes[i]);
-		if (lastANSIIdxTmp > lastANSIIdx)
-			lastANSIIdx = lastANSIIdxTmp;
-	}
-	return lastANSIIdx;
-}
-
-// Returns the index of the first ANSI code in a string.
-//
-// Parameters:
-//  pStr: The string to search in
-//  pANSIRegexes: An array of regular expressions to use for searching for ANSI codes
-//
-// Return value: The index of the first ANSI code in the string, or -1 if not found
-function idxOfFirstANSICode(pStr, pANSIRegexes)
-{
-	var firstANSIIdx = -1;
-	for (var i = 0; i < pANSIRegexes.length; ++i)
-	{
-		var firstANSIIdxTmp = regexFirstIndexOf(pStr, pANSIRegexes[i]);
-		if (firstANSIIdxTmp > firstANSIIdx)
-			firstANSIIdx = firstANSIIdxTmp;
-	}
-	return firstANSIIdx;
-}
-
-// Returns the number of times an ANSI code is matched in a string.
-//
-// Parameters:
-//  pStr: The string to search in
-//  pANSIRegexes: An array of regular expressions to use for searching for ANSI codes
-//
-// Return value: The number of ANSI code matches in the string
-function countANSICodes(pStr, pANSIRegexes)
-{
-	var ANSICount = 0;
-	for (var i = 0; i < pANSIRegexes.length; ++i)
-	{
-		var matches = pStr.match(pANSIRegexes[i]);
-		if (matches != null)
-			ANSICount += matches.length;
-	}
-	return ANSICount;
-}
-
-// Removes ANSI codes from a string.
-//
-// Parameters:
-//  pStr: The string to remove ANSI codes from
-//  pANSIRegexes: An array of regular expressions to use for searching for ANSI codes
-//
-// Return value: A version of the string without ANSI codes
-function removeANSIFromStr(pStr, pANSIRegexes)
-{
-	if (typeof(pStr) != "string")
-		return "";
-
-	var theStr = pStr;
-	for (var i = 0; i < pANSIRegexes.length; ++i)
-		theStr = theStr.replace(pANSIRegexes[i], "");
-	return theStr;
-}
-
-// Returns the last index in a string where a regex is found.
-// From this page:
-// http://stackoverflow.com/questions/273789/is-there-a-version-of-javascripts-string-indexof-that-allows-for-regular-expr
-//
-// Parameters:
-//  pStr: The string to search
-//  pRegex: The regular expression to match in the string
-//  pStartPos: Optional - The starting position in the string.  If this is not
-//             passed, then the end of the string will be used.
-//
-// Return value: The last index in the string where the regex is found, or -1 if not found.
-function regexLastIndexOf(pStr, pRegex, pStartPos)
-{
-	pRegex = (pRegex.global) ? pRegex : new RegExp(pRegex.source, "g" + (pRegex.ignoreCase ? "i" : "") + (pRegex.multiLine ? "m" : ""));
-	if (typeof(pStartPos) == "undefined")
-		pStartPos = pStr.length;
-	else if (pStartPos < 0)
-		pStartPos = 0;
-	var stringToWorkWith = pStr.substring(0, pStartPos + 1);
-	var lastIndexOf = -1;
-	var nextStop = 0;
-	while ((result = pRegex.exec(stringToWorkWith)) != null)
-	{
-		lastIndexOf = result.index;
-		pRegex.lastIndex = ++nextStop;
-	}
-    return lastIndexOf;
-}
-
-// Returns the first index in a string where a regex is found.
-//
-// Parameters:
-//  pStr: The string to search
-//  pRegex: The regular expression to match in the string
-//
-// Return value: The first index in the string where the regex is found, or -1 if not found.
-function regexFirstIndexOf(pStr, pRegex)
-{
-	pRegex = (pRegex.global) ? pRegex : new RegExp(pRegex.source, "g" + (pRegex.ignoreCase ? "i" : "") + (pRegex.multiLine ? "m" : ""));
-	var indexOfRegex = -1;
-	var nextStop = 0;
-	while ((result = pRegex.exec(pStr)) != null)
-	{
-		indexOfRegex = result.index;
-		pRegex.lastIndex = ++nextStop;
-	}
-    return indexOfRegex;
-}
-
-// Returns whether or not a string has any ASCII drawing characters (typically above ASCII value 127).
-//
-// Parameters:
-//  pText: The text to check
-//
-// Return value: Boolean - Whether or not the text has any ASCII drawing characters
-function textHasDrawingChars(pText)
-{
-	if (typeof(pText) !== "string" || pText.length == 0)
-		return false;
-
-	if (typeof(textHasDrawingChars.chars) === "undefined")
-	{
-		textHasDrawingChars.chars = [ascii(169), ascii(170)];
-		for (var asciiVal = 174; asciiVal <= 223; ++asciiVal)
-			textHasDrawingChars.chars.push(ascii(asciiVal));
-		textHasDrawingChars.chars.push(ascii(254));
-	}
-	var drawingCharsFound = false;
-	for (var i = 0; i < textHasDrawingChars.chars.length && !drawingCharsFound; ++i)
-		drawingCharsFound = drawingCharsFound || (pText.indexOf(textHasDrawingChars.chars[i]) > -1);
-	return drawingCharsFound;
-}
-
-// Returns a string with a character repeated a given number of times
-//
-// Parameters:
-//  pChar: The character to repeat in the string
-//  pNumTimes: The number of times to repeat the character
-//
-// Return value: A string with the given character repeated the given number of times
-function charStr(pChar, pNumTimes)
-{
-	if (typeof(pChar) !== "string" || pChar.length == 0 || typeof(pNumTimes) !== "number" || pNumTimes < 1)
-		return "";
-
-	var str = "";
-	for (var i = 0; i < pNumTimes; ++i)
-		str += pChar;
-	return str;
-}
-
-// Returns whether the logged-in user can view deleted messages.
-function canViewDeletedMsgs()
-{
-	var usersVDM = ((system.settings & SYS_USRVDELM) == SYS_USRVDELM);
-	var sysopVDM = ((system.settings & SYS_SYSVDELM) == SYS_SYSVDELM);
-	return (usersVDM || (user.is_sysop && sysopVDM));
-}
-
-// Returns whether or not a message header is a vote header
-//
-// Parameters:
-//  pMsgHdrOrIndex: A message header or index object
-//
-// Return value: Boolean - Whether or not the header is a vote header
-function isVoteHdr(pMsgHdrOrIndex)
-{
-	if (typeof(pMsgHdrOrIndex) !== "object" || !pMsgHdrOrIndex.hasOwnProperty("attr"))
-		return false;
-	return (((pMsgHdrOrIndex.attr & MSG_VOTE) == MSG_VOTE) || ((pMsgHdrOrIndex.attr & MSG_UPVOTE) == MSG_UPVOTE) || ((pMsgHdrOrIndex.attr & MSG_DOWNVOTE) == MSG_DOWNVOTE));
-}
-
-// Updates scan_ptr and/or last_read for a sub-board
-//
-// Parameters:
-//  pSubCode: The internal code for the sub-board being read
-//  pMsgHdr: A message header or index object (the message number will be used)
-//  pDoingMsgScan: Boolean - Whether or not a scan is being done
-function updateScanPtrAndOrLastRead(pSubCode, pMsgHdr, pDoingMsgScan)
-{
-	// If not reading personal email, then update the scan & last read message pointers.
-	if (pSubCode != "mail")
-	{
-		if (pDoingMsgScan)
-		{
-			if (typeof(msg_area.sub[pSubCode].scan_ptr) === "number")
-			{
-				if (!msgNumIsLatestMsgSpecialVal(msg_area.sub[pSubCode].scan_ptr) && msg_area.sub[pSubCode].scan_ptr < pMsgHdr.number)
-					msg_area.sub[pSubCode].scan_ptr = pMsgHdr.number;
-			}
-			else
-				msg_area.sub[pSubCode].scan_ptr = pMsgHdr.number;
-			//if (pMsgHdr.number > GetScanPtrOrLastMsgNum(pSubCode))
-			//	msg_area.sub[pSubCode].scan_ptr = pMsgHdr.number;
-
-			if (pMsgHdr.number > msg_area.sub[pSubCode].last_read)
-				msg_area.sub[pSubCode].last_read = pMsgHdr.number;
-		}
-		else
-		{
-			msg_area.sub[pSubCode].last_read = pMsgHdr.number;
-			//if (pMsgHdr.number > msg_area.sub[pSubCode].last_read)
-			//	msg_area.sub[pSubCode].last_read = pMsgHdr.number;
-		}
-	}
-}
-
-
-///////////////////////////////////////////////////////////////////////////////////
-// ChoiceScrollbox stuff (this was copied from SlyEdit_Misc.js; maybe there's a better way to do this)
-
-// Returns the minimum width for a ChoiceScrollbox
-function ChoiceScrollbox_MinWidth()
-{
-	return 73; // To leave room for the navigation text in the bottom border
-}
-
-// ChoiceScrollbox constructor
-//
-// Parameters:
-//  pLeftX: The horizontal component (column) of the upper-left coordinate
-//  pTopY: The vertical component (row) of the upper-left coordinate
-//  pWidth: The width of the box (including the borders)
-//  pHeight: The height of the box (including the borders)
-//  pTopBorderText: The text to include in the top border
-//  pCfgObj: The script/program configuration object (color settings are used)
-//  pAddTCharsAroundTopText: Optional, boolean - Whether or not to use left & right T characters
-//                           around the top border text.  Defaults to true.
-// pReplaceTopTextSpacesWithBorderChars: Optional, boolean - Whether or not to replace
-//                           spaces in the top border text with border characters.
-//                           Defaults to false.
-function ChoiceScrollbox(pLeftX, pTopY, pWidth, pHeight, pTopBorderText, pCfgObj,
-                         pAddTCharsAroundTopText, pReplaceTopTextSpacesWithBorderChars)
-{
-	if (pCfgObj == null || typeof(pCfgObj) !== "object")
-		pCfgObj = {};
-	if (pCfgObj.colors == null || typeof(pCfgObj.colors) !== "object")
-	{
-		pCfgObj.colors = {
-			listBoxBorder: "\x01n\x01g",
-			listBoxBorderText: "\x01n\x01b\x01h",
-			listBoxItemText: "\x01n\x01c",
-			listBoxItemHighlight: "\x01n\x01" + "4\x01w\x01h"
-		};
-	}
-	else
-	{
-		if (!pCfgObj.colors.hasOwnProperty("listBoxBorder"))
-			pCfgObj.colors.listBoxBorder = "\x01n\x01g";
-		if (!pCfgObj.colors.hasOwnProperty("listBoxBorderText"))
-			pCfgObj.colors.listBoxBorderText = "\x01n\x01b\x01h";
-		if (!pCfgObj.colors.hasOwnProperty("listBoxItemText"))
-			pCfgObj.colors.listBoxItemText = "\x01n\x01c";
-		if (!pCfgObj.colors.hasOwnProperty("listBoxItemHighlight"))
-			pCfgObj.colors.listBoxItemHighlight = "\x01n\x01" + "4\x01w\x01h";
-	}
-
-	// The default is to add left & right T characters around the top border
-	// text.  But also use pAddTCharsAroundTopText if it's a boolean.
-	var addTopTCharsAroundText = true;
-	if (typeof(pAddTCharsAroundTopText) == "boolean")
-		addTopTCharsAroundText = pAddTCharsAroundTopText;
-	// If pReplaceTopTextSpacesWithBorderChars is true, then replace the spaces
-	// in pTopBorderText with border characters.
-	if (pReplaceTopTextSpacesWithBorderChars)
-	{
-		var startIdx = 0;
-		var firstSpcIdx = pTopBorderText.indexOf(" ", 0);
-		// Look for the first non-space after firstSpaceIdx
-		var nonSpcIdx = -1;
-		for (var i = firstSpcIdx; (i < pTopBorderText.length) && (nonSpcIdx == -1); ++i)
-		{
-			if (pTopBorderText.charAt(i) != " ")
-				nonSpcIdx = i;
-		}
-		var firstStrPart = "";
-		var lastStrPart = "";
-		var numSpaces = 0;
-		while ((firstSpcIdx > -1) && (nonSpcIdx > -1))
-		{
-			firstStrPart = pTopBorderText.substr(startIdx, (firstSpcIdx-startIdx));
-			lastStrPart = pTopBorderText.substr(nonSpcIdx);
-			numSpaces = nonSpcIdx - firstSpcIdx;
-			if (numSpaces > 0)
-			{
-				pTopBorderText = firstStrPart + "\x01n" + pCfgObj.colors.listBoxBorder;
-				for (var i = 0; i < numSpaces; ++i)
-					pTopBorderText += HORIZONTAL_SINGLE;
-				pTopBorderText += "\x01n" + pCfgObj.colors.listBoxBorderText + lastStrPart;
-			}
-
-			// Look for the next space and non-space character after that.
-			firstSpcIdx = pTopBorderText.indexOf(" ", nonSpcIdx);
-			// Look for the first non-space after firstSpaceIdx
-			nonSpcIdx = -1;
-			for (var i = firstSpcIdx; (i < pTopBorderText.length) && (nonSpcIdx == -1); ++i)
-			{
-				if (pTopBorderText.charAt(i) != " ")
-					nonSpcIdx = i;
-			}
-		}
-	}
-
-	this.programCfgObj = pCfgObj;
-
-	var minWidth = ChoiceScrollbox_MinWidth();
-
-	this.dimensions = {
-		topLeftX: pLeftX,
-		topLeftY: pTopY,
-		width: 0,
-		height: pHeight,
-		bottomRightX: 0,
-		bottomRightY: 0
-	};
-	// Make sure the width is the minimum width
-	if ((pWidth < 0) || (pWidth < minWidth))
-		this.dimensions.width = minWidth;
-	else
-		this.dimensions.width = pWidth;
-	this.dimensions.bottomRightX = this.dimensions.topLeftX + this.dimensions.width - 1;
-	this.dimensions.bottomRightY = this.dimensions.topLeftY + this.dimensions.height - 1;
-
-	// The text item array and member variables relating to it and the items
-	// displayed on the screen during the input loop
-	this.txtItemList = [];
-	this.chosenTextItemIndex = -1;
-	this.topItemIndex = 0;
-	this.bottomItemIndex = 0;
-
-	// Top border string
-	var innerBorderWidth = this.dimensions.width - 2;
-	// Calculate the maximum top border text length to account for the left/right
-	// T chars and "Page #### of ####" text
-	var maxTopBorderTextLen = innerBorderWidth - (pAddTCharsAroundTopText ? 21 : 19);
-	if (strip_ctrl(pTopBorderText).length > maxTopBorderTextLen)
-		pTopBorderText = pTopBorderText.substr(0, maxTopBorderTextLen);
-	this.topBorder = "\x01n" + pCfgObj.colors.listBoxBorder + UPPER_LEFT_SINGLE;
-	if (addTopTCharsAroundText)
-		this.topBorder += RIGHT_T_SINGLE;
-	this.topBorder += "\x01n" + pCfgObj.colors.listBoxBorderText
-	               + pTopBorderText + "\x01n" + pCfgObj.colors.listBoxBorder;
-	if (addTopTCharsAroundText)
-		this.topBorder += LEFT_T_SINGLE;
-	const topBorderTextLen = strip_ctrl(pTopBorderText).length;
-	var numHorizBorderChars = innerBorderWidth - topBorderTextLen - 20;
-	if (addTopTCharsAroundText)
-		numHorizBorderChars -= 2;
-	for (var i = 0; i <= numHorizBorderChars; ++i)
-		this.topBorder += HORIZONTAL_SINGLE;
-	this.topBorder += RIGHT_T_SINGLE + "\x01n" + pCfgObj.colors.listBoxBorderText
-	               + "Page    1 of    1" + "\x01n" + pCfgObj.colors.listBoxBorder + LEFT_T_SINGLE
-	               + UPPER_RIGHT_SINGLE;
-
-	// Bottom border string
-	this.btmBorderNavText = "\x01n\x01h\x01c" + UP_ARROW + "\x01b, \x01c" + DOWN_ARROW + "\x01b, \x01cN\x01y)\x01bext, \x01cP\x01y)\x01brev, "
-	                      + "\x01cF\x01y)\x01birst, \x01cL\x01y)\x01bast, \x01cHOME\x01b, \x01cEND\x01b, \x01cEnter\x01y=\x01bSelect, "
-	                      + "\x01cESC\x01n\x01c/\x01h\x01cQ\x01y=\x01bEnd";
-	this.bottomBorder = "\x01n" + pCfgObj.colors.listBoxBorder + LOWER_LEFT_SINGLE
-	                  + RIGHT_T_SINGLE + this.btmBorderNavText + "\x01n" + pCfgObj.colors.listBoxBorder
-	                  + LEFT_T_SINGLE;
-	var numCharsRemaining = this.dimensions.width - strip_ctrl(this.btmBorderNavText).length - 6;
-	for (var i = 0; i < numCharsRemaining; ++i)
-		this.bottomBorder += HORIZONTAL_SINGLE;
-	this.bottomBorder += LOWER_RIGHT_SINGLE;
-
-	// Item format strings
-	this.listIemFormatStr = "\x01n" + pCfgObj.colors.listBoxItemText + "%-"
-	                      + +(this.dimensions.width-2) + "s";
-	this.listIemHighlightFormatStr = "\x01n" + pCfgObj.colors.listBoxItemHighlight + "%-"
-	                               + +(this.dimensions.width-2) + "s";
-
-	// Key functionality override function pointers
-	this.enterKeyOverrideFn = null;
-
-	// inputLoopeExitKeys is an object containing additional keypresses that will
-	// exit the input loop.
-	this.inputLoopExitKeys = {};
-
-	// For drawing the menu
-	this.pageNum = 0;
-	this.numPages = 0;
-	this.numItemsPerPage = 0;
-	this.maxItemWidth = 0;
-	this.pageNumTxtStartX = 0;
-
-	// Input loop quit override (to be used in overridden enter function if needed to quit the input loop there
-	this.continueInputLoopOverride = true;
-
-	// Object functions
-	this.addTextItem = ChoiceScrollbox_AddTextItem; // Returns the index of the item
-	this.getTextItem = ChoiceScrollbox_GetTextIem;
-	this.replaceTextItem = ChoiceScrollbox_ReplaceTextItem;
-	this.delTextItem = ChoiceScrollbox_DelTextItem;
-	this.chgCharInTextItem = ChoiceScrollbox_ChgCharInTextItem;
-	this.getChosenTextItemIndex = ChoiceScrollbox_GetChosenTextItemIndex;
-	this.setItemArray = ChoiceScrollbox_SetItemArray; // Sets the item array; returns whether or not it was set.
-	this.clearItems = ChoiceScrollbox_ClearItems; // Empties the array of items
-	this.setEnterKeyOverrideFn = ChoiceScrollbox_SetEnterKeyOverrideFn;
-	this.clearEnterKeyOverrideFn = ChoiceScrollbox_ClearEnterKeyOverrideFn;
-	this.addInputLoopExitKey = ChoiceScrollbox_AddInputLoopExitKey;
-	this.setBottomBorderText = ChoiceScrollbox_SetBottomBorderText;
-	this.drawBorder = ChoiceScrollbox_DrawBorder;
-	this.drawInnerMenu = ChoiceScrollbox_DrawInnerMenu;
-	this.refreshOnScreen = ChoiceScrollbox_RefreshOnScreen;
-	this.refreshItemCharOnScreen = ChoiceScrollbox_RefreshItemCharOnScreen;
-	// Does the input loop.  Returns an object with the following properties:
-	//  itemWasSelected: Boolean - Whether or not an item was selected
-	//  selectedIndex: The index of the selected item
-	//  selectedItem: The text of the selected item
-	//  lastKeypress: The last key pressed by the user
-	this.doInputLoop = ChoiceScrollbox_DoInputLoop;
-}
-function ChoiceScrollbox_AddTextItem(pTextLine, pStripCtrl)
-{
-   var stripCtrl = true;
-   if (typeof(pStripCtrl) == "boolean")
-      stripCtrl = pStripCtrl;
-
-   if (stripCtrl)
-      this.txtItemList.push(strip_ctrl(pTextLine));
-   else
-      this.txtItemList.push(pTextLine);
-   // Return the index of the added item
-   return this.txtItemList.length-1;
-}
-function ChoiceScrollbox_GetTextIem(pItemIndex)
-{
-   if (typeof(pItemIndex) != "number")
-      return "";
-   if ((pItemIndex < 0) || (pItemIndex >= this.txtItemList.length))
-      return "";
-
-   return this.txtItemList[pItemIndex];
-}
-function ChoiceScrollbox_ReplaceTextItem(pItemIndexOrStr, pNewItem)
-{
-   if (typeof(pNewItem) != "string")
-      return false;
-
-   // Find the item index
-   var itemIndex = -1;
-   if (typeof(pItemIndexOrStr) == "number")
-   {
-      if ((pItemIndexOrStr < 0) || (pItemIndexOrStr >= this.txtItemList.length))
-         return false;
-      else
-         itemIndex = pItemIndexOrStr;
-   }
-   else if (typeof(pItemIndexOrStr) == "string")
-   {
-      itemIndex = -1;
-      for (var i = 0; (i < this.txtItemList.length) && (itemIndex == -1); ++i)
-      {
-         if (this.txtItemList[i] == pItemIndexOrStr)
-            itemIndex = i;
-      }
-   }
-   else
-      return false;
-
-   // Replace the item
-   var replacedIt = false;
-   if ((itemIndex > -1) && (itemIndex < this.txtItemList.length))
-   {
-      this.txtItemList[itemIndex] = pNewItem;
-      replacedIt = true;
-   }
-   return replacedIt;
-}
-function ChoiceScrollbox_DelTextItem(pItemIndexOrStr)
-{
-   // Find the item index
-   var itemIndex = -1;
-   if (typeof(pItemIndexOrStr) == "number")
-   {
-      if ((pItemIndexOrStr < 0) || (pItemIndexOrStr >= this.txtItemList.length))
-         return false;
-      else
-         itemIndex = pItemIndexOrStr;
-   }
-   else if (typeof(pItemIndexOrStr) == "string")
-   {
-      itemIndex = -1;
-      for (var i = 0; (i < this.txtItemList.length) && (itemIndex == -1); ++i)
-      {
-         if (this.txtItemList[i] == pItemIndexOrStr)
-            itemIndex = i;
-      }
-   }
-   else
-      return false;
-
-   // Remove the item
-   var removedIt = false;
-   if ((itemIndex > -1) && (itemIndex < this.txtItemList.length))
-   {
-      this.txtItemList = this.txtItemList.splice(itemIndex, 1);
-      removedIt = true;
-   }
-   return removedIt;
-}
-function ChoiceScrollbox_ChgCharInTextItem(pItemIndexOrStr, pStrIndex, pNewText)
-{
-	// Find the item index
-	var itemIndex = -1;
-	if (typeof(pItemIndexOrStr) == "number")
-	{
-		if ((pItemIndexOrStr < 0) || (pItemIndexOrStr >= this.txtItemList.length))
-			return false;
-		else
-			itemIndex = pItemIndexOrStr;
-	}
-	else if (typeof(pItemIndexOrStr) == "string")
-	{
-		itemIndex = -1;
-		for (var i = 0; (i < this.txtItemList.length) && (itemIndex == -1); ++i)
-		{
-			if (this.txtItemList[i] == pItemIndexOrStr)
-				itemIndex = i;
-		}
-	}
-	else
-		return false;
-
-	// Change the character in the item
-	var changedIt = false;
-	if ((itemIndex > -1) && (itemIndex < this.txtItemList.length))
-	{
-		this.txtItemList[itemIndex] = chgCharInStr(this.txtItemList[itemIndex], pStrIndex, pNewText);
-		changedIt = true;
-	}
-	return changedIt;
-}
-function ChoiceScrollbox_GetChosenTextItemIndex()
-{
-   return this.chosenTextItemIndex;
-}
-function ChoiceScrollbox_SetItemArray(pArray, pStripCtrl)
-{
-	var safeToSet = false;
-	if (Object.prototype.toString.call(pArray) === "[object Array]")
-	{
-		if (pArray.length > 0)
-			safeToSet = (typeof(pArray[0]) == "string");
-		else
-			safeToSet = true; // It's safe to set an empty array
-	}
-
-	if (safeToSet)
-	{
-		delete this.txtItemList;
-		this.txtItemList = pArray;
-
-		var stripCtrl = true;
-		if (typeof(pStripCtrl) == "boolean")
-			stripCtrl = pStripCtrl;
-		if (stripCtrl)
-		{
-			// Remove attribute/color characters from the text lines in the array
-			for (var i = 0; i < this.txtItemList.length; ++i)
-				this.txtItemList[i] = strip_ctrl(this.txtItemList[i]);
-		}
-	}
-
-	return safeToSet;
-}
-function ChoiceScrollbox_ClearItems()
-{
-   this.txtItemList.length = 0;
-}
-function ChoiceScrollbox_SetEnterKeyOverrideFn(pOverrideFn)
-{
-   if (Object.prototype.toString.call(pOverrideFn) == "[object Function]")
-      this.enterKeyOverrideFn = pOverrideFn;
-}
-function ChoiceScrollbox_ClearEnterKeyOverrideFn()
-{
-   this.enterKeyOverrideFn = null;
-}
-function ChoiceScrollbox_AddInputLoopExitKey(pKeypress)
-{
-   this.inputLoopExitKeys[pKeypress] = true;
-}
-function ChoiceScrollbox_SetBottomBorderText(pText, pAddTChars, pAutoStripIfTooLong)
-{
-	if (typeof(pText) != "string")
-		return;
-
-	const innerWidth = (pAddTChars ? this.dimensions.width-4 : this.dimensions.width-2);
-
-	if (pAutoStripIfTooLong)
-	{
-		if (strip_ctrl(pText).length > innerWidth)
-			pText = pText.substr(0, innerWidth);
-	}
-
-	// Re-build the bottom border string based on the new text
-	this.bottomBorder = "\x01n" + this.programCfgObj.colors.listBoxBorder + LOWER_LEFT_SINGLE;
-	if (pAddTChars)
-		this.bottomBorder += RIGHT_T_SINGLE;
-	if (pText.indexOf("\x01n") != 0)
-		this.bottomBorder += "\x01n";
-	this.bottomBorder += pText + "\x01n" + this.programCfgObj.colors.listBoxBorder;
-	if (pAddTChars)
-		this.bottomBorder += LEFT_T_SINGLE;
-	var numCharsRemaining = this.dimensions.width - strip_ctrl(this.bottomBorder).length - 3;
-	for (var i = 0; i < numCharsRemaining; ++i)
-		this.bottomBorder += HORIZONTAL_SINGLE;
-	this.bottomBorder += LOWER_RIGHT_SINGLE;
-}
-function ChoiceScrollbox_DrawBorder()
-{
-	console.gotoxy(this.dimensions.topLeftX, this.dimensions.topLeftY);
-	console.print(this.topBorder);
-	// Draw the side border characters
-	var screenRow = this.dimensions.topLeftY + 1;
-	for (var screenRow = this.dimensions.topLeftY+1; screenRow <= this.dimensions.bottomRightY-1; ++screenRow)
-	{
-		console.gotoxy(this.dimensions.topLeftX, screenRow);
-		console.print(VERTICAL_SINGLE);
-		console.gotoxy(this.dimensions.bottomRightX, screenRow);
-		console.print(VERTICAL_SINGLE);
-	}
-	// Draw the bottom border
-	console.gotoxy(this.dimensions.topLeftX, this.dimensions.bottomRightY);
-	console.print(this.bottomBorder);
-}
-function ChoiceScrollbox_DrawInnerMenu(pSelectedIndex)
-{
-	var selectedIndex = (typeof(pSelectedIndex) == "number" ? pSelectedIndex : -1);
-	var startArrIndex = this.pageNum * this.numItemsPerPage;
-	var endArrIndex = startArrIndex + this.numItemsPerPage;
-	if (endArrIndex > this.txtItemList.length)
-		endArrIndex = this.txtItemList.length;
-	var selectedItemRow = this.dimensions.topLeftY+1;
-	var screenY = this.dimensions.topLeftY + 1;
-	for (var i = startArrIndex; i < endArrIndex; ++i)
-	{
-		console.gotoxy(this.dimensions.topLeftX+1, screenY);
-		if (i == selectedIndex)
-		{
-			printf(this.listIemHighlightFormatStr, this.txtItemList[i].substr(0, this.maxItemWidth));
-			selectedItemRow = screenY;
-		}
-		else
-			printf(this.listIemFormatStr, this.txtItemList[i].substr(0, this.maxItemWidth));
-		++screenY;
-	}
-	// If the current screen row is below the bottom row inside the box,
-	// continue and write blank lines to the bottom of the inside of the box
-	// to blank out any text that might still be there.
-	while (screenY < this.dimensions.topLeftY+this.dimensions.height-1)
-	{
-		console.gotoxy(this.dimensions.topLeftX+1, screenY);
-		printf(this.listIemFormatStr, "");
-		++screenY;
-	}
-
-	// Update the page number in the top border of the box.
-	console.gotoxy(this.pageNumTxtStartX, this.dimensions.topLeftY);
-	printf("\x01n" + this.programCfgObj.colors.listBoxBorderText + "Page %4d of %4d", this.pageNum+1, this.numPages);
-	return selectedItemRow;
-}
-function ChoiceScrollbox_RefreshOnScreen(pSelectedIndex)
-{
-	this.drawBorder();
-	this.drawInnerMenu(pSelectedIndex);
-}
-function ChoiceScrollbox_RefreshItemCharOnScreen(pItemIndex, pCharIndex)
-{
-	if ((typeof(pItemIndex) != "number") || (typeof(pCharIndex) != "number"))
-		return;
-	if ((pItemIndex < 0) || (pItemIndex >= this.txtItemList.length) ||
-	    (pItemIndex < this.topItemIndex) || (pItemIndex > this.bottomItemIndex))
-	{
-		return;
-	}
-	if ((pCharIndex < 0) || (pCharIndex >= this.txtItemList[pItemIndex].length))
-		return;
-
-	// Save the current cursor position so that we can restore it later
-	const originalCurpos = console.getxy();
-	// Go to the character's position on the screen and set the highlight or
-	// normal color, depending on whether the item is the currently selected item,
-	// then print the character on the screen.
-	const charScreenX = this.dimensions.topLeftX + 1 + pCharIndex;
-	const itemScreenY = this.dimensions.topLeftY + 1 + (pItemIndex - this.topItemIndex);
-	console.gotoxy(charScreenX, itemScreenY);
-	if (pItemIndex == this.chosenTextItemIndex)
-		console.print(this.programCfgObj.colors.listBoxItemHighlight);
-	else
-		console.print(this.programCfgObj.colors.listBoxItemText);
-	console.print(this.txtItemList[pItemIndex].charAt(pCharIndex));
-	// Move the cursor back to where it was originally
-	console.gotoxy(originalCurpos);
-}
-function ChoiceScrollbox_DoInputLoop(pDrawBorder)
-{
-	var retObj = {
-		itemWasSelected: false,
-		selectedIndex: -1,
-		selectedItem: "",
-		lastKeypress: ""
-	};
-
-	// Don't do anything if the item list doesn't contain any items
-	if (this.txtItemList.length == 0)
-		return retObj;
-
-	//////////////////////////////////
-	// Locally-defined functions
-
-	// This function returns the index of the bottommost item that
-	// can be displayed in the box.
-	//
-	// Parameters:
-	//  pArray: The array containing the items
-	//  pTopindex: The index of the topmost item displayed in the box
-	//  pNumItemsPerPage: The number of items per page
-	function getBottommostItemIndex(pArray, pTopIndex, pNumItemsPerPage)
-	{
-		var bottomIndex = pTopIndex + pNumItemsPerPage - 1;
-		// If bottomIndex is beyond the last index, then adjust it.
-		if (bottomIndex >= pArray.length)
-			bottomIndex = pArray.length - 1;
-		return bottomIndex;
-	}
-
-
-
-	//////////////////////////////////
-	// Code
-
-	// Variables for keeping track of the item list
-	this.numItemsPerPage = this.dimensions.height - 2;
-	this.topItemIndex = 0;    // The index of the message group at the top of the list
-	// Figure out the index of the last message group to appear on the screen.
-	this.bottomItemIndex = getBottommostItemIndex(this.txtItemList, this.topItemIndex, this.numItemsPerPage);
-	this.numPages = Math.ceil(this.txtItemList.length / this.numItemsPerPage);
-	const topIndexForLastPage = (this.numItemsPerPage * this.numPages) - this.numItemsPerPage;
-
-	if (pDrawBorder)
-		this.drawBorder();
-
-	// User input loop
-	// For the horizontal location of the page number text for the box border:
-	// Based on the fact that there can be up to 9999 text replacements and 10
-	// per page, there will be up to 1000 pages of replacements.  To write the
-	// text, we'll want to be 20 characters to the left of the end of the border
-	// of the box.
-	this.pageNumTxtStartX = this.dimensions.topLeftX + this.dimensions.width - 19;
-	this.maxItemWidth = this.dimensions.width - 2;
-	this.pageNum = 0;
-	var startArrIndex = 0;
-	this.chosenTextItemIndex = retObj.selectedIndex = 0;
-	var endArrIndex = 0; // One past the last array item
-	var curpos = { // For keeping track of the current cursor position
-		x: 0,
-		y: 0
-	};
-	var refreshList = true; // For screen redraw optimizations
-	this.continueInputLoopOverride = true;
-	var continueOn = true;
-	while (continueOn && this.continueInputLoopOverride)
-	{
-		if (refreshList)
-		{
-			this.bottomItemIndex = getBottommostItemIndex(this.txtItemList, this.topItemIndex, this.numItemsPerPage);
-
-			// Write the list of items for the current page.  Also, drawInnerMenu()
-			// will return the selected item row.
-			var selectedItemRow = this.drawInnerMenu(retObj.selectedIndex);
-
-			// Just for sane appearance: Move the cursor to the first character of
-			// the currently-selected row and set the appropriate color.
-			curpos.x = this.dimensions.topLeftX+1;
-			curpos.y = selectedItemRow;
-			console.gotoxy(curpos.x, curpos.y);
-			console.print(this.programCfgObj.colors.listBoxItemHighlight);
-
-			refreshList = false;
-		}
-
-		// Get a key from the user (upper-case) and take action based upon it.
-		retObj.lastKeypress = getKeyWithESCChars(K_UPPER|K_NOCRLF|K_NOSPIN, this.programCfgObj);
-		switch (retObj.lastKeypress)
-		{
-			case 'N': // Next page
-			case KEY_PAGE_DOWN:
-				refreshList = (this.pageNum < this.numPages-1);
-				if (refreshList)
-				{
-					++this.pageNum;
-					this.topItemIndex += this.numItemsPerPage;
-					this.chosenTextItemIndex = retObj.selectedIndex = this.topItemIndex;
-					// Note: this.bottomItemIndex is refreshed at the top of the loop
-				}
-				break;
-			case 'P': // Previous page
-			case KEY_PAGE_UP:
-				refreshList = (this.pageNum > 0);
-				if (refreshList)
-				{
-					--this.pageNum;
-					this.topItemIndex -= this.numItemsPerPage;
-					this.chosenTextItemIndex = retObj.selectedIndex = this.topItemIndex;
-					// Note: this.bottomItemIndex is refreshed at the top of the loop
-				}
-				break;
-			case 'F': // First page
-				refreshList = (this.pageNum > 0);
-				if (refreshList)
-				{
-					this.pageNum = 0;
-					this.topItemIndex = 0;
-					this.chosenTextItemIndex = retObj.selectedIndex = this.topItemIndex;
-					// Note: this.bottomItemIndex is refreshed at the top of the loop
-				}
-				break;
-			case 'L': // Last page
-				refreshList = (this.pageNum < this.numPages-1);
-				if (refreshList)
-				{
-					this.pageNum = this.numPages-1;
-					this.topItemIndex = topIndexForLastPage;
-					this.chosenTextItemIndex = retObj.selectedIndex = this.topItemIndex;
-					// Note: this.bottomItemIndex is refreshed at the top of the loop
-				}
-				break;
-			case KEY_UP:
-				// Move the cursor up one item
-				if (retObj.selectedIndex > 0)
-				{
-					// If the previous item index is on the previous page, then we'll
-					// want to display the previous page.
-					var previousItemIndex = retObj.selectedIndex - 1;
-					if (previousItemIndex < this.topItemIndex)
-					{
-						--this.pageNum;
-						this.topItemIndex -= this.numItemsPerPage;
-						// Note: this.bottomItemIndex is refreshed at the top of the loop
-						refreshList = true;
-					}
-					else
-					{
-						// Display the current line un-highlighted
-						console.gotoxy(this.dimensions.topLeftX+1, curpos.y);
-						printf(this.listIemFormatStr, this.txtItemList[retObj.selectedIndex].substr(0, this.maxItemWidth));
-						// Display the previous line highlighted
-						curpos.x = this.dimensions.topLeftX+1;
-						--curpos.y;
-						console.gotoxy(curpos);
-						printf(this.listIemHighlightFormatStr, this.txtItemList[previousItemIndex].substr(0, this.maxItemWidth));
-						console.gotoxy(curpos); // Move the cursor into place where it should be
-						refreshList = false;
-					}
-					this.chosenTextItemIndex = retObj.selectedIndex = previousItemIndex;
-				}
-				break;
-			case KEY_DOWN:
-				// Move the cursor down one item
-				if (retObj.selectedIndex < this.txtItemList.length - 1)
-				{
-					// If the next item index is on the next page, then we'll want to
-					// display the next page.
-					var nextItemIndex = retObj.selectedIndex + 1;
-					if (nextItemIndex > this.bottomItemIndex)
-					{
-						++this.pageNum;
-						this.topItemIndex += this.numItemsPerPage;
-						// Note: this.bottomItemIndex is refreshed at the top of the loop
-						refreshList = true;
-					}
-					else
-					{
-						// Display the current line un-highlighted
-						console.gotoxy(this.dimensions.topLeftX+1, curpos.y);
-						printf(this.listIemFormatStr, this.txtItemList[retObj.selectedIndex].substr(0, this.maxItemWidth));
-						// Display the previous line highlighted
-						curpos.x = this.dimensions.topLeftX+1;
-						++curpos.y;
-						console.gotoxy(curpos);
-						printf(this.listIemHighlightFormatStr, this.txtItemList[nextItemIndex].substr(0, this.maxItemWidth));
-						console.gotoxy(curpos); // Move the cursor into place where it should be
-						refreshList = false;
-					}
-					this.chosenTextItemIndex = retObj.selectedIndex = nextItemIndex;
-				}
-				break;
-			case KEY_HOME: // Go to the first row in the box
-				if (retObj.selectedIndex > this.topItemIndex)
-				{
-					// Display the current line un-highlighted
-					console.gotoxy(this.dimensions.topLeftX+1, curpos.y);
-					printf(this.listIemFormatStr, this.txtItemList[retObj.selectedIndex].substr(0, this.maxItemWidth));
-					// Select the top item, and display it highlighted.
-					this.chosenTextItemIndex = retObj.selectedIndex = this.topItemIndex;
-					curpos.x = this.dimensions.topLeftX+1;
-					curpos.y = this.dimensions.topLeftY+1;
-					console.gotoxy(curpos);
-					printf(this.listIemHighlightFormatStr, this.txtItemList[retObj.selectedIndex].substr(0, this.maxItemWidth));
-					console.gotoxy(curpos); // Move the cursor into place where it should be
-					refreshList = false;
-				}
-				break;
-			case KEY_END: // Go to the last row in the box
-				if (retObj.selectedIndex < this.bottomItemIndex)
-				{
-					// Display the current line un-highlighted
-					console.gotoxy(this.dimensions.topLeftX+1, curpos.y);
-					printf(this.listIemFormatStr, this.txtItemList[retObj.selectedIndex].substr(0, this.maxItemWidth));
-					// Select the bottommost item, and display it highlighted.
-					this.chosenTextItemIndex = retObj.selectedIndex = this.bottomItemIndex;
-					curpos.x = this.dimensions.topLeftX+1;
-					curpos.y = this.dimensions.bottomRightY-1;
-					console.gotoxy(curpos);
-					printf(this.listIemHighlightFormatStr, this.txtItemList[retObj.selectedIndex].substr(0, this.maxItemWidth));
-					console.gotoxy(curpos); // Move the cursor into place where it should be
-					refreshList = false;
-				}
-				break;
-			case KEY_ENTER:
-				// If the enter key override function is set, then call it and pass
-				// this object into it.  Otherwise, just select the item and quit.
-				if (this.enterKeyOverrideFn !== null)
-				this.enterKeyOverrideFn(this);
-				else
-				{
-					retObj.itemWasSelected = true;
-					// Note: retObj.selectedIndex is already set.
-					retObj.selectedItem = this.txtItemList[retObj.selectedIndex];
-					refreshList = false;
-					continueOn = false;
-				}
-				break;
-			case KEY_ESC: // Quit
-			case CTRL_A:  // Quit
-			case 'Q':     // Quit
-				this.chosenTextItemIndex = retObj.selectedIndex = -1;
-				refreshList = false;
-				continueOn = false;
-				break;
-			default:
-				// If the keypress is an additional key to exit the input loop, then
-				// do so.
-				if (this.inputLoopExitKeys.hasOwnProperty(retObj.lastKeypress))
-				{
-					this.chosenTextItemIndex = retObj.selectedIndex = -1;
-					refreshList = false;
-					continueOn = false;
-				}
-				else
-				{
-					// Unrecognized command.  Don't refresh the list of the screen.
-					refreshList = false;
-				}
-				break;
-		}
-	}
-
-	this.continueInputLoopOverride = true; // Reset
-
-	console.attributes = "N"; // To prevent outputting highlight colors, etc..
-	return retObj;
-}
-
-///////////////////////////////////////////////////////////////////////////////////
-
-// Writes a default twitlist for the user if it doesn't exist
-function writeDefaultUserTwitListIfNotExist()
-{
-	if (file_exists(gUserTwitListFilename))
-		return;
-
-	var outFile = new File(gUserTwitListFilename);
-	if (outFile.open("w"))
-	{
-		outFile.writeln("; This is a personal twitlist for Digital Distortion Message Reader to block");
-		outFile.writeln("; messages from (and to) certain usernames. The intention is that if you are");
-		outFile.writeln("; being harassed by a specific person, or you simply do not wish to see their");
-		outFile.writeln("; messages, you can filter that person by adding their name (or email address)");
-		outFile.writeln("; to this file.");
-
-		outFile.close();
-	}
-}
-
-// Returns whether 2 arrays have the same values
-function arraysHaveSameValues(pArray1, pArray2)
-{
-	if (pArray1 == null && pArray2 == null)
-		return true;
-	else if (pArray1 != null && pArray2 == null)
-		return false;
-	else if (pArray1 == null && pArray2 != null)
-		return false;
-
-	var arraysHaveSameValues = true;
-	if (pArray1.length != pArray2.length)
-		arraysHaveSameValues = false;
-	else
-	{
-		for (var a1i = 0; a1i < pArray1.length && arraysHaveSameValues; ++a1i)
-		{
-			var seenInArray2 = false;
-			for (var a2i = 0; a2i < pArray2.length && !seenInArray2; ++a2i)
-				seenInArray2 = (pArray2[a2i] == pArray1[a1i]);
-			arraysHaveSameValues = seenInArray2;
-		}
-	}
-	return arraysHaveSameValues;
-}
-
-// Returns whether or not the sender of a message is a sysop.
-//
-// Parameters:
-//  pMsgHdr: A message header
-//
-// Return value: Boolean: Whether or not the sender of the message is a sysop
-function msgSenderIsASysop(pMsgHdr)
-{
-	if (typeof(pMsgHdr) !== "object")
-		return false;
-
-	var senderUserNum = 0;
-	if (pMsgHdr.hasOwnProperty("sender_userid"))
-		senderUserNum = system.matchuser(pMsgHdr.sender_userid);
-	else if (pMsgHdr.hasOwnProperty("from"))
-	{
-		senderUserNum = system.matchuser(pMsgHdr.from);
-		if (senderUserNum < 1)
-			senderUserNum = system.matchuserdata(U_NAME, pMsgHdr.from);
-	}
-
-	var senderIsSysop = false;
-	if (senderUserNum >= 1)
-	{
-		if (senderUserNum == 1)
-			senderIsSysop = true;
-		else
-		{
-			var senderUser = new User(senderUserNum);
-			senderIsSysop = senderUser.is_sysop;
-		}
-	}
-	return senderIsSysop;
-}
-
-// Gets the quote wrap settings for an external editor
-//
-// Parameters:
-//  pEditorCode: The internal code of an external editor
-//
-// Return value: An object containing the following properties:
-//               quoteWrapEnabled: Boolean: Whether or not quote wrapping is enabled for the editor
-//               quoteWrapCols: The number of columns to wrap quote lines
-//  If the given editor code is not found, quoteWrapEnabled will be false and quoteWrapCols will be -1
-function getExternalEditorQuoteWrapCfgFromSCFG(pEditorCode)
-{
-	var retObj = {
-		quoteWrapEnabled: false,
-		quoteWrapCols: -1
-	};
-
-	if (typeof(pEditorCode) !== "string")
-		return retObj;
-	if (pEditorCode.length == 0)
-		return retObj;
-
-	var editorCode = pEditorCode.toLowerCase();
-	if (!xtrn_area.editor.hasOwnProperty(editorCode))
-		return retObj;
-
-	// Set up a cache so that we don't have to keep repeatedly parsing the Synchronet
-	// config every time the user replies to a message
-	if (typeof(getExternalEditorQuoteWrapCfgFromSCFG.cache) === "undefined")
-		getExternalEditorQuoteWrapCfgFromSCFG.cache = {};
-	// If we haven't looked up the quote wrap cols setting yet, then do so; otherwise, use the
-	// cached setting.
-	if (!getExternalEditorQuoteWrapCfgFromSCFG.cache.hasOwnProperty(editorCode))
-	{
-		if ((xtrn_area.editor[editorCode].settings & XTRN_QUOTEWRAP) == XTRN_QUOTEWRAP)
-		{
-			retObj.quoteWrapEnabled = true;
-			retObj.quoteWrapCols = console.screen_columns - 1;
-
-			// For Synchronet 3.20 and newer, read the quote wrap setting from xtrn.ini
-			if (system.version_num >= 32000)
-			{
-				// The INI section for the editor should be something like [editor:SLYEDICE], and
-				// it should have a quotewrap_cols property
-				var xtrnIniFile = new File(system.ctrl_dir + "xtrn.ini");
-				if (xtrnIniFile.open("r"))
-				{
-					var quoteWrapCols = xtrnIniFile.iniGetValue("editor:" + pEditorCode.toUpperCase(), "quotewrap_cols", console.screen_columns - 1);
-					if (quoteWrapCols > 0)
-						retObj.quoteWrapCols = quoteWrapCols;
-					xtrnIniFile.close();
-				}
-			}
-			else
-			{
-				// Synchronet below version 3.20: Read the quote wrap setting from xtrn.cnf
-				var cnflib = load({}, "cnflib.js");
-				var xtrnCnf = cnflib.read("xtrn.cnf");
-				if (typeof(xtrnCnf) === "object")
-				{
-					for (var i = 0; i < xtrnCnf.xedit.length; ++i)
-					{
-						if (xtrnCnf.xedit[i].code.toLowerCase() == editorCode)
-						{
-							if (xtrnCnf.xedit[i].hasOwnProperty("quotewrap_cols"))
-							{
-								if (xtrnCnf.xedit[i].quotewrap_cols > 0)
-									retObj.quoteWrapCols = xtrnCnf.xedit[i].quotewrap_cols;
-							}
-							break;
-						}
-					}
-				}
-			}
-		}
-		getExternalEditorQuoteWrapCfgFromSCFG.cache[editorCode] = retObj;
-	}
-	else
-		retObj = getExternalEditorQuoteWrapCfgFromSCFG.cache[editorCode];
-
-	return retObj;
-}
-
-// Changes a character in a string, and returns the new string.  If any of the
-// parameters are invalid, then the original string will be returned.
-//
-// Parameters:
-//  pStr: The original string
-//  pCharIndex: The index of the character to replace
-//  pNewText: The new character or text to place at that position in the string
-//
-// Return value: The new string
-function chgCharInStr(pStr, pCharIndex, pNewText)
-{
-	if (typeof(pStr) != "string")
-		return "";
-	if ((pCharIndex < 0) || (pCharIndex >= pStr.length))
-		return pStr;
-	if (typeof(pNewText) != "string")
-		return pStr;
-
-	return (pStr.substr(0, pCharIndex) + pNewText + pStr.substr(pCharIndex+1));
-}
-
-// Given a string of attribute characters, this function inserts the control code
-// in front of each attribute character and returns the new string.
-//
-// Parameters:
-//  pAttrCodeCharStr: A string of attribute characters (i.e., "YH" for yellow high)
-//
-// Return value: A string with the control character inserted in front of the attribute characters
-function attrCodeStr(pAttrCodeCharStr)
-{
-	if (typeof(pAttrCodeCharStr) !== "string")
-		return "";
-
-	var str = "";
-	// See this page for Synchronet color attribute codes:
-	// http://wiki.synchro.net/custom:ctrl-a_codes
-	for (var i = 0; i < pAttrCodeCharStr.length; ++i)
-	{
-		var currentChar = pAttrCodeCharStr.charAt(i);
-		if (/[krgybmcwKRGYBMCWHhIiEeFfNn01234567]/.test(currentChar))
-			str += "\x01" + currentChar;
-	}
-	return str;
-}
-
-// Replaces @-codes in a string and removes any newlines and carriage returns from the end
-// of the string
-//
-// Parameters:
-//  pText: The text to modify
-//
-// Return value: The text with @-codes replaced and newlines & carriage returns removed
-//               from the end of the text
-function replaceAtCodesAndRemoveCRLFs(pText)
-{
-	if (typeof(pText) !== "string")
-		return "";
-
-	var formattedText = replaceAtCodesInStr(pText);
-	formattedText = word_wrap(formattedText, console.screen_columns-1, formattedText.length, false).replace(/\r|\n/g, "\r\n");
-	while (formattedText.lastIndexOf("\r\n") == formattedText.length-2)
-		formattedText = formattedText.substr(0, formattedText.length-2);
-	while (formattedText.lastIndexOf("\r") == formattedText.length-1)
-		formattedText = formattedText.substr(0, formattedText.length-1);
-	while (formattedText.lastIndexOf("\n") == formattedText.length-1)
-		formattedText = formattedText.substr(0, formattedText.length-1);
-	return formattedText;
-}
-
-function getLatestPostTimeWithMsgbase(pMsgbase, pSubCode)
-{
-	if (typeof(pMsgbase) !== "object")
-		return 0;
-	if (!pMsgbase.is_open)
-		return 0;
-
-	var latestMsgTimestamp = 0;
-	if (pMsgbase.total_msgs > 0)
-	{
-		var msgIdx = pMsgbase.total_msgs - 1;
-		var msgHeader = pMsgbase.get_msg_header(true, msgIdx, false);
-		while (!isReadableMsgHdr(msgHeader, pSubCode) && (msgIdx >= 0))
-		{
-			// TODO: I think we should be able to call get_msg_header() and get valid vote information,
-			// but that doesn't seem to be the case:
-			msgHeader = pMsgbase.get_msg_header(true, --msgIdx, true, true);
-		}
-		if (this.msgAreaList_lastImportedMsg_showImportTime)
-			latestMsgTimestamp = msgHeader.when_imported_time;
-		else
-		{
-			var msgWrittenLocalTime = msgWrittenTimeToLocalBBSTime(msgHeader);
-			if (msgWrittenLocalTime != -1)
-				latestMsgTimestamp = msgWrittenTimeToLocalBBSTime(msgHeader);
-			else
-				latestMsgTimestamp = msgHeader.when_written_time;
-		}
-	}
-	return latestMsgTimestamp;
-}
-
-// Given a messagebase and message header object, this returns a mode flag for
-// use with console.print() to affect the character set based on the terminal.
-function msg_pmode(pMsgbase, pMsgHdr)
-{
-	var pmode = pMsgHdr.hasOwnProperty("is_utf8") && pMsgHdr.is_utf8 ? P_UTF8 : P_NONE;
-	if (pMsgHdr.from_ext !== "1")
-		pmode |= P_NOATCODES;
-	if (pMsgbase.cfg)
-	{
-		pmode |= pMsgbase.cfg.print_mode;
-		pmode &= ~pMsgbase.cfg.print_mode_neg;
-	}
-	return pmode;
-}
-
-// Returns whether a value is a valid scan scope value.
-function isValidScanScopeVal(pScanScope)
-{
-	return (typeof(pScanScope) === "number" && (pScanScope == SCAN_SCOPE_SUB_BOARD || pScanScope == SCAN_SCOPE_GROUP || pScanScope == SCAN_SCOPE_ALL));
-}
-
-// Lets the user (if they're a sysop) apply a quick-validation value (from SCFG > System > Security Options > Quick-Validation Values)
-// to a user by username
-//
-// Parameters:
-//  pUsername: The name of the user to apply the quick-validation set to
-//  pUseANSI: Optional boolean - Whether or not to use ANSI
-//  pQuickValSetIdx: Optional - The index of the quick validation set to apply (0-9, as they
-//                   appear in SCFG). If this is omitted, a menu will be displayed to allow
-//                   choosing one of them
-//
-// Return value: An object containing the following properties:
-//               needWholeScreenRefresh: Boolean - Whether or not the whole screen needs to be
-//                                       refreshed (i.e., when the user has edited their twitlist)
-//               refreshBottomLine: Boolean - Whether or not the bottom line on the screen needs to be refreshed
-//               optionBoxTopLeftX: The top-left screen column of the option box (0 if none was used)
-//               optionBoxTopLeftY: The top-left screen row of the option box (0 if none was used)
-//               optionBoxWidth: The width of the option box (0 if none was used)
-//               optionBoxHeight: The height of the option box (0 if none was used)
-function quickValidateLocalUser(pUsername, pUseANSI, pQuickValSetIdx)
-{
-	var retObj = {
-		needWholeScreenRefresh: false,
-		refreshBottomLine: false,
-		optionBoxTopLeftX: 0,
-		optionBoxTopLeftY: 0,
-		optionBoxWidth: 0,
-		optionBoxHeight: 0
-	};
-
-	if (!user.is_sysop)
-		return retObj;
-	if (typeof(pUsername) !== "string" || pUsername == "")
-		return retObj;
-
-	var useANSI = typeof(pUseANSI) === "boolean" ? pUseANSI : console.term_supports(USER_ANSI);
-
-	var userNum = system.matchuser(pUsername);
-	if (userNum == 0)
-	{
-		var msgText = bbs.text(UnknownUser).replace(/\r|\n/g, ""); // Or UNKNOWN_USER
-		msgText += "\x01."; // Delay for 2 seconds
-		if (useANSI)
-		{
-			retObj.refreshBottomLine = true;
-			console.gotoxy(1, console.screen_columns);
-			console.cleartoeol("\x01n");
-			console.gotoxy(1, console.screen_columns);
-			console.attributes = msgAttrs;
-			console.putmsg(msgText);
-		}
-		else
-		{
-			console.attributes = "N";
-			console.crlf();
-			console.putmsg(msgText);
-		}
-		return retObj;
-	}
-
-
-	// Get an array of the quick-validation values from SCFG
-	var quickValidationVals = getQuickValidationVals();
-	// If pQuickValSetIdx is a number specifying a valid index, then use it; otherwise, display
-	// a menu of the quick-validation values to choose from
-	var quickValidationValSet = null;
-	var displayedMenu = false;
-	if (typeof(pQuickValSetIdx) === "number" && pQuickValSetIdx >= 0 && pQuickValSetIdx < quickValidationVals.length)
-		quickValidationValSet = quickValidationVals[pQuickValSetIdx];
-	else
-	{
-		// No valid validation set index given; display the menu
-		var menuX = 2;
-		var menuY = 3;
-		var valHdrLineWithUsername = "Quick validation for " + pUsername;
-		var menuHdrStr = " Level E 1 2 3 4 C E R";
-		console.attributes = "N";
-		if (useANSI)
-		{
-			menuX = 25;
-			menuY = 12;
-		}
-		else
-		{
-			menuX = 2;
-			menuY = 3;
-			menuHdrStr = "   " + menuHdrStr;
-			console.clear("\x01n");
-			console.print(valHdrLineWithUsername);
-			console.crlf();
-			console.print("Quick-Validation sets:");
-			console.crlf();
-			console.print(menuHdrStr);
-			console.crlf();
-		}
-		// Create the menu of quick-validation sets
-		var valSetMenu = makeQuickValidationValLightbarMenu(useANSI, menuX, menuY, quickValidationVals);
-		// If using ANSI, draw some border characters at the left, bottom, and right sides of the menu
-		if (useANSI)
-		{
-			// Screen refresh values for returning from this function
-			retObj.needWholeScreenRefresh = false;
-			retObj.optionBoxTopLeftX = valSetMenu.pos.x-1;
-			retObj.optionBoxTopLeftY = valSetMenu.pos.y-4;
-			retObj.optionBoxWidth = valSetMenu.size.width+2;
-			retObj.optionBoxHeight = valSetMenu.size.height+5;
-			retObj.refreshBottomLine = false;
-
-			// Display the menu
-			console.attributes = "GH";
-			// Top border
-			var screenRow = valSetMenu.pos.y - 4;
-			console.gotoxy(valSetMenu.pos.x-1, screenRow);
-			console.print(UPPER_LEFT_DOUBLE);
-			for (var i = 0; i < valSetMenu.size.width; ++i)
-				console.print(HORIZONTAL_DOUBLE);
-			console.print(UPPER_RIGHT_DOUBLE);
-			// Side border characters
-			screenRow = valSetMenu.pos.y - 3;
-			var height = valSetMenu.size.height + 3;
-			for (var i = 0; i < height; ++i)
-			{
-				console.gotoxy(valSetMenu.pos.x-1, screenRow);
-				console.print(VERTICAL_DOUBLE);
-				console.gotoxy(valSetMenu.pos.x+valSetMenu.size.width, screenRow);
-				console.print(VERTICAL_DOUBLE);
-				++screenRow;
-			}
-			// Bottom border characters
-			screenRow = valSetMenu.pos.y+valSetMenu.size.height;
-			console.gotoxy(valSetMenu.pos.x-1, screenRow);
-			console.print(LOWER_LEFT_DOUBLE);
-			for (var i = 0; i < valSetMenu.size.width; ++i)
-				console.print(HORIZONTAL_DOUBLE);
-			console.print(LOWER_RIGHT_DOUBLE);
-			console.attributes = "N";
-
-			console.gotoxy(menuX, menuY-3);
-			//printf("%-*s", valSetMenu.size.width, "Quick validation for:");
-			printf("%-*s", valSetMenu.size.width, "Quick validation sets:");
-			console.gotoxy(menuX, menuY-2);
-			printf("%-*s", valSetMenu.size.width, pUsername.substr(0, valSetMenu.size.width));
-			console.gotoxy(menuX, menuY-1);
-			console.print(menuHdrStr);
-		}
-		else
-		{
-			retObj.needWholeScreenRefresh = true;
-			// Use green for the item color and high cyan for the item number color
-			valSetMenu.colors.itemColor = "\x01n\x01g";
-			valSetMenu.colors.itemNumColor = "\x01n\x01c\x01h";
-		}
-		quickValidationValSet = valSetMenu.GetVal();
-		displayedMenu = true;
-		console.attributes = "N";
-	}
-	var statusMsg = "";
-	var msgAttrs = "N";
-	if (quickValidationValSet != null && typeof(quickValidationValSet) === "object")
-	{
-		var userToEdit = new User(userNum);
-		/*
-		user.security properties
-		Name			Type	Ver		Description
-		password		string	3.10	password
-		password_date	number	3.10	date password last modified (time_t format)
-		level			number	3.10	security level (0-99)
-		flags1			number	3.10	flag set #1 (bitfield) can use +/-[A-?] notation
-		flags2			number	3.10	flag set #2 (bitfield) can use +/-[A-?] notation
-		flags3			number	3.10	flag set #3 (bitfield) can use +/-[A-?] notation
-		flags4			number	3.10	flag set #4 (bitfield) can use +/-[A-?] notation
-		exemptions		number	3.10	exemption flags (bitfield) can use +/-[A-?] notation
-		restrictions	number	3.10	restriction flags (bitfield) can use +/-[A-?] notation
-		credits			number	3.10	credits
-		free_credits	number	3.10	free credits (for today only)
-		minutes			number	3.10	extra minutes (time bank)
-		extra_time		number	3.10	extra minutes (for today only)
-		expiration_date	number	3.10	expiration date/time (time_t format)
-		*/
-		// Each object in the returned array will have the following properties:
-		//  level (numeric)
-		//  expire
-		//  flags1
-		//  flags2
-		//  flags3
-		//  flags4
-		//  credits
-		//  exemptions
-		//  restrictions
-		userToEdit.security.level = quickValidationValSet.level;
-		userToEdit.security.flags1 |= quickValidationValSet.flags1;
-		userToEdit.security.flags2 |= quickValidationValSet.flags2;
-		userToEdit.security.flags3 |= quickValidationValSet.flags3;
-		userToEdit.security.flags4 |= quickValidationValSet.flags4;
-		userToEdit.security.exemptions |= quickValidationValSet.exemptions;
-		userToEdit.security.restrictions |= quickValidationValSet.restrictions;
-		userToEdit.security.credits = quickValidationValSet.credits;
-		statusMsg = "Validation set applied";
-		msgAttrs = "CH";
-	}
-	else
-	{
-		statusMsg = "Aborted";
-		msgAttrs = "YH";
-	}
-
-	// Display the final status message
-	if (useANSI)
-	{
-		if (displayedMenu)
-		{
-			// Clear the box on the screen and write that the validation set was applied to the user
-			var topBoxScreenRow = valSetMenu.pos.y - 3;
-			clearScreenRectangle(valSetMenu.pos.x, topBoxScreenRow, valSetMenu.size.width, valSetMenu.size.height+3);
-			console.gotoxy(valSetMenu.pos.x, topBoxScreenRow);
-			console.attributes = msgAttrs;
-			console.print(statusMsg + "\x01;\x01;");
-		}
-		else
-		{
-			retObj.refreshBottomLine = true;
-			console.gotoxy(1, console.screen_columns);
-			console.cleartoeol("\x01n");
-			console.gotoxy(1, console.screen_columns);
-			console.attributes = msgAttrs;
-			console.print(statusMsg + "\x01;\x01;");
-		}
-	}
-	else
-	{
-		console.crlf();
-		console.attributes = msgAttrs;
-		console.print(statusMsg + "\x01n\r\n\x01p");
-	}
-
-	console.attributes = "N";
-
-	return retObj;
-}
-
-// Creates a DDLightbarMenu object to use for the quick-validation values.
-//
-// Parameters:
-//  pUseANSI: Boolean - Whether or not to enable the use of ANSI
-//  pMenuX: The top-left X coordinate for the menu
-//  pMenuY: The top-left Y coordinate for the menu
-//  pQuickValidationVals: An array of the quick-validation values from SCFG > System > Security Settings > Quick-Validation Values
-//
-// Return value: A DDLightbarMenu object to use for the quick-validation values menu
-function makeQuickValidationValLightbarMenu(pUseANSI, pMenuX, pMenuY, pQuickValidationVals)
-{
-	var useANSI = (typeof(pUseANSI) === "boolean" ? pUseANSI : true);
-
-	//  Level E 1 2 3 4 C E R
-	//     60 Y Y Y Y Y Y Y Y
-	var quickValsMenuWidth = 22; //console.screen_columns - 4;
-	if (!console.term_supports(USER_ANSI))
-		quickValsMenuWidth += 3;
-	var quickValsMenuHeight = pQuickValidationVals.length;
-	var quickValsMenu = new DDLightbarMenu(pMenuX, pMenuY, quickValsMenuWidth, quickValsMenuHeight);
-	quickValsMenu.AddAdditionalQuitKeys("qQ");
-	quickValsMenu.scrollbarEnabled = true;
-	quickValsMenu.borderEnabled = false;
-	quickValsMenu.allowANSI = useANSI;
-
-	//SetBorderChars(pBorderChars)
-	//"upperLeft", "upperRight", "lowerLeft", "lowerRight", "top", "bottom", "left", "right"
-
-	var colors = {
-		level: "\x01n\x01w",
-		levelHi: "\x01n\x01w\x014",
-		YN: "\x01n\x01w",
-		YNHi: "\x01n\x01w\x014"
-	};
-	var itemTextIdxes = {
-		levelStart: 0,
-		levelEnd: 7,
-		YN1Start: 7,
-		YN1End: 9,
-		YN2Start: 9,
-		YN2End: 11,
-		YN3Start: 11,
-		YN3End: 13,
-		YN4Start: 13,
-		YN4End: 15,
-		YN5Start: 15,
-		YN5End: 17,
-		YN6Start: 17,
-		YN6End: 19,
-		YN7Start: 19,
-		YN7End: 21,
-		YN8Start: 21,
-		YN8End: 23
-	};
-
-	quickValsMenu.SetColors({
-		itemColor: [{start: itemTextIdxes.levelStart, end: itemTextIdxes.levelEnd, attrs: colors.level},
-		            {start: itemTextIdxes.YN1Start, end: itemTextIdxes.YN1End, attrs: colors.YN},
-					{start: itemTextIdxes.YN2Start, end: itemTextIdxes.YN2End, attrs: colors.YN},
-					{start: itemTextIdxes.YN3Start, end: itemTextIdxes.YN3End, attrs: colors.YN},
-					{start: itemTextIdxes.YN4Start, end: itemTextIdxes.YN4End, attrs: colors.YN},
-					{start: itemTextIdxes.YN5Start, end: itemTextIdxes.YN5End, attrs: colors.YN},
-					{start: itemTextIdxes.YN6Start, end: itemTextIdxes.YN6End, attrs: colors.YN},
-					{start: itemTextIdxes.YN7Start, end: itemTextIdxes.YN7End, attrs: colors.YN},
-					{start: itemTextIdxes.YN8Start, end: itemTextIdxes.YN8End, attrs: colors.YN}],
-		selectedItemColor: [{start: itemTextIdxes.levelStart, end: itemTextIdxes.levelEnd, attrs: colors.levelHi},
-		                    {start: itemTextIdxes.YN1Start, end: itemTextIdxes.YN1End, attrs: colors.YNHi},
-		                    {start: itemTextIdxes.YN2Start, end: itemTextIdxes.YN2End, attrs: colors.YNHi},
-		                    {start: itemTextIdxes.YN3Start, end: itemTextIdxes.YN3End, attrs: colors.YNHi},
-		                    {start: itemTextIdxes.YN4Start, end: itemTextIdxes.YN4End, attrs: colors.YNHi},
-		                    {start: itemTextIdxes.YN5Start, end: itemTextIdxes.YN5End, attrs: colors.YNHi},
-		                    {start: itemTextIdxes.YN6Start, end: itemTextIdxes.YN6End, attrs: colors.YNHi},
-		                    {start: itemTextIdxes.YN7Start, end: itemTextIdxes.YN7End, attrs: colors.YNHi},
-		                    {start: itemTextIdxes.YN8Start, end: itemTextIdxes.YN8End, attrs: colors.YNHi}]
-	});
-
-	quickValsMenu.quickValidationVals = pQuickValidationVals;
-	//format("%*s", this.numTabSpaces, "")
-	quickValsMenu.itemFormatStr = "%6d %s %s %s %s %s %s %s %s";
-	quickValsMenu.NumItems = function() {
-		return this.quickValidationVals.length;
-	};
-	quickValsMenu.GetItem = function(pItemIndex) {
-		var valYNStrs = [
-			this.quickValidationVals[pItemIndex].expire > 0 ? CHECK_CHAR : " ",
-			this.quickValidationVals[pItemIndex].flags1 > 0 ? CHECK_CHAR : " ",
-			this.quickValidationVals[pItemIndex].flags2 > 0 ? CHECK_CHAR : " ",
-			this.quickValidationVals[pItemIndex].flags3 > 0 ? CHECK_CHAR : " ",
-			this.quickValidationVals[pItemIndex].flags4 > 0 ? CHECK_CHAR : " ",
-			this.quickValidationVals[pItemIndex].credits > 0 ? CHECK_CHAR : " ",
-			this.quickValidationVals[pItemIndex].exemptions > 0 ? CHECK_CHAR : " ",
-			this.quickValidationVals[pItemIndex].restrictions > 0 ? CHECK_CHAR : " "
-		];
-
-		var menuItemObj = this.MakeItemWithRetval(-1);
-		menuItemObj.retval = this.quickValidationVals[pItemIndex];
-		menuItemObj.text = format("%6d", this.quickValidationVals[pItemIndex].level);
-		for (var i = 0; i < valYNStrs.length; ++i)
-			menuItemObj.text += " " + valYNStrs[i];
-		return menuItemObj;
-	};
-
-	return quickValsMenu;
-}
-
-// Returns an array of the quick-validation sets configured in
-// SCFG > System > Security > Quick-Validation Values.  This reads
-// from main.ini, which exists with Synchronet 3.20 and newer.
-// In SCFG:
-//
-// Level                 60     |
-// Flag Set #1                  |
-// Flag Set #2                  |
-// Flag Set #3                  |
-// Flag Set #4                  |
-// Exemptions                   |
-// Restrictions                 |
-// Extend Expiration     0 days |
-// Additional Credits    0      |
-//
-// Each object in the returned array will have the following properties:
-//  level (numeric)
-//  expire
-//  flags1
-//  flags2
-//  flags3
-//  flags4
-//  credits
-//  exemptions
-//  restrictions
-function getQuickValidationVals()
-{
-	var validationValSets = [];
-	// In SCFG > System > Security > Quick-Validation Values, there are 10 sets of
-	// validation values.  These are in main.ini as [valset:0] through [valset:9]
-	// This reads from main.ini, which exists with Synchronet 3.20 and newer.
-	//system.version_num >= 32000
-	var mainIniFile = new File(system.ctrl_dir + "main.ini");
-	if (mainIniFile.open("r"))
-	{
-		for (var i = 0; i < 10; ++i)
-		{
-			var valSection = mainIniFile.iniGetObject(format("valset:%d", i));
-			if (valSection != null)
-				validationValSets.push(valSection);
-		}
-		mainIniFile.close();
-	}
-	return validationValSets;
-}
-
-// Clears a rectangle on the screen
-function clearScreenRectangle(pX, pY, pWidth, pHeight)
-{
-	console.attributes = "N";
-	var lastScreenRow = pY + pHeight - 1;
-	for (var screenRow = pY; screenRow <= lastScreenRow; ++screenRow)
-	{
-		console.gotoxy(pX, screenRow);
-		printf("%-*s", pWidth, "");
-	}
-}
-
-
-// With a group & sub-board index, this function gets the date
-// & time of the latest posted message from a sub-board (or group of sub-boards, if using
-// sub-board name collapsing).  This function also gets the description of the sub-board (or
-// group of sub-boards if using sub-board name collapsing).
-//
-// Parameters:
-//  pGrpIdx: The index of the message group
-//  pSubIdx: The index of the sub-board in the message group (if using
-//           sub-board name collapsing, this could be the index of a set
-//           of sub-subboards).
-//  pShowImportTime: Boolean - Whether or not to use import time. If false, this will use the
-//                   message written time.
-//
-// Return value: An object containing the following properties:
-//               desc: The description of the sub-board (or group of sub-subboards if using name collapsing)
-//               numItems: The number of messages in the sub-board or number of sub-subboards in the group,
-//                         if using sub-board name collapsing
-//               subCode: The internal code of the sub-board (this will be an empty string if it's a group of sub-subboards)
-//               newestTime: A value containing the date & time of the newest post in the sub-board or group of sub-boards
-function getSubBoardInfo(pGrpIdx, pSubIdx, pShowImportTime)
-{
-	var retObj = {
-		desc: "",
-		numItems: 0,
-		subCode: "",
-		newestTime: 0
-	};
-
-	var showImportTime = (typeof(pShowImportTime) === "boolean" ? pShowImportTime : false);
-
-	retObj.desc = msg_area.grp_list[pGrpIdx].sub_list[pSubIdx].description;
-	retObj.subCode = msg_area.grp_list[pGrpIdx].sub_list[pSubIdx].code;
-	// Get the number of messages in the sub-board
-	var numMsgs = numReadableMsgs(null, msg_area.grp_list[pGrpIdx].sub_list[pSubIdx].code);
-	if (numMsgs > 0)
-	{
-		retObj.numItems = numMsgs;
-		//var msgHeader = msgBase.get_msg_header(true, msgBase.total_msgs-1, true);
-		var msgHeader = getLatestMsgHdr(retObj.subCode);
-		if (msgHeader != null)
-		{
-			// Set the newest post time
-			if (showImportTime)
-				retObj.newestTime = msgHeader.when_imported_time;
-			else
-			{
-				var msgWrittenLocalBBSTime = msgWrittenTimeToLocalBBSTime(msgHeader);
-				if (msgWrittenLocalBBSTime != -1)
-					retObj.newestTime = msgWrittenLocalBBSTime;
-				else
-					retObj.newestTime = msgHeader.when_written_time;
-			}
-		}
-	}
-
-	return retObj;
-}
-
-// Gets the header of the latest readable message in a sub-board,
-// given a number of messages to look at.
-//
-// Paramters:
-//  pSubCode: The internal code of the message sub-board
-//
-// Return value: The message header of the latest readable message.  If
-//               none is found, this will be null.
-function getLatestMsgHdr(pSubCode)
-{
-	var msgHdr = null;
-	var msgBase = new MsgBase(pSubCode);
-	if (msgBase.open())
-	{
-		msgHdr = getLatestMsgHdrWithMsgbase(msgBase, pSubCode);
-		msgBase.close();
-	}
-	return msgHdr;
-}
-// Gets the header of the latest readable message in a sub-board,
-// given a number of messages to look at.
-//
-// Paramters:
-//  pMsgbase: A MsgBase object for the sub-board, already opened
-//  pSubCode: The internal code of the sub-board
-//
-// Return value: The message header of the latest readable message.  If
-//               none is found, this will be null.
-function getLatestMsgHdrWithMsgbase(pMsgbase, pSubCode)
-{
-	if (typeof(pMsgbase) !== "object")
-		return null;
-	if (!pMsgbase.is_open)
-		return null;
-
-	// Look through the message headers to find the latest readable one
-	var msgHdrToReturn = null;
-	var msgIdx = pMsgbase.total_msgs-1;
-	var msgHeader = pMsgbase.get_msg_index(true, msgIdx, false);
-	while (!isReadableMsgHdr(msgHeader, pSubCode) && (msgIdx >= 0))
-		msgHeader = pMsgbase.get_msg_index(true, --msgIdx, true);
-	if (msgHeader != null)
-		msgHdrToReturn = pMsgbase.get_msg_header(true, msgIdx, false);
-	return msgHdrToReturn;
-}
-
-// For a sub-board, updates the user's newscan pointers for a sub-board so
-// that there are no more new messages, and marks any messages written to
-// the user as read.
-//
-// Parameters:
-//  pSubCode: The internal code for the sub-board
-//
-// Return value: Boolean - Whether or not this function was successful
-function subBoardNewscanAllRead(pSubCode)
-{
-	if (pSubCode == "mail")
-		return false;
-
-	var wasSuccessful = true;
-	var msgbase = new MsgBase(pSubCode);
-	if (msgbase.open())
-	{
-		msg_area.sub[pSubCode].scan_ptr = msgbase.last_msg;
-		msg_area.sub[pSubCode].last_read = msgbase.last_msg;
-
-		// Mark any unread messages to the user as read
-		var indexRecords = msgbase.get_index();
-		if (indexRecords != null)
-		{
-			for (var i = 0; i < indexRecords.length; ++i)
-			{
-				if (msgIsToCurrentUserByName(indexRecords[i]) && !Boolean(indexRecords[i].attr & MSG_READ))
-				{
-					var msgHdr = msgbase.get_msg_header(false, indexRecords[i].number, false);
-					if (msgHdr != null)
-					{
-						msgHdr.attr |= MSG_READ;
-						if (!msgbase.put_msg_header(false, indexRecords[i].number, msgHdr))
-							wasSuccessful = false;
-					}
-				}
-			}
-		}
-		msgbase.close();
-	}
-	else
-		wasSuccessful = false;
-	return wasSuccessful;
-}
-
-// Hepler function for getting a message header: Opens a messagebase, calls
-// get_msg_header(), closes the messagebase, and returns the header
-function getMsgHdr(pSubCode, pByOffset, pNumOrOffset, pExpandFields, pIncludeVotes)
-{
-	var msgHdr = null;
-	var msgbase = new MsgBase(pSubCode);
-	if (msgbase.open())
-	{
-		var expandFields = (typeof(pExpandFields) === "boolean" ? pExpandFields : true);
-		var includeVotes = (typeof(pIncludeVotes) === "boolean" ? pIncludeVotes : false);
-		msgHdr = msgbase.get_msg_header(pByOffset, pNumOrOffset, expandFields, includeVotes);
-		msgbase.close();
-	}
-	return msgHdr;
-}
-
-// Adds to the twit list.
-//
-// Parameters:
-//  pStr: A name/email or netmail address
-//
-// Return value: Boolean - Whether or not this was successful
-function addToTwitList(pStr)
-{
-	if (typeof(pStr) !== "string")
-		return false;
-
-	var wasSuccessful = true;
-	if (!entryExistsInTwitList(pStr))
-	{
-		wasSuccessful = false;
-		var twitFile = new File(system.ctrl_dir + "twitlist.cfg");
-		//if (twitFile.open(twitFile.exists ? "r+" : "w+"))
-		if (twitFile.open("a"))
-		{
-			wasSuccessful = twitFile.writeln(pStr);
-			twitFile.close();
-		}
-	}
-	return wasSuccessful;
-}
-// Returns whether an entry exists in the twit list.
-//
-// Parameters:
-//  pStr: An entry to check in the twit list
-//
-// Return value: Boolean - Whether or not the given string exists in the twit list
-function entryExistsInTwitList(pStr)
-{
-	if (typeof(pStr) !== "string")
-		return false;
-
-	var entryExists = false;
-	var twitFile = new File(system.ctrl_dir + "twitlist.cfg");
-	if (twitFile.open("r"))
-	{
-		while (!twitFile.eof && !entryExists)
-		{
-			//// Read the next line from the config file.
-			var fileLine = twitFile.readln(2048);
-			// fileLine should be a string, but I've seen some cases
-			// where for some reason it isn't.  If it's not a string,
-			// then continue onto the next line.
-			if (typeof(fileLine) != "string")
-				continue;
-			// If the line starts with with a semicolon (the comment
-			// character) or is blank, then skip it.
-			if ((fileLine.substr(0, 1) == ";") || (fileLine.length == 0))
-				continue;
-
-			// See if this line matches the given string
-			entryExists = (pStr == skipsp(truncsp(fileLine)));
-		}
-
-		twitFile.close();
-	}
-	return entryExists;
-}
-
-// Adds to the global email filter (email.can).
-//
-// Parameters:
-//  pEmailAddr: An email address
-//
-// Return value: Boolean - Whether or not this was successful
-function addToGlobalEmailFilter(pEmailAddr)
-{
-	if (typeof(pEmailAddr) !== "string")
-		return false;
-
-	var wasSuccessful = true;
-	if (!entryExistsInGlobalEmailFilter(pEmailAddr))
-	{
-		wasSuccessful = false;
-		var filterFile = new File(system.text_dir + "email.can");
-		//if (filterFile.open(filterFile.exists ? "r+" : "w+"))
-		if (filterFile.open("a"))
-		{
-			wasSuccessful = filterFile.writeln(pEmailAddr);
-			filterFile.close();
-		}
-	}
-	return wasSuccessful;
-}
-// Returns whether an entry exists in the global email filter (email.can).
-//
-// Parameters:
-//  pEmailAddr: An entry to check in the twit list
-//
-// Return value: Boolean - Whether or not the given string exists in the twit list
-function entryExistsInGlobalEmailFilter(pEmailAddr)
-{
-	if (typeof(pEmailAddr) !== "string")
-		return false;
-
-	var entryExists = false;
-	var filterFile = new File(system.text_dir + "email.can");
-	if (filterFile.open("r"))
-	{
-		while (!filterFile.eof && !entryExists)
-		{
-			//// Read the next line from the config file.
-			var fileLine = filterFile.readln(2048);
-			// fileLine should be a string, but I've seen some cases
-			// where for some reason it isn't.  If it's not a string,
-			// then continue onto the next line.
-			if (typeof(fileLine) != "string")
-				continue;
-			// If the line starts with with a semicolon (the comment
-			// character) or is blank, then skip it.
-			if ((fileLine.substr(0, 1) == ";") || (fileLine.length == 0))
-				continue;
-
-			// See if this line matches the given string
-			entryExists = (pEmailAddr == skipsp(truncsp(fileLine)));
-		}
-
-		filterFile.close();
-	}
-	return entryExists;
-}
-
-///////////////////////////////////////////////////////////////////////////////////
-
-// For debugging: Writes some text on the screen at a given location with a given pause.
-//
-// Parameters:
-//  pX: The column number on the screen at which to write the message
-//  pY: The row number on the screen at which to write the message
-//  pText: The text to write
-//  pPauseMS: The pause time, in milliseconds
-//  pClearLineAttrib: Optional - The color/attribute to clear the line with.
-//                    If not specified or null is specified, defaults to normal attribute.
-//  pClearLineAfter: Whether or not to clear the line again after the message is dispayed and
-//                   the pause occurred.  This is optional.
-function writeWithPause(pX, pY, pText, pPauseMS, pClearLineAttrib, pClearLineAfter)
-{
-	var clearLineAttrib = "\x01n";
-	if ((pClearLineAttrib != null) && (typeof(pClearLineAttrib) == "string"))
-		clearLineAttrib = pClearLineAttrib;
-	console.gotoxy(pX, pY);
-	console.cleartoeol(clearLineAttrib);
-	console.print(pText);
-	if (pPauseMS > 0)
-		mswait(pPauseMS);
-	if (pClearLineAfter)
-	{
-		console.gotoxy(pX, pY);
-		console.cleartoeol(clearLineAttrib);
-	}
-}
diff --git a/readme.txt b/readme.txt
deleted file mode 100644
index f0f8dec8d2d92fcef5487e8d03f3e611f827aa44..0000000000000000000000000000000000000000
--- a/readme.txt
+++ /dev/null
@@ -1,1373 +0,0 @@
-                      Digital Distortion Message Reader
-                                 Version 1.93a
-                           Release date: 2024-01-07
-
-                                     by
-
-                                Eric Oulashin
-                          Sysop of Digital Distortion
-                  BBS internet address: digitaldistortionbbs.com
-                     Alternate address: digdist.bbsindex.com
-                        Email: eric.oulashin@gmail.com
-
-
-
-This file describes the Digital Distortion Message Reader.
-
-Contents
-========
-1. Disclaimer
-2. Introduction
-3. Installation & Setup
-   - Loadable Modules setup
-   - Command shell setup
-   - Background: Running JavaScript scripts in Synchronet
-   - Command-line parameters
-   - Synchronet command shell background
-   - Installing into a command shell
-4. Header ANSI/asc file
-5. User avatars
-6. Configuration file & color/text theme configuration file
-   - Main configuration file (DDMsgReader.cfg)
-   - Theme configuration file
-7. Indexed reader mode
-8. Quick-Validating users (while reading their message)
-9. Drop file for replying to messages with Synchronet message editors
-10. text.dat lines used in Digital Distortion Message Reader
-
-
-1. Disclaimer
-=============
-I cannot guarantee that this script is 100% free of bugs.  However, I have
-tested it, and I used it often during development, so I have some confidence
-that there are no serious issues with it (at least, none that I have seen).
-
-
-2. Introduction
-===============
-Digital Distortion Message Reader is a script for Synchronet that provides an
-alternate message reading interface.  For ANSI users, a reader interface is
-provided which allows scrolling the message up & down (with the up & down arrow
-keys, as well as the PageUp & PageDown keys), navigating through the messages
-in the message area (AKA sub-board) using the left & right arrow keys, replying
-to messages, and other common messagebase functionality.  The Digital
-Distortion Message Reader can also list messages in the message area and allows
-forward & reverse navigation through the message list using a lightbar or
-traditional user interface.  An integrated message area chooser feature is also
-included, allowing the user to change to a different message area to read/list
-messages.  Message newscan and various types of message searching are also
-available.  This script requires Synchronet version 3.15 or newer.
-
-When using Synchronet 3.17, the message voting features added in Synchronet
-3.17 are supported.  For regular messages, users can vote a message up or down,
-and users can also vote in poll messages.
-
-If the user's terminal does not support ANSI, the reader will fall back to a
-traditional user interface (which does not support scrolling).  The user
-interface style can also be toggled by the sysop in the configuration file
-in case the sysop wants the reader to use the traditional interface even for
-ANSI users.
-
-With this message reader, users can have their own personal twitlists. This
-allows users to not see messages from (or to) specified usernames.  A user can
-edit their twit list by going into their user settings (with the Ctrl-U hotkey)
-and choosing the option to edit their personal twit list.
-
-This reader effectively replaces the Digital Distortion Message Lister, which
-provided a traditional message reader interface but not the scrollable reader
-interface for ANSI users.
-
-Thanks goes to Accession/Access Denied (sysop of The Pharcyde) and Psi-Jack
-(sysop of Decker's Heaven) for testing the reader while it was in development.
-
-The following is a list of features:
-- Provides an enhanced, very functional yet intuitive user interface for
-  reading messages for ANSI users, including the ability to scroll the message,
-  navigate forward & back through the messages in the message area, reply to
-  messages, etc.  If the user's terminal does not support ANSI, the reader will
-  fall back to a more traditional user interface (which, for instance, does not
-  allow scrolling the message up & down).
-- Can be used to read message sub-boards or personal email
-- Allows switching between the enhanced reader interface and the message list
-  to allow the user to browse messages & select another message to read.  The
-  message list displays a formatted summary list of the messages in the user's
-  current message area and can be  navigated forward & backward, and allows
-  selection of a message to read (basically, switching into reader mode).  The
-  message list can be configured for a lightbar or traditional user interface
-  (the lightbar interface will only be available for ANSI users).
-- Message scanning and searching is supported: New-message scan, new-to-you
-  scan, all-to-you scan, keyword search, from name search, and to name search
-- Allows a custom message header ANSI/.asc file to be used in the reader mode.
-  If there is no custom header file, a default header style will be used.  For
-  There can also be different custom header files for various terminal widths.
-- Allows changing to a different message area from within the reader or message
-  list.  The area chooser will use a lightbar interface for ANSI users or a
-  traditional interface for users whose terminal doesn't support ANSI.
-- Allows the user to delete and edit existing messages that they've written, if the
-  sub-board supports those operations.
-- Allows the user to download file attachments, whether uploaded to their
-  mailbox on Synchronet or attached to internet emails.  When a message has
-  attachments, it will appear in the message list with an "A" between the
-  message number and sender name.
-- Allows the user to forward a message to an email address or another user
-  (using the O key).  This can be useful, for instance, if the user wants to
-  send a message in a public sub-board to their personal email for future
-  reference or send a message from a public sub-board to another user to
-  discuss the topic privately.
-- Allows sysops to save a message to the BBS machine for future reference
-- Allows sysops to edit the user account of the message author, if the user's
-  account exists on the BBS.  This is done with the U key while reading a
-  message.  This can be useful for BBSes that require new users to send a
-  message to the sysop when they sign up, in case the sysop needs to edit their
-  account.
-- Allows the ability to batch-delete multiple messages.  This is most useful,
-  for instance, if a user gets many spam emails in their personal inbox.  Batch
-  deleting is only allowed when the user has permission to delete messages
-  (such as their own personal email).  To batch-delete messages, the user can
-  select multiple messages (from the message list) and then press CTRL-D (from
-  the message list) to delete them.  Messages can be selected in the following
-  ways:
-  o Lightbar message list: The spacebar selects an individual message.  CTRL-A
-    lets the user select or un-select all messages.
-  o Traditional message list: The S key lets the user select or un-select
-    messages, by typing message numbers, A to select all, or N to select none
-    (un-select all).  The list of message numbers is comma-separated or
-    space-separated, allowing for number ranges such as 120-130 for instance.
-  o Reader interface: The spacebar selects the message.
-To delete the selected messages, the user must be in the message list; the
-CTRL-D key combo is used for batch delete, and it will prompt the user for
-confirmation before deleting the messages.
-- The program settings, colors, and some text can be changed via configuration
-  files.  The configuration files may be placed in the same directory as the
-  .js script or in the sbbs/ctrl directory.
-- Allows a personal twit list, editable via user settings (Ctrl-U)
-- Has an "indexed" mode, which displays a menu of sub-boards that includes the
-  total number of messages and number of new messages in each, and lets the user
-  choose a sub-board to read.  This can be used for a regular "read", which
-  lists all sub-boards, or a newscan, which lists the sub-boards enabled for
-  newscan by the user.
-- Allows the sysop to quick-validate a local user while reading one of their
-  messages. The hotkey to do so is Ctrl-Q.
-
-If a message has been marked for deletion, it will appear in the message list
-with a blinking red asterisk (*) after the message number.
-
-When displaying a message to the user, this script will honor the attribute
-code toggles set up under Synchronet's configuration program (SCFG),
-under Message Options > Extra Attribute Codes.
-
-As the sysop, when reading a message, the hotkey Ctrl-O will show the operator
-menu. Most of the operator menu items are already available, but the operator
-menu also has the additional option to add the author of the message (the 'from'
-name) to the twit list.
-
-
-3. Installation & Setup
-=======================
-Digital Distortion Message Reader is comprised of the following files:
-1. DDMsgReader.js         The Digital Distortion Message Reader script
-
-2. DDMsgReader.cfg        The reader configuration file
-
-3. ddmr_cfg.js            A menu-driven configuration script to help with
-                          changing configuration options. You can run it at a
-                          command prompt in the DDMsgReader directory with the
-                          following command:
-                          jsexec ddmr_cfg
-                          Alternately (with the filename extension):
-                          jsexec ddmr_cfg.js
-
-4. DefaultTheme.cfg       The default theme file containing colors & some
-                          configurable text strings used in the reader
-
-5. ddmr_lm.js             Loadable module script for setup in SCFG (only needed
-                          for Synchronet 3.19 and earlier, or Synchronet built
-                          before 2023-02-20)
-
-The configuration files are plain text files, so they can be edited using any
-editor.
-
-The first 3 files (DDMsgReader.js, DDMsgReader.cfg, and DefaultTheme.cfg) can be
-placed together in any directory. ddmr_lm.js is only needed if you're using
-Synchronet 3.19 or earlier (or a Synchronet development build before
-2023-02-20); if so, ddmr_lm.js should be copied to your sbbs/mods directory.
-
-The examples in this document will assume the first 3 files are in the
-sbbs/xtrn/DDMsgReader directory.
-
-Loadable Modules setup
-----------------------
-The easiest way to get Digital Distortion Message Reader set up is via the
-Loadable Module options in SCFG > System > LOadable Modules.
-
-The Loadable Modules options let you specify scripts to run for various events
-in Synchronet.  As of Synchronet 3.19, the following Loadable Modules options
-are available in SCFG for message reading/scanning events:
-- Read Mail (added in Synchronet 3.16)
-- Scan Msgs (added in Synchronet 3.16)
-- Scan Subs (added in Synchronet 3.16)
-- List Msgs (added in Synchronet 3.18)
-
-The Loadable Modules options take the filename of the script (sometimes without
-the filename extension).
-
-Depending on your Synchronet version and where you have DDMsgReader.js, you may
-be able to specify DDMsgReader.js directly as the loadable module for the above
-settings. However, if you're using an earlier verison of Synchronet (before
-3.20) and you have DDMsgReader.js in ../xtrn/DDMsgReader or another path, you
-will need to use ddmr_lm.js.  Refer to one of the below sections, depending on
-which version of Synchronet you're using.
-
-For Synchronet 3.20 (& builds from 2023-02-20) and newer
---------------------------------------------------------
-As of Synchronet 3.20, Synchronet allows up to 63 characters with a full
-command-line for a loadable module, allowing DDMsgReader.js to be specified
-directly as the loadable module for the 4 entries mentioned above. I've noticed
-that it must include the .js filename extension. For example, if you have
-DDMsgReader.js in ../xtrn/DDMsgReader, you would specify the following for all
-4 of the above loadable module settings:
-
- Read Mail       ../xtrn/DDMsgReader/DDMsgReader.js
- Scan Msgs       ../xtrn/DDMsgReader/DDMsgReader.js
- Scan Subs       ../xtrn/DDMsgReader/DDMsgReader.js
- List Msgs       ../xtrn/DDMsgReader/DDMsgReader.js
-
-For Synchronet 3.19 (& builds before 2023-02-20)
-------------------------------------------------
-For older versions of Synchronet, the Loadable Modules options don't allow a
-leading path in front of the name; also, many/most loadable modules were limited
-to just 8 characters. So, if you have DDMsgReader.js in a path other than
-sbbs/exec or sbbs/mods, one solution is to copy the included ddmr_lm.js to
-either your sbbs/exec or sbbs/mods directory (ideally sbbs/mods so it wouldn't
-get accidentally deleted) and specify ddmr_lm in your Loadable Modules as
-follows:
-
- Read Mail       ddmr_lm
- Scan Msgs       ddmr_lm
- Scan Subs       ddmr_lm
- List Msgs       ddmr_lm
-
-Also, if you will be running the script from a directory other than
-xtrn/DDMsgReader, edit ddmr_lm.js and look for the text "SYSOPS:" (without the
-double-quotes).  One or two lines below that, there is a variable called
-msgReaderPath - Change that so that it contains the path where you copied
-DDMsgReader.js.
-
-Alternately, you can copy DDMsgReader.js to your sbbs/exec or sbbs/mods
-directory and specify DDMsgReader in your Loadable Modules for the above
-modules in SCFG.  For that to work, you would also need to copy DDMsgReader.cfg
-to your sbbs/ctrl directory or to sbbs/mods along with DDMsgReader.js.
-
-There are a few search modes that Synchronet provides that Digital Distortion
-Message Reader doesn't support yet (such as continuous newscan and browse new
-scan), and for those situations, the Loadable Modules scripts will fall back to
-the stock Synchronet behavior.
-
-
-Command shell setup
--------------------
-Digital Distortion Message Reader can be set up by adding options to your
-command shell to run DDMsgReader.js for any or all of the desired functionality
-(reading, searching, message scanning, etc).  The command-line parameters are
-described in the subsection "Command-line parameters".  Installing into a
-command shell is described in the subsection "Installing into a command shell".
-
-Background: Running JavaScript scripts in Synchronet
-----------------------------------------------------
-The general syntax for a command to run a JavaScript script in Synchronet is
-with a question mark before the .js file.  For example:
-?../xtrn/DDMsgReader/DDMsgReader.js
-
-In a Baja script, you can use the 'exec' command to run a JavaScript script, as
-in the following example:
-exec "?../xtrn/DDMsgReader/DDMsgReader.js"
-
-In a JavaScript script, you can use the bbs.exec() function to run a JavaScript
-script, as in the following example:
-bbs.exec("?../xtrn/DDMsgReader/DDMsgReader.js");
-
-Alternately, the reader can be installed as an external program (in SCFG in
-External Programs > Online Programs (Doors)).  See the following document for
-more information:
-http://wiki.synchro.net/howto:door:index?s[]=doors
-
-Command-line parameters
------------------------
-The Digital Distortion Message Reader supports command-line parameters to
-specify some behavior options. The command-line parameters are used in the
-command string after DDMsgReader.js.
-
-Most of the command-line arguments are in -arg=val format, where arg is the
-argument (parameter) name, and val is the value for that argument.  Some of
-the command-line parameters are simply in -arg format, to enable an option.
-For example, -search=new_msg_scan will start the reader to do a new message
-scan.  Another example is -personalEmail which lets the user read their
-personal email.
-
-The following are the command-line parameters supported by DDMsgReader.js:
--indexedMode: Starts DDMsgreader in "indexed" reader mode, which lists all
-              sub-boards, with total number of messages, number of new messages,
-              and last post date, allowing the user to select a sub-board to
-              read. This will prompt the user for "Group or All": Whether the
-              user wants to list sub-boards in the current group, or all
-              sub-boards.
-              This is intended to work if it is the only command-line option.
--search: A search type.  Available options:
- keyword_search: Do a keyword search in message subject/body text (current message area)
- from_name_search: 'From' name search (current message area)
- to_name_search: 'To' name search (current message area)
- to_user_search: To user search (current message area)
- new_msg_scan: New message scan (prompt for current sub-board, current
-               group, or all)
- new_msg_scan_all: New message scan (all sub-boards)
- new_msg_scan_cur_grp: New message scan (current message group only)
- new_msg_scan_cur_sub: New message scan (current sub-board only)
-                       This can (optionally) be used with the -subBoard
-								       command-line parameter, which specifies an internal
-								       code for a sub-board, which may be different from the
-								       user's currently selected sub-board.
- to_user_new_scan: Scan for new (unread) messages to the user (prompt
-                   for current sub-board, current group, or all)
- to_user_new_scan_all: Scan for new (unread) messages to the user
-                       (all sub-boards)
- to_user_new_scan_cur_grp: Scan for new (unread) messages to the user
-                           (current group)
- to_user_new_scan_cur_sub: Scan for new (unread) messages to the user
-                           (current sub-board)
- to_user_all_scan: Scan for all messages to the user (prompt for current
-                   sub-board, current group, or all)
- prompt: Prompt the user for one of several search/scan options to
-         choose from
-Note that if the -personalEmail option is specified (to read personal email),
-the only valid search types are keyword_search and from_name_search.
-
--suppressSearchTypeText: Disable the search type text that would appear
-                         above searches or scans (such as "New To You
-                         Message Scan", etc.)
--startMode: Startup mode.   This overrides the startMode option in the
-           confiruation file.  Available options are read (or reader) for
-           reader mode and list (or lister) for message list mode.
--configFilename: Specifies the name of the configuration file to use.  Defaults
-                 to DDMsgReader.cfg.
--subBoard: The sub-board (internal code or number) to read, other than the
-           user's current sub-board.  This is optional; if this is specified,
-           the sub-board specified by this option will be used instead of the
-           user's current sub-board.  If this option is specified, the
-           -chooseAreaFirst option will be ignored.
--personalEmail: Read personal email to the user.  This is a true/false value.
-                It doesn't need to explicitly have a =true or =false afterward;
-                simply including -personalEmail will enable it.  If this option
-                is specified, the -chooseAreaFirst and -subBoard options will
-                be ignored.
--personalEmailSent: Read personal email to the user.  This is a true/false
-                    value.  It doesn't need to explicitly have a =true or =false
-                    afterward; simply including -personalEmailSent will enable it.
--chooseAreaFirst: Display the message area chooser before reading/listing
-                  messages.  This is a true/false value.  It doesn't need
-                  to explicitly have a =true or =false afterward; simply
-                  including -chooseAreaFirst will enable it.  If -personalEmail
-					   or -subBoard is specified, then this option won't have any
-                  effect.
-
-The following parameters generally shouldn't be used unless you know what
-you're doing.  These were added for use by the Loadable Modules scripts, which
-Synchronet will load for various scenarios:
-
--userNum: Specify a user number for reading personal email.  This parameter is
-          there because although usually the current user will be reading their
-          own personal mail, there are situations where a sysop can read other
-          users' personal mail.
--allPersonalEmail: Read all personal email (to/from all).  There are instances
-                   where Synchronet supports this, but more than likely only
-                   for sysops.
-
-Synchronet command shell background
------------------------------------
-If you are already familiar with Synchronet's command shell concepts, you can
-skip to the subsection "Installing into a command shell" below.  If you are not
-yet familiar with how Synchronet's menus are controlled, the key is that
-Synchronet doesn't have a menu editor like some other BBS packages do.
-Instead, Synchronet uses a "command shell", which is a script that runs when a
-user logs in and controls the flow of activity as it responds to the user's
-commands.  Synchronet supports two languages for its scripts: Baja and
-JavaScript.  Baja is Synchronet's own scripting language; JavaScript is an
-industry standard scripting language that Synchronet has provided extensions
-for to allow scripting while also being able to take advantage of some of
-JavaScript's other features.  Baja scripts need to be compiled before running,
-whereas JavaScript scripts don't.  For more information on Synchronet command
-shells & scripting, see the following documentation:
-- Synchronet command shells:
-http://wiki.synchro.net/custom:command_shell
-- Synchronet modules:
-http://wiki.synchro.net/module:index?s[]=scripts
-- Baja language reference:
-http://www.synchro.net/docs/baja.html
-- Synchronet's JavaScript object reference (it's best to become familiar with
-  JavaScript before referring to this document):
-http://www.synchro.net/docs/jsobjs.html
-
-Installing into a command shell
--------------------------------
-Examples for running the reader will assume that it's installed in the
-directory sbbs/xtrn/DDMsgReader.
-
-If you are unsure which command shell you are using, you are likely using
-Synchronet's "default" command shell, which is contained in the files
-default.src and default.bin in the synchronet/exec directory.  To modify the
-default command shell, you'll need to open default.src with a text editor.
-These are the key things to modify in default.src:
-- Search for msg_read and replace that with the following:
-  exec "?../xtrn/DDMsgReader/DDMsgReader.js -startMode=read"
-- Where msg_your_scan appears, run DDMsgReader.js with the following:
-  exec "?../xtrn/DDMsgReader/DDMsgReader.js -search=to_user_new_scan -startMode=read"
-- Where msg_your_scan_all appears, run DDMsgReader.js with the command-line
-  exec "?../xtrn/DDMsgReader/DDMsgReader.js -search=to_user_new_scan_all -startMode=read"
-
-If you are using a JavaScript command shell, the process would be similar.  You
-will need to determine where the current message operations are done in the
-shell and replace them with the appropriate commands for running
-DDMsgReader.js.
-
-The following are example command line strings for running the reader to
-perform some common message operations.  These command lines can be used with
-the exec command in Baja, bbs.exec() method in JavaScript, or as set up as an
-external door in SCFG.  This list is not complete but provides examples of some
-common message operations.
-
-- Read messages in the current sub-board:
-?../xtrn/DDMsgReader/DDMsgReader.js -startMode=read
-
-- List messages in the current sub-board:
-?../xtrn/DDMsgReader/DDMsgReader.js -startMode=list
-
-- New message scan:
-?../xtrn/DDMsgReader/DDMsgReader.js -startMode=read -search=new_msg_scan
-
-- New-to-user message scan (scan for new messages to the user):
-?../xtrn/DDMsgReader/DDMsgReader.js -startMode=read -search=to_user_new_scan
-
-- Scan for all messages to the user:
-?../xtrn/DDMsgReader/DDMsgReader.js -startMode=read -search=to_user_all_scan
-
-- Start in indexed reader mode:
-?../xtrn/DDMsgReader/DDMsgReader.js -indexedMode
-
-- Text (keyword) search in the current sub-board, and list the messages found:
-?../xtrn/DDMsgReader/DDMsgReader.js -search=keyword_search -startMode=list
-
-- 'From' name message search in the current sub-board, and list messages found:
-?../xtrn/DDMsgReader/DDMsgReader.js -search=from_name_search -startMode=list
-
-- 'To' name message search in the current sub-board, and list messages found:
-?../xtrn/DDMsgReader/DDMsgReader.js -search=to_name_search -startMode=list
-
-- Search for all messages to the logged-in user in the current sub-board, and
-  list the messages found:
-?../xtrn/DDMsgReader/DDMsgReader.js -search=to_user_search -startMode=list
-
-- Read personal email:
-?../xtrn/DDMsgReader/DDMsgReader.js -personalEmail -startMode=read
-
-- List personal email:
-?../xtrn/DDMsgReader/DDMsgReader.js -personalEmail -startMode=list
-
-- Read sent personal email:
-?../xtrn/DDMsgReader/DDMsgReader.js -personalEmailSent -startMode=read
-
-- Search personal email with a keyword, and start with the message list:
-?../xtrn/DDMsgReader/DDMsgReader.js -search=keyword_search -personalEmail -startMode=list
-
-Alternately, for searching personal email with a keyword, you can specify -subBoard=mail
-instead of -personalEmail:
-?../xtrn/DDMsgReader/DDMsgReader.js -search=keyword_search -subBoard=mail -startMode=list
-
-
-Text customization using text.dat
----------------------------------
-Digital Distortion Message Reader uses several lines of text from text.dat
-(included with Synchronet in the sbbs/ctrl directory):
-- Text # 10 (i.e., "E-mail (User name or number):"): Used when prompting the
-user to confirm an email address/user number when sending a private reply to a
-message
-- Text # 30 (i.e., "Aborted."): Used when saving a reaply message was aborted
-(i.e., because the user typed an empty email address)
-- Text # 54 (i.e., "Delete mail from %s"): Used to confirm whether the user
-wants to delete a personal email
-- Text # 563 (i.e., "[Hit a key] "): Used for the screen pause when displaying
-the help screen
-- Text # 662 (i.e., "Download attached file: xyz.txt (500 bytes)"): Used to
-confirm downloading an attached file
-
-When the user chooses to downloaded attached files, the reader will prompt to
-confirm downloading.  With Synchronet versions prior to 3.17, for the prompt
-text, the reader will use text number 662 from text.dat (in the sbbs/ctrl
-directory).  If you want to customize the text for that confirmation prompt,
-you would need to open text.dat and modify text number 662.
-
-4. Header ANSI/asc file
-=======================
-Digital Distortion Message Reader supports its own message header ANSI/.asc
-file to be displayed above a message.  This is separate from Synchronet's
-msghdr file in the sbbs/text/menu directory.  Digital Distortion Message
-Reader's header file is read from the same directory as DDMsgReader.js and
-needs to have the filename enhMsgHeader and can be in .ans or .asc format.
-enhMsgHeader is short for "enhanced message header".  If an ANSI-format (.ans)
-version is found, it will be converted to a Synchronet .asc file using
-Synchronet's ans2asc tool before being displayed.  Also, you can create header
-files for multiple terminal widths: To do so, the header filename format is
-enhMsgHeader-width.  For example, to create a header for a 132-column terminal,
-the header filename would be enhMsgHeader-132.asc (or .ans).  Digital
-Distortion Message Reader will choose the one matching the user's terminal
-width, if one exists.  If no others are found, enhMsgHeader.asc (or .ans) will
-be used, if it exists.  If no enhMsgHdr file exists, Digital Distortion Message
-Reader will use a default header (which adjusts to any terminal width).
-
-Many of Synchronet's @-codes (message variables) related to message information
-are supported in the enhMsgHeader file.  For a list of Synchronet's @-codes,
-refer to the following web page:
-http://www.synchro.net/docs/customization.html#MessageVariables
-In addition, the @-codes can be displayed in fixed-width fields in the
-enhMsgHeader file, using # characters in the @-code.  For instance, to display
-a message's subject left-justified in a width of 30 characters, the @-code
-would look like this:
-@MSG_SUBJECT-L###############@
-
-There is also an additional @-code supported by Digital Distortion Message
-Reader, MSG_NUM_AND_TOTAL (and MSG_NUM_AND_TOTAL-L), which will display both
-the current message number and the total number of messages in the message
-area.  It can also be used in a fixed-width field.  For example, to display it
-left-justified in a width of 30 characters, the @-code would look like this:
-@MSG_NUM_AND_TOTAL-L#########@
-
-Another additional @-code supported by Digital Distortion Message Reader is
-@MSG_FROM_AND_FROM_NET@, which shows the 'from' username along with the 'from'
-network type in paranthesis.  There is also a -L version for left-
-justification with length.  For instance:
-@MSG_FROM_AND_FROM_NET-L######@
-
-
-5. User avatars
-===============
-Digital Distortion Message Reader supports Synchronet's Avatar feature, which
-was added in Synchronet 3.17.  Basically, this feature allows users to have a
-small (10x6) text-based artwork that represents themselves and is displayed in
-the message header when reading messages.  Digital Distortion Message Reader
-has a setting in the configuration file, displayAvatars, which lets you toggle
-whether or not to dislpay user avatars.  Valid values are true and false.  For
-more information on Synchronet's Avatar feature, see the following wiki page
-with a web browser:
-http://wiki.synchro.net/module:avatars
-For the avatars feature to work, ensure you have all the latest Synchronet .js
-files in your sbbs/exec and sbbs/exec/load directories.  Specifically, Digital
-Distortion Message Reader loads smbdefs.js and avatar_lib.js, which are both
-in the sbbs/exec/load directory.  If those files are not there, then Digital
-Distortion Message Reader will still work but it won't display avatars.
-When avatars are enabled with Digital Distortion Message Reader, they will be
-displayed in the message header above a message, on the right side of the
-screen.  If you create a custom message header file for Digital Distortion
-Message Reader, you may want to reserve the rightmost 12 characters for the
-user avatar.
-
-
-6. Configuration file & color/text theme configuration file
-===========================================================
-Digital Distortion Message Reader allows changing some settings, colors, and
-some of the text via configuration files.
-
-Also, ddmr_cfg.js is a menu-driven configuration script to help with changing
-configuration options. You can run it at a command prompt in the DDMsgReader
-directory with the following command:
-jsexec ddmr_cfg
-Alternately (with the filename extension):
-jsexec ddmr_cfg.js
-
-If you have DDMsgReader in the standard location (xtrn/DDMsgReader), ddmr_cfg
-will copy the configuration file to your sbbs/mods directory to help prevent it
-from being accidentally overridden by updating the standard Synchronet files.
-
-The configuration files are plain text and can be edited with any text editor.
-These are the configuration files used by Digital Distortion Message Reader:
-- DDMsgReader.cfg: The main configuration file
-- DefaultTheme.cfg: Defines colors & some text strings used in the reader.
-  The name of this file can be specified in DDMsgReader.cfg, so that alternate
-  "theme" configuration files can be used if desired.
-
-Each setting in the configuration files has the format setting=value, where
-"setting" is the name of the setting or color, and "value" is the corresponding
-value to use.  The colors specified in the theme configuration file are
-Synchronet color/attribute codes.  Comments are allowed in the configuration
-files - Commented lines begin with a semicolon (;).
-
-Digital Distortion Message Reader will look for the configuration files in the
-following directories, in the following order:
-1. sbbs/mods
-2. sbbs/ctrl
-3. The same directory as DDMsgReader.js
-If you customize your configuration files, you can copy them to your sbbs/mods
-or sbbs/ctrl directory so that they'll be more difficutl to accidentally
-override if you update your xtrn/DDMsgReader from the Synchronet CVS
-repository, where this reader's files are checked in.
-
-The configuration settings are described in the sections below:
-
-Main configuration file (DDMsgReader.cfg)
------------------------------------------
-Setting                               Description
--------                               -----------
-listInterfaceStyle                    String: The user interface to use for message
-                                      lists.  Valid values are Traditional (non-
-                                      lightbar user interface with user prompted for
-                                      input at the end of each screenful) or Lightbar
-                                      (use the lightbar user interface).
-
-reverseListOrder                      Default for the user setting for whether or
-                                      not to display message lists in reverse order.
-                                      Valid values are true or false. When a message
-                                      list is displayed in reverse, it will be listed
-                                      in descending order by date & time.
-
-readerInterfaceStyle                  The user interface style to use for the
-                                      message reader.  Valid values are
-                                      Scrollable and Traditional.  The
-                                      scrollable interface allows scrolling the
-                                      message up and down and is only available
-                                      for ANSI users.  If a user is not using
-                                      ANSI, the reader will use the traditional
-                                      user interface instead, regardless of
-                                      this setting.
-
-readerInterfaceStyleForANSIMessages   The user interface style to use for
-                                      reading messages with ANSI content.  Valid
-                                      values are Scrollable and Traditional.
-                                      The scrollable interface allows scrolling
-                                      the message up and down.  If false, the
-                                      reader will use a traditional (non-
-                                      scrolling) user interface to display
-                                      messages with ANSI content.  If a user is
-                                      not using ANSI, the reader will use the
-                                      traditional user interface instead,
-                                      regardless of this setting.
-
-displayBoardInfoInHeader              true/false: Whether or not to display sub-board
-                                      information above the column headers when listing
-                                      the message information.  If this is set to true,
-                                      2 extra lines on the screen will be used at the top
-                                      to display message group and sub-board.
-
-promptToContinueListingMessages       true/false: Whether or not to prompt the
-                                      user to continue listing messages after
-                                      a message is read.
-
-promptConfirmReadMessage              true/false: Whether or not to prompt the
-                                      user to read a message when one is
-                                      selected.
-
-msgListDisplayTime                    Specifies the message date to display.
-                                      Valid values are imported and written.
-                                      imported: Display the message import dates
-                                      written: Display the message written dates
-                                      When using the message written dates, the
-                                      message written dates will be adjusted to
-                                      the BBS's local time zone so that they are
-                                      all consistent.
-
-msgAreaList_lastImportedMsg_time      In the message sub-board lists, the date
-                                      & time of the last message in the
-                                      sub-board.  This setting specifies
-                                      whether to use the imported time or the
-                                      written time.  Valid values are imported
-                                      and written.  When using the message
-                                      written dates, the message written dates
-                                      will be adjusted to the BBS's local time
-                                      zone so that they are all consistent.
-
-startMode                             Specifies whether to start in list mode
-                                      or reader mode.  Valid values are Reader
-                                      (or Read) and Lister (or List).  Note
-                                      that this setting can be overridden by
-                                      the -startMode command-line argument
-                                      (valid values are list and read).
-
-tabSpaces                             The number of spaces to use for tabs in
-                                      the message reader (tabs will be replaced
-                                      by this many spaces).
-
-pauseAfterNewMsgScan                  Whether or not to pause (i.e., with a
-                                      "finished" message) after doing a new
-                                      message scan.  Valid values are true
-                                      and false.
-
-readingPostOnSubBoardInsteadOfGoToNext  For reading messages (not for a newscan,
-                                      etc.): Whether or not to ask the user
-                                      whether to post on the sub-board in reader
-                                      mode after reading the last message
-                                      instead of prompting to go to the next
-                                      sub-board.  This is like the stock
-                                      Synchronet behavior. Valid values are true
-                                      and false.  This defaults to false.
-
-areaChooserHdrFilenameBase            The filename to use (without the
-                                      extension) for a header to display above
-                                      the message area chooser list.  For
-                                      example, if areaChgHeader is specified,
-                                      then the reader will look for
-                                      areaChgHeader.ans if it exists, and if
-                                      not, the reader will look for
-                                      areaChgHeader.asc.  Additionally, you
-                                      can have multiple header files for
-                                      different terminal widths; fpr example,
-                                      areaChgHeader-80.ans for an 80-column
-                                      terminal, areaChgHeader-140.ans for a
-                                      140-column terminal, etc.
-
-areaChooserHdrMaxLines                The maximum number of lines to use from
-                                      the message area chooser header file.
-
-displayAvatars                        Whether or not to display user avatars in
-                                      message headers.  Valid values are true
-                                      and false.
-
-rightJustifyAvatars                   Whether or not to right-justify avatars.
-                                      Valid values are true and false.  Flase
-                                      means to left-justify avatars.
-
-msgListSort                           How to sort the message list.  This can
-                                      be either Received (to sort by date/time
-                                      received) or Written (to sort by
-                                      date/time written).  Received is the
-                                      fastest, as it does not sort the list;
-                                      Written adds time since sorting is
-                                      required.
-
-convertYStyleMCIAttrsToSync           Whether or not to convert Y-style MCI
-                                      attribute codes to Synchronet attribute
-                                      codes. Valid values are true and false.
-
-prependFowardMsgSubject               Whether or not to prepend the subject for
-                                      forwarded messages with "Fwd: ". Valid
-                                      values are true and false. Defaulse to
-                                      true.
-
-enableIndexedModeMsgListCache         For indexed reader mode, whether or not to
-                                      enable caching the message header lists
-                                      for performance
-
-quickUserValSetIndex                  The index of the quick-validation set to
-                                      use for quick-validating a local user.
-                                      Normally, this should be 0-9, as there are
-                                      10 sets of values in SCFG). Alternately,
-                                      quickUserValSetIndex can be set to
-                                      something invalid (like -1) to have a menu
-                                      of the quick-validation sets displayed for
-                                      you to choose from one.
-
-saveAllHdrsWhenSavingMsgToBBSPC       For the sysop, whether to save all message
-                                      headers when saving a message to the BBS
-                                      PC. This could be a boolean (true/false)
-                                      or the string "ask" to prompt every time
-
-useIndexedModeForNewscan              Default for a user setting for whether or
-                                      not to use indexed mode for doing a
-                                      newscan. For newscan only (not new
-                                      to-you). If enabled, a newscan will appear
-                                      as a menu listing the various sub-boards
-                                      and how many total messages and number of
-                                      new messages they have. This is the
-                                      default for a user setting; users can
-                                      toggle this for themselves as they like.
-
-displayIndexedModeMenuIfNoNewMessages Default for a user setting for whether or
-                                      not to use the indexed menu for newscans
-                                      even when there are no new messages.
-                                      Valid values are true or false.
-
-newscanOnlyShowNewMsgs                Default for a user setting: whether or not
-                                      to only show new messages during a
-                                      newscan.  This can help speed up newscans
-                                      if your BBS has a lot of messages in the
-                                      sub-boards. Users can toggle this as they
-                                      like.
-
-indexedModeMenuSnapToFirstWithNew     For the indexed newscan sub-board menu in
-                                      lightbar mode, whether or not to 'snap'
-                                      the selected item to the next sub-board
-                                      with new messages upon displaying or
-                                      returning to the indexed newscan sub-board
-                                      menu. This is a default for a user setting
-                                      that users can toggle for themselves.
-
-
-promptDelPersonalEmailAfterReply      Default for a user setting: When reading
-                                      personal email, whether or not to propmt
-                                      the user if they want to delete a message
-                                      after replying to it
-
-themeFilename                         The name of the configuration file to
-                                      use for colors & string settings
-
-Theme configuration file
-------------------------
-The convention for the setting names in the theme configuration file is that
-setting names ending in 'Text' are for whole text strings, and the setting
-names that don't end in 'Text' are for colors.
-
-Note that if one of the strings has a space at the end, a : must be used
-instead of a = to separate the setting name from the value.  The value can also
-be inside double-quotes.  See the following for more information:
-https://wiki.synchro.net/config:ini_files#string_literals
-
-Setting                              Description
--------                              -----------
-headerMsgGroupTextColor              Color for the header line in the message
-                                     list displaying the message group, for the
-                                     text "Current msg group:"
-
-headerMsgGroupNameColor              Color for the message list header line
-                                     displaying the message group
-
-headerSubBoardTextColor              Color for the message list header line
-                                     displaying the message sub-board, for the
-                                     text "Current msg sub-board:"
-
-headerMsgSubBoardNameColor           Color for the message list header line
-                                     displaying the message sub-board
-
-listColHeaderColor                   Color for the column names in the message
-                                     list
-
-msgListMsgNumColor                   Color for the message number (in the
-                                     message list)
-msgListFromColor                     Color for the sender name (in the message
-                                     list)
-msgListToColor                       Color for the destination name (in the
-                                     message list)
-msgListSubjectColor                  Color for the message subject (in the
-                                     message list)
-msgListScoreColor                    Color for the message score (in the
-                                     message list) - For terminals at least
-                                     86 characters wide
-msgListDateColor                     Color for the message date (in the message
-                                     list)
-msgListTimeColor                     Color for the message time (in the message
-                                     list)
-
-msgListToUserMsgNumColor             Color for the message numer, for messages
-                                     to the user (in the message list)
-msgListToUserFromColor               Color for the sender name, for messages to
-                                     the user (in the message list)
-msgListToUserToColor                 Color for the destination name, for
-                                     messages to the user (in the message list)
-msgListToUserSubjectColor            Color for the message subject, for
-                                     messages to the user (in the message list)
-msgListToUserScoreColor              Color for the message score, for
-                                     messages to the user (in the message list)
-                                     - For terminals at least 86 characters
-                                     wide
-msgListToUserDateColor               Color for the message date, for messages
-                                     to the user (in the message list)
-msgListToUserTimeColor               Color for the message time, for messages
-                                     to the user (in the message list)
-
-msgListFromUserMsgNumColor           Color for the message number, for messages
-                                     from the user (in the message list)
-msgListFromUserFromColor             Color for the mender name, for messages
-                                     from the user (in the message list)
-msgListFromUserToColor               Color for the mestination name, for
-                                     messages from the user (in the message
-                                     list)
-msgListFromUserSubjectColor          Color for the message subject, for
-                                     messages from the user (in the message
-                                     list)
-msgListFromUserScoreColor            Color for the message score, for
-                                     messages from the user (in the message
-                                     list) - For terminals at least 86
-                                     characters wide
-msgListFromUserDateColor             Color for the message date, for messages
-                                     from the user (in the message list)
-msgListFromUserTimeColor             Color for the message time, for messages
-                                     from the user (in the message list)
-
-msgListHighlightBkgColor             Message list highlighting background color
-                                     (for lightbar mode)
-msgListMsgNumHighlightColor          Message list highlighted message number
-                                     color (for lightbar mode)
-msgListFromHighlightColor            Message list highlighted 'from' name color
-                                     (for lightbar mode)
-msgListToHighlightColor              Message list highlighted 'to' name color
-                                     (for lightbar mode)
-msgListSubjHighlightColor            Message list highlighted subject color
-                                     (for lightbar mode)
-msgListScoreHighlightColor           Message list highlighted score color
-                                     (for lightbar mode) - For terminals at
-                                     least 86 characters wide
-msgListDateHighlightColor            Message list highlighted date color (for
-                                     lightbar mode)
-msgListTimeHighlightColor            Message list highlighted time color (for
-                                     lightbar mode)
-
- Colors for the indexed mode sub-board menu:
-indexMenuHeader                      Header text above the indexed mode menu
-
-indexMenuNewIndicator                "NEW" indicator text at the start of the
-                                     indexed mode menu sub-boards that have new
-                                     messages
- 
-indexMenuDesc                        Indexed mode menu item description
-
-indexMenuTotalMsgs                   Indexed mode menu total number of messages
-
-indexMenuNumNewMsgs                  Indexed mode menu number of new messages
-
-indexMenuLastPostDate                Indexed mode menu last post date
-
-indexMenuHighlightBkg                Indexed mode menu highlighted item
-                                     background
-
-indexMenuNewIndicatorHighlight       Indexed mode highlighted "NEW" indicator
-                                     text at the start of the sub-boards that
-                                     have new messages
-
-indexMenuDescHighlight               Indexed mode menu highlighted description
-
-indexMenuTotalMsgsHighlight          Indexed mode menu highlighted total number
-                                     of messages
-
-indexMenuNumNewMsgsHighlight         Indexed mode menu highlighted number of new
-                                     messages
-
-indexMenuLastPostDateHighlight       Indexed mode menu highlighted last post
-                                     date
-
-indexMenuSeparatorLine               Indexed mode menu: Horizontal line
-                                     separating sub-boards
-
-indexMenuSeparatorText               Indexed mode menu: Sub-board name
-                                     separating sub-boards
-
-Colors for the indexed mode lightbar help line text:
-lightbarIndexedModeHelpLineBkgColor  Indexed help line background
-
-lightbarIndexedModeHelpLineHotkeyColor  Indexed help line hotkey color
-
-lightbarIndexedModeHelpLineGeneralColor Indexed help line general text color
-
-lightbarIndexedModeHelpLineParenColor  Indexed help line - For the ) separating
-                                       the hotkeys from general text
-
-Lightbar message list help line colors:
-
-lightbarMsgListHelpLineBkgColor      Background color for the lightbar message
-                                     list help line
-lightbarMsgListHelpLineGeneralColor  Color for the general text in the lightbar
-                                     message list help line
-lightbarMsgListHelpLineHotkeyColor   Color for the hotkeys in the lightbar
-                                     message list help line
-lightbarMsgListHelpLineParenColor    Color for the ) characters in the lightbar
-                                     message list help line
-
-tradInterfaceContPromptMainColor     Color for the text displayed in the
-                                     'continue' prompt in the traditional user
-                                     interface in the message list
-
-tradInterfaceContPromptHotkeyColor   Color for the hotkeys displayed in the
-                                     'continue' prompt in the traditional user
-                                     interface in the message list
-
-tradInterfaceContPromptUserInputColor Color for the user input at screen pauses
-                                      in the traditional user interface for the
-                                      message list
-
-msgBodyColor                         Color for the message body when reading a
-                                     message
-
-readMsgConfirmColor                  Color for the text used to confirm whether
-                                     to read a message
-
-readMsgConfirmNumberColor            Color for the message number displayed
-                                     when asking the user if they really want
-                                     to read the message
-
-afterReadMsg_ListMorePromptColor     Color for the text used for asking the user
-                                     if they want to continue listing messages
-
-tradInterfaceHelpScreenColor         Color for the text used in the traditional
-                                     user interface message list help screen
-
-areaChooserMsgAreaNumColor            Color for the message area numbers when
-                                     choosing a different message area
-
-areaChooserMsgAreaDescColor          Color for the message area descriptions
-                                     when choosing a different message area
-  
-areaChooserMsgAreaNumItemsColor      Color for the number of items when
-                                     choosing a different message area
-
-areaChooserMsgAreaHeaderColor        Color for the message area header line
-                                     when choosing a different message area.
-                                     This is the line that shows the
-                                     message group and page number.
-
-areaChooserSubBoardHeaderColor       Color for the sub-board information line
-                                     when choosing a different message area
-
-areaChooserMsgAreaMarkColor           Color for the mark character showing the
-                                     current message area when choosing a
-                                     different message area
-
-areaChooserMsgAreaLatestDateColor    Color for the latest message date when
-                                     choosing a different message area
-
-areaChooserMsgAreaLatestTimeColor    Color for the latest message date when
-                                     choosing a different message area
-
-- Colors for the built-in header displayed above a message (if not using your
-  own message header ANSI -
-msgHdrMsgNumColor                    Color for the message number displayed in
-                                     the header above a message
-
-msgHdrFromColor                      Color for the 'From' name displayed in the
-                                     header above a message
-
-msgHdrToColor                        Color for the 'To' name displayed in the
-                                     header above a message
-
-msgHdrToUserColor                    Color for the 'To' name displayed in the
-                                     header above a message when the message is
-                                     written to the current user
-
-msgHdrSubjColor                      Color for the subject displayed in the
-                                     header above a message
-
-msgHdrDateColor                      Color for the message date displayed in
-                                     the header above a message
-
-- Highlighted versions of the above message area list colors:
-areaChooserMsgAreaBkgHighlightColor
-areaChooserMsgAreaNumHighlightColor
-areaChooserMsgAreaDescHighlightColor
-areaChooserMsgAreaDateHighlightColor
-areaChooserMsgAreaTimeHighlightColor
-areaChooserMsgAreaNumItemsHighlightColor
-
-lightbarAreaChooserHelpLineBkgColor     Background color for the lightbar
-                                        message area chooser help line
-lightbarAreaChooserHelpLineGeneralColor Color for the general text in the
-                                        lightbar message area chooser help
-                                        line
-lightbarAreaChooserHelpLineHotkeyColor  Color for the hotkeys in the lightbar
-                                        message area chooser help line
-lightbarAreaChooserHelpLineParenColor   Color for the ) characters in the
-                                        lightbar message area chooser help line
-
-scrollbarBGColor                     Color for the scrollbar background in the
-                                     scrollable message reader interface
-
-scrollbarBGChar                      The character to use for the scrollbar
-                                     background characters in the scrollable
-                                     message reader interface
-
-scrollbarScrollBlockColor            Color for the moving scrollbar block in
-                                     the scrollable message reader interface
-
-scrollbarScrollBlockChar             The character to use for the moving
-                                     scrollbar block characters in the
-                                     scrollable message reader interface
-
-enhReaderPromptSepLineColor          Color for the line drawn in the 2nd to
-                                     last line of the message area in the
-                                     scrollable message reader interface
-                                     before a prompt is displayed on the next
-                                     line
-
-goToPrevMsgAreaPromptText            Text to use for prompting the user whether
-                                     or not to go to the previous message area
-                                     (without the ? on the end)
-
-goToNextMsgAreaPromptText            Text to use for prompting the user whether
-                                     or not to go to the next message area
-                                     (without the ? on the end)
-
-enhReaderHelpLineBkgColor            Color to use for the background of the
-                                     hotkey help line displayed at the bottom
-                                     of the scrollable message reader interface
-
-enhReaderHelpLineGeneralColor        Color to use for general text in the
-                                     hotkey help line displayed at the bottom
-                                     of the scrollable message reader interface
-
-enhReaderHelpLineHotkeyColor         Color to use for hotkeys in the hotkey
-                                     help line displayed at the bottom of the
-                                     scrollable message reader interface
-
-enhReaderHelpLineParenColor          Color to use for ) characters in the
-                                     hotkey help line displayed at the bottom
-                                     of the scrollable message reader interface
-
-postOnSubBoard                       The text to use for asking the user whether
-                                     they want to post on a sub-board (for
-                                     instance, after reading the last message).
-                                     The two %s will be replaced with the
-                                     message group name and sub-board
-                                     description, respectively.
-
-newMsgScanText                       The first text displayed when doing a new
-                                     message scan, before the sub-board/group/
-                                     all prompt is displayed
-
-newToYouMsgScanText                  The first text displayed when doing a
-                                     new-to-you message scan, before the
-                                     sub-board/group/all prompt is displayed
-
-allToYouMsgScanText                  The first text displayed when doing a
-                                     all-messages-to-you message scan, before
-                                     the sub-board/group/all prompt is
-                                     displayed
-
-msgScanCompleteText                  Text to display when the message scan is
-                                     complete
-
-msgScanAbortedText                   Text to display when the message scan has
-                                     been aborted
-
-msgSearchAbortedText                 Text to display when the message search has
-                                     been aborted
-
-searchingSubBoardAbovePromptText     Text for "Searching message sub-board: .."
-                                     above the search text prompt (%s is
-                                     replaced with the sub-board name)
-
-searchingSubBoardText                For displaying the sub-board name when
-                                     doing a search.  %s will be replaced with
-                                     a sub-board name.
-
-scanningSubBoardText                 For displaying the sub-board name when
-                                     doing a scan (i.e., a newscan).  %s will be
-                                     replaced with a sub-board name.
-
-noMessagesInSubBoardText             No messages in a sub-board (i.e., trying
-                                     to read a sub-board that has no messages).
-                                     %s will be replaced with a sub-board name.
-
-noSearchResultsInSubBoardText        For no search results found in a
-                                     sub-board.  %s will be replaced with a
-                                     sub-board name.
-
-readMsgNumPromptText                 Prompt text to input a number of a message
-                                     to read
-
-invalidMsgNumText                    Text to display for an invalid message
-                                     number.  %d will be replaced with the
-                                     message number.
-
-msgHasBeenDeletedText                Text to display when a message has been
-                                     deleted.  %d will be replaced with the
-                                     message number.
-
-noKludgeLinesForThisMsgText          Text to display when the user tries to
-                                     display kludge lines but there are no
-                                     kludge lines in the message
-
-searchingPersonalMailText            Text for "Seraching personal mail"
-
-searchTextPromptText                 Text for prompting for search text
-
-fromNamePromptText                   Text for prompting for a 'from' name
-
-toNamePromptText                     Text for prompting for a 'To' name
-
-abortedText                          Text for an aborted operation (i.e.,
-                                     "Aborted")
-
-loadingPersonalMailText              Text for the status message "Loading
-                                     personal mail..."  %s will be replaced
-                                     with "Personal mail".
-
-msgDelConfirmText                    Prompt text to confirm message deletion
-                                     (without the ? at the end).  %d will be
-                                     replaced with the message number.
-
-delSelectedMsgsConfirmText           Prompt text to confirm deletion of
-                                     selected messages (i..e, for batch message
-                                     deletion), without the ? at the end.
-
-msgDeletedText                       Text for when a message has been marked
-                                     for deletion.  %d will be replaced with
-                                     the message number.
-
-selectedMsgsDeletedText              Text for when selected messages have been
-                                     marked for deletion
-
-cannotDeleteMsgText_notYoursNotASysop  Error text for cannot delete a message
-                                       because the message is not the user's
-                                       message or the user is not a sysop.  %d
-                                       will be replaced with the message
-                                       number.
-
-cannotDeleteMsgText_notLastPostedMsg   Error text for cannot delete a message
-                                       because it's not the user's last posted
-                                       message in a sub-board.  %d will be
-                                       replaced with the message number.
-
-msgEditConfirmText                   Prompt text to confirm message edit
-                                     (without the ? at the end).  %d will be
-                                     replaced with the message number.
-
-noPersonalEmailText                  The text to output when the user has no
-                                     personal email messages (i.e., "You have
-                                     no messages.")
-
-deleteMsgNumPromptText               The text to output when prompting for the
-                                     number of a message to delete
-
-editMsgNumPromptText                 The text to output when prompting for the
-                                     number of a message to edit
-
-hdrLineLabelColor                    The color to use for the header/kludge
-                                     line labels
-
-hdrLineValueColor                    The color to use for the header/kludge
-                                     line values
-
-selectedMsgMarkColor                 The color to use for the checkmark for
-                                     selected messages (used in the message
-                                     list)
-
-unreadMsgMarkColor                   The color to use for the 'unread' message
-                                     marker character in the message list
-                                     (appears as a U)
-
-7. Indexed reader mode
-======================
-"Indexed" reader mode is a new mode that was added to DDMsgReader v1.70.  This
-mode displays a menu/list of message sub-boards within their groups with total
-and number of new messages and allows the user to select one to read.  There is
-also a user setting to allow the user to use this mode for a newscan (rather
-than the traditional newscan) if they wish.
-
-Indexed reader mode may also be started with the -indexedMode command-line
-parameter.  For example, if you are using a JavaScript command shell:
-  bbs.exec("?../xtrn/DDMsgReader/DDMsgReader.js -indexedMode");
-With the above command-line parameter, DDMsgReader will show all sub-boards the
-user is allowed to read and which they have in their newscan configuration.
-If the user has enabled indexed mode for newscans, then during a newscan, it
-will show sub-boards based on the user's chosen option for current
-sub-board/group/all.
-  
-This is an example of the sub-board menu that appears in indexed mode - And from
-here, the user can choose a sub-board to read:
-
-Description                                                 Total New Last Post
-───────────────────────────────────────────────────────────────────────────────
-    AGN GEN - General Chat                                   1004  0 2023-04-02
-    AGN BBS - BBS Discussion                                 1000  0 2023-01-17
-NEW AGN ART - Art/Demo Scene                                  603  1 2023-04-02
-    AGN DEV - Software Development                            398  0 2021-11-09
-    AGN NIX - Unix/Linux Related                              297  0 2023-04-02
-    AGN L46 - League Scores & Recons                         1000  0 2016-09-10
-NEW AGN TST - Testing Setups                                 2086 10 2023-04-03
-    AGN SYS - Sysops Only                                    1000  0 2023-01-19
-───── FIDO - FidoNet ──────────────────────────────────────────────────────────
-NEW BBS CARNIVAL - BBS Software Chatter                       660  5 2023-04-04
-    BBS INTERNET - DOS/Win/OS2/Unix Internet BBS Applicatio    18  0 2023-03-04
-    CHWARE - Cheepware Support/Discussion                     111  0 2023-03-16
-    CLASSIC COMPUTER - Classic Computers                      191  0 2023-02-10
-    CONSPRCY - Conspiracy Discussions                          59  0 2023-03-14
-    CONTROVERSIAL - Controversial Topics, current events, at    3  0 2023-01-31
-NEW DOORGAMES - BBS Doorgames and discussions                 288  1 2023-04-03
-NEW FUNNY - FUNNY Jokes and Stories                          1184  3 2023-04-04
-    FUTURE4FIDO - Discussion of new and future Fidonet tec    152  0 2023-04-01
-    LINUX BBS - Linux BBSing                                   46  0 2023-04-01
-    LINUX - Linux operating system (OS), a Unix vari         1076  0 2023-04-01
-    LINUX-UBUNTU - The Ubuntu Linux Distribution Discussion    18  0 2023-02-17
-NEW MEMORIES - NOSTALGIA                                     2434  3 2023-04-03
-
-
-8. Quick-Validating users (while reading their message)
-=======================================================
-While reading messages, the sysop may apply quick-validation values to a local
-user using the Ctrl-Q hotkey. Quick-Validation sets are configured in SCFG >
-System > Security Options > Quick-Validation Values. In DDMsgReader.cfg, the
-option quickUserValSetIndex can be used to set the index of the quick-validation
-set to use (normally it would be 0-9, as there are 10 sets of values in SCFG).
-Alternately, quickUserValSetIndex can be set to something invalid (like -1) for
-DDMsgReader to display a menu of the quick-validation sets to let you choose
-one.
-
-DDMsgReader applies the flag sets, exemptions, and restrictions to a user in
-addition to any that the user might already have, so that any that you have
-added for a user will be preserved (DDMsgReader does a bitwise 'or').
-
-A quick-validation set in CFG is a set that includes a security level, flag
-sets, exemptions, restrictions, and additional credits. For example:
-╔[■][?]═══════════════════╗
-║ Quick-Validation Values ║
-╠═════════════════════════╣
-║ ?0  SL: 5   F1:         ║
-║ ?1  SL: 10  F1:         ║
-║ ?2  SL: 20  F1:         ║
-║ ?3  SL: 30  F1:         ║
-║ ?4  SL: 40  F1:         ║
-║ ?5  SL: 50  F1:         ║
-║ ?6  SL: 60  F1:         ║
-║ ?7  SL: 70  F1:         ║
-║ ?8  SL: 80  F1:         ║
-║ ?9  SL: 90  F1:         ║
-╚═════════════════════════╝
-
-
-9. Drop file for replying to messages with Synchronet message editors
-=====================================================================
-When reading a message, the message lister will write a drop file in the node
-directory, called DDML_SyncSMBInfo.txt, which contains some information about
-the message being read, for use by Synchronet message editors.  This drop file
-is provided for Synchronet message editors to get information about the current
-message being read if needed.  In Synchronet's JavaScript object model, there
-are certain variables that are provided about the current message and sub-board
-being read that are normally updated by Synchronet's built-in message read
-prompt, but JavaScript scripts cannot modify those values.  Because of that,
-the solution was for the message lister to provide the message information in a
-drop file in the node directory just before allowing the user to reply to a
-message.  When the user is done replying to the message, DDML_SyncSMBInfo.txt
-will be deleted.
-DDML_SyncSMBInfo.txt contains the following information:
-Line 1: The highest message number in the sub-board.  Equivalent to
-        bbs.smb_last_msg in Synchronet's JavaScript object model.
-Line 2: The total number of messages in the sub-board.  Equivalent to
-        bbs.smb_total_msgs in Synchronet's JavaScript object model.
-Line 3: The (absolute) message number of the message being read.  Equivalent to
-        bbs.msg_number in Synchronet's JavaScript object model and the "header"
-        property of a message header.
-Line 4: The sub-board code (text).  Equivalent to bbs.smb_sub_code in
-        Synchronet's JavaScript object model.
-
-One message editor in particular, SlyEdit, needs to be able to determine which
-message is currently being read and replied to so that it can retrieve the
-message author's initials for use in quoting the messsage.  Normally, SlyEdit
-will get the message information from the aforementioned JavaScript variables
-provided by Synchronet, but if DDML_SyncSMBInfo.txt exists, SlyEdit will read
-the message information from that file instead.
-Note that if you have SlyEdit installed on your BBS, this version of Digital
-Distortion Message Reader (1.00) requires version 1.27 or newer of SlyEdit in
-order for SlyEdit to properly get the correct message from the message lister.
-
-10. text.dat lines used in Digital Distortion Message Reader
-===========================================================
-This message reader uses the following lines from Synchronet's text.dat file
-(located in the sbbs/ctrl directory):
-10 (Email)
-30 (Aborted)
-54 (DeleteMailQ)
-117 (MessageScanComplete)
-390 (UnknownUser)
-501 (SelectItemHdr)
-503 (SelectItemWhich)
-563 (Pause)
-578 (QWKNoNewMessages)
-621 (SubGroupOrAll)
-662 (DownloadAttachedFileQ)
-759 (CantReadSub)
-779 (VotingNotAllowed)
-780 (VotedAlready)
-781 (R_Voting)
-783 (VoteMsgUpDownOrQuit)
-787 (PollVoteNotice)
diff --git a/revision_history.txt b/revision_history.txt
deleted file mode 100644
index d432b8fbb3d40c20ae467fc1f953b7511e69dc53..0000000000000000000000000000000000000000
--- a/revision_history.txt
+++ /dev/null
@@ -1,581 +0,0 @@
-This file lists all of the changes made for each release of the Digital
-Distortion Message Reader.
-
-Revision History (change log)
-=============================
-Version  Date         Description
--------  ----         -----------
-1.93a    2024-01-07   Fix: For indexed read mode (not doing a newscan), when
-                      choosing a sub-board to read, the correct (first unread)
-                      message is displayed. Also, the user's scan pointer is
-                      also updated to the last_read pointer.
-                      New operator option for read mode: Add author email to email.can
-1.93     2024-01-01   New user-toggleable behavior: Show indexed menu after
-                      reading all new messages
-                      Also, indexed reader mode (started with the -indexedMode
-                      command-line option) now lists ALL sub-boards, rather
-                      than only sub-boards the user has enabled for newscan.
-                      It also prompts the user to list sub-boards in the current
-                      group or all.
-1.92     2023-12-29   Indexed newscan: By default, if there are no new messages,
-                      it now shows "No new messages." (578 QWKNoNewMessages from
-                      text.dat). There's a new user setting to toggle whether to
-                      use the indexed newscan menu even if there are no new
-                      messages.
-                      New configuration file option:
-                      displayIndexedModeMenuIfNoNewMessages, which is a default
-                      for a user setting to toggle whether or not to use the
-                      Indexed newscan menu even when there are no new messages.
-1.91     2023-12-26   New sysop features while reading a message: Show message
-                      hex (with the X key) and save message hex to a file (with
-                      Ctrl-X)
-1.90b    2023-12-15   New configurable colors in the theme file for the indexed
-                      newscan menu header text (indexMenuHeader), "NEW"
-                      indicator text (indexMenuNewIndicator), and highlighted
-                      "NEW" indicator text (indexMenuNewIndicatorHighlight)
-1.90a    2023-12-12   New configurable colors in the theme file for the indexed
-                      mode newscan menu: indexMenuSeparatorLine (sub-board
-                      separator line) and indexMenuSeparatorText (sub-board
-                      separator text)
-1.90     2023-12-04   New: operator menu for read mode, with the option to add
-                      the author to the twit list, etc.
-                      Fix: When refreshing a rectangular area of a message, if
-                      it's a poll message, the background color for the voted
-                      responses was used for the non-selected responses.
-                      Removed the setting useScrollingInterfaceForANSIMessages.
-1.89     2023-11-30   New: User option to toggle whether to display the email
-                      'replied' indicator (defaults to true).
-                      Fix for setting colors for the key help lines so that the
-                      background won't get un-done if the other help line colors
-                      have a N (normal) attribute.
-1.88     2023-11-24   New user setting/configuration option to prompt the user
-                      whether or not to delete a personal email after replying
-                      to it (defaults to false).
-                      New: Displays whether a personal email has been replied
-                      to.
-                      Fix: Now displaying message vote score in the default
-                      header again.
-                      Fix: When viewing message headers (for the sysop), now
-                      correctly shows the message attributes.
-1.87     2023-11-18   Possible speed improvement when loading messages.
-                      New: User setting to only show new messages in a newscan
-                      (defaults to true/enabled)
-                      In the message list, there is now an additional space
-                      before the 'from' name, in case one of the status
-                      characters is a letter (this should look better).
-                      New: In lightbar mode, the indexed newscan menu can
-                      optionally 'snap' to the next sub-board with new messages
-                      when showing/returning to the menu
-                      Fix: When listing personal email, messages to the user
-                      were written with the to-user color wuen unread. Now the
-                      regular colors are always used (since all of a user's
-                      personal emails are 'to' them).
-                      Fix: For indexed newscan, if there are no sub-boards
-                      selected for scan in the user's newscan configuration,
-                      then output a message and exit. Otherwise, it would end
-                      up in an infinite loop.
-                      Updated how user settings are loaded, to ensure that
-                      default user settings from DDMsgReader.cfg actually get
-                      set properly in the user settings.
-1.86     2023-11-09   New feature: For indexed mode, when choosing a sub-board,
-                      the R key can be used to mark all messages as read in the
-                      sub-board.
-                      Fix: For continuous newscan or browse newscan (SCAN_BACK),
-                      call the stock Synchronet behavior (DDMsgReader did this
-                      previously, as DDMsgReader doesn't implement those yet).
-                      Fix: In the message list, to-user alternate colors weren't
-                      being used unless the message was read. The correct colors
-                      are used again.
-1.85     2023-11-01   Mark personal email as read if the user is just reading
-                      personal email
-1.84     2023-10-26   Fix in reader mode for refreshing the message area after
-                      closing another window (necessary with recent changes to
-                      substrWithAttrCodes())
-1.83     2023-10-25   Personal emails to the sysop received as "sysop" (or
-                      starting with "sysop") are now correctly identified and
-                      marked as read when read
-1.82     2023-10-18   Fix for # posts and missing dates in sub-board list when
-                      changing sub-board
-1.81     2023-10-11   Updated permission check functions (speed improvement)
-1.80     2023-10-10   Improved speed of new-to-you scans, and to an extent
-                      (hopefully) overall speed
-                      Bug fix: Setting reverseListOrder to "ask" in the .cfg
-                      file works properly again.
-                      Bug fix: When listing messages in reverse order, the
-                      selected menu index (for lightbar mode) is now correct.
-                      Bug fix: If the user is allowed to read deleted messages,
-                      then allow the left & right arrow keys to to the next or
-                      previous message if it's deleted.
-                      Small fixes for indexed scanning mode.
-                      New: For personal email, unread emails will have an
-                      'unread' message indicator in the message list as a U
-                      between the message number and the 'from' name.
-                      New user setting: "Quit from reader to message list":
-                      When enabled, quitting from reader mode goes to the
-                      message list instead of exiting out of DDMsgReader fully.
-                      New user setting: Enter/selection from indexed mode menu
-                      shows message list (instead of going into reader mode)
-                      New user setting: List messages in reverse order
-1.79     2023-09-20   Fixed poll voting for single-answer polls
-1.78     2023-08-30   Bug fix for going to a specific message in the message
-                      list (especially for lightbar mode)
-1.77a    2023-08-26   When saving a message on the local BBS PC without all the
-                      headers, the date is now included
-1.77     2023-08-20   Including all message headers when saving a message (sysop
-                      only) is now optional.
-1.76     2023-08-18   Fix for "Message header has 'expanded fields'" error when
-                      updating message header attributes in certain conditions
-1.75     2023-08-16   Made some changes to allow easy searching of personal
-                      email with command-line arguments.
-1.74     2023-04-29   Settings for users being able to read deleted messages now
-                      applies to personal email. Also, allows reading messages
-                      that are marked for deletion in addition to just seeing
-                      them in the message list
-1.73a    2023-04-25   For viewing message headers, now all message header
-                      information is displayed & sorted alphabetically by field
-                      name (same with saving a message to a file).
-1.73     2023-04-17   Bug fix: When getting header lines to view, ensure the
-                      header lines are not too wide for the user's terminal.
-                      Header lines that are too long will be split into no more
-                      than 2 lines.
-1.72     2023-04-16   Added a quick-validation hotkey, Ctrl-Q, for sysops to
-                      use to apply a quick-validation set to a user when
-                      reading their message. Quick-Validation sets are
-                      configured in SCFG > System > Security Options >
-                      Quick-Validation Values.
-1.71     2023-04-07   Ctrl-C is now supported for message searches to abort the
-                      search. A new configurable string was added for this
-                      situation: msgSearchAbortedText
-1.70     2023-04-04   Added "indexed" reader mode, which lists sub-boards with
-                      total and number of new/unread messages and lets the user
-                      read messages in those sub-boards. Also, utf-8 characters
-                      should now be converted properfly for non utf-8 terminals.
-1.69     2023-03-24   Bug fix for deleting multiple selected messages: When
-                      updating message headers in the cached arrays, don't try
-                      to save them back to the database, because that was
-                      already done (this avoids a 'header has expanded fields'
-                      error).
-1.68     2023-03-15   Makes use of console.aborted when displaying help screens
-                      so that screen updates work better after pausing output.
-                      Also, when running a new message scan (not new-to-you),
-                      the current sub-board being scanned is now outputted.
-                      There is a new configurable text string:
-                      scanningSubBoardText
-1.67     2023-03-09   Fixes for time zone alignment & list key help for wide
-                      terminals
-1.66     2023-03-02   When forwarding a message, the subject can now be edited
-                      before sending the message
-1.65     2023-02-24   Ctrl-C can now be used to cancel message scans. Output
-                      from the scan is now word-wrapped to the terminal width.
-1.64     2023-02-09   When reading personal email (received or sent), as a
-                      loadable module, now it can allow reading another user's
-                      mail (for the sysop when deleting a user account).
-1.63     2023-02-01   Fix for reading colors from the theme file. Also, the
-                      theme file now no longer needs the control character for
-                      color codes.
-1.62     2023-01-30   (Hopefully) Improved display of ANSI messages which would
-                      previously look bad with empty lines evrey other line
-1.61     2023-01-22   Fix: When replying to an email with an unknown sender
-                      (empty or "All"), no longer gives the error "Invalid user
-                      field: 0"; also, if the sender is unknown, prompts the
-                      user for a user name/number/email address to send the
-                      reply to.
-1.60     2023-01-20   DDMsgReader can now optionally convert Y-style MCI
-                      attribute codes to to Synchronet attribute codes, with
-                      the new configuration setting convertYStyleMCIAttrsToSync
-                      (true/false).
-1.59     2022-12-29   For Synchronet above 3.20, now reads the external editor
-                      quote wrap setting from xtrn.ini.  Below version 3.20, the
-                      quote wrap setting is read from xtrn.cnf.
-                      Also, there's a new user setting to toggle whether or not
-                      to use the scrollbar in the scrolling reader. Currently
-                      there is no alternate progress displayed if not using the
-                      scrollbar, but that is planned for a future update.
-1.58     2022-12-14   Now wraps quote lines, if applicable, according to the
-                      quote line wrap settings of the user's external editor,
-                      if the user uses one
-1.57.1   2022-12-12   Fix for "assignment to undeclared variable" error
-1.57     2022-12-02   @-codes were only expanded when reading personal mail;
-                      now, DDMsgReader also checks to make sure the sender is a
-                      sysop.
-1.56     2022-11-25   Fixed bug startup mode for scanning all groups for un-read
-                      messages to you where the reader was bringing up personal
-                      email instead.
-1.55     2022-09-23   Refactored how email replies are done (passing the header
-                      to the appropriate functions, not using ungetstr() when
-                      prompting for the message subject)
-1.54     2022-08-06   Users now have a personal twit list, configurable via
-                      user settings, with the Ctrl-U hotkey.
-1.53     2022-07-18   Deleted messages can now be un-marked for deletion from
-                      the message list (if the user has delete permissions).
-                      Also, the reader now honors the system setting for whether
-                      users can view deleted messages.
-1.52     2022-07-09   Mouse click support for the bottom help lines in scrolling
-                      mode (thanks to help from Nelgin)
-1.51     2022-07-05   Graphic is now only used when using the scrollable
-                      interface. Also, when creating the Graphic, now
-                      subtracting 1 from the reading area height to avoid making
-                      the Graphic one line too tall to avoid unnecessary
-                      scrolling.
-                      When saving messages with ANSI codes, Graphic is only used
-                      if the message has any ASCII drawing characters. (not sure
-                      if this really matters much though).
-                      Also, applied "use strict" and made some changes as necessary.
-1.50     2022-06-20   When doing a text search, it now ignores the user scan
-                      configuration for sub-boards, to ensure it will show any
-                      results of the text search.
-1.49     2022-06-13   Refactor: Simplified saving a message to BBS machine for
-                      sysop (as-is, less processing); removed attachment stuff
-                      for pre-Synchronet 3.17; moved hasSyncAttrCodes() to
-                      attr_conv.js because that's where it needs to be.
-1.48     2022-06-12   Improved display of ANSI messages
-1.47a    2022-03-23   Internal change: Now calls bbs.edit_msg() for editing an
-                      existing message (for Synchronet 3.18 and up).
-                      Functionally no change.
-1.47     2022-03-14   DDMsgReader can now be called directly as a loadable
-                      module by Synchronet (though requires the included
-                      ddmr_lm.js if DDMsgReader.js is not in sbbs/exec or
-                      sbbs/mods)
-1.46     2022-03-07   Fix: When changing to an empty sub-board from within the
-                      reader (either from read mode or list mode), it now
-                      properly says there are no messages and exits, rather than
-                      showing a list of bogus messages.  Unsure when this bug
-                      was introduced.
-1.45d    2022-02-26   Fix for no group information available when displaying the
-                      sub-board header above the message list when listing
-                      personal email
-1.45c    2022-02-25   Fixed score display and related colors in the message list
-                      for wide terminals
-1.45b    2022-02-25   Fixed message list time colors for wide terminals (above
-                      80 columns)
-1.45     2022-02-24   Fixed message scanning & searching issue introduced in the
-                      previous version.
-1.44     2022-02-19   Removed the scanScopePromptText text line and used the
-                      SubGroupOrAll line (621) from text.dat instead.  Also, the
-                      reader now supports @-code expansion in configured text
-                      strings.
-                      Text search now can search sub-board, group, or all like
-                      the other text searching.
-                      When reading the theme file, color settings are now
-                      checked to ensure they only have Synchronet attribute
-                      codes.
-1.43     2022-02-10   Fixed the memory error when viewing message header info.
-                      Also, when viewing message header information, it will no
-                      longer show JS functions.
-1.42     2022-01-13   Fixed attachment downloading.
-                      Also, the first attempt at converting HTML entities in
-                      HTML-formatted messages.
-                      Also, added the ability to sort the message list by date
-                      & time written rather than the import date/time. This is
-                      specified in the configuration file via the msgListSort
-                      option.
-1.41     2021-02-12   Bug fix: When changing to another area with the lightbar
-                      interface, if the user's current sub-board is a
-                      high-numbered sub-board and they select a message group
-                      with fewer sub-boards, the highlighted sub-board in that
-                      group would be set to that high number and would be
-                      incorrect.  That has been fixed.  Copied a fix from my
-                      stand-alone message area chooser.  In that scenario, the
-                      current highlighted sub-board in the other group will be
-                      the first one.
-1.40     2021-01-31   (Michael Long) Fixed left/right colors not being
-                      customizable on message list lightbar
-1.39     2020-12-01   When forwarding a message, added the ability to optinally
-                      edit the message before forwarding it.
-1.38     2020-11-26   Bug fix: When forwarding a message, it now correctly sets
-                      sets the to_net_type property in the message header to
-                      FidoNet or internet for those types of message
-                      destinations
-1.37     2020-07-11   Added mouse support to the scrollable reader interface.
-                      The integrated area changer functionality doesn't have
-                      mouse support yet.
-1.36     2020-05-23   Added a command-line parameter, -onlyNewPersonalEmail,
-                      which specifies to list/read only new/unread personal
-                      email to the user.  And for integration with Synchronet
-                      via the "Read Email" loadable module, this is to be used
-                      together with the updated DDReadPersonalEmail.js.  This
-                      is intended to support the "read your unread mail only"
-                      option in the email menu.
-1.35     2020-05-13   Fixed some logic in determining how to address a personal
-                      email when replying (either to a local user or via their
-                      network address).
-1.34     2020-05-11   The message list mode now honors anonymous posts, showing
-                      the 'from' name as "Anonymous" (for non-sysops).  The
-                      sysop can still see the real name of the poster.  The
-                      reader mode already honored the 'anonymous' flag.
-1.33     2020-04-21   Fixed: A new user starting to read messages in a sub-board
-                      no longer causes an error (it checks for the scan_ptr
-                      being 0xffffffff).  This had been fixed in a couple places
-                      previously, but apparently not this particular case.
-1.32     2020-04-19   Removed some code that's no longer used.  Also, fixed an
-                      issue when changing to another sub-board with the
-                      traditional-style (non-lightbar) list where it was slow to
-                      list sub-boards.  For the number of messages, it was
-                      checking all headers to ignore ones marked as deleted,
-                      etc., but that can be fairly slow..  Now it just uses
-                      total_msgs for the MessageBase object, which is a lot
-                      faster and still gives an idea of how many messages are
-                      there.
-1.31     2020-04-13   The area change feature now uses DDLightbarMenu.  There
-                      is no more internal lightbar code in this message reader.
-1.30     2020-04-07   The message list features now uses DDLightbarMenu rather
-                      than the internal lightbar menu code.  Requires the
-                      latest dd_lightbar_menu.js (in sbbs/exec/load).
-                      Later I also plan to update the area chooser code to also
-                      use DDLightbarMenu and remove the internal lightbar
-                      chooser code from DDMsgReader altogether.
-1.29     2020-04-03   When reading a message, if a message is written to the
-                      current user, the 'To' username in the header above the
-                      message is now written in a different color.  Also, there
-                      are new color settings available in the theme
-                      configuration file (see the readme for descriptions):
-                      msgHdrMsgNumColor, msgHdrFromColor, msgHdrToColor,
-                      msgHdrToUserColor, msgHdrSubjColor, msgHdrDateColor
-1.28     2019-12-21   Bug fix: When the user changes to a different message
-                      area while reading a message, the reader would exit with
-                      an error due to an invalid last-read message number.
-                      This has been fixed.
-1.27     2019-09-16   Bug fix: Now displays the message score in the header
-                      even if the message only has downvotes
-1.26     2019-09-12   Fixed a bug that was causing some of the message vote
-                      tally information to be displayed as "undefined"
-1.25     2019-08-29   Added the ability to search for message groups and
-                      sub-boards when changing to another sub-board. Search
-                      can be started with the / key or CTRL-F (Find). Also, in
-                      lightbar mode, the N key can be used to highlight the
-                      next match in the list.
-1.24     2019-08-17   When making a private reply on local email, an error is
-                      now outputted if the recipient's user number is not
-                      found.  Also, fixed an 'undefined' bug that happened when
-                      searching for messages sometimes.
-1.23     2019-07-27   If a message is in UTF-8 format and the user's terminal
-                      doesn't support UTF-8, the message text will be converted
-                      to CP437.  Also, if there is a color/attribute code in
-                      the message before the message text and there are no
-                      other color/attribute codes, the color/attribute codes
-                      will be removed so  that the entire message isn't colored
-1.22     2019-05-12   If the message score is 0, with upvotes and total_votes
-                      both 0, then don't show the score when using the default
-                      header ANSI.  This is what was intended, but the 0 score
-                      started showing up in more recent builds of Synchronet.
-1.21     2019-05-04   New uses require() instead of load(), if the require()
-                      function exists, to load required .js library scripts.
-                      This helps avoid 'multiple definition' errors.  The
-                      require() function was added in Synchronet 3.17, so
-                      if the require() function doesn't exist, then the reader
-                      will use load().
-1.20     2019-04-26   Added configurable options for the message score colors
-                      for the message list: msgListScoreColor,
-                      msgListToUserScoreColor, msgListFromUserScoreColor, and
-                      msgListScoreHighlightColor
-1.19     2019-04-25   If the terminal size is wide enough (at least 86
-                      characters), the overall vote scores for the messages
-                      is now displayed in the message list.  Also, fixed a
-                      bug introduced in the previous version where the vote
-                      scores were no longer being displayed when reading a
-                      message.  It's no longer using MsgBase.get_index() and
-                      uses get_all_msg_headers(), as before, since that's what
-                      is required for message tallies to be included in the
-                      message headers.
-1.18     2019-04-15   Made use of the new MsgBase.get_index() function (if
-                      available) for better performance.  Added 'undefined'
-                      checks for some of the messaeg attribute definitions
-                      before adding them to the attribute strings, since some
-                      of them have changed.
-1.17     2019-01-02   Added support for Synchronet's new voting feature that
-                      was added in Synchronet 1.17.  Added support to allow the
-                      sysop to validate messages in moderated message areas.
-                      Fixed various bugs related to doing a newscan, displaying
-                      messages with ANSI content, out-of-bounds error when
-                      deleting a message, etc.  Updated to set the message
-                      number field length dynamically based on the number of
-                      messages in the sub-board.  It will be at least 4 but
-                      can be more than 4 if there are 10000 messages or more
-                      in a sub-board.  Updated so that when listing personal
-                      email, it will use the regular formatting colors rather
-                      than the colors for messages to the user, since all
-                      personal emails are to the user (the 'to user' colors for
-                      each email might be obnoxious).
-                      Also, updated to support Synchronet's Avatar feature
-                      which was added in version 3.17.  For that feature to
-                      work, you will need the latest .js files - Specifically,
-                      for this reader, you would need smbdefs.js and
-                      avatar_lib.js.  Added a new configuration setting,
-                      displayAvatars, which toggles whether or not to display
-                      user avatars in the message headers.  Valid values are
-                      true and false.
-                      Also Added the new @-code MSG_FROM_AND_FROM_NET and
-                      MSG_FROM_AND_FROM_NET-L (for left-justification with
-                      field length), which shows the 'from' name with the from
-                      network in parenthesis.  Updated the default message
-                      header to show that information.
-                      Also contains various bug fixes.
-1.16     2016-09-11   Added a new feature that allows users to forward a
-                      message to an email address or to another user on the
-                      BBS (using the O key).  This can be useful, for
-                      instance, if the user wants to send a message in a
-                      public sub-board to their personal email for future
-                      reference or send a message from a public sub-board to
-                      another user to discuss the topic privately.
-1.15     2016-08-29   - New user edit feature for sysops only: While reading
-                      a message, the U key will edit the user account of the
-                      author of the message (only if it's a local user account
-                      on the BBS)
-                      - Bug fix: Private reply on a networked sub-board
-                      was no longer working (different from the bug fixed
-                      in 1.14)
-                      - Implemented a check to (hopefully) prevent a crash
-                      related to parsing and replacing @-codes related to
-                      file areas
-1.14     2016-08-17   Bug fix: Version 1.13 was failing to reply to
-                      private emails
-1.13     2016-08-16   - Bug fix: Message number error when a new user starts
-                      reading messages.
-                      - Bug fix: Now, it should always successfully save a
-                      message header with the READ attribute when the user
-                      it was addressed to has read the message.  This should
-                      fix an issue where the same message would keep coming
-                      up in a newscan, etc.
-1.12     2016-05-11   - Updated the way the pause prompt is shown in the help
-                      screen, in case the sysop has configured an external
-                      module (Baja/JS) to run for a pause prompt.
-                      - Potential bug fix: When translating a message number to
-                      a message index, added a check to ensure the value is
-                      a number, to (hopefully) avoid a potential crash.
-1.11     2016-03-25   The reader now updates the number of new posts read by
-                      the user during the session.  This is represented by
-                      bbs.posts_read in JavaScript.  Also, did some internal
-                      refactoring of the code, removing some old code leftover
-                      from my message lister that is no longer needed in this
-                      reader.  DDMsgReader.js is a bit smaller due to the
-                      refactor.
-1.10     2016-02-19   Added a new configuration option,
-                      readingPostOnSubBoardInsteadOfGoToNext, that affects what
-                      happens after the user reads the last message on a
-                      sub-board (for normal reading, not for newscans etc.): If
-                      this is set to true, then the reader will prompt the user
-                      if they want to post on the sub-board, then exit (this
-                      is the stock Synchronet behavior).  If this is set to
-                      false, then the reader will prompt the user whether to go
-                      to the next sub-board after reading the last message on a
-                      sub-board.  Also, added the postOnSubBoard text
-                      configuration parameter for the theme filename, which
-                      specifies the text to use for prompting the user if they
-                      want to post on the sub-board after reading the last
-                      message.
-                      Added new configuration options areaChooserHdrFilenameBase
-                      and areaChooserHdrMaxLines.  These options specify the
-                      filename base for a header file to use for the message
-                      area chooser list and the maximum number of lines to use
-                      from the area chooser header file.  The filaname is
-                      without the extension - The reader will first look for an
-                      .ans version, then an .asc version.  Additionally,
-                      multiple header files can be used for different terminal
-                      widths - For example, chooserMsgHdr-80.ans for an
-                      80-column terminal, choosrMsgHdr-140.ans for a 140-column
-                      terminal, etc.
-                      Updated so that when using the message written dates
-                      (instead of the imported dates) in the message list & area
-                      chooser, it will adjust the message written dates to the
-                      BBS's local time zone so that they are all consistent.
-1.09     2016-01-15   Updated to not center the message header lines
-                      horizontally.  Now, it will display the header lines
-                      starting on column 1.  This was done to fix a display
-                      issue in some terminal software.
-1.08     2016-01-10   Bug fix: When scanning message sub-boards, it wasn't
-                      always closing the sub-board when there were no new
-                      messages, resulting in further sub-boards failing to open
-                      after a while.  That has been fixed.
-1.07     2015-12-24   - Added the ability to select multiple messages (for
-                      actions such as batch delete), and added the ability to
-                      delete multiple selected messages.  Batch deleting is
-                      only allowed when the user has permission to delete
-                      messages (such as their own personal email).  Messages
-                      can be selected in the following ways:
-                        o Lightbar message list: The spacebar selects an
-                          individual message.  CTRL-A lets the user select or
-                          un-select all messages.
-                        o Traditional message list: The S key lets the user
-                          select or un-select messages, by typing message
-                          numbers, A to select all, or N to select none
-                          (un-select all).  The list of message numbers is
-                          comma-separated or space-separated, allowing
-                          for number ranges such as 120-130 for instance.
-                        o Reader interface: The spacebar selects the message.
-                      To delete the selected messages, the user must be in the
-                      message list; the CTRL-D key combo is used for batch
-                      delete, and it will prompt the user for confirmation
-                      before deleting the messages.
-                      - Added the following configurable items in the theme
-                      file:
-                      delSelectedMsgsConfirmText
-                      selectedMsgsDeletedText
-                      cannotDeleteAllSelectedMsgsText
-                      selectedMsgMarkColor
-1.06     2015-12-13   - Updated so that a sub-board new-message-scan (with the
-                      new_msg_scan_cur_sub command-line parameter) can make
-                      use of the -subBoard command-line option to scan a
-                      specific sub-board, which may be different than the
-                      user's current sub-board.
-                      - Added a new configuration option, pauseAfterNewMsgScan,
-                      which specifies whether or not to pause (i.e., with a
-                      "finished" message) after doing a new message scan.
-                      - Bug fix: The configFilename command-line parameter was
-                      not being read correctly on startup; this has been fixed.
-                      - Bug fix: Doing a new-message-scan should now always
-                      display the correct sub-board @-code information in the
-                      header above the message.
-1.05     2015-12-06   - Improved displaying of messages with ANSI codes.  The
-                      reader now makes use of frame.js and scrollbar.js (in the
-                      sync/exec/load directory) to enable a scrollable user
-                      interface when displaying messages with ANSI content.
-                      There is also a new configuration option,
-                      readerInterfaceStyleForANSIMessages, which lets the sysop
-                      configure whether to use a scrollable or traditional user
-                      interface for ANSI messages.  The reason for that option
-                      is in case ANSI messages don't look good when using
-                      frame.js & scrollbar.js - When set to a traditional user
-                      interface for ANSI messages, the reader will use a
-                      non-scrolling user interface for displaying messages with
-                      ANSI codes, which simply sends the message to the client
-                      and lets the client display the ANSI content.
-                      - More kludge lines displayed (with the 'K' key), and
-                      all message header lines are now displayed (with the
-                      'H' key).  This is a sysop feature.
-                      - Color configuration options for the kludge/header line
-                      labels (hdrLineLabelColor) and kludge/header line values
-                      (hdrLineValueColor)
-                      - Bug fix related to interpreting colors from other BBS
-                      software (WWIV, PCBoard, Wildcat, Celerity, Renegade)
-1.04     2015-10-10   - New feature: Users can now download attached files,
-                      whether uploaded to their mailbox in Synchronet or
-                      attached via internet email.
-                      - New feature: Sysops can save a message to the BBS
-                      machine (using the Ctrl-S key combo).
-                      - User experience improvements: Added a pause after
-                      saving a message so that the user can see Synchronet's
-                      message save screen before going back to the reader or
-                      message list.  Also, in the message list, PageDown now
-                      goes to the last message when on the last page, and
-                      similarly, PageUp goes to the first message when on the
-                      first page.
-                      - Updated the DDReadPersonalMail.js loadable module
-                      script to start reading personal email in lister mode
-                      by default, which is more in line with what Synchronet
-                      does by default.  That will let the user select a message
-                      to read first.
-1.03     2015-07-12   Bug fix: In Linux, when replying to the last message in
-                      a sub-board during a newscan or in read mode, it would
-                      not immediately refresh the messagebase information, so
-                      it would not see the new message posted.  This has been
-                      fixed by closing the messagebase while the user is
-                      posting a reply message and re-opening the messagebase
-                      when the user is done posting the reply.
-1.02     2015-06-10   Bug fix in DDScanMsgs.js: Switched to bbs.scan_msgs()
-                      instead of bbs.scan_subs() for all other scan modes
-                      besides SCAN_READ.  Updated the version number to
-                      reflect that; no change to the actual reader.
-1.01     2015-05-17   Bug fix: The enhanced reader header file is now correctly
-                      displayed even if the lengths of its lines are
-                      inconsistent.
-1.00     2015-05-06   Initial release
diff --git a/xtrn/DDMsgReader/DDMsgReader.js b/xtrn/DDMsgReader/DDMsgReader.js
index 88db114104a676c4393b60a6b65d4568b9d9508c..14f3f96e0e6182834deb6e81d89fad9e8bbab245 100644
--- a/xtrn/DDMsgReader/DDMsgReader.js
+++ b/xtrn/DDMsgReader/DDMsgReader.js
@@ -124,7 +124,10 @@
  *                              Fix: For indexed read mode (not doing a newscan), when choosing a sub-board to
  *                              read, the correct (first unread) message is displayed. Also, the user's scan
  *                              pointer is also updated to the last_read pointer.
- * 2024-01-06 Eric Oulashin     Version 1.93a
+ * 2024-01-07 Eric Oulashin     Version 1.93a
+ *                              New operator option for read mode: Add author email to email.can.
+ *                              New command-line option: -indexModeScope, which can specify the indexed
+ *                              reader scope (group/all) without prompting the user.
  *                              Releasing this version
  */
 
@@ -232,7 +235,7 @@ var hexdump = load('hexdump_lib.js');
 
 // Reader version information
 var READER_VERSION = "1.93a";
-var READER_DATE = "2024-01-06";
+var READER_DATE = "2024-01-07";
 
 // Keyboard key codes for displaying on the screen
 var UP_ARROW = ascii(24);
@@ -574,14 +577,26 @@ if (gDoDDMR)
 	{
 		console.attributes = "N";
 		console.crlf();
-		console.mnemonics("Indexed read: ~Group: @GRP@, or ~@All@: ");
-		var scopeChar = console.getkeys("GA").toString();
+		var scopeChar = "";
+		if (typeof(gCmdLineArgVals.indexmodescope) === "string")
+		{
+			var argScopeLower = gCmdLineArgVals.indexmodescope.toLowerCase();
+			if (argScopeLower == "group" || argScopeLower == "grp")
+				scopeChar = "G";
+			else if (argScopeLower == "all")
+				scopeChar = "A";
+		}
+		if (scopeChar == "")
+		{
+			console.mnemonics("Indexed read: ~Group: @GRP@, or ~@All@: ");
+			scopeChar = console.getkeys("GA").toString();
+		}
 		if (typeof(scopeChar) === "string" && scopeChar != "")
 		{
 			var scanScope = SCAN_SCOPE_ALL;
 			if (scopeChar == "G")
 				scanScope = SCAN_SCOPE_GROUP;
-			else if (scopeChar == "G")
+			else if (scopeChar == "A")
 				scanScope = SCAN_SCOPE_ALL;
 			msgReader.DoIndexedMode(scanScope, false);
 		}
@@ -1082,10 +1097,10 @@ function DigDistMsgReader(pSubBoardCode, pScriptArgs)
 		userSettings: CTRL_U,
 		validateMsg: "A", // Only if the user is a sysop
 		quickValUser: CTRL_Q,
+		threadView: "*", // TODO: Implement this
 		operatorMenu: CTRL_O,
 		showMsgHex: "X",
-		hexDump: CTRL_X,
-		threadView: "*" // TODO: Implement this
+		hexDump: CTRL_X
 	};
 	//if (user.is_sysop)
 	//	this.enhReaderKeys.validateMsg = "A";
@@ -1576,7 +1591,8 @@ function DigDistMsgReader(pSubBoardCode, pScriptArgs)
 		showMsgHex: 5,
 		saveMsgHexToFile: 6,
 		quickValUser: 7,
-		addAuthorToTwitList: 8
+		addAuthorToTwitList: 8,
+		addAuthorEmailToEmailFilter: 9
 	};
 
 	// For indexed mode, whether to set the indexed mode menu item index to 1 more when showing
@@ -6448,6 +6464,24 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 									this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
 								}
 								break;
+							case this.readerOpMenuOptValues.addAuthorEmailToEmailFilter:
+								var fromEmailAddr = "";
+								if (typeof(msgHeader.from_net_addr) === "string" && msgHeader.from_net_addr.length > 0)
+								{
+									if (msgHeader.from_net_type == NET_INTERNET)
+										fromEmailAddr = msgHeader.from_net_addr;
+									else
+										fromEmailAddr = msgHeader.from + "@" + msgHeader.from_net_addr;
+								}
+								var promptTxt = format("Add %s to global email filter", fromEmailAddr);
+								if (this.EnhReaderPromptYesNo(promptTxt, msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr, solidBlockStartRow, numSolidScrollBlocks, true))
+								{
+									var statusMsg = "\x01n" + addToGlobalEmailFilter(fromEmailAddr) ? "\x01w\x01hSuccessfully updated the email filter" : "\x01y\x01hFailed to update the email filter!"
+									writeWithPause(1, console.screen_rows, statusMsg, ERROR_PAUSE_WAIT_MS, "\x01n", true);
+									console.attributes = "N";
+									this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
+								}
+								break;
 						}
 					}
 				}
@@ -7768,6 +7802,25 @@ function DigDistMsgReader_ReadMessageEnhanced_Traditional(msgHeader, allowChgMsg
 									console.pause();
 								}
 								break;
+							case this.readerOpMenuOptValues.addAuthorEmailToEmailFilter:
+								var fromEmailAddr = "";
+								if (typeof(msgHeader.from_net_addr) === "string" && msgHeader.from_net_addr.length > 0)
+								{
+									if (msgHeader.from_net_type == NET_INTERNET)
+										fromEmailAddr = msgHeader.from_net_addr;
+									else
+										fromEmailAddr = msgHeader.from + "@" + msgHeader.from_net_addr;
+								}
+								var promptTxt = format("Add %s to global email filter", fromEmailAddr);
+								if (!console.noyes(promptTxt))
+								{
+									var statusMsg = "\x01n" + addToGlobalEmailFilter(fromEmailAddr) ? "\x01w\x01hSuccessfully updated the email filter" : "\x01y\x01hFailed to update the email filter!"
+									console.print(statusMsg);
+									console.attributes = "N";
+									console.crlf();
+									console.pause();
+								}
+								break;
 						}
 					}
 				}
@@ -7859,7 +7912,7 @@ function DigDistMsgReader_CreateReadModeOpMenu()
 	var subBoardIsModerated = (this.subBoardCode != "mail" && msg_area.sub[this.subBoardCode].is_moderated);
 
 	var opMenuWidth = 35;
-	var opMenuHeight = 10;
+	var opMenuHeight = 11;
 	if (subBoardIsModerated)
 		++opMenuHeight;
 	var opMenuX = Math.floor(console.screen_columns/2) - Math.floor(opMenuWidth/2);
@@ -7882,6 +7935,7 @@ function DigDistMsgReader_CreateReadModeOpMenu()
 		opMenu.Add("&E: Save message hex to file", this.readerOpMenuOptValues.saveMsgHexToFile);
 		opMenu.Add("&A: Quick validate the user", this.readerOpMenuOptValues.quickValUser);
 		opMenu.Add("&I: Add author to twit list", this.readerOpMenuOptValues.addAuthorToTwitList);
+		opMenu.Add("&M: Add author to email filter", this.readerOpMenuOptValues.addAuthorEmailToEmailFilter);
 		// Use cyan for the item color, and cyan with blue background for selected item color
 		opMenu.colors.itemColor = "\x01n\x01c";
 		opMenu.colors.selectedItemColor = "\x01n\x01c\x014";
@@ -7896,6 +7950,7 @@ function DigDistMsgReader_CreateReadModeOpMenu()
 		opMenu.Add("Save message hex to file", this.readerOpMenuOptValues.saveMsgHexToFile);
 		opMenu.Add("Quick validate the user", this.readerOpMenuOptValues.quickValUser);
 		opMenu.Add("Add author to twit list", this.readerOpMenuOptValues.addAuthorToTwitList);
+		opMenu.Add("Add author to email filter", this.readerOpMenuOptValues.addAuthorEmailToEmailFilter);
 		// Use green for the item color and high cyan for the item number color
 		opMenu.colors.itemColor = "\x01n\x01g";
 		opMenu.colors.itemNumColor = "\x01n\x01c\x01h";
@@ -23999,6 +24054,69 @@ function entryExistsInTwitList(pStr)
 	return entryExists;
 }
 
+// Adds to the global email filter (email.can).
+//
+// Parameters:
+//  pEmailAddr: An email address
+//
+// Return value: Boolean - Whether or not this was successful
+function addToGlobalEmailFilter(pEmailAddr)
+{
+	if (typeof(pEmailAddr) !== "string")
+		return false;
+
+	var wasSuccessful = true;
+	if (!entryExistsInGlobalEmailFilter(pEmailAddr))
+	{
+		wasSuccessful = false;
+		var filterFile = new File(system.text_dir + "email.can");
+		//if (filterFile.open(filterFile.exists ? "r+" : "w+"))
+		if (filterFile.open("a"))
+		{
+			wasSuccessful = filterFile.writeln(pEmailAddr);
+			filterFile.close();
+		}
+	}
+	return wasSuccessful;
+}
+// Returns whether an entry exists in the global email filter (email.can).
+//
+// Parameters:
+//  pEmailAddr: An entry to check in the twit list
+//
+// Return value: Boolean - Whether or not the given string exists in the twit list
+function entryExistsInGlobalEmailFilter(pEmailAddr)
+{
+	if (typeof(pEmailAddr) !== "string")
+		return false;
+
+	var entryExists = false;
+	var filterFile = new File(system.text_dir + "email.can");
+	if (filterFile.open("r"))
+	{
+		while (!filterFile.eof && !entryExists)
+		{
+			//// Read the next line from the config file.
+			var fileLine = filterFile.readln(2048);
+			// fileLine should be a string, but I've seen some cases
+			// where for some reason it isn't.  If it's not a string,
+			// then continue onto the next line.
+			if (typeof(fileLine) != "string")
+				continue;
+			// If the line starts with with a semicolon (the comment
+			// character) or is blank, then skip it.
+			if ((fileLine.substr(0, 1) == ";") || (fileLine.length == 0))
+				continue;
+
+			// See if this line matches the given string
+			entryExists = (pEmailAddr == skipsp(truncsp(fileLine)));
+		}
+
+		filterFile.close();
+	}
+	return entryExists;
+}
+
 ///////////////////////////////////////////////////////////////////////////////////
 
 // For debugging: Writes some text on the screen at a given location with a given pause.
diff --git a/xtrn/DDMsgReader/readme.txt b/xtrn/DDMsgReader/readme.txt
index 27ff1d99ed56d11924ad61f6fecd01e5dc768ead..1be848e6750091e7ff63bd5239763c522e3e91fb 100644
--- a/xtrn/DDMsgReader/readme.txt
+++ b/xtrn/DDMsgReader/readme.txt
@@ -1,6 +1,6 @@
                       Digital Distortion Message Reader
                                  Version 1.93a
-                           Release date: 2024-01-06
+                           Release date: 2024-01-07
 
                                      by
 
@@ -312,6 +312,8 @@ The following are the command-line parameters supported by DDMsgReader.js:
               user wants to list sub-boards in the current group, or all
               sub-boards.
               This is intended to work if it is the only command-line option.
+-indexModeScope: Specifies the scope (set of sub-boards) for indexed reader mode
+                 with -indexedMode. Valid values are "group" or "all".
 -search: A search type.  Available options:
  keyword_search: Do a keyword search in message subject/body text (current message area)
  from_name_search: 'From' name search (current message area)
@@ -452,6 +454,14 @@ common message operations.
 - Start in indexed reader mode:
 ?../xtrn/DDMsgReader/DDMsgReader.js -indexedMode
 
+- Start in indexed reader mode for the sub-boards in the current group (without
+prompting):
+?../xtrn/DDMsgReader/DDMsgReader.js -indexedMode -indexModeScope=group
+
+- Start in indexed reader mode for all sub-boards(without prompting):
+?../xtrn/DDMsgReader/DDMsgReader.js -indexedMode -indexModeScope=all
+
+
 - Text (keyword) search in the current sub-board, and list the messages found:
 ?../xtrn/DDMsgReader/DDMsgReader.js -search=keyword_search -startMode=list
 
@@ -1248,11 +1258,15 @@ Indexed reader mode may also be started with the -indexedMode command-line
 parameter.  For example, if you are using a JavaScript command shell:
   bbs.exec("?../xtrn/DDMsgReader/DDMsgReader.js -indexedMode");
 With the above command-line parameter, DDMsgReader will show all sub-boards the
-user is allowed to read and which they have in their newscan configuration.
-If the user has enabled indexed mode for newscans, then during a newscan, it
-will show sub-boards based on the user's chosen option for current
-sub-board/group/all.
-  
+user is allowed to read.  It will prompt the user to use sub-boards in the
+current group or all sub-boards.
+
+To have it start in indexed reader for the current group without prompting:
+bbs.exec("?../xtrn/DDMsgReader/DDMsgReader.js -indexedMode -indexModeScope=group");
+
+To have it start in indexed reader for all sub-boards without prompting:
+bbs.exec("?../xtrn/DDMsgReader/DDMsgReader.js -indexedMode -indexModeScope=all");
+
 This is an example of the sub-board menu that appears in indexed mode - And from
 here, the user can choose a sub-board to read:
 
diff --git a/xtrn/DDMsgReader/revision_history.txt b/xtrn/DDMsgReader/revision_history.txt
index 0869282b9762d9dd68e46355e8e02a6247b8f124..ead58f5b52e909c978a40472eef78378dec377e2 100644
--- a/xtrn/DDMsgReader/revision_history.txt
+++ b/xtrn/DDMsgReader/revision_history.txt
@@ -5,10 +5,15 @@ Revision History (change log)
 =============================
 Version  Date         Description
 -------  ----         -----------
-1.93a    2024-01-06   Fix: For indexed read mode (not doing a newscan), when
+1.93a    2024-01-07   Fix: For indexed read mode (not doing a newscan), when
                       choosing a sub-board to read, the correct (first unread)
                       message is displayed. Also, the user's scan pointer is
                       also updated to the last_read pointer.
+                      New operator option for read mode: Add author email to
+                      email.can
+                      New command-line option: -indexModeScope, which can
+                      specify the indexed reader scope (group/all) without
+                      prompting the user.
 1.93     2024-01-01   New user-toggleable behavior: Show indexed menu after
                       reading all new messages
                       Also, indexed reader mode (started with the -indexedMode