Skip to content
Snippets Groups Projects
DDMsgReader.js 653 KiB
Newer Older
/* 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)
 * 2015-05-06 Eric Oulashin     Version 1.0
 *                              Finally releasing it, as it seems fairly stable
 *                              and has the basic features implemented.
 * 2015-05-17 Eric Oulashin     Version 1.01
 *                              Bug fix: Updated the setting of the enhanced reader
 *                              header width to use the longest line in the header
 *                              (rather than the length of only the first line) to
 *                              ensure that the header displays correctly.
 * 2015-06-10 Eric Oulashin     Version 1.02
 *                              Updated the version to reflect a bug fix in
 *                              DDScanMsgs.js.  No change to the actual reader.
 * 2015-07-11 Eric Oulashin     Version 1.03 Beta
 *                              Started looking into & fixing an issue in Linux
 *                              where after replying to a message, the number of
 *                              messages was not immediately refreshed, so for
 *                              instance, replying to the last message in read
 *                              mode, the reader would not be able to navigate
 *                              to the next message without first going to the
 *                              previous message.
 * 2015-07-12 Eric Oulashin     Version 1.03
 *                              Releasing this version after having done more testing.
 * 2015-07-19 Eric Oulashin     Version 1.04 Beta
 *                              Updated to pause (wait for user keypress) after saving
 *                              a reply message to allow the user to see Synchronet's
 *                              success/fail message on saving a message.
 * 2015-08-09 Eric Oulashin     Adding the ability for the sysop to save a message
 *                              to a file on the BBS machine
 * 2015-09-07 Eric Oulashin     Updated so that in lightbar mode, pressing PageDown
 *                              on the last page will go to the last message, and
 *                              pressing PageUp on the first page will go to the
 *                              first message.  Also, in the enhanced reader mode,
 *                              added a console pause after posting a message in
 *                              the sub-board so that the user can see the info
 *                              screen that Synchronet displays after saving a
 *                              message.
 * 2015-09-19 Eric Oulashin     Started working on adding the ability to download file
 *                              attachments.  Started working on a new function,
 *                              determineMsgAttachments(), which can parse message text
 *                              to save any base64-encoded attachments that might be
 *                              present, and also to check the message subject for a
 *                              filename for a file uploaded to the user's inbox.
 * 2015-10-10 Eric Oulashin     Version 1.04
 *                              Releasing this version after development & testing,
 *                              since attachment downloading and the other new features
 *                              seem to be working fairly well.
 * 2015-10-25 Eric Oulashin     Version 1.05 Beta
 *                              Started updating the reader to display more header &
 *                              kludge lines.
 * 2015-10-28 Eric Oulashin     Started working on updating the ANSIAttrsToSyncAttrs()
 *                              function to use Synchronet's ans2asc tool to convert
 *                              from ANSI to Synchronet codes, to get ANSI messsages
 *                              to look better.
 * 2015-11-07 Eric Oulashin     Expanded the list of @-codes interpreted for message
 *                              headers.  Also, renamed that method to ParseMsgAtCodes().
 *                              Updated the ReadMessageEnhanced method to interpret
 *                              @-codes, but only when reading personal mail, to avoid
 *                              weird behavior on message networks from malicious users
 *                              on other BBSes.
 * 2015-11-24 Eric Oulashin     Started working on using the Frame class (in frame.js)
 *                              to display messages with ANSI codes using a scrollable
 *                              user interface
 * 2015-12-06 Eric Oulashin     Version 1.05
 *                              Officially releasing this version, as it seems to be
 *                              fairly stable after testing.
 * 2015-12-10 Eric Oulashin     Version 1.06 beta
 *                              Bug fix: The scriptFilename command-line argument was
 *                              not being referenced properly in the DigDistMsgReader
 *                              constructor; that has been fixed.
 * 2015-12-11 Eric Oulashin     Updated DigDistMsgReader_MessageAreaScan() so that
 *                              the current sub-board newscan functionality 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.
 *                              Bug fix: Updated DigDistMsgReader_MessageAreaScan()
 *                              to temporarily set bbs.curgrp and bbs.cursub so that
 *                              all @-codes for the sub-boards will be displayed
 *                              correctly by Synchronet.
 * 2015-12-12 Eric Oulashin     Added a new configuration options, pauseAfterNewMsgScan,
 *                              which specifies whether or not to pause after doing
 *                              a new message scan.
 * 2015-12-13 Eric Oulashin     Version 1.06
 *                              Releasing this version after testing showed it's
 *                              working as expected
 */

/* 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.
*/

// TODO:
// - Idea for future release: Add some extra functionality to the enhanced
//   reader interface (perhaps accessible on their own small menu or via
//   CTRL hotkeys):
//   - Forward the current message (to username, user #, internet email address,
//     FTN email address, QWK email address, etc.)
//   - Save the message to the BBS computer (sysop only)

load("sbbsdefs.js");
load("text.js"); // Text string definitions (referencing text.dat)

// This script requires Synchronet version 3.15 or higher.
// Exit if the Synchronet version is below the minimum.
if (system.version_num < 31500)
{
	var message = "\1n\1h\1y\1i* Warning:\1n\1h\1w Digital Distortion Message Reader "
	             + "requires version \1g3.15\1w or\r\n"
	             + "higher of Synchronet.  This BBS is using version \1g" + system.version
	             + "\1w.  Please notify the sysop.";
	console.crlf();
	console.print(message);
	console.crlf();
	console.pause();
	exit();
}

var READER_VERSION = "1.06";
var READER_DATE = "2015-12-13";

// 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);
// PageUp & PageDown keys - Not real key codes, but codes defined
// to be used & recognized in this script
var KEY_PAGE_UP = "\1PgUp";
var KEY_PAGE_DOWN = "\1PgDn";
// 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;
// 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)


// Characters for display
// Box-drawing/border characters: Single-line
var UPPER_LEFT_SINGLE = "Ú";
var HORIZONTAL_SINGLE = "Ä";
var UPPER_RIGHT_SINGLE = "¿";
var VERTICAL_SINGLE = "³";
var LOWER_LEFT_SINGLE = "À";
var LOWER_RIGHT_SINGLE = "Ù";
var T_SINGLE = "Â";
var LEFT_T_SINGLE = "Ã";
var RIGHT_T_SINGLE = "´";
var BOTTOM_T_SINGLE = "Á";
var CROSS_SINGLE = "Å";
// Box-drawing/border characters: Double-line
var UPPER_LEFT_DOUBLE = "É";
var HORIZONTAL_DOUBLE = "Í";
var UPPER_RIGHT_DOUBLE = "»";
var VERTICAL_DOUBLE = "º";
var LOWER_LEFT_DOUBLE = "È";
var LOWER_RIGHT_DOUBLE = "¼";
var T_DOUBLE = "Ë";
var LEFT_T_DOUBLE = "Ì";
var RIGHT_T_DOUBLE = "¹";
var BOTTOM_T_DOUBLE = "Ê";
var CROSS_DOUBLE = "Î";
// Box-drawing/border characters: Vertical single-line with horizontal double-line
var UPPER_LEFT_VSINGLE_HDOUBLE = "Õ";
var UPPER_RIGHT_VSINGLE_HDOUBLE = "¸";
var LOWER_LEFT_VSINGLE_HDOUBLE = "Ô";
var LOWER_RIGHT_VSINGLE_HDOUBLE = "¾";
// Other special characters
var DOT_CHAR = "ú";
var CHECK_CHAR = "û";
var THIN_RECTANGLE_LEFT = "Ý";
var THIN_RECTANGLE_RIGHT = "Þ";
var BLOCK1 = "°"; // Dimmest block
var BLOCK2 = "±";
var BLOCK3 = "²";
var BLOCK4 = "Û"; // Brightest block


const ERROR_PAUSE_WAIT_MS = 1500;

// gIsSysop stores whether or not the user is a sysop.
var gIsSysop = user.compare_ars("SYSOP"); // Whether or not the user is a sysop
// Store whether or not the Synchronet compile date is at least May 12, 2013
// so that we don't have to call compileDateAtLeast2013_05_12() multiple times.
var gSyncCompileDateAtLeast2013_05_12 = compileDateAtLeast2013_05_12();
// 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;

const THREAD_BY_ID = 15;
const THREAD_BY_TITLE = 16;
const THREAD_BY_AUTHOR = 17;
const THREAD_BY_TO_USER = 18;

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;

// Strings for the various message attributes (used by makeAllAttrStr(),
// makeMainMsgAttrStr(), makeAuxMsgAttrStr(), and makeNetMsgAttrStr())
var gMainMsgAttrStrs = new Object();
gMainMsgAttrStrs[MSG_DELETE] = "Del";
gMainMsgAttrStrs[MSG_PRIVATE] = "Priv";
gMainMsgAttrStrs[MSG_READ] = "Read";
gMainMsgAttrStrs[MSG_PERMANENT] = "Perm";
gMainMsgAttrStrs[MSG_LOCKED] = "Lock";
gMainMsgAttrStrs[MSG_ANONYMOUS] = "Anon";
gMainMsgAttrStrs[MSG_KILLREAD] = "Killread";
gMainMsgAttrStrs[MSG_MODERATED] = "Mod";
gMainMsgAttrStrs[MSG_VALIDATED] = "Valid";
gMainMsgAttrStrs[MSG_REPLIED] = "Repl";
gMainMsgAttrStrs[MSG_NOREPLY] = "NoRepl";
var gAuxMsgAttrStrs = new Object();
gAuxMsgAttrStrs[MSG_FILEREQUEST] = "Freq";
gAuxMsgAttrStrs[MSG_FILEATTACH] = "Attach";
gAuxMsgAttrStrs[MSG_TRUNCFILE] = "TruncFile";
gAuxMsgAttrStrs[MSG_KILLFILE] = "KillFile";
gAuxMsgAttrStrs[MSG_RECEIPTREQ] = "RctReq";
gAuxMsgAttrStrs[MSG_CONFIRMREQ] = "ConfReq";
gAuxMsgAttrStrs[MSG_NODISP] = "NoDisp";
var gNetMsgAttrStrs = new Object();
gNetMsgAttrStrs[MSG_LOCAL] = "FromLocal";
gNetMsgAttrStrs[MSG_INTRANSIT] = "Transit";
gNetMsgAttrStrs[MSG_SENT] = "Sent";
gNetMsgAttrStrs[MSG_KILLSENT] = "KillSent";
gNetMsgAttrStrs[MSG_ARCHIVESENT] = "ArcSent";
gNetMsgAttrStrs[MSG_HOLD] = "Hold";
gNetMsgAttrStrs[MSG_CRASH] = "Crash";
gNetMsgAttrStrs[MSG_IMMEDIATE] = "Now";
gNetMsgAttrStrs[MSG_DIRECT] = "Direct";
gNetMsgAttrStrs[MSG_GATE] = "Gate";
gNetMsgAttrStrs[MSG_ORPHAN] = "Orphan";
gNetMsgAttrStrs[MSG_FPU] = "FPU";
gNetMsgAttrStrs[MSG_TYPELOCAL] = "ForLocal";
gNetMsgAttrStrs[MSG_TYPEECHO] = "ForEcho";
gNetMsgAttrStrs[MSG_TYPENET] = "ForNetmail";




// 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 frame.js and scrollbar.js exist in sbbs/exec/load on the BBS machine.
// If so, load them.  They will be used for displaying messages with ANSI content
// with a scrollable user interface.
var gFrameJSAvailable = file_exists(backslash(system.exec_dir) + "load/frame.js");
if (gFrameJSAvailable)
	load("frame.js");
var gScrollbarJSAvailable = file_exists(backslash(system.exec_dir) + "load/scrollbar.js");
if (gScrollbarJSAvailable)
	load("scrollbar.js");

/////////////////////////////////////////////
// Script execution code

// Parse the command-line arguments
var gCmdLineArgVals = parseArgs(argv);
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.print("\1n");
	console.crlf();
	console.print("\1cMessage search:");
	console.crlf();
	var allowedKeys = "";
	if (!gListPersonalEmailCmdLineOpt)
	{
		allowedKeys = "ANKFTYUS";
		console.print(" \1g\1hN\1y = \1n\1cNew message scan");
		console.crlf();
		console.print(" \1g\1hK\1y = \1n\1cKeyword");
		console.crlf();
		console.print(" \1h\1gF\1y = \1n\1cFrom name");
		console.crlf();
		console.print(" \1h\1gT\1y = \1n\1cTo name");
		console.crlf();
		console.print(" \1h\1gY\1y = \1n\1cTo you");
		console.crlf();
		console.print(" \1h\1gU\1y = \1n\1cUnread (new) messages to you");
		console.crlf();
		console.print(" \1h\1gS\1y = \1n\1cScan for msgs to you");
		console.crlf();
	}
	else
	{
		// Reading personal email - Allow fewer choices
		allowedKeys = "KF";
		console.print(" \1g\1hK\1y = \1n\1cKeyword");
		console.crlf();
		console.print(" \1h\1gF\1y = \1n\1cFrom name");
		console.crlf();
	}
	console.print(" \1h\1gA\1y = \1n\1cAbort");
	console.crlf();
	console.print("\1n\1cMake a selection\1g\1h: \1c");
	// 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:
			console.print("\1n\1h\1y\1iAborted\1n");
			console.crlf();
			console.pause();
			break;
	}
}

{
	// 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 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;
			msgReader.ReadOrListSubBoard();
			break;
		case SEARCH_KEYWORD:
			msgReader.SearchMessages("keyword_search");
			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.print(msgReader.text.newMsgScanText);
				console.crlf();
			}
			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.print(msgReader.text.newToYouMsgScanText);
				console.crlf();
			}
			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.print(msgReader.text.allToYouMsgScanText);
				console.crlf();
			}
			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 directory if it exists
	deltree(gFileAttachDir);

	// 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.print("\1n");
	if (console.term_supports(USER_ANSI))
		console.print("");
}

// End of script execution.  Functions below:

///////////////////////////////////////////////////////////////////////////////////
// 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)
{
	// 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 pArgv can specify
	// another mode.
	this.startMode = READER_MODE_LIST;

	// 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 = new Object();
	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.subBoardCode = bbs.cursub_code; // The message sub-board code
	this.readingPersonalEmail = false;
	// A method to set subBoardCode and readingPersonalEmail
	this.setSubBoardCode = DigDistMsgReader_SetSubBoardCode;

	// this.colors will be an array of colors to use in the message list
	this.colors = getDefaultColors();
	this.msgbase = null;    // Will be a MsgBase object.
	this.readingPersonalEmailFromUser = false;
	if (typeof(pSubBoardCode) == "string")
		var subCodeLowerCase = pSubBoardCode.toLowerCase();
		if (subBoardCodeIsValid(subCodeLowerCase))
		{
			this.setSubBoardCode(subCodeLowerCase);
			this.msgbase = new MsgBase(this.subBoardCode);
			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.
	this.promptToContinueListingMessages = false;
	// Whether or not to prompt the user to confirm to read a message
	this.promptToReadMessage = false;

	// String lengths for the columns to write
	// Fixed field widths: Message number, date, and time
	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 (based on a screen width of
	// 80 columns)
	this.FROM_LEN = (console.screen_columns * (15/80)).toFixed(0);
	this.TO_LEN = (console.screen_columns * (15/80)).toFixed(0);
	this.SUBJ_LEN = (console.screen_columns * (22/80)).toFixed(0);

	// 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;

	// reverseListOrder stores whether or not to arrange the message list descending

	// 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;

	// Construct the header format string
	this.sHdrFormatStr = "%" + this.MSGNUM_LEN + "s %-" + this.FROM_LEN + "s %-"
	                   + this.TO_LEN + "s %-" + this.SUBJ_LEN + "s %-"
	                   + this.DATE_LEN + "s %-" + this.TIME_LEN + "s";
	// 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.sHdrFormatStr += "\r\n";

	// this.text is an object containing text used for various functionality.
	this.text = new Object();
	this.text.scrollbarBGChar = BLOCK1;
	this.text.scrollbarScrollBlockChar = BLOCK2;
	this.text.goToPrevMsgAreaPromptText = "\1n\1c\1hGo to the previous message area";
	this.text.goToNextMsgAreaPromptText = "\1n\1c\1hGo to the next message area";
	this.text.newMsgScanText = "\1c\1hN\1n\1cew \1hM\1n\1cessage \1hS\1n\1ccan";
	this.text.newToYouMsgScanText = "\1c\1hN\1n\1cew \1hT\1n\1co \1hY\1n\1cou \1hM\1n\1cessage \1hS\1n\1ccan";
	this.text.allToYouMsgScanText = "\1c\1hA\1n\1cll \1hM\1n\1cessages \1hT\1n\1co \1hY\1n\1cou \1hS\1n\1ccan";
	this.text.scanScopePromptText = "\1n\1h\1wS\1n\1gub-board, \1h\1wG\1n\1group, or \1h\1wA\1n\1gll \1h(\1wENTER\1n\1g to cancel\1h)\1n\1g: \1h\1c";
	this.text.goToMsgNumPromptText = "\1n\1cGo to message # (or \1hENTER\1n\1c to cancel)\1g\1h: \1c";
	this.text.msgScanAbortedText = "\1n\1h\1cM\1n\1cessage scan \1h\1y\1iaborted\1n";
	this.text.deleteMsgNumPromptText = "\1n\1cNumber of the message to be deleted (or \1hENTER\1n\1c to cancel)\1g\1h: \1c";
	this.text.editMsgNumPromptText = "\1n\1cNumber of the message to be edited (or \1hENTER\1n\1c to cancel)\1g\1h: \1c";
	this.text.searchingSubBoardAbovePromptText = "\1n\1cSearching (current sub-board: \1b\1h%s\1n\1c)";
	this.text.searchingSubBoardText = "\1n\1cSearching \1h%s\1n\1c...";
	this.text.noMessagesInSubBoardText = "\1n\1h\1bThere are no messages in the area \1w%s\1b.";
	this.text.noSearchResultsInSubBoardText = "\1n\1h\1bNo messages were found in the area \1w%s\1b with the given search criteria.";
	this.text.msgScanCompleteText = "\1n\1h\1cM\1n\1cessage scan complete\1h\1g.\1n";
	this.text.invalidMsgNumText = "\1n\1y\1hInvalid message number: %d";
	this.text.readMsgNumPromptText = "\1n\1g\1h\1i* \1n\1cRead message #: \1h";
	this.text.msgHasBeenDeletedText = "\1n\1h\1g* \1yMessage #\1w%d \1yhas been deleted.";
	this.text.noKludgeLinesForThisMsgText = "\1n\1h\1yThere are no kludge lines for this message.";
	this.text.searchingPersonalMailText = "\1w\1hSearching personal mail\1n";
	this.text.searchTextPromptText = "\1cEnter the search text\1g\1h:\1n\1c ";
	this.text.fromNamePromptText = "\1cEnter the 'from' name to search for\1g\1h:\1n\1c ";
	this.text.toNamePromptText = "\1cEnter the 'to' name to search for\1g\1h:\1n\1c ";
	this.text.abortedText = "\1n\1y\1h\1iAborted\1n";
	this.text.loadingPersonalMailText = "\1n\1cLoading %s...";
	this.text.msgDelConfirmText = "\1n\1h\1yDelete\1n\1c message #\1h%d\1n\1c: Are you sure";
	this.text.msgDeletedText = "\1n\1cMessage #\1h%d\1n\1c has been marked for deletion.";
	this.text.cannotDeleteMsgText_notYoursNotASysop = "\1n\1h\1wCannot delete message #\1y%d \1wbecause it's not yours or you're not a sysop.";
	this.text.cannotDeleteMsgText_notLastPostedMsg = "\1n\1h\1g* \1yCannot delete message #%d. You can only delete your last message in this area.\1n";
	this.text.msgEditConfirmText = "\1n\1cEdit message #\1h%d\1n\1c: Are you sure";
	this.text.noPersonalEmailText = "\1n\1cYou have no messages.";

	// Set the methods for the object
	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.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.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.ReadMessage = DigDistMsgReader_ReadMessage;
	this.ReadMessageEnhanced = DigDistMsgReader_ReadMessageEnhanced;
	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.CanDelete = DigDistMsgReader_CanDelete;
	this.CanDeleteLastMsg = DigDistMsgReader_CanDeleteLastMsg;
	this.CanEdit = DigDistMsgReader_CanEdit;
	this.CanQuote = DigDistMsgReader_CanQuote;
	this.ReadConfigFile = DigDistMsgReader_ReadConfigFile;
	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.NumMessages = DigDistMsgReader_NumMessages;
	this.HighestMessageNum = DigDistMsgReader_HighestMessageNum;
	this.IsValidMessageNum = DigDistMsgReader_IsValidMessageNum;
	this.PromptForMsgNum = DigDistMsgReader_PromptForMsgNum;
	this.ParseMsgAtCodes = DigDistMsgReader_ParseMsgAtCodes;
	this.ReplaceMsgAtCodeFormatStr = DigDistMsgReader_ReplaceMsgAtCodeFormatStr;
	this.FindNextNonDeletedMsgIdx = DigDistMsgReader_FindNextNonDeletedMsgIdx;
	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.DisplayEnhancedReaderWholeScrollbar = DigDistMsgReader_DisplayEnhancedReaderWholeScrollbar;
	this.UpdateEnhancedReaderScollbar = DigDistMsgReader_UpdateEnhancedReaderScollbar;
	this.MessageIsDeleted = DigDistMsgReader_MessageIsDeleted;
	this.MessageIsLastFromUser = DigDistMsgReader_MessageIsLastFromUser;
	this.DisplayEnhReaderError = DigDistMsgReader_DisplayEnhReaderError;
	this.EnhReaderPromptYesNo = DigDistMsgReader_EnhReaderPromptYesNo;
	this.DeleteMessage = DigDistMsgReader_DeleteMessage;
	this.GetExtdMsgHdrInfo = DigDistMsgReader_GetExtdMsgHdrInfo;
	this.GetMsgInfoForEnhancedReader = DigDistMsgReader_GetMsgInfoForEnhancedReader;
	this.GetLastReadMsgIdx = DigDistMsgReader_GetLastReadMsgIdx;
	this.GetScanPtrMsgIdx = DigDistMsgReader_GetScanPtrMsgIdx;
	this.SearchingAndResultObjsDefinedForCurSub = DigDistMsgReader_SearchingAndResultObjsDefinedForCurSub;
	this.RemoveFromSearchResults = DigDistMsgReader_RemoveFromSearchResults;
	this.FindThreadNextOffset = DigDistMsgReader_FindThreadNextOffset;
	this.FindThreadPrevOffset = DigDistMsgReader_FindThreadPrevOffset;

	// 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;

	// An option for using the scrollable interface for messages with ANSI
	// content - The sysop can set this to false if the sysop thinks the
	// scrolling ANSI interface (using frame.js and scrollbar.js) doesn't
	// look good enough
	this.useScrollingInterfaceForANSIMessages = true;

	// Whether or not to pause (with a message) after doing a new message scan
	this.pauseAfterNewMsgScan = true;

	this.cfgFilename = "DDMsgReader.cfg";
	// Check the command-line arguments for a custom configuration file name
	// before reading the configuration file.
	var scriptArgsIsValid = (typeof(pScriptArgs) == "object");
	if (scriptArgsIsValid && pScriptArgs.hasOwnProperty("configfilename"))
		this.cfgFilename = pScriptArgs["configfilename"];
	// Read the settings from the config file
	this.cfgFileSuccessfullyRead = false;
	this.ReadConfigFile();
	// 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 = "\1n" + this.colors.msgListMsgNumHighlightColor + this.colors.msgListHighlightBkgColor;
	this.colors.msgListFromHighlightColor = "\1n" + this.colors.msgListFromHighlightColor + this.colors.msgListHighlightBkgColor;
	this.colors.msgListToHighlightColor = "\1n" + this.colors.msgListToHighlightColor + this.colors.msgListHighlightBkgColor;
	this.colors.msgListSubjHighlightColor = "\1n" + this.colors.msgListSubjHighlightColor + this.colors.msgListHighlightBkgColor;
	this.colors.msgListDateHighlightColor = "\1n" + this.colors.msgListDateHighlightColor + this.colors.msgListHighlightBkgColor;
	this.colors.msgListTimeHighlightColor = "\1n" + this.colors.msgListTimeHighlightColor + this.colors.msgListHighlightBkgColor;
	// Similar for the area chooser lightbar highlight colors
	this.colors.areaChooserMsgAreaNumHighlightColor = "\1n" + this.colors.areaChooserMsgAreaNumHighlightColor + this.colors.areaChooserMsgAreaBkgHighlightColor;
	this.colors.areaChooserMsgAreaDescHighlightColor = "\1n" + this.colors.areaChooserMsgAreaDescHighlightColor + this.colors.areaChooserMsgAreaBkgHighlightColor;
	this.colors.areaChooserMsgAreaDateHighlightColor = "\1n" + this.colors.areaChooserMsgAreaDateHighlightColor + this.colors.areaChooserMsgAreaBkgHighlightColor;
	this.colors.areaChooserMsgAreaTimeHighlightColor = "\1n" + this.colors.areaChooserMsgAreaTimeHighlightColor + this.colors.areaChooserMsgAreaBkgHighlightColor;
	this.colors.areaChooserMsgAreaNumItemsHighlightColor = "\1n" + this.colors.areaChooserMsgAreaNumItemsHighlightColor + this.colors.areaChooserMsgAreaBkgHighlightColor;
	// Similar for the enhanced reader help line colors
	this.colors.enhReaderHelpLineGeneralColor = "\1n" + this.colors.enhReaderHelpLineGeneralColor + this.colors.enhReaderHelpLineBkgColor;
	this.colors.enhReaderHelpLineHotkeyColor = "\1n" + this.colors.enhReaderHelpLineHotkeyColor + this.colors.enhReaderHelpLineBkgColor;
	this.colors.enhReaderHelpLineParenColor = "\1n" + this.colors.enhReaderHelpLineParenColor + this.colors.enhReaderHelpLineBkgColor;
	// Similar for the lightbar message list help line colors
	this.colors.lightbarMsgListHelpLineGeneralColor = "\1n" + this.colors.lightbarMsgListHelpLineGeneralColor + this.colors.lightbarMsgListHelpLineBkgColor;
	this.colors.lightbarMsgListHelpLineHotkeyColor = "\1n" + this.colors.lightbarMsgListHelpLineHotkeyColor + this.colors.lightbarMsgListHelpLineBkgColor;
	this.colors.lightbarMsgListHelpLineParenColor = "\1n" + this.colors.lightbarMsgListHelpLineParenColor + this.colors.lightbarMsgListHelpLineBkgColor;
	// Similar for the lightbar area chooser help line colors
	this.colors.lightbarAreaChooserHelpLineGeneralColor = "\1n" + this.colors.lightbarAreaChooserHelpLineGeneralColor + this.colors.lightbarAreaChooserHelpLineBkgColor;
	this.colors.lightbarAreaChooserHelpLineHotkeyColor = "\1n" + this.colors.lightbarAreaChooserHelpLineHotkeyColor + this.colors.lightbarAreaChooserHelpLineBkgColor;
	this.colors.lightbarAreaChooserHelpLineParenColor = "\1n" + 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) != "\1n"))
				this.text[prop] = "\1n" + 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", "");

	// Construct the message information format string.  These must be done after
	// reading the configuration file, because the configuration file specifies the
	// colors to use.
	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 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 = new Array();
	var enhHsgHdrFileExists = true;
	var enhMsgHdrFilenameBase = "enhMsgHeader";
	var enhMsgHdrFilenameBaseFullPath = gStartupPath + enhMsgHdrFilenameBase;
	// See if there is a header file that is made for the user's terminal
	// width (msgHeader-<width>.ans/asc).  If not, then just go with
	// msgHeader.ans/asc.
	var enhMsgHdrFilename = "";
	if (file_exists(enhMsgHdrFilenameBaseFullPath + "-" + console.screen_columns + ".ans"))
		enhMsgHdrFilename = enhMsgHdrFilenameBaseFullPath + "-" + console.screen_columns + ".ans";
	else if (file_exists(enhMsgHdrFilenameBaseFullPath + "-" + console.screen_columns + ".asc"))
		enhMsgHdrFilename = enhMsgHdrFilenameBaseFullPath + "-" + console.screen_columns + ".asc";
	else if (file_exists(enhMsgHdrFilenameBaseFullPath + ".ans"))
		enhMsgHdrFilename = enhMsgHdrFilenameBaseFullPath + ".ans";
	else if (file_exists(enhMsgHdrFilenameBaseFullPath + ".asc"))
		enhMsgHdrFilename = enhMsgHdrFilenameBaseFullPath + ".asc";
	else
	{
		// The enhanced reader header file doesn't exist, so provide some default
		// header lines.
		enhHsgHdrFileExists = false;
		// 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 = "\1n\1h\1c" + UPPER_LEFT_SINGLE + HORIZONTAL_SINGLE + "\1n\1c"
		             + HORIZONTAL_SINGLE + " \1h@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 += "@\1k";
		numChars = console.screen_columns - console.strlen(hdrLine1) - 4;
		for (var i = 0; i < numChars; ++i)
			hdrLine1 += HORIZONTAL_SINGLE;
		hdrLine1 += "\1n\1c" + HORIZONTAL_SINGLE + HORIZONTAL_SINGLE + "\1h"
		         + HORIZONTAL_SINGLE + UPPER_RIGHT_SINGLE;
		this.enhMsgHeaderLines.push(hdrLine1);
		var hdrLine2 = "\1n\1c" + VERTICAL_SINGLE + "\1h\1k" + BLOCK1 + BLOCK2
		             + BLOCK3 + "\1gM\1n\1gsg#\1h\1c: \1b@MSG_NUM_AND_TOTAL-L";
		numChars = console.screen_columns - 32;
		for (var i = 0; i < numChars; ++i)
			hdrLine2 += "#";
		hdrLine2 += "@\1n\1c" + VERTICAL_SINGLE;
		this.enhMsgHeaderLines.push(hdrLine2);
		var hdrLine3 = "\1n\1h\1k" + VERTICAL_SINGLE + BLOCK1 + BLOCK2 + BLOCK3
		             + "\1gF\1n\1grom\1h\1c: \1b@MSG_FROM-L";
		numChars = console.screen_columns - 23;
		for (var i = 0; i < numChars; ++i)
			hdrLine3 += "#";
		hdrLine3 += "@\1k" + VERTICAL_SINGLE;
		this.enhMsgHeaderLines.push(hdrLine3);
		var hdrLine4 = "\1n\1h\1k" + VERTICAL_SINGLE + BLOCK1 + BLOCK2 + BLOCK3
		             + "\1gT\1n\1go  \1h\1c: \1b@MSG_TO-L";
		numChars = console.screen_columns - 21;
		for (var i = 0; i < numChars; ++i)
			hdrLine4 += "#";
		hdrLine4 += "@\1k" + VERTICAL_SINGLE;
		this.enhMsgHeaderLines.push(hdrLine4);
		var hdrLine5 = "\1n\1h\1k" + VERTICAL_SINGLE + BLOCK1 + BLOCK2 + BLOCK3
		             + "\1gS\1n\1gubj\1h\1c: \1b@MSG_SUBJECT-L";
		numChars = console.screen_columns - 26;
		for (var i = 0; i < numChars; ++i)
			hdrLine5 += "#";
		hdrLine5 += "@\1k" + VERTICAL_SINGLE;
		this.enhMsgHeaderLines.push(hdrLine5);
		var hdrLine6 = "\1n\1c" + VERTICAL_SINGLE + "\1h\1k" + BLOCK1 + BLOCK2 + BLOCK3
		             + "\1gD\1n\1gate\1h\1c: \1b@MSG_DATE-L";
		numChars = console.screen_columns - 23;
		for (var i = 0; i < numChars; ++i)
			hdrLine6 += "#";
		hdrLine6 += "@\1n\1c" + VERTICAL_SINGLE;
		this.enhMsgHeaderLines.push(hdrLine6);
		var hdrLine7 = "\1n\1h\1c" + BOTTOM_T_SINGLE + HORIZONTAL_SINGLE + "\1n\1c"
		             + HORIZONTAL_SINGLE + HORIZONTAL_SINGLE + "\1h\1k";
		numChars = console.screen_columns - 8;
		for (var i = 0; i < numChars; ++i)
			hdrLine7 += HORIZONTAL_SINGLE;
		hdrLine7 += "\1c" + HORIZONTAL_SINGLE + HORIZONTAL_SINGLE + "\1h"
		         + HORIZONTAL_SINGLE + BOTTOM_T_SINGLE;
		this.enhMsgHeaderLines.push(hdrLine7);
	}
	if (enhHsgHdrFileExists)
	{
		// 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 = enhMsgHdrFilenameBaseFullPath + "_converted.asc";
		if (!file_exists(syncConvertedHdrFilename))
		{
			var dotIdx = enhMsgHdrFilename.lastIndexOf(".");
			if (dotIdx > -1)
			{
				// If header file is ANSI, then convert it to Synchronet attribute
				// format and save it as an .asc file.  Otherwise, just use the
				// header file without conversion since it's already ASCII or
				// Synchronet attribute code format.
				var isANSI = (enhMsgHdrFilename.substr(dotIdx+1).toUpperCase() == "ANS");
				if (isANSI)
				{
					var filenameBase = enhMsgHdrFilename.substr(0, dotIdx);
					var cmdLine = system.exec_dir + "ans2asc \"" + enhMsgHdrFilename + "\" \""
					            + 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 = enhMsgHdrFilename;
			}
		}
		// Read the header file into this.enhMsgHeaderLines
		var hdrFile = new File(syncConvertedHdrFilename);
		if (hdrFile.open("r"))
		{
			var fileLine = null;
			while (!hdrFile.eof && (this.enhMsgHeaderLines.length <= 10))
			{
				// 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);
				this.enhMsgHeaderLines.push(fileLine);
			}
		}
	}
	// 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.WriteGrpListHdrLine = DigDistMsgReader_WriteGrpListTopHdrLine;
	this.WriteSubBrdListHdr1Line = DMsgAreaChooser_WriteSubBrdListHdr1Line;
	this.SelectMsgArea = DigDistMsgReader_SelectMsgArea;
	this.SelectMsgArea_Lightbar = DigDistMsgReader_SelectMsgArea_Lightbar;
	this.SelectSubBoard_Lightbar = DigDistMsgReader_SelectSubBoard_Lightbar;
	this.SelectMsgArea_Traditional = DigDistMsgReader_SelectMsgArea_Traditional;
	this.ListMsgGrps = DigDistMsgReader_ListMsgGrps_Traditional;
	this.ListSubBoardsInMsgGroup = DigDistMsgReader_ListSubBoardsInMsgGroup_Traditional;
	// Lightbar-specific methods
	this.ListScreenfulOfMsgGrps = DigDistMsgReader_listScreenfulOfMsgGrps;
	this.WriteMsgGroupLine = DigDistMsgReader_writeMsgGroupLine;
	this.UpdateMsgAreaPageNumInHeader = DigDistMsgReader_updateMsgAreaPageNumInHeader;
	this.ListScreenfulOfSubBrds = DigDistMsgReader_ListScreenfulOfSubBrds;
	this.WriteMsgSubBoardLine = DigDistMsgReader_WriteMsgSubBrdLine;
	// 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.SaveMsgToFile = DigDistMsgReader_SaveMsgToFile;
1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333 1334 1335 1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353 1354 1355 1356 1357 1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382 1383 1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397 1398 1399 1400 1401 1402 1403 1404 1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425 1426 1427 1428 1429 1430 1431 1432 1433 1434 1435 1436 1437 1438 1439 1440 1441 1442 1443 1444 1445 1446 1447 1448 1449 1450 1451 1452 1453 1454 1455 1456 1457 1458 1459 1460 1461 1462 1463 1464 1465 1466 1467 1468 1469 1470 1471 1472 1473 1474 1475 1476 1477 1478 1479 1480 1481 1482 1483 1484 1485 1486 1487 1488 1489 1490 1491 1492 1493 1494 1495 1496 1497 1498 1499 1500 1501 1502 1503 1504 1505 1506 1507 1508 1509 1510 1511 1512 1513 1514 1515 1516 1517 1518 1519 1520 1521 1522 1523 1524 1525 1526 1527 1528 1529 1530 1531 1532 1533 1534 1535 1536 1537 1538 1539 1540

	// printf strings for message group/sub-board lists
	// Message group information (printf strings)
	this.msgGrpListPrintfStr = "\1n " + this.colors.areaChooserMsgAreaNumColor + "%" + this.areaNumLen
	                         + "d " + this.colors.areaChooserMsgAreaDescColor + "%-"
	                         + this.msgGrpDescLen + "s " + this.colors.areaChooserMsgAreaNumItemsColor
	                         + "%" + this.numItemsLen + "d";
	this.msgGrpListHilightPrintfStr = "\1n" + this.colors.areaChooserMsgAreaBkgHighlightColor + " "
	                                + "\1n" + this.colors.areaChooserMsgAreaBkgHighlightColor
	                                + this.colors.areaChooserMsgAreaNumHighlightColor + "%" + this.areaNumLen
	                                + "d \1n" + this.colors.areaChooserMsgAreaBkgHighlightColor
	                                + this.colors.areaChooserMsgAreaDescHighlightColor + "%-"
	                                + this.msgGrpDescLen + "s \1n" + 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
	// TODO: Account for wide terminals?
	this.lightbarAreaChooserHelpLine = "\1n"
	                          + this.colors.lightbarAreaChooserHelpLineHotkeyColor + ""
							  + this.colors.lightbarAreaChooserHelpLineGeneralColor + ", "
							  + this.colors.lightbarAreaChooserHelpLineHotkeyColor + ""
							  + this.colors.lightbarAreaChooserHelpLineGeneralColor + ", "
							  + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "ENTER"
							  + this.colors.lightbarAreaChooserHelpLineGeneralColor + ", "
							  + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "HOME"
							  + this.colors.lightbarAreaChooserHelpLineGeneralColor + ", "
							  + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "END"
							  + this.colors.lightbarAreaChooserHelpLineGeneralColor + ", "
							  + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "#"
							  + this.colors.lightbarAreaChooserHelpLineGeneralColor + ", "
							  + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "N"
							  + this.colors.lightbarAreaChooserHelpLineParenColor + ")"
							  + this.colors.lightbarAreaChooserHelpLineGeneralColor + "ext pg, "
							  + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "P"
							  + this.colors.lightbarAreaChooserHelpLineParenColor + ")"
							  + this.colors.lightbarAreaChooserHelpLineGeneralColor + "rev pg, "
							  + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "F"
							  + this.colors.lightbarAreaChooserHelpLineParenColor + ")"
							  + this.colors.lightbarAreaChooserHelpLineGeneralColor + "irst pg, "
							  + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "L"
							  + this.colors.lightbarAreaChooserHelpLineParenColor + ")"
							  + this.colors.lightbarAreaChooserHelpLineGeneralColor + "ast pg, "
							  + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "Q"
							  + this.colors.lightbarAreaChooserHelpLineParenColor + ")"
							  + this.colors.lightbarAreaChooserHelpLineGeneralColor + "uit, "
							  + this.colors.lightbarAreaChooserHelpLineHotkeyColor + "?   ";
	// 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 = new Array();

	// 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;
}

// 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 == "mail");
}

// 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.
function DigDistMsgReader_RefreshSearchResultMsgHdr(pMsgIndex, pAttrib)
{
   if (typeof(pMsgIndex) != "number")
      return;

   if (this.msgSearchHdrs.hasOwnProperty(this.subBoardCode))
   {
		var msgNum = pMsgIndex + 1;
      if (typeof(pAttrib) != "undefined")
      {
         if (this.msgSearchHdrs[this.subBoardCode].indexed.hasOwnProperty(pMsgIndex))
         {
            this.msgSearchHdrs[this.subBoardCode].indexed[pMsgIndex].attr = this.msgSearchHdrs[this.subBoardCode].indexed[pMsgIndex].attr | pAttrib;
            var msgOffsetFromHdr = this.msgSearchHdrs[this.subBoardCode].indexed[pMsgIndex].offset;
            this.msgbase.put_msg_header(true, msgOffsetFromHdr, this.msgSearchHdrs[this.subBoardCode].indexed[pMsgIndex]);
         }
      }
      else
      {
         var msgHeader = this.GetMsgHdrByIdx(pMsgIndex);
         if (this.msgSearchHdrs[this.subBoardCode].indexed.hasOwnProperty(pMsgIndex))
         {
            this.msgSearchHdrs[this.subBoardCode].indexed[pMsgIndex] = msgHeader;
            this.msgbase.put_msg_header(true, msgHeader.offset, msgHeader);
         }
      }
   }
}

// 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.
function DigDistMsgReader_SearchMessages(pSearchModeStr, pSubBoardCode)
{
   // 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.print("\1n");
      console.crlf();
      var subCode = (typeof(pSubBoardCode) == "string" ? pSubBoardCode : this.subBoardCode);
      if (subCode == "mail")
			console.print("\1n" + this.text.searchingPersonalMailText);
      else
			console.print("\1n" + this.text.searchingSubBoardAbovePromptText.replace("%s", subBoardGrpAndName(bbs.cursub_code)) + "\1n");
      console.crlf();
      // Output the prompt text to the user (for modes where a prompt is needed)
      switch (this.searchType)
      {
         case SEARCH_KEYWORD:
            console.print("\1n" + this.text.searchTextPromptText);
            break;
         case SEARCH_FROM_NAME:
            console.print("\1n" + this.text.fromNamePromptText);
            break;
         case SEARCH_TO_NAME_CUR_MSG_AREA:
            console.print("\1n" + this.text.toNamePromptText);
            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)
         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("\1n" + this.text.abortedText);
         console.crlf();
         console.pause();
         return;
      }
      else
      {
			// List/read messages
         this.ReadOrListSubBoard(pSubBoardCode);
         // Clear the search data so that subsequent listing or reading sessions
         // don't repeat the same search
         this.ClearSearchData();
      }
   }
}

// 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];
		}
		delete this.msgSearchHdrs;
		this.msgSearchHdrs = new Object();
   }
}

// 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.
//
// 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)
{
   var retObj = new Object();
   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("\1n\1h\1yWarning: \1wThe 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;
      }
   }
   // (re)-open the message base
   if (previousSubBoardCode != this.subBoardCode)
   {
      if ((this.msgbase != null) && (this.msgbase.is_open))
         this.msgbase.close();
      this.msgbase = new MsgBase(this.subBoardCode);
   }
   else if (this.msgbase == null)
       this.msgbase = new MsgBase(this.subBoardCode);

   // Open the sub-board.  If the message base was not opened, then output
   // an error and return.
	if (!this.msgbase.is_open && !this.msgbase.open())
	{
      console.print("\1n");
      console.crlf();
      console.print("\1h\1y* \1wUnable to open message sub-board:");
      console.crlf();
      console.print(subBoardGrpAndName(this.subBoardCode));
      console.crlf();
      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);
	if (!this.PopulateHdrsIfSearch_DispErrorIfNoMsgs(true, true, pauseOnNoSearchResults))
	{
		retObj.stoppedReading = false;
		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.msgbase, 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;
	// 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.GetLastReadMsgIdx(false); // Used to be true
			if ((selectedMessageOffset > -1) && (selectedMessageOffset < this.NumMessages() - 1))
				++selectedMessageOffset;
		}
		else
			selectedMessageOffset = 0;
	}
	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);
				// 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.
				// Call the ListMessages method - Don't change the sub-board, and
				// have it return if the user chooses a message to read.
				otherRetObj = this.ListMessages(null, true, 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;
		}
	}

   // Close the message base object (if it has not been closed already),
   // re-enable the normal text attribute, and clear the screen.
   if (this.msgbase != null)
   {
      this.msgbase.close();
      this.msgbase = null;
   }
	console.clear("\1n");

	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();
				if (this.readingPersonalEmail)
					console.print("\1n" + this.text.loadingPersonalMailText.replace("%s", subBoardGrpAndName(this.subBoardCode)));
				else
					console.print(this.text.searchingSubBoardText.replace("%s", subBoardGrpAndName(this.subBoardCode)));
			}
			this.msgSearchHdrs[this.subBoardCode] = searchMsgbase(this.subBoardCode, this.msgbase, this.searchType, this.searchString, this.readingPersonalEmailFromUser);
		}
	}
	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;
		var closeMsgbase = (typeof(pCloseMsgbaseAndSetNullIfNoMsgs) == "boolean" ? pCloseMsgbaseAndSetNullIfNoMsgs : true);
		if (closeMsgbase)
		{
			this.msgbase.close();
			this.msgbase = null;
		}
		if (outputMessages)
		{
			console.print("\1n");
			console.crlf();
			if (this.readingPersonalEmail)
				console.print(this.text.noPersonalEmailText);
			else
			{
				if (this.msgSearchHdrs.hasOwnProperty(this.subBoardCode))
					console.print(this.text.noSearchResultsInSubBoardText.replace("%s", subBoardGrpAndName(this.subBoardCode)));
				else
					console.print(this.text.noMessagesInSubBoardText.replace("%s", subBoardGrpAndName(this.subBoardCode)));
			}
			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.
function DigDistMsgReader_MessageAreaScan(pScanCfgOpt, pScanMode, pScanScopeChar)
{
	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.print(this.text.scanScopePromptText);
		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(this.text.msgScanAbortedText);
			console.crlf();
			console.pause();
			return;
		}
	}

	// 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);
	}

	// 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();

	// If the object's message base is currently open, then close it.  The object's
	// message base object will be used to open each sub-board to scan for & read
	// unread messages.
	if ((this.msgbase != null) && (this.msgbase.is_open))

	// Perform the message scan
	var continueNewScan = true;
	var userAborted = false;
	if (scanScopeChar == "A") // All sub-board scan
	{
		this.doingMultiSubBoardScan = true;
		// Iterate through all message groups & sub-boards looking for ones with unread
		// messages.  When a sub-board with unread messages is found, then let the user
		// read messages in that sub-board.
		for (var grpIndex = 0; (grpIndex < msg_area.grp_list.length) && continueNewScan; ++grpIndex)
		{
			// Group description: msg_area.grp_list[grpIndex].description
			// Iterate through the sub-boards in this message group looking for unread messages
			for (var subIndex = 0; (subIndex < msg_area.grp_list[grpIndex].sub_list.length) && continueNewScan; ++subIndex)
			{
				// 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(msg_area.grp_list[grpIndex].sub_list[subIndex].code); // 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))
				{
					// 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.
					//this.msgbase = new MsgBase(msg_area.grp_list[grpIndex].sub_list[subIndex].code);
					this.msgbase = new MsgBase(this.subBoardCode);
						//this.setSubBoardCode(msg_area.grp_list[grpIndex].sub_list[subIndex].code); // Needs to be set before getting the last read/scan pointer index

						// If the current sub-board contains only deleted messages,
						// then skip it.
						var scanPtrMsgIdx = this.GetScanPtrMsgIdx();
						var nonDeletedMsgsExist = (this.FindNextNonDeletedMsgIdx(scanPtrMsgIdx-1, true) > -1);
						if (!nonDeletedMsgsExist)
							continue;

						// 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.
								if ((this.msgbase.total_msgs > 0) && ((scanPtrMsgIdx == -1) || (scanPtrMsgIdx < this.msgbase.total_msgs-1)))
								{
									bbs.curgrp = grpIndex;
									bbs.cursub = subIndex;
									// Start at the first unread message.
									var startMsgIdx = scanPtrMsgIdx + 1;
									if (this.SearchingAndResultObjsDefinedForCurSub())
										startMsgIdx = 0;
									// 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);
									// 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);
								// 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 readRetObj = this.ReadOrListSubBoard(null, 0, false, true, 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 (this.msgbase != null)
							this.msgbase.close();
					}
				}
			}
		}
	}
	else if (scanScopeChar == "G") // Group scan
	{
		this.doingMultiSubBoardScan = true;
		// Iterate through the sub-boards in the current message group looking for messages
		for (var subIndex = 0; (subIndex < msg_area.grp_list[bbs.curgrp].sub_list.length) && continueNewScan; ++subIndex)
		{
			// 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.
			this.setSubBoardCode(msg_area.grp_list[bbs.curgrp].sub_list[subIndex].code); // Needs to be set before the last read/scan pointer message
			if (msg_area.sub[this.subBoardCode].can_read &&
			    ((msg_area.sub[this.subBoardCode].scan_cfg & pScanCfgOpt) == pScanCfgOpt))
			{
				// Sub-board description: msg_area.grp_list[bbs.curgrp].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.
				//this.msgbase = new MsgBase(msg_area.grp_list[bbs.curgrp].sub_list[subIndex].code);
				this.msgbase = new MsgBase(this.subBoardCode);
					// The following line is now done before the 'if' statement above
					//this.setSubBoardCode(msg_area.grp_list[bbs.curgrp].sub_list[subIndex].code); // Needs to be set before the last read/scan pointer message

					// If the current sub-board contains only deleted messages,
					// then skip it.
					var scanPtrMsgIdx = this.GetScanPtrMsgIdx();
					var nonDeletedMsgsExist = (this.FindNextNonDeletedMsgIdx(scanPtrMsgIdx-1, true) > -1);
					if (!nonDeletedMsgsExist)
						continue;

					// Temporarily change the user's sub-board to the current
					// sub-board so that certain @-codes (such as @GRP-L@, etc.)
					// are displayed by Synchronet correctly.
					bbs.curgrp = msg_area.sub[this.subBoardCode].grp_index;
					bbs.cursub = msg_area.sub[this.subBoardCode].index;

					// 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.
							if ((this.msgbase.total_msgs > 0) && ((scanPtrMsgIdx == -1) || (scanPtrMsgIdx < this.msgbase.total_msgs-1)))
							{
								//bbs.cursub = subIndex; // Now done a bit earlier
								// Start at the first unread message.
								var startMsgIdx = scanPtrMsgIdx + 1;
								if (this.SearchingAndResultObjsDefinedForCurSub())
									startMsgIdx = 0;
								// 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);
								// 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.cursub = subIndex; // Now done a bit earlier
							// 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);
							// 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.cursub = subIndex; // Now done a bit earlier
							// Search for unread 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_CUR_GRP;
							var readRetObj = this.ReadOrListSubBoard(null, 0, false, true, 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 (this.msgbase != null)
						this.msgbase.close();
				}
			}
		}
	}
	else if (scanScopeChar == "S") // Current sub-board scan
	{
		this.doingMultiSubBoardScan = false;
		// If the command-line arguments don't specify the sub-board code or
		// the user is reading personal email, then set the object's sub-board
		// code to the user's current sub-board code (bbs.cursub_code) to ensure
		// that we open the correct messagebase and so that @-codes, etc. display
		// for the correct sub-board.
		if (!gCmdLineArgVals.hasOwnProperty("subboard") || gListPersonalEmailCmdLineOpt)
			this.setSubBoardCode(bbs.cursub_code);
		// Make sure the user has access permissions for the current sub-board and
		// has it set up in their scan configuration before letting the user read
		// it.
		if (msg_area.sub[this.subBoardCode].can_read &&
		    ((msg_area.sub[this.subBoardCode].scan_cfg & pScanCfgOpt) == pScanCfgOpt))
			this.msgbase = new MsgBase(this.subBoardCode);
			//this.msgbase = new MsgBase(bbs.cursub_code);
				// Temporarily change the user's sub-board to the current
				// sub-board so that certain @-codes (such as @GRP-L@, etc.)
				// are displayed by Synchronet correctly.
				bbs.curgrp = msg_area.sub[this.subBoardCode].grp_index;
				bbs.cursub = msg_area.sub[this.subBoardCode].index;

				//this.setSubBoardCode(bbs.cursub_code); // Needs to be set before getting the last read/scan pointer message

				// Only scan this sub-board if it contains messages that are not
				// marked as deleted.
				var scanPtrMsgIdx = this.GetScanPtrMsgIdx();
				var nonDeletedMsgsExist = (this.FindNextNonDeletedMsgIdx(scanPtrMsgIdx-1, true) > -1);
				if (nonDeletedMsgsExist)
				{
					// 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.
							if ((this.msgbase.total_msgs > 0) && ((scanPtrMsgIdx == -1) || (scanPtrMsgIdx < this.msgbase.total_msgs-1)))
							{
								if (this.subBoardCode != "mail")
									bbs.cursub = msg_area.sub[bbs.cursub_code].index;
								// Start at the first unread message.
								var startMsgIdx = scanPtrMsgIdx + 1;
								if (this.SearchingAndResultObjsDefinedForCurSub())
									startMsgIdx = 0;
								// 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, true);
								userAborted = readRetObj.stoppedReading;
							}
							break;
						case SCAN_TOYOU: // All messages to the user
							if (this.subBoardCode != "mail")
								bbs.cursub = msg_area.sub[bbs.cursub_code].index;
							// Set the search type to messages to the user and let the user
							// read the sub-board.  ReadOrListSubBoard() will do the search.
							// Don't allow the user to change to a different message area.
							this.searchType = SEARCH_TO_USER_CUR_MSG_AREA;
							var readRetObj = this.ReadOrListSubBoard(null, 0, false, true, true);
							userAborted = readRetObj.stoppedReading;
							break;
						case SCAN_UNREAD: // New (unread) messages to the user
							bbs.cursub = msg_area.sub[bbs.cursub_code].index;
							// Set the search type to messages to the user and let the user
							// read the sub-board.  ReadOrListSubBoard() will do the search.
							// Don't allow the user to change to a different message area.
							this.searchType = SEARCH_TO_USER_NEW_SCAN_CUR_SUB;
							bbs.cursub = msg_area.sub[bbs.cursub_code].index;
							var readRetObj = this.ReadOrListSubBoard(null, 0, false, true, true);
							userAborted = readRetObj.stoppedReading;
							break;
						default:
							break;
					}
				}

				if (this.msgbase != null)
					this.msgbase.close();
			}
		}
	}

	// 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 ((this.msgbase != null) && (this.msgbase.is_open))
		this.msgbase.close();
	this.msgbase = new MsgBase(this.subBoardCode);
	this.doingMultiSubBoardScan = false;

	if (this.pauseAfterNewMsgScan)
	{
		console.crlf();
		if (userAborted)
			console.print("\1n" + this.text.msgScanAbortedText + "\1n");
		else
			console.print("\1n" + this.text.msgScanCompleteText + "\1n");
		console.crlf();
		console.pause();
	}
}

// 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))
//
// 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,
{
	var retObj = new Object();
	retObj.lastUserInput = "";
	retObj.lastAction = ACTION_NONE;
	retObj.stoppedReading = false;
	retObj.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("\1n\1h\1yWarning: \1wThe 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("\1n\1h\1yWarning: \1wThe 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 (previousSubBoardCode != this.subBoardCode)
	{
		if ((this.msgbase != null) && (this.msgbase.is_open))
		this.msgbase = new MsgBase(this.subBoardCode);
	}
	else if (this.msgbase == null)
		this.msgbase = new MsgBase(this.subBoardCode);

	// If the message base was not opened, then output an error and return.
	if (!this.msgbase.is_open && !this.msgbase.open())
	{
		console.print("\1n");
		console.crlf();
		console.print("\1h\1y* \1wUnable 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.
	if (this.NumMessages() == 0)
	{
		this.msgbase.close();
		this.msgbase = null;
		console.clear("\1n");
		console.center("\1n\1h\1yThere 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.msgbase, 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.GetLastReadMsgIdx();
		if (msgIndex == -1)
			msgIndex = 0;
	}

	// If the current message index is for a message that has been
	// deleted, then find the next non-deleted message.
	var testMsgHdr = this.GetMsgHdrByIdx(msgIndex);
	if ((testMsgHdr == null) || ((testMsgHdr.attr & MSG_DELETE) == MSG_DELETE))
	{
		// First try going forward
		var nonDeletedMsgIdx = this.FindNextNonDeletedMsgIdx(msgIndex, true);
		// If a non-deleted message was not found, then try going backward.
		if (nonDeletedMsgIdx == -1)
			nonDeletedMsgIdx = this.FindNextNonDeletedMsgIdx(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 (nonDeletedMsgIdx > -1)
			msgIndex = nonDeletedMsgIdx;
		else
		{
			this.msgbase.close();
			this.msgbase = null;
			console.clear("\1n");
			console.center("\1h\1yThere 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("\1n");
	// 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 marked as deleted (not by the user), then go to the
		// next/previous message
		else if (readMsgRetObj.msgDeleted)
		{
			// 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.FindNextNonDeletedMsgIdx(msgIndex, false);
				msgIndex = this.FindNextNonDeletedMsgIdx(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)
				{
					var goToPrevRetval = this.GoToPrevSubBoardForEnhReader(allowChgMsgArea);
					retObj.stoppedReading = goToPrevRetval.shouldStopReading;
					// If we're going to stop reading, then 
					if (retObj.stoppedReading)
						msgIndex = 0;
					else if (goToPrevRetval.changedMsgArea)
				}

				// If the caller wants this method to return instead of going to the next
				// sub-board with messages, then do so.
				if (pReturnOnNextAreaNav)
			}
		}
		// 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)
				{
					var goToNextRetval = this.GoToNextSubBoardForEnhReader(allowChgMsgArea);
					retObj.stoppedReading = goToNextRetval.shouldStopReading;
					// If we're going to stop reading, then 
					if (retObj.stoppedReading)
						msgIndex = 0;
					else if (goToNextRetval.changedMsgArea)
				}
				// If the caller wants this method to return instead of going to the next
				// sub-board with messages, then do so.
				if (pReturnOnNextAreaNav)
			}
		}
		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 FindNextNonDeletedMsgIdx() will increment it
			// before searching in order to find the "next" message.
			msgIndex = this.FindNextNonDeletedMsgIdx(-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.FindNextNonDeletedMsgIdx(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.
				this.SelectMsgArea();
				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 = chgSubBoardRetObj.lastReadMsgIdx;
					// If the current message index is for a message that has been
					// deleted, then find the next non-deleted message.
					testMsgHdr = this.GetMsgHdrByIdx(msgIndex);
					if ((testMsgHdr == null) || ((testMsgHdr.attr & MSG_DELETE) == MSG_DELETE))
					{
						// First try going forward
						var nonDeletedMsgIdx = this.FindNextNonDeletedMsgIdx(msgIndex, true);
						// If a non-deleted message was not found, then try going backward.
						if (nonDeletedMsgIdx == -1)
							nonDeletedMsgIdx = this.FindNextNonDeletedMsgIdx(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 (nonDeletedMsgIdx > -1)
						else
						{
							if (this.NumMessages() != 0)
							{
								// There are messages, but none that are not deleted.
								console.clear("\1n");
								console.center("\1h\1yThere are no messages to display.");
								console.crlf();
								console.pause();
							}
							this.msgbase.close();
							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);
				retObj.stoppedReading = goToPrevRetval.shouldStopReading;
				// If we're going to stop reading, then 
				if (retObj.stoppedReading)
					msgIndex = 0;
				else if (goToPrevRetval.changedMsgArea)
			}
			// If the caller wants this method to return instead of going to the next
			// sub-board with messages, then do so.
			if (pReturnOnNextAreaNav)
		}
		else if (readMsgRetObj.nextAction == ACTION_GO_NEXT_MSG_AREA) // Go to the next message area
		{
			if (allowChgMsgArea && !pReturnOnNextAreaNav)
			{
				var goToNextRetval = this.GoToNextSubBoardForEnhReader(allowChgMsgArea);
				retObj.stoppedReading = goToNextRetval.shouldStopReading;
				if (retObj.stoppedReading)
					msgIndex = 0;
				else if (goToNextRetval.changedMsgArea)
			}
			// If the caller wants this method to return instead of going to the next
			// sub-board with messages, then do so.
			if (pReturnOnNextAreaNav)
		}
		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
			{
				// If this.reverseListOrder is the string "ASK", the user will be prompted
				// on the last line of the screen for whether they want to list the
				// messages in reverse order.  So, erase the help line on the bottom of
				// the screen.
				if ((typeof(this.reverseListOrder) == "string") && (this.reverseListOrder.toUpperCase() == "ASK"))
				{
					if (this.scrollingReaderInterface && console.term_supports(USER_ANSI))
					{
						console.gotoxy(1, console.screen_rows);
						console.cleartoeol("\1n");
					}
				}

				// Call the ListMessages method - Don't change the sub-board, and
				// have it return if the user chooses a message to read.
				var listRetObj = this.ListMessages(null, true, 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))
		{
			// The user selected a different message in this sub-board
			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.
//  pReturnOnMsgSelect: Optional - A boolean to specify whether or not to
//                      return when a message is selected to read.  Defaults
//                      to false.
//  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, pReturnOnMsgSelect, pAllowChgSubBoard)
	var retObj = new Object();
	retObj.lastUserInput = "";
	retObj.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("\1n\1h\1yWarning: \1wThe 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("\1n\1h\1yWarning: \1wThe Message Reader connot continue because no message\r\n");
		console.print("sub-board was specified. Please notify the sysop.\r\n\1p");
		return retObj;
	}
	if (previousSubBoardCode != this.subBoardCode)
	{
		this.msgbase = null;
		this.msgbase = new MsgBase(this.subBoardCode);
	}
	var openSucceeded = true;
	if (!this.msgbase.is_open)
		openSucceeded = this.msgbase.open();
		// If there are no messages to display in the current sub-board, then let the
		// user know and exit.
		if (this.NumMessages() == 0)
		{
			this.msgbase.close();
			this.msgbase = null;
			console.clear("\1n");
			console.center("\1n\1h\1yThere are no messages to display.\r\n\1p");
			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();

	// If this.reverseListOrder is the string "ASK", prompt the user for whether
	// they want to list the messages in reverse order.
	if ((typeof(this.reverseListOrder) == "string") && (this.reverseListOrder.toUpperCase() == "ASK"))
	{
		if (numMessages(bbs.cursub_code) > 0)
			this.reverseListOrder = !console.noyes("\1n\1cList in reverse (newest on top)");
	}
	// 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(pReturnOnMsgSelect, pAllowChgSubBoard);
		retObj = this.ListMessages_Traditional(pReturnOnMsgSelect, 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.
// Note: This function requires this.msgbase to be valid and open.
//
// Parameters:
//  pReturnOnMsgSelect: Optional - A boolean to specify whether or not
//                      to return when a message is selected to read.
//  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(pReturnOnMsgSelect, pAllowChgSubBoard)
	var retObj = new Object();
	retObj.lastUserInput = "";
	retObj.selectedMsgOffset = -1;

	// 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.msgbase must be valid before continuing.
	if ((typeof(this.msgbase) == "undefined") || (this.msgbase == null))
	{
		console.center("\1n\1h\1yError: \1wUnable to list messages because the sub-board is not open.\r\n\1p");
		return retObj;
		console.center("\1n\1h\1yError: \1wUnable to list messages because the sub-board is not open.\r\n\1p");
		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;

	// Clear the screen and write the header at the top
	console.clear("\1n");

	// 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)
	// 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).
			retvalObj = this.PromptContinueOrReadMsg((this.tradListTopMsgIdx == this.NumMessages()-1), lastScreen, pReturnOnMsgSelect, allowChgSubBoard);
		else
			retvalObj = this.PromptContinueOrReadMsg((this.tradListTopMsgIdx == 0), lastScreen, pReturnOnMsgSelect, 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")
			{
				{
					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")
			{
					this.tradListTopMsgIdx -= this.tradMsgListNumLines;
				else
					this.tradListTopMsgIdx += this.tradMsgListNumLines;
			}
			// First page
			else if (retvalObj.userInput == "F")
			{
					this.tradListTopMsgIdx = this.NumMessages() - 1;
				else
					this.tradListTopMsgIdx = 0;
			}
			// Last page
			else if (retvalObj.userInput == "L")
			{
				{
					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")
			{
				if (this.CanDelete() || this.CanDeleteLastMsg())
				{
					var msgNum = this.PromptForMsgNum({ x: curpos.x, y: curpos.y+1 }, 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)
					// Refresh the top header on the screen for continuing to list
					// messages.
					console.clear("\1n");
					this.WriteMsgListScreenTopHeader();
				}
			}
			// E: Edit a message
			else if (retvalObj.userInput == "E")
			{
				if (this.CanEdit())
				{
					var msgNum = this.PromptForMsgNum({ x: curpos.x, y: curpos.y+1 }, 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)
						var returnObj = this.EditExistingMsg(msgNum-1);

					// Refresh the top header on the screen for continuing to list
					// messages.
					console.clear("\1n");
					this.WriteMsgListScreenTopHeader();
				}
			}
			// G: Go to a specific message by # (place that message on the top)
			else if (retvalObj.userInput == "G")
			{
				var msgNum = this.PromptForMsgNum(curpos, "\1n" + this.text.goToMsgNumPromptText, false, ERROR_PAUSE_WAIT_MS, false);
				if (msgNum > 0)
				// Refresh the top header on the screen for continuing to list
				// messages.
				console.clear("\1n");
				this.WriteMsgListScreenTopHeader();
			}
			// ?: Display help
			else if (retvalObj.userInput == "?")
			{
				console.clear("\1n");
				this.DisplayMsgListHelp(allowChgSubBoard, true);
				console.clear("\1n");
				this.WriteMsgListScreenTopHeader();
			}
			// C: Change to another message area (sub-board)
			else if (retvalObj.userInput == "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("\1n");
						this.WriteMsgListScreenTopHeader();
					}
				}
			}
			else
			{
				// If pReturnOnMsgSelect is true and the user selected a message to
				// read, then exit out of this input loop so we can return from
				// this method - The calling method will call the enhanced reader
				// method.
				if (pReturnOnMsgSelect && (retObj.selectedMsgOffset >= 0))
					continueOn = false;
			}
			if (!pReturnOnMsgSelect)
			{
				// If the user chose to read a message or denied confirmation, then:
				// - Re-draw the column headers at the top of the screen.
				// - Subtract this.tradMsgListNumLines from msgNum so that this script displays
				//   the same page where the user left off.
				if (this.readAMessage || this.deniedReadingMessage)
				{
					if (canDoHighASCIIAndANSI()) // Could also be console.term_supports(USER_ANSI)
						this.WriteMsgListScreenTopHeader();
				}
				this.readAMessage = false;
				this.deniedReadingMessage = false;

				// If the user's terminal doesn't support ANSI, then adjust
				// this.tradMsgListNumLines to 1 less than the number of screen rows, because
				// after the first page, we no longer need to display the message
				// list header line.
				if (!canDoHighASCIIAndANSI()) // Could also be !console.term_supports(USER_ANSI)
					this.tradMsgListNumLines = console.screen_rows - 1;
			}
		}
	}
}
// For the DigDistMsgReader class: Performs the message listing, given a
// sub-board code.  This verison uses a lightbar interface for message
// navigation.  Note: This function requires this.msgbase to be valid and
// open.
//
// Parameters:
//  pReturnOnMsgSelect: Optional - A boolean to specify whether or not
//                      to return when a message is selected to read.
//  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(pReturnOnMsgSelect, pAllowChgSubBoard)
	var retObj = new Object();
	retObj.lastUserInput = "";
	retObj.selectedMsgOffset = -1;

	// 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\1h\1ySorry, an ANSI terminal is required for this operation.\1n\1w\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.msgbase must be valid before continuing.
	if ((typeof(this.msgbase) == "undefined") || (this.msgbase == null))
	{
		console.center("\1n\1h\1yError: \1wUnable to list messages because the sub-board is not open.\r\n\1p");
		return retObj;
		console.center("\1n\1h\1yError: \1wUnable to list messages because the sub-board is not open.\r\n\1p");
		return retObj;
	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);
		console.print(pHelpLineText);
		console.cleartoeol("\1n");
	}
	// Clear the screen and write the header at the top
	console.clear("\1n");
	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();
	}

	// List a screenful of message headers
	console.gotoxy(1, this.lightbarMsgListStartScreenRow);
	var lastPage = this.ListScreenfulOfMessages(this.lightbarListTopMsgIdx, this.lightbarMsgListNumLines);
	// Move the cursor to where it needs to be
	console.gotoxy(this.lightbarListCurPos);
	// User input loop
	var bottomMsgIndex = 0;
	var userInput = "";
	var msgHeader = null;
	var continueOn = true;
	while (continueOn)
	{
		bbs.command_str = ""; // To prevent weirdness

		retObj.selectedMsgOffset = -1;

		// Calculate the message number (0-based) of the message
		// appearing on the bottom of the screen.
			bottomMsgIndex = this.lightbarListTopMsgIdx - this.lightbarMsgListNumLines + 1;
			if (bottomMsgIndex < 0)
				bottomMsgIndex = 0;
			var totalNumMessages = this.NumMessages();
			bottomMsgIndex = this.lightbarListTopMsgIdx + this.lightbarMsgListNumLines - 1;
			if (bottomMsgIndex >= totalNumMessages)
				bottomMsgIndex = totalNumMessages - 1;
		// Write the current message information with highlighting colors
		msgHeader = this.GetMsgHdrByIdx(this.lightbarListSelectedMsgIdx);
		this.PrintMessageInfo(msgHeader, true, this.lightbarListSelectedMsgIdx+1);
		console.gotoxy(this.lightbarListCurPos); // Make sure the cursor is still in the right place
		// Get a key from the user (upper-case) and take appropriate action.
		userInput = getKeyWithESCChars(K_UPPER|K_NOCRLF|K_NOECHO|K_NOSPIN);
		retObj.lastUserInput = userInput;
		// Q: Quit
		if (userInput == "Q")
		{
			// Quit
			continueOn = false;
			break;
		}
		// ?: Show help
		else if (userInput == "?")
		{
			// Display help
			console.clear("\1n");
			this.DisplayMsgListHelp(allowChgSubBoard, true);
			// Re-draw the message list on the screen
			console.clear("\1n");
			this.WriteMsgListScreenTopHeader();
			DisplayHelpLine(this.msgListLightbarModeHelpLine);
			console.gotoxy(1, this.lightbarMsgListStartScreenRow);
			lastPage = this.ListScreenfulOfMessages(this.lightbarListTopMsgIdx, this.lightbarMsgListNumLines);
			console.gotoxy(this.lightbarListCurPos); // Put the cursor back where it should be
		}
		// Up arrow: Highlight the previous message
		else if (userInput == KEY_UP)
		{
			// Make sure this.lightbarListSelectedMsgIdx is within bounds before moving down.
			{
				if (this.lightbarListSelectedMsgIdx >= this.NumMessages() - 1)
					continue;
			}
			else
			{
				if (this.lightbarListSelectedMsgIdx <= 0)
					continue;
			}
			// Print the current message information with regular colors
			this.PrintMessageInfo(msgHeader, false, this.lightbarListSelectedMsgIdx+1);
				++this.lightbarListSelectedMsgIdx;
			else
				--this.lightbarListSelectedMsgIdx;
			// If the current screen row is above the first line allowed, then
			// move the cursor up one row.
			if (this.lightbarListCurPos.y > this.lightbarMsgListStartScreenRow)
			{
				console.gotoxy(1, this.lightbarListCurPos.y-1);
				this.lightbarListCurPos.x = 1;
				--this.lightbarListCurPos.y;
			}
			else
			{
				// Go onto the previous page, with the cursor highlighting
				// the last message on the page.
					this.lightbarListTopMsgIdx = this.lightbarListSelectedMsgIdx + this.lightbarMsgListNumLines - 1;
				else
					this.lightbarListTopMsgIdx = this.lightbarListSelectedMsgIdx - this.lightbarMsgListNumLines + 1;
				console.gotoxy(1, this.lightbarMsgListStartScreenRow);
				lastPage = this.ListScreenfulOfMessages(this.lightbarListTopMsgIdx, this.lightbarMsgListNumLines);
				console.gotoxy(1, this.lightbarMsgListStartScreenRow+this.lightbarMsgListNumLines-1);
				this.lightbarListCurPos.x = 1;
				this.lightbarListCurPos.y = this.lightbarMsgListStartScreenRow+this.lightbarMsgListNumLines-1;
			}
		}
		// Down arrow: Highlight the next message
		else if (userInput == KEY_DOWN)
		{
			// Make sure this.lightbarListSelectedMsgIdx is within bounds before moving down.
			{
				if (this.lightbarListSelectedMsgIdx <= 0)
					continue;
			}
			else
			{
				if (this.lightbarListSelectedMsgIdx >= this.NumMessages() - 1)
					continue;
			}
			// Print the current message information with regular colors
			this.PrintMessageInfo(msgHeader, false, this.lightbarListSelectedMsgIdx+1);
				--this.lightbarListSelectedMsgIdx;
			else
				++this.lightbarListSelectedMsgIdx;
			// If the current screen row is below the last line allowed, then
			// move the cursor down one row.
			if (this.lightbarListCurPos.y < this.lightbarMsgListStartScreenRow+this.lightbarMsgListNumLines-1)
			{
				console.gotoxy(1, this.lightbarListCurPos.y+1);
				this.lightbarListCurPos.x = 1;
				++this.lightbarListCurPos.y;
			}
			else
			{
				// Go onto the next page, with the cursor highlighting
				// the first message on the page.
				console.gotoxy(1, this.lightbarMsgListStartScreenRow);
				this.lightbarListTopMsgIdx = this.lightbarListSelectedMsgIdx;
				lastPage = this.ListScreenfulOfMessages(this.lightbarListTopMsgIdx, this.lightbarMsgListNumLines);
				// If we were on the last page, then clear the screen from
				// the current line to the end of the screen.
				if (lastPage)
				{
					this.lightbarListCurPos = console.getxy();
					clearToEOS(this.lightbarListCurPos.y);
					// Make sure the help line is still there
					DisplayHelpLine(this.msgListLightbarModeHelpLine);
				}
				// Move the cursor to the top of the list
				console.gotoxy(1, this.lightbarMsgListStartScreenRow);
				this.lightbarListCurPos.x = 1;
				this.lightbarListCurPos.y = this.lightbarMsgListStartScreenRow;
			}
		}
		// HOME key: Go to the first message on the screen
		else if (userInput == KEY_HOME)
		{
			// Print the current message information with regular colors
			this.PrintMessageInfo(msgHeader, false, this.lightbarListSelectedMsgIdx+1);
			// Go to the first message of the current page
				this.lightbarListSelectedMsgIdx += (this.lightbarListCurPos.y - this.lightbarMsgListStartScreenRow);
			else
				this.lightbarListSelectedMsgIdx -= (this.lightbarListCurPos.y - this.lightbarMsgListStartScreenRow);
			// Move the cursor to the first message line
			console.gotoxy(1, this.lightbarMsgListStartScreenRow);
			this.lightbarListCurPos.x = 1;
			this.lightbarListCurPos.y = this.lightbarMsgListStartScreenRow;
		}
		// END key: Go to the last message on the screen
		else if (userInput == KEY_END)
		{
			// Print the current message information with regular colors
			this.PrintMessageInfo(msgHeader, false, this.lightbarListSelectedMsgIdx+1);
			// Update the selected message #
			this.lightbarListSelectedMsgIdx = bottomMsgIndex;
			// Go to the last message of the current page
				this.lightbarListCurPos.y = this.lightbarMsgListStartScreenRow + this.lightbarListTopMsgIdx - bottomMsgIndex;
			else
				this.lightbarListCurPos.y = this.lightbarMsgListStartScreenRow + bottomMsgIndex - this.lightbarListTopMsgIdx;
			console.gotoxy(this.lightbarListCurPos);
		}
		// Enter key: Select a message to read
		else if (userInput == KEY_ENTER)
		{
			var originalCurpos = console.getxy();
			// 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"]
				                        + +(msgHeader.offset+1)
				                        + this.colors["readMsgConfirmColor"]
				                        + ": Are you sure";
				console.gotoxy(1, console.screen_rows);
				console.print("\1n");
				console.clearline();
				readMsg = console.yesno(sReadMsgConfirmText);
			}
			var repliedToMessage = false;
			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;
				if (pReturnOnMsgSelect)
					return retObj;
				else
				{
					this.readAMessage = true;
					console.clear("\1n");
					var readRetObj = null;
					if (this.SearchingAndResultObjsDefinedForCurSub())
						readRetObj = this.ReadMessage(this.lightbarListSelectedMsgIdx);
						readRetObj = this.ReadMessage(msgHeader.offset);
					repliedToMessage = readRetObj.userReplied;
				}
			}
			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)
			{
				console.clear("\1n");
				this.WriteMsgListScreenTopHeader();
				DisplayHelpLine(this.msgListLightbarModeHelpLine);
				console.gotoxy(1, this.lightbarMsgListStartScreenRow);
				// If we're dispaying in reverse order and the user replied
				// to the message, then we'll have to re-arrange the screen
				// a bit to make way for the new message that will appear
				// in the list.
				if (this.reverseListOrder && repliedToMessage)
				{
					// Make way for the new message, which will appear at the
					// top.
					++this.lightbarListTopMsgIdx;
					// If the cursor is below the bottommost line displaying
					// messages, then advance the cursor down one position.
					// Otherwise, increment this.lightbarListSelectedMsgIdx (since a new message
					// will appear at the top, the previous selected message
					// will be pushed to the next page).
					if (this.lightbarListCurPos.y < console.screen_rows - 1)
					{
						++originalCurpos.y;
						++this.lightbarListCurPos.y;
					}
					else
						++this.lightbarListSelectedMsgIdx;
				}
				lastPage = this.ListScreenfulOfMessages(this.lightbarListTopMsgIdx, this.lightbarMsgListNumLines);
				console.gotoxy(originalCurpos); // Put the cursor back where it should be
			}
		}
		// PageDown: Next page
		else if (userInput == KEY_PAGE_DOWN)
		{
			// Next page
			if (!lastPage)
			{
					this.lightbarListTopMsgIdx -= this.lightbarMsgListNumLines;
				else
					this.lightbarListTopMsgIdx += this.lightbarMsgListNumLines;
				this.lightbarListSelectedMsgIdx = this.lightbarListTopMsgIdx;
				console.gotoxy(1, this.lightbarMsgListStartScreenRow);
				this.lightbarListCurPos.x = 1;
				this.lightbarListCurPos.y = this.lightbarMsgListStartScreenRow;
				lastPage = this.ListScreenfulOfMessages(this.lightbarListTopMsgIdx, this.lightbarMsgListNumLines);

				// If we were on the last page, then clear the screen from
				// the current line to the end of the screen.
				if (lastPage)
				{
					this.lightbarListCurPos = console.getxy();
					clearToEOS(this.lightbarListCurPos.y);
					// Make sure the help line is still there
					DisplayHelpLine(this.msgListLightbarModeHelpLine);
				}
				// Move the cursor back to the first message info line
				console.gotoxy(1, this.lightbarMsgListStartScreenRow);
				this.lightbarListCurPos.x = 1;
				this.lightbarListCurPos.y = this.lightbarMsgListStartScreenRow;
			}
			else {
				// The user is on the last page - Go to the last message on the page.
				if (this.lightbarListSelectedMsgIdx != bottomMsgIndex)
				{
					// Print the current message information with regular colors
					this.PrintMessageInfo(msgHeader, false, this.lightbarListSelectedMsgIdx+1);
					// Update the selected message #
					this.lightbarListSelectedMsgIdx = bottomMsgIndex;
					this.lightbarListCurPos.x = 1;
					if (this.reverseListOrder)
						this.lightbarListCurPos.y = this.lightbarMsgListStartScreenRow + this.lightbarListTopMsgIdx - bottomMsgIndex;
					else
						this.lightbarListCurPos.y = this.lightbarMsgListStartScreenRow + bottomMsgIndex - this.lightbarListTopMsgIdx;
					console.gotoxy(this.lightbarListCurPos);
				}
			}
		}
		// PageUp: Previous page
		else if (userInput == KEY_PAGE_UP)
		{
			var canGoToPrevious = false;
				canGoToPrevious = (this.lightbarListTopMsgIdx < this.NumMessages() - 1);
			else
				canGoToPrevious = (this.lightbarListTopMsgIdx > 0);
					this.lightbarListTopMsgIdx += this.lightbarMsgListNumLines;
				else
					this.lightbarListTopMsgIdx -= this.lightbarMsgListNumLines;
				this.lightbarListSelectedMsgIdx = this.lightbarListTopMsgIdx;
				console.gotoxy(1, this.lightbarMsgListStartScreenRow);
				lastPage = this.ListScreenfulOfMessages(this.lightbarListTopMsgIdx, this.lightbarMsgListNumLines);
				console.gotoxy(1, this.lightbarMsgListStartScreenRow);
				this.lightbarListCurPos.x = 1;
				this.lightbarListCurPos.y = this.lightbarMsgListStartScreenRow;
			}
			else
			{
				// The user is on the first page - Go to the first message on the page.
				if (this.lightbarListSelectedMsgIdx != 0)
				{
					// Print the current message information with regular colors
					this.PrintMessageInfo(msgHeader, false, this.lightbarListSelectedMsgIdx+1);
					// Go to the first message of the current page
					if (this.reverseListOrder)
						this.lightbarListSelectedMsgIdx += (this.lightbarListCurPos.y - this.lightbarMsgListStartScreenRow);
					else
						this.lightbarListSelectedMsgIdx -= (this.lightbarListCurPos.y - this.lightbarMsgListStartScreenRow);
					// Move the cursor to the first message line
					console.gotoxy(1, this.lightbarMsgListStartScreenRow);
					this.lightbarListCurPos.x = 1;
					this.lightbarListCurPos.y = this.lightbarMsgListStartScreenRow;
				}
			}
		}
		// F: First page
		else if (userInput == "F")
		{
			var canGoToFirst = false;
				canGoToFirst = (this.lightbarListTopMsgIdx < this.NumMessages() - 1);
			else
				canGoToFirst = (this.lightbarListTopMsgIdx > 0);

			if (canGoToFirst)
			{
					this.lightbarListTopMsgIdx = this.NumMessages() - 1;
				else
					this.lightbarListTopMsgIdx = 0;
				this.lightbarListSelectedMsgIdx = this.lightbarListTopMsgIdx;
				console.gotoxy(1, this.lightbarMsgListStartScreenRow);
				lastPage = this.ListScreenfulOfMessages(this.lightbarListTopMsgIdx, this.lightbarMsgListNumLines);
				console.gotoxy(1, this.lightbarMsgListStartScreenRow);
				this.lightbarListCurPos.x = 1;
				this.lightbarListCurPos.y = this.lightbarMsgListStartScreenRow;
			}
		}
		// L: Last page
		else if (userInput == "L")
		{
			if (!lastPage)
			{
				// Set the top message index.  If this.lightbarListTopMsgIdx is beyond the last
				// message in the sub-board, then move back a full page of messages.
				{
					this.lightbarListTopMsgIdx = (this.NumMessages() % this.lightbarMsgListNumLines) - 1;
					// If this.lightbarListTopMsgIdx is now invalid (below 0), then adjust it
					// to properly display the last page of messages.
					if (this.lightbarListTopMsgIdx < 0)
						this.lightbarListTopMsgIdx = this.lightbarMsgListNumLines - 1;
				}
				else
				{
					var totalNumMessages = this.NumMessages();
					this.lightbarListTopMsgIdx = totalNumMessages - (totalNumMessages % this.lightbarMsgListNumLines);
					if (this.lightbarListTopMsgIdx >= totalNumMessages)
						this.lightbarListTopMsgIdx = totalNumMessages - this.lightbarMsgListNumLines;
				}
				this.lightbarListSelectedMsgIdx = this.lightbarListTopMsgIdx;
				console.gotoxy(1, this.lightbarMsgListStartScreenRow);
				lastPage = this.ListScreenfulOfMessages(this.lightbarListTopMsgIdx, this.lightbarMsgListNumLines);
				// If we were on the last page, then clear the screen from
				// the current line to the end of the screen.
				if (lastPage)
				{
					this.lightbarListCurPos = console.getxy();
					clearToEOS(this.lightbarListCurPos.y);
					// Make sure the help line is still there
					DisplayHelpLine(this.msgListLightbarModeHelpLine);
				}
				// Move the cursor back to the first message info line
				console.gotoxy(1, this.lightbarMsgListStartScreenRow);
				this.lightbarListCurPos.x = 1;
				this.lightbarListCurPos.y = this.lightbarMsgListStartScreenRow;
			}
		}
		// Numeric digit: The start of a number of a message to read
		else if (userInput.match(/[0-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(userInput);
			// Move the cursor to the bottom of the screen and
			// prompt the user for the message number.
			console.gotoxy(1, console.screen_rows);
			userInput = this.PromptForMsgNum({ x: 1, y: console.screen_rows }, this.text.readMsgNumPromptText, true, ERROR_PAUSE_WAIT_MS, false);
			if (userInput > 0)
			{
				// 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);
					// Let the user read the message
					retObj.selectedMsgOffset = userInput - 1;
					if (pReturnOnMsgSelect)
						return retObj;
					else
					{
						this.readAMessage = true;
						if (this.SearchingAndResultObjsDefinedForCurSub())
							this.ReadMessage(this.lightbarListSelectedMsgIdx);
				// Prompt the user whether or not to continue listing
				// messages.
				if (this.promptToContinueListingMessages)
				{
					continueOn = console.yesno(this.colors["afterReadMsg_ListMorePromptColor"] +
					                           "Continue listing messages");
				}
			}
			// If the user chose to continue listing messages, then re-draw
			// the screen.
			if (continueOn)
			{
				console.clear("\1n");
				this.WriteMsgListScreenTopHeader();
				DisplayHelpLine(this.msgListLightbarModeHelpLine);
				console.gotoxy(1, this.lightbarMsgListStartScreenRow);
				lastPage = this.ListScreenfulOfMessages(this.lightbarListTopMsgIdx, this.lightbarMsgListNumLines);
				console.gotoxy(originalCurpos); // Put the cursor back where it should be
			}
		}
		// DEL key: Delete a message
		else if (userInput == KEY_DEL)
		{
			if (this.CanDelete() || this.CanDeleteLastMsg())
			{
				var originalCurpos = console.getxy();
				console.gotoxy(1, console.screen_rows);
				console.print("\1n");
				console.clearline();

				// The DeleteMessage() methdo will prompt the user for confirmation
				// to delete the message and then delete it if confirmed.
				this.DeleteMessage(this.lightbarListSelectedMsgIdx, { x: 1, y: console.screen_rows});

				// Refresh the screen
				console.clear("\1n");
				this.WriteMsgListScreenTopHeader();
				DisplayHelpLine(this.msgListLightbarModeHelpLine);
				console.gotoxy(1, this.lightbarMsgListStartScreenRow);
				lastPage = this.ListScreenfulOfMessages(this.lightbarListTopMsgIdx, this.lightbarMsgListNumLines);
				console.gotoxy(originalCurpos); // Put the cursor back where it should be
			}
		}
		// E: Edit a message
		else if (userInput == "E")
		{
			if (this.CanEdit())
			{
				var originalCurpos = console.getxy();
				// Ask the user if they really want to edit the message
				console.gotoxy(1, console.screen_rows);
				console.print("\1n");
				console.clearline();
				// Let the user edit the message
				//var returnObj = this.EditExistingMsg(msgHeader.offset);
				var returnObj = this.EditExistingMsg(this.lightbarListSelectedMsgIdx);
				// Refresh the screen
				console.clear("\1n");
				this.WriteMsgListScreenTopHeader();
				DisplayHelpLine(this.msgListLightbarModeHelpLine);
				console.gotoxy(1, this.lightbarMsgListStartScreenRow);
				lastPage = this.ListScreenfulOfMessages(this.lightbarListTopMsgIdx, this.lightbarMsgListNumLines);
				console.gotoxy(originalCurpos); // Put the cursor back where it should be
			}
		}
		// G: Go to a specific message by # (highlight or place that message on the top)
		else if (userInput == "G")
		{
			var originalCurpos = console.getxy();

			// Move the cursor to the bottom of the screen and
			// prompt the user for a message number.
			console.gotoxy(1, console.screen_rows);
			userInput = this.PromptForMsgNum({ x: 1, y: console.screen_rows }, "\n" + this.text.goToMsgNumPromptText, true, ERROR_PAUSE_WAIT_MS, false);
			if (userInput > 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(userInput) != 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 = userInput - 1;
					if ((chosenMsgIndex <= bottomMsgIndex) && (chosenMsgIndex >= this.lightbarListTopMsgIdx))
					{
						this.lightbarListSelectedMsgIdx = chosenMsgIndex;
						originalCurpos.y = this.lightbarListCurPos.y = this.lightbarListSelectedMsgIdx - this.lightbarListTopMsgIdx + this.lightbarMsgListStartScreenRow;
					}
					else
					{
						this.lightbarListTopMsgIdx = this.lightbarListSelectedMsgIdx = chosenMsgIndex;
						originalCurpos.y = this.lightbarListCurPos.y = this.lightbarMsgListStartScreenRow;
					}
				}
				else
				{
					// The user entered an invalid message number
					console.print("\1n" + this.text.invalidMsgNumText.replace("%d", userInput) + "\1n");
					console.inkey(K_NONE, ERROR_PAUSE_WAIT_MS);
				}
			// Clear & re-draw the screen, to fix any possible alignment problems
			// caused by newline output after the user inputs their choice.
			console.clear("\1n");
			this.WriteMsgListScreenTopHeader();
			DisplayHelpLine(this.msgListLightbarModeHelpLine);
			console.gotoxy(1, this.lightbarMsgListStartScreenRow);
			lastPage = this.ListScreenfulOfMessages(this.lightbarListTopMsgIdx, this.lightbarMsgListNumLines);
			console.gotoxy(originalCurpos); // Put the cursor back where it should be
		}
		// C: Change to another message area (sub-board)
		else if (userInput == "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 lightbar list variables and refresh the screen
				if (continueOn)
				{
					this.SetUpLightbarMsgListVars();
					console.clear("\1n");
					this.WriteMsgListScreenTopHeader();
					DisplayHelpLine(this.msgListLightbarModeHelpLine);
					// List a screenful of message headers
					console.gotoxy(1, this.lightbarMsgListStartScreenRow);
					var lastPage = this.ListScreenfulOfMessages(this.lightbarListTopMsgIdx, this.lightbarMsgListNumLines);
					// Move the cursor to where it needs to be
					console.gotoxy(this.lightbarListCurPos);
				}
			}
		}
	}
}
// 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 or
//              use the standard colors.
//  pMsgNum: Optional - A number to use for the message instead of the number/offset
//           in the message header
function DigDistMsgReader_PrintMessageInfo(pMsgHeader, pHighlight, pMsgNum)
{
	// pMsgHeader must be a valid object.
	if (typeof(pMsgHeader) == "undefined")
		return;
	if (pMsgHeader == null)
		return;

	var highlight = false;
	if (typeof(pHighlight) != "undefined")
		highlight = pHighlight;

   // Determine if the message has been deleted.
   var msgDeleted = ((pMsgHeader.attr & MSG_DELETE) == MSG_DELETE);

   // 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);
   }

	// Write the message header information.
	// 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']
	var msgNum = (typeof(pMsgNum) == "number" ? pMsgNum : pMsgHeader.offset+1);
	if (highlight)
	{
		printf(this.sMsgInfoFormatHighlightStr,
			   msgNum,
			   (msgDeleted ? "\1r\1i*\1n\1h" + this.colors["msgListHighlightBkgColor"] : " "),
			   pMsgHeader.from.substr(0, this.FROM_LEN),
			   pMsgHeader.to.substr(0, this.TO_LEN),
			   pMsgHeader.subject.substr(0, this.SUBJ_LEN),
			   sDate, sTime);
	}
	else
	{
		// 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 toNameUpper = pMsgHeader.to.toUpperCase();
		var msgToUser = ((toNameUpper == user.alias.toUpperCase()) || (toNameUpper == user.name.toUpperCase()) || (toNameUpper == user.handle.toUpperCase()));
		var fromNameUpper = pMsgHeader.from.toUpperCase();
		var msgIsFromUser = ((fromNameUpper == user.alias.toUpperCase()) || (fromNameUpper == user.name.toUpperCase()) || (fromNameUpper == user.handle.toUpperCase()));
		printf((msgToUser ? this.sMsgInfoToUserFormatStr :
		        (msgIsFromUser ? this.sMsgInfoFromUserFormatStr :
		        this.sMsgInfoFormatStr)),
			   msgNum,
			   (msgDeleted ? "\1r\1i*\1n" : " "),
			   pMsgHeader.from.substr(0, this.FROM_LEN),
			   pMsgHeader.to.substr(0, this.TO_LEN),
			   pMsgHeader.subject.substr(0, this.SUBJ_LEN),
			   sDate, sTime);
	}
	console.cleartoeol("\1"); // To clear away any extra text that may have been entered by the user
}
// 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)
//  pReturnOnMsgSelect: Optional - A boolean to specify whether or not
//                      to return when a message is selected to read.
//  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, pReturnOnMsgSelect, pAllowChgSubBoard)
	// Create the return object and set some initial default values
	var retObj = new Object();
	retObj.continueOn = true;
	retObj.userInput = "";
	retObj.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 = "?G"; // ? = help, G = Go to message #
	if (allowChgSubBoard)
		allowedKeys += "C"; // Change to another message area
	if (this.CanDelete() || this.CanDeleteLastMsg())
		allowedKeys += "D"; // Delete
	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 the keys in allowedKeys or a number from 1
	// to the highest message number.
	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))
		{
			// 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);
				// Let the user read the message
				if (pReturnOnMsgSelect)
				{
					// Fill a return object with the required values, and return it.
					retObj.continueOn = continueOn;
					retObj.userInput = userInput;
					retObj.selectedMsgOffset = userInput-1;
					return retObj;
				}
				else
				{
					this.readAMessage = true;
					this.ReadMessage(userInput-1);
				}
			}
			else
				this.deniedReadingMessage = true;

			// Prompt the user whether or not to continue listing
			// messages.
			if (this.promptToContinueListingMessages)
			{
				continueOn = console.yesno(this.colors["afterReadMsg_ListMorePromptColor"] +
			}
		}
		else
		{
			// The user entered an invalid message number.
			console.print("\1n\1h\1w" + userInput + " \1y is not a valid message number.\1n");
			console.crlf();
			console.pause();
			continueOn = true;
		}
	}

	// Make sure color highlighting is turned off
	console.print("\1n");

	// 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, shows the message to the user and allows the user to
// respond.
//
// Parameters:
//  pOffset: The offset of the message to be read
//
// Return value: And object with the following properties:
//               offsetValid: Boolean - Whether or not the passed-in offset was valid
//               userReplied: Boolean - Whether or not the user replied to the message.
//               msgbaseReOpened: Boolean - Whether or not the messagebase is open after
//                                the user replied to the message.  Will be true if
//                                the user didn't reply to the message.
function DigDistMsgReader_ReadMessage(pOffset)
{
	var retObj = new Object();
	retObj.offsetValid = true;
	retObj.userReplied = false;

	// Get the message header
	var msgHeader = this.GetMsgHdrByMsgNum(pOffset+1);
	if (msgHeader == null)
	{
		console.print("\1n" + this.text.invalidMsgNumText.replace("%d", +(pOffset+1)) + "\1n");
		console.inkey(K_NONE, ERROR_PAUSE_WAIT_MS);
		retObj.offsetValid = false;
		return retObj;
	}

	// Show the message header.
	this.DisplaySyncMsgHeader(msgHeader);

	// Show the message body.  Make sure the text is word-wrapped
	// so that it looks good.
	var msgText = this.msgbase.get_msg_body(true, msgHeader.offset);
	var msgTextWrapped = word_wrap(msgText, console.screen_columns-1);
	console.print("\1n" + this.colors["msgBodyColor"]);
	console.putmsg(msgTextWrapped, P_NOATCODES);

	// Hack: If the "from" name in the header is blank (as it might be sometimes), then
	// set it to "All".  This prevents Synchronet from crashing, and it will also default
	// the "to" name in the user's reply to "All".
	if (msgHeader["from"] == "")
		msgHeader["from"] = "All";

	// Mark the message as read, if it was written to the current
	// user.
	var msgToUpper = msgHeader["to"].toUpperCase();
	if ((msgToUpper == user.alias.toUpperCase()) || (msgToUpper == user.name.toUpperCase()))
	{
		msgHeader.attr = (msgHeader.attr | MSG_READ);
		var wroteHeader = this.msgbase.put_msg_header(true, msgHeader.offset, msgHeader);
	}

	// If not reading personal email, then update the scan & last read message pointers.
	if (this.subBoardCode != "mail") // && !this.SearchTypePopulatesSearchResults()
	{
		if (msgHeader.number > msg_area.sub[this.subBoardCode].scan_ptr)
			msg_area.sub[this.subBoardCode].scan_ptr = msgHeader.number;
		msg_area.sub[this.subBoardCode].last_read = msgHeader.number;
	}

	// Allow the user to reply to the message, either publicly or privately.
	console.print("\1n\1cEnd of message. \1hR\1b)\1n\1ceply\1h\1b, " +
	              "\1cP\1b)\1n\1crivate reply\1h\1b, \1cENTER\1b/\1cN\1b)\1n\1co reply\1h\1g: \1n\1c");
	var userKey = console.getkeys("RPN").toString();
	var privateReply = (userKey == "P");
	if ((userKey == "R") || privateReply)
	{
		var replyRetObj = this.ReplyToMsg(msgHeader, msgText, privateReply, pOffset);
		retObj.userReplied = replyRetObj.postSucceeded;
		retObj.msgbaseReOpened = replyRetObj.msgbaseReOpened;
	}

	return retObj;
}
// For the DigDistMsgReader Class: Given a message number of a message in the
// current message area, shows the message to the user and allows the user to
// respond.  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 = new Object();
	retObj.offsetValid = true;
	retObj.msgDeleted = false;
	retObj.userReplied = false;
	retObj.lastKeypress = "";
	retObj.newMsgOffset = -1;
	retObj.nextAction = ACTION_NONE;
	retObj.refreshEnhancedRdrHelpLine = false;

	// Get the message header
	var msgHeader = this.GetMsgHdrByIdx(pOffset);
	if (msgHeader == null)
	{
		console.print("\1n" + this.text.invalidMsgNumText.replace("%d", +(pOffset+1)) + "\1n");
		console.crlf();
		console.inkey(K_NONE, ERROR_PAUSE_WAIT_MS);
		retObj.offsetValid = false;
		return retObj;
	}

	// See if the message is marked as deleted.  If so, don't let the
	// user read it, just silently return.
	retObj.msgDeleted = ((msgHeader.attr & MSG_DELETE) == MSG_DELETE);
	if (retObj.msgDeleted)
		return retObj;

	// 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");

	// Hack: If the "from" name in the header is empty (as it might be sometimes), then
	// set it to "All".  This prevents Synchronet from crashing, and it will also default
	// the "to" name in the user's reply to "All".
	if (msgHeader.from.length == 0)
		msgHeader.from = "All";

	// Some key bindings
	var enhReaderKeys = new Object();
	enhReaderKeys.prevMsgByTitle = "<";
	enhReaderKeys.nextMsgByTitle = ">";
	enhReaderKeys.prevMsgByAuthor = "{";
	enhReaderKeys.nextMsgByAuthor = "}";
	enhReaderKeys.prevMsgByToUser = "[";
	enhReaderKeys.nextMsgByToUser = "]";
	enhReaderKeys.prevMsgByThreadID = "(";
	enhReaderKeys.nextMsgByThreadID = ")";
	enhReaderKeys.prevSubBoard = "-";
	enhReaderKeys.nextSubBoard = "+";
	enhReaderKeys.downloadAttachments = CTRL_D;
	enhReaderKeys.saveToBBSMachine = CTRL_S;

	// This function converts a thread navigation key character to its
	// corresponding thread type value
	function keypressToThreadType(pKeypress)
	{
		var threadType = THREAD_BY_ID;
		switch (pKeypress)
		{
			case enhReaderKeys.prevMsgByTitle:
			case enhReaderKeys.nextMsgByTitle:
				threadType = THREAD_BY_TITLE;
				break;
			case enhReaderKeys.prevMsgByAuthor:
			case enhReaderKeys.nextMsgByAuthor:
				threadType = THREAD_BY_AUTHOR;
				break;
			case enhReaderKeys.prevMsgByToUser:
			case enhReaderKeys.nextMsgByToUser:
				threadType = THREAD_BY_TO_USER;
				break;
			case enhReaderKeys.prevMsgByThreadID:
			case enhReaderKeys.nextMsgByThreadID:
			default:
				threadType = THREAD_BY_ID;
				break;
		}
		return threadType;
	}

	// Get the message text and see if it has any ANSI codes.  If it has ANSI codes,
	// then don't use the scrolling interface so that the ANSI gets displayed properly.
	var messageText = this.msgbase.get_msg_body(true, msgHeader.offset);
	// If the message has ANSI content, then use the scrolling interface only
	// if frame.js is available on the BBS machine and the option to use the
	// scrolling interface for ANSI messages is enabled.
	var msgHasANSICodes = textHasANSICodes(messageText);
	var useScrollingInterface = this.scrollingReaderInterface && console.term_supports(USER_ANSI);
	if (useScrollingInterface && msgHasANSICodes)
		useScrollingInterface = gFrameJSAvailable && this.useScrollingInterfaceForANSIMessages;
	// 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);
	// Use the scrollable reader interface if the setting is enabled & the user's
	// terminal supports ANSI.  Otherwise, use a more traditional user interface.
	{
		// 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, msgHasANSICodes);

		var topMsgLineIdxForLastPage = msgInfo.topMsgLineIdxForLastPage;
		var msgFractionShown = msgInfo.msgFractionShown;
		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;

		// 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.
		this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);

		// Input loop (for scrolling the message up & down)
		var msgLineFormatStr = "%-" + this.msgAreaWidth + "s";
		var writeMessage = true;
		// msgAreaHeight, msgReaderObj, and scrollbarUpdateFunction are for use
		// with scrollTextLines().
		var msgAreaHeight = this.msgAreaBottom - this.msgAreaTop + 1;
		var msgReaderObj = this;
		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.UpdateEnhancedReaderScollbar(solidBlockStartRow, solidBlockLastStartRow, numSolidScrollBlocks);
			solidBlockLastStartRow = solidBlockStartRow;
			console.gotoxy(1, console.screen_rows);
		}
		while (continueOn)
		{
			// Display the message lines (depending on the value of writeMessage)
			// and handle scroll keys via scrollTextLines().  Handle other keypresses
			// here.
			var scrollRetObj = null;
			if (msgInfo.displayFrame != null)
			{
				msgInfo.displayFrame.draw();
				scrollRetObj = scrollFrame(msgInfo.displayFrame, msgInfo.displayFrameScrollbar,
				                           topMsgLineIdx, this.colors["msgBodyColor"],
				                           writeMessage, 1, console.screen_rows,
										   msgScrollbarUpdateFn);
			}
			else
			{
				scrollRetObj = scrollTextLines(msgInfo.messageLines, topMsgLineIdx,
				                               this.colors["msgBodyColor"], writeMessage,
				                               this.msgAreaLeft, this.msgAreaTop, this.msgAreaWidth,
				                               msgAreaHeight, 1, console.screen_rows,
				                               msgScrollbarUpdateFn);
			}
			topMsgLineIdx = scrollRetObj.topLineIdx;
			retObj.lastKeypress = scrollRetObj.lastKeypress;
			switch (retObj.lastKeypress)
			{
				case KEY_DEL: // Delete message
					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.DeleteMessage() 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.DeleteMessage(pOffset, promptPos, true, this.msgAreaWidth,
					                                       true, msgInfo.attachments);
					if (msgWasDeleted)
					{
						var msgSearchObj = this.LookForNextOrPriorNonDeletedMsg(pOffset);
						continueOn = msgSearchObj.continueInputLoop;
						retObj.newMsgOffset = msgSearchObj.newMsgOffset;
						retObj.nextAction = msgSearchObj.nextAction;
						if (msgSearchObj.promptGoToNextArea)
						{
							if (this.EnhReaderPromptYesNo(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 "E": // 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 = "\1n\1h\1k" + BLOCK1; // Dim block
							// Dim block
							var scrollBarBlock = this.colors.scrollbarBGColor + this.text.scrollbarBGChar;
							if (solidBlockStartRow + numSolidScrollBlocks - 1 == this.msgAreaBottom)
							{
								//scrollBarBlock = "\1w" + BLOCK2; // Bright block
								// Bright block
								scrollBarBlock = this.colors.scrollbarScrollBlockColor + this.text.scrollbarScrollBlockChar;
							}
							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("\1n");
								// 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
								solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
								this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
								writeMessage = true; // We want to refresh the message on the screen
							}
						}
					}
					else
						writeMessage = false; // Don't write the current message again
					break;
				case "?": // Show the help screen
					this.DisplayEnhancedReaderHelp(allowChgMsgArea, msgInfo.attachments.length > 0);
					// 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("\1n");
					// 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
					solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
					this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
					writeMessage = true; // We want to refresh the message on the screen
					break;
				case "R": // Reply to the message
				case "I": // Private message reply
					// If the user pressed P (private reply) while reading private
					// mail, then do nothing (allow only the "R" key to reply).
					var privateReply = (retObj.lastKeypress == "I");
					if (privateReply && this.readingPersonalEmail)
						writeMessage = false; // Don't re-write the current message again
					else
					{
						// Let the user reply to the message.
						var replyRetObj = this.ReplyToMsg(msgHeader, msgInfo.msgText, privateReply, pOffset);
						                                  retObj.userReplied = replyRetObj.postSucceeded;
						//retObj.msgDeleted = replyRetObj.msgWasDeleted;
						var msgWasDeleted = replyRetObj.msgWasDeleted;
						//if (retObj.msgDeleted)
						if (msgWasDeleted)
						{
							var msgSearchObj = this.LookForNextOrPriorNonDeletedMsg(pOffset);
							continueOn = msgSearchObj.continueInputLoop;
							retObj.newMsgOffset = msgSearchObj.newMsgOffset;
							retObj.nextAction = msgSearchObj.nextAction;
							if (msgSearchObj.promptGoToNextArea)
							{
								if (this.EnhReaderPromptYesNo(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 messagebase object was successfully re-opened after
							// posting the message, then refresh the screen.  Otherwise,
							// we'll want to quit.
							if (replyRetObj.msgbaseReOpened)
							{
								// 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("\1n");
								// 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
								solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
								this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
								writeMessage = true; // We want to refresh the message on the screen
							}
							else
							{
								retObj.nextAction = ACTION_QUIT;
								continueOn = false;
								// Display an error
								console.print("\1n");
								console.crlf();
								console.print("\1h\1yMessagebase error after replying.  Aborting.\1n");
								mswait(ERROR_PAUSE_WAIT_MS);
							}
						}
					}
					break;
				case "P": // 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?
							}
						}

						// 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
						solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
						this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
						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, 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,
							               "\1n" + this.text.msgHasBeenDeletedText.replace("%d", msgNumInput) + "\1n",
							               ERROR_PAUSE_WAIT_MS, "\1n", 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.print("\1n");
								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 enhReaderKeys.prevMsgByTitle: // Previous message by title
				case enhReaderKeys.prevMsgByAuthor: // Previous message by author
				case enhReaderKeys.prevMsgByToUser: // Previous message by 'to user'
				case 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),
																			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 enhReaderKeys.nextMsgByTitle: // Next message by title (subject)
				case enhReaderKeys.nextMsgByAuthor: // Next message by author
				case enhReaderKeys.nextMsgByToUser: // Next message by 'to user'
				case 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),
																			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 KEY_LEFT: // 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.FindNextNonDeletedMsgIdx(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(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 KEY_RIGHT: // Next message
					// 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.FindNextNonDeletedMsgIdx(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)
							writeMessage = false;
						else
						{
							if (this.EnhReaderPromptYesNo(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
					{
						// 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;
					// First & last message: Quit out of this input loop and let the
					// calling function, this.ReadMessages(), handle the action.
				case "F": // 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 "L": // 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 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 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 "H":
				case "K":
					if (gIsSysop)
					{
						// 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(msgHeader, (retObj.lastKeypress == "K"));
						if (extdHdrInfoLines.length > 0)
						{
							// 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;
							// Define a scrollbar update function for the header info/kludge lines
							function msgInfoScrollbarUpdateFn(pFractionToLastPage)
							{
								var infoSolidBlockStartRow = msgReaderObj.msgAreaTop + Math.floor(numNonSolidInfoScrollBlocks * pFractionToLastPage);
								if (infoSolidBlockStartRow != lastInfoSolidBlockStartRow)
									msgReaderObj.UpdateEnhancedReaderScollbar(infoSolidBlockStartRow, lastInfoSolidBlockStartRow, numInfoSolidScrollBlocks);
								lastInfoSolidBlockStartRow = infoSolidBlockStartRow;
								console.gotoxy(1, console.screen_rows);
							}
							// 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, this.msgAreaWidth,
							msgAreaHeight, 1, console.screen_rows,
							msgInfoScrollbarUpdateFn);
							// Display the scrollbar for the message to refresh it on the screen
							solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
							this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
							writeMessage = true; // We want to refresh the message on the screen
						}
						else
						{
							// There are no kludge lines for this message
							this.DisplayEnhReaderError(this.text.noKludgeLinesForThisMsgText, msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr);
							console.gotoxy(originalCurPos);
							writeMessage = false;
						}
					}
					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 "M": // Message list
					retObj.nextAction = ACTION_DISPLAY_MSG_LIST;
					continueOn = false;
					break;
				case "C": // 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 enhReaderKeys.downloadAttachments: // Download attachments
					if (msgInfo.attachments.length > 0)
					{
						console.print("\1n");
						console.gotoxy(1, console.screen_rows);
						console.crlf();
						console.print("\1c- Download Attached Files -\1n");
						// Note: sendAttachedFiles() will output a CRLF at the beginning.
						sendAttachedFiles(msgInfo.attachments);

						// Refresh things on the screen
						console.clear("\1n");
						// 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
						solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
						this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
						writeMessage = true; // We want to refresh the message on the screen
					}
					else
						writeMessage = false;
					break;
					// Save the message to the BBS machine - Only allow this
					// if the user is a sysop.
						// Prompt the user for a filename to save the message to the
						// BBS machine
						var promptPos = this.EnhReaderPrepLast2LinesForPrompt();
						console.print("\1n\1cFilename:\1h");
						var inputLen = console.screen_columns - 10; // 10 = "Filename:" length + 1
						var filename = console.getstr(inputLen, K_NOCRLF);
						console.print("\1n");
						if (filename.length > 0)
						{
							//var saveMsgRetObj = this.SaveMsgToFile(msgHeader, filename, true, msgInfo.messageLines);
							var saveMsgRetObj = this.SaveMsgToFile(msgHeader, filename, true);
							console.gotoxy(promptPos);
							console.cleartoeol("\1n");
							console.gotoxy(promptPos);
							if (saveMsgRetObj.succeeded)
								console.print("\1n\1cThe message has been saved.\1n");
							else
								console.print("\1n\1y\1hFailed: " + saveMsgRetObj.errorMsg + "\1n");
							mswait(ERROR_PAUSE_WAIT_MS);
						}
						else
						{
							console.gotoxy(promptPos);
							console.print("\1n\1y\1hMessage not exported\1n");
							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
				case "Q": // Quit
					retObj.nextAction = ACTION_QUIT;
					continueOn = false;
					break;
				default:
					writeMessage = false;
					break;
			}
		}
	}
	else
	{
		// Use the non-scrolling interface.
		// Separate the message text from any attachments in the message.
		var msgAndAttachmentInfo = determineMsgAttachments(msgHeader, messageText, true);
		// Only interpret @-codes if the user is reading personal email.  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)
			msgAndAttachmentInfo.msgText = replaceAtCodesInStr(msgAndAttachmentInfo.msgText); // Or this.ParseMsgAtCodes(msgAndAttachmentInfo.msgText, msgHeader) to replace only some @ codes
		var msgTextWrapped = word_wrap(msgAndAttachmentInfo.msgText, console.screen_columns-1);

		// Generate the key help text
		var keyHelpText = "\1n\1c\1h#\1n\1b, \1c\1hLeft\1n\1b, \1c\1hRight\1n\1b, ";
		if (this.CanDelete() || this.CanDeleteLastMsg())
			keyHelpText += "\1c\1hDEL\1b, ";
		if (this.CanEdit())
			keyHelpText += "\1c\1hE\1y)\1n\1cdit\1b, ";
		keyHelpText += "\1c\1hF\1y)\1n\1cirst\1b, \1c\1hL\1y)\1n\1cast\1b, \1c\1hR\1y)\1n\1ceply\1b, ";
		// 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 += "\1c\1hP\1y)\1n\1crivate reply\1b, ";
			keyHelpText += "\1c\1hC\1y)\1n\1chg area\1b, ";
		}
		else
		{
			// The user isn't allowed to change to a different message area.
			// Go ahead and include the private reply option.
			keyHelpText += "\1c\1hP\1y)\1n\1crivate reply\1b, ";
		}
		keyHelpText += "\1c\1hQ\1y)\1n\1cuit\1b, \1c\1h?\1g: \1c";

		// User input loop
		var writeMessage = true;
		var writePromptText = true;
		var continueOn = true;
		while (continueOn)
		{
			if (writeMessage)
			{
				if (console.term_supports(USER_ANSI))
					console.clear("\1n");
				// Write the message header & message body to the screen
				this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
				console.print("\1n" + this.colors["msgBodyColor"]);
				console.putmsg(msgTextWrapped, 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*/);
			switch (retObj.lastKeypress)
			{
				case KEY_DEL: // Delete message
					console.crlf();
					// Prompt the user for confirmation to delete the message.
					// Note: this.DeleteMessage() 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.
					// TODO: For the DeleteMessage() call, pass the array of file
					// attachments for it to delete (i.e., msgInfo.attachments)
					var msgWasDeleted = this.DeleteMessage(pOffset);
					if (msgWasDeleted)
					{
						var msgSearchObj = this.LookForNextOrPriorNonDeletedMsg(pOffset);
						continueOn = msgSearchObj.continueInputLoop;
						retObj.newMsgOffset = msgSearchObj.newMsgOffset;
						retObj.nextAction = msgSearchObj.nextAction;
						if (msgSearchObj.promptGoToNextArea)
						{
							if (console.yesno(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 "E": // 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 "?": // Show help
					if (!console.term_supports(USER_ANSI))
					{
						console.crlf();
						console.crlf();
					}
					this.DisplayEnhancedReaderHelp(allowChgMsgArea, msgAndAttachmentInfo.attachments.length > 0);
					if (!console.term_supports(USER_ANSI))
					{
						console.crlf();
						console.crlf();
					}
					break;
				case "R": // Reply to the message
				case "I": // Private reply
					// If the user pressed P (private reply) while reading private
					// mail, then do nothing (allow only the "R" key to reply).
					// If not reading personal email, go ahead and let the user reply
					// with either the "P" or "R" keypress.
					var privateReply = (retObj.lastKeypress == "I");
					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();
						var replyRetObj = this.ReplyToMsg(msgHeader, msgAndAttachmentInfo.msgText, privateReply, pOffset);
						retObj.userReplied = replyRetObj.postSucceeded;
						//retObj.msgDeleted = replyRetObj.msgWasDeleted;
						var msgWasDeleted = replyRetObj.msgWasDeleted;
						if (msgWasDeleted)
						{
							var msgSearchObj = this.LookForNextOrPriorNonDeletedMsg(pOffset);
							continueOn = msgSearchObj.continueInputLoop;
							retObj.newMsgOffset = msgSearchObj.newMsgOffset;
							retObj.nextAction = msgSearchObj.nextAction;
							if (msgSearchObj.promptGoToNextArea)
							{
								if (console.yesno(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
						{
							// If the messagebase object was not successfully re-opened
							// after posting the message, then  we'll want to quit.
							if (!replyRetObj.msgbaseReOpened)
							{
								retObj.nextAction = ACTION_QUIT;
								continueOn = false;
								// Display an error
								console.print("\1n");
								console.crlf();
								console.print("\1h\1yMessagebase error after replying.  Aborting.\1n");
								mswait(ERROR_PAUSE_WAIT_MS);
							}
						}
					}
					break;
				case "P": // 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?
						}

						// 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, 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("\1n" + this.text.msgHasBeenDeletedText.replace("%d", msgNumInput) + "\1n");
							console.crlf();
							console.pause();
						}
						else
						{
							// Confirm with the user whether to read the message
							var readMsg = true;
							if (this.promptToReadMessage)
							{
								readMsg = console.yesno("\1n" + 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 enhReaderKeys.prevMsgByTitle: // Previous message by title
				case enhReaderKeys.prevMsgByAuthor: // Previous message by author
				case enhReaderKeys.prevMsgByToUser: // Previous message by 'to user'
				case 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),
																			false);
						if (threadPrevMsgOffset > -1)
						{
							retObj.newMsgOffset = threadPrevMsgOffset;
							retObj.nextAction = ACTION_GO_SPECIFIC_MSG;
							continueOn = false;
						}
					}
					else
					{
						writeMessage = false;
						writePromptText = false;
					}
					break;
				case enhReaderKeys.nextMsgByTitle: // Next message by title (subject)
				case enhReaderKeys.nextMsgByAuthor: // Next message by author
				case enhReaderKeys.nextMsgByToUser: // Next message by 'to user'
				case 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),
																			false);
						if (threadNextMsgOffset > -1)
							retObj.newMsgOffset = threadNextMsgOffset;
							retObj.nextAction = ACTION_GO_SPECIFIC_MSG;
							continueOn = false;
						}
					}
					else
					{
						writeMessage = false;
						writePromptText = false;
					}
					break;
				case KEY_LEFT: // 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.FindNextNonDeletedMsgIdx(pOffset, false);
					var goToPrevMessage = false;
					if ((retObj.newMsgOffset > -1) || allowChgMsgArea)
					{
						if (retObj.newMsgOffset == -1 && !curMsgSubBoardIsLast())
						{
							console.crlf();
							goToPrevMessage = console.yesno(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 KEY_RIGHT: // Next message
					// 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.FindNextNonDeletedMsgIdx(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.crlf();
						if (console.yesno(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 "F": // 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 "L": // 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 "H":
				case "K":
					if (gIsSysop)
					{
						console.crlf();
						// Get an array of the extended header info/kludge lines and then
						// display them.
						var extdHdrInfoLines = this.GetExtdMsgHdrInfo(msgHeader, (retObj.lastKeypress == "K"));
						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(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 "M": // Message list
					retObj.nextAction = ACTION_DISPLAY_MSG_LIST;
					continueOn = false;
					break;
				case "C": // Change message area, if allowed
					if (allowChgMsgArea)
					{
						retObj.nextAction = ACTION_CHG_MSG_AREA;
						continueOn = false;
					}
					else
					{
						writeMessage = false;
						writePromptText = false;
					}
					break;
				case enhReaderKeys.downloadAttachments: // Download attachments
					if (msgAndAttachmentInfo.attachments.length > 0)
					{
						console.print("\1n");
						console.crlf();
						console.print("\1c- Download Attached Files -\1n");
						// Note: sendAttachedFiles() will output a CRLF at the beginning.
						sendAttachedFiles(msgAndAttachmentInfo.attachments);

						// Ensure the message is refreshed on the screen
						writeMessage = true;
						writePromptText = true;
					}
					else
					{
						writeMessage = false;
						writePromptText = false;
					}
					break;
				case enhReaderKeys.saveToBBSMachine:
					// Save the message to the BBS machine - Only allow this
					// if the user is a sysop.
					if (gIsSysop)
					{
						console.crlf();
						console.print("\1n\1cFilename:\1h");
						var inputLen = console.screen_columns - 10; // 10 = "Filename:" length + 1
						var filename = console.getstr(inputLen, K_NOCRLF);
						console.print("\1n");
						console.crlf();
						if (filename.length > 0)
						{
							var saveMsgRetObj = this.SaveMsgToFile(msgHeader, filename, true);
							if (saveMsgRetObj.succeeded)
								console.print("\1n\1cThe message has been saved.\1n");
							else
								console.print("\1n\1y\1hFailed: " + saveMsgRetObj.errorMsg + "\1n");
							mswait(ERROR_PAUSE_WAIT_MS);
						}
						else
						{
							console.print("\1n\1y\1hMessage not exported\1n");
							mswait(ERROR_PAUSE_WAIT_MS);
						}
						writeMessage = true;
					}
					else
						writeMessage = false;
					break;
				case "Q": // Quit
					retObj.nextAction = ACTION_QUIT;
					continueOn = false;
					break;
				default:
					// No need to do anything
					writeMessage = false;
					writePromptText = false;
					break;
			}
		}
	}

	// Mark the message as read if it was written to the current user or if
	// the user is reading personal email.
	if (userNameHandleAliasMatch(msgHeader.to))
	{
		msgHeader.attr |= MSG_READ;
		this.msgbase.put_msg_header(true, msgHeader.offset, msgHeader);
		if (this.SearchTypePopulatesSearchResults())
			this.RefreshSearchResultMsgHdr(pOffset, MSG_READ);
	}

	// If not reading personal email, then update the scan & last read message pointers.
	if (this.subBoardCode != "mail") // && !this.SearchTypePopulatesSearchResults()
	{
		if (msgHeader.number > msg_area.sub[this.subBoardCode].scan_ptr)
			msg_area.sub[this.subBoardCode].scan_ptr = msgHeader.number;
		msg_area.sub[this.subBoardCode].last_read = msgHeader.number;
	}

	return retObj;
} // DigDistMsgReader_ReadMessageEnhanced

// 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("\1n" + 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.print("\1n");
	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 = new Object();
	retObj.newMsgOffset = 0;
	retObj.nextAction = ACTION_NONE;
	retObj.continueInputLoop = true;
	retObj.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.FindNextNonDeletedMsgIdx(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.FindNextNonDeletedMsgIdx(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);
   console.print(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
// 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)
{
	var retObj = new Object();
	retObj.changedMsgArea = false;
	retObj.msgIndex = -1;
	retObj.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;
				// Open the new sub-board
				this.msgbase.close();
				this.setSubBoardCode(readMsgRetObj.subCode);
				this.msgbase = new MsgBase(this.subBoardCode);
				if (this.msgbase.open())
				{
					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.
						retObj.msgIndex = this.AbsMsgNumToIdx(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 nonDeletedMsgIdx = this.FindNextNonDeletedMsgIdx(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 (nonDeletedMsgIdx > -1)
								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
						{
							continueGoingToPrevSubBoard = !console.noyes("Continue searching");
							if (!continueGoingToPrevSubBoard)
							{
								retObj.shouldStopReading = true;
								return retObj;
							}
						}
					}
					else
					{
						retObj.changedMsgArea = true;
						if ((oldHotkeyHelpLine != this.enhReadHelpLine) && this.scrollingReaderInterface && console.term_supports(USER_ANSI))
							this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, pAllowChgMsgArea);
					}
				}
				else // The message base failed to open
				{
					console.clear("\1n");
					console.print("\1h\1y* \1wUnable to open message sub-board:");
					console.crlf();
					console.print(subBoardGrpAndName(this.subBoardCode));
					console.crlf();
					console.pause();
					retObj.shouldStopReading = true;
					continueGoingToPrevSubBoard = false;
					return retObj;
				}
			}
			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,
									"\1n\1h\1y* No prior messages or no message in prior message areas.",
									ERROR_PAUSE_WAIT_MS, "\1n", 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
// 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)
{
	var retObj = new Object();
	retObj.changedMsgArea = false;
	retObj.msgIndex = -1;
	retObj.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;
				// Open the new sub-board
				this.msgbase.close();
				this.setSubBoardCode(readMsgRetObj.subCode);
				this.msgbase = new MsgBase(this.subBoardCode);
				if (this.msgbase.open())
				{
					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);
						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 nonDeletedMsgIdx = this.FindNextNonDeletedMsgIdx(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 (nonDeletedMsgIdx > -1)
							{
								retObj.msgIndex = nonDeletedMsgIdx;
								retObj.changedMsgArea = true;
								var newLastRead = this.IdxToAbsMsgNum(nonDeletedMsgIdx);
								if (newLastRead > -1)
									msg_area.sub[this.subBoardCode].last_read = newLastRead;
							}
						}
					}
					// Set the 
Loading
Loading full blame...