Skip to content
Snippets Groups Projects
DDMsgReader.js 685 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
 * 2015-12-19 Eric Oulashin     Version 1.07 Beta
 *                              Started working on a way of tagging message (i.e., to
 *                              do a batch delete).
 * 2015-12-24 Eric Oulashin     Version 1.07
 *                              Releasing this version, as it seems to be working as
 *                              it should after testing & development.
 */

/* 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.07";
var READER_DATE = "2015-12-24";

// 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
	// TODO: It might be good to figure out the longest message number for a
	// sub-board and set the message number length dynamically.  It would have
	// to change whenever the user changes to a different sub-board, and the
	// message list format string would have to change too.
	this.MSGNUM_LEN = 4;
	this.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.delSelectedMsgsConfirmText = "\1n\1h\1yDelete selected messages: Are you sure";
	this.text.msgDeletedText = "\1n\1cMessage #\1h%d\1n\1c has been marked for deletion.";
	this.text.selectedMsgsDeletedText = "\1n\1cSelected messages have 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.cannotDeleteAllSelectedMsgsText = "\1n\1y\1h* Cannot delete all selected messages";
	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.NonDeletedMessagesExist = DigDistMsgReader_NonDeletedMessagesExist;
	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.PromptAndDeleteMessage = DigDistMsgReader_PromptAndDeleteMessage;
	this.PromptAndDeleteSelectedMessages = DigDistMsgReader_PromptAndDeleteSelectedMessages;
	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;
	this.SaveMsgToFile = DigDistMsgReader_SaveMsgToFile;
	this.ToggleSelectedMessage = DigDistMsgReader_ToggleSelectedMessage;
	this.MessageIsSelected = DigDistMsgReader_MessageIsSelected;
	this.AllSelectedMessagesCanBeDeleted = DigDistMsgReader_AllSelectedMessagesCanBeDeleted;
	this.DeleteSelectedMessages = DigDistMsgReader_DeleteSelectedMessages;
	this.NumSelectedMessages = DigDistMsgReader_NumSelectedMessages;

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

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

	// selectedMessages will be an object (indexed by sub-board internal code)
	// containing objects that contain message indexes (as properties) for the
	// sub-boards.  Messages can be selected by the user for doing things such
	// as a batch delete, etc.
	this.selectedMessages = new Object();
}

// 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.
// pSubBoardCode: Optional - An internal sub-board code.  If not specified, then
//                this method will default to this.subBoardCode.
function DigDistMsgReader_RefreshSearchResultMsgHdr(pMsgIndex, pAttrib, pSubBoardCode)
	if (typeof(pMsgIndex) != "number")
		return;
	var subCode = (typeof(pSubBoardCode) == "string" ? pSubBoardCode : this.subBoardCode);
	if (this.msgSearchHdrs.hasOwnProperty(subCode))
	{
		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;
					}
Loading
Loading full blame...