Skip to content
Snippets Groups Projects
DDMsgReader.js 631 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.
 */

/* 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)
			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.05 Beta";
var READER_DATE = "2015-11-01";

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

// Threading types
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);

/////////////////////////////////////////////
// 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 doDDMR = 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:
			doDDMR = false;
			console.print("\1n\1h\1y\1iAborted\1n");
			console.crlf();
			console.pause();
			break;
	}
}

if (doDDMR)
{
	// 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");
}

// 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") && subBoardCodeIsValid(pSubBoardCode))
	{
		this.setSubBoardCode(pSubBoardCode);
		this.msgbase = new MsgBase(this.subBoardCode);
		if (gCmdLineArgVals.hasOwnProperty("personalemailsent") && gCmdLineArgVals.personalemailsent)
			this.readingPersonalEmailFromUser = true;
	}

	// This property controls whether or not the user will be prompted to
	// continue listing messages after selecting a message to read.
	this.promptToContinueListingMessages = false;
	// Whether or not to prompt the user to confirm to read a message
	this.promptToReadMessage = false;

	// String lengths for the columns to write
	// Fixed field widths: Message number, date, and time
	this.MSGNUM_LEN = 4;
	this.DATE_LEN = 10; // i.e., YYYY-MM-DD
	this.TIME_LEN = 8;  // i.e., HH:MM:SS
	// Variable field widths: From, to, and subject (based on a screen width of
	// 80 columns)
	this.FROM_LEN = (console.screen_columns * (15/80)).toFixed(0);
	this.TO_LEN = (console.screen_columns * (15/80)).toFixed(0);
	this.SUBJ_LEN = (console.screen_columns * (22/80)).toFixed(0);

	// Whether or not the user chose to read a message
	this.readAMessage = false;
	// Whether or not the user denied confirmation to read a message
	this.deniedReadingMessage = false;

	// msgListUseLightbarListInterface specifies whether or not to use the lightbar
	// interface for the message list.  The lightbar interface will only be used if
	// the user's terminal supports ANSI.
	this.msgListUseLightbarListInterface = true;

	// Whether or not to use the scrolling interface when reading a message
	// (will only be used for ANSI terminals).
	this.scrollingReaderInterface = true;

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

	// displayBoardInfoInHeader specifies whether or not to display
	// the message group and sub-board lines in the header at the
	// top of the screen (an additional 2 lines).
	this.displayBoardInfoInHeader = false;

	// msgList_displayMessageDateImported specifies whether or not to use the
	// message import date as the date displayed in the message list.  If false,
	// the message written date will be displayed.
	this.msgList_displayMessageDateImported = true;

	// The number of spaces to use for tab characters - Used in the
	// extended read mode
	this.numTabSpaces = 3;

	// Construct the header format string
	this.sHdrFormatStr = "%" + this.MSGNUM_LEN + "s %-" + this.FROM_LEN + "s %-"
	                   + this.TO_LEN + "s %-" + this.SUBJ_LEN + "s %-"
	                   + this.DATE_LEN + "s %-" + this.TIME_LEN + "s";
	// If the user's terminal doesn't support ANSI, then append a newline to
	// the end of the format string (we won't be able to move the cursor).
	if (!canDoHighASCIIAndANSI())
		this.sHdrFormatStr += "\r\n";

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

	// Set the methods for the object
	this.RefreshSearchResultMsgHdr = DigDistMsgReader_RefreshSearchResultMsgHdr;   // Refreshes a message header in the search results
	this.SearchMessages = DigDistMsgReader_SearchMessages; // Prompts the user for search text, then lists/reads messages, performing the search
	this.ReadMessages = DigDistMsgReader_ReadMessages;
	this.DisplayEnhancedMsgReadHelpLine = DigDistMsgReader_DisplayEnhancedMsgReadHelpLine;
	this.GoToPrevSubBoardForEnhReader = DigDistMsgReader_GoToPrevSubBoardForEnhReader;
	this.GoToNextSubBoardForEnhReader = DigDistMsgReader_GoToNextSubBoardForEnhReader;
	this.SetUpTraditionalMsgListVars = DigDistMsgReader_SetUpTraditionalMsgListVars;
	this.SetUpLightbarMsgListVars = DigDistMsgReader_SetUpLightbarMsgListVars;
	this.ListMessages = DigDistMsgReader_ListMessages;
	this.ListMessages_Traditional = DigDistMsgReader_ListMessages_Traditional;
	this.ListMessages_Lightbar = DigDistMsgReader_ListMessages_Lightbar;
	this.ClearSearchData = DigDistMsgReader_ClearSearchData;
	this.ReadOrListSubBoard = DigDistMsgReader_ReadOrListSubBoard;
	this.PopulateHdrsIfSearch_DispErrorIfNoMsgs = DigDistMsgReader_PopulateHdrsIfSearch_DispErrorIfNoMsgs;
	this.SearchTypePopulatesSearchResults = DigDistMsgReader_SearchTypePopulatesSearchResults;
	this.SearchTypeRequiresSearchText = DigDistMsgReader_SearchTypeRequiresSearchText;
	this.MessageAreaScan = DigDistMsgReader_MessageAreaScan;
	this.PromptContinueOrReadMsg = DigDistMsgReader_PromptContinueOrReadMsg;
	this.WriteMsgListScreenTopHeader = DigDistMsgReader_WriteMsgListScreenTopHeader;
	this.ReadMessage = DigDistMsgReader_ReadMessage;
	this.ReadMessageEnhanced = DigDistMsgReader_ReadMessageEnhanced;
	this.EnhReaderPrepLast2LinesForPrompt = DigDistMsgReader_EnhReaderPrepLast2LinesForPrompt;
	this.LookForNextOrPriorNonDeletedMsg = DigDistMsgReader_LookForNextOrPriorNonDeletedMsg;
	this.PrintMessageInfo = DigDistMsgReader_PrintMessageInfo;
	this.ListScreenfulOfMessages = DigDistMsgReader_ListScreenfulOfMessages;
	this.DisplayMsgListHelp = DigDistMsgReader_DisplayMsgListHelp;
	this.DisplayTraditionalMsgListHelp = DigDistMsgReader_DisplayTraditionalMsgListHelp;
	this.DisplayLightbarMsgListHelp = DigDistMsgReader_DisplayLightbarMsgListHelp;
	this.DisplayMessageListNotesHelp = DigDistMsgReader_DisplayMessageListNotesHelp;
	this.SetMsgListPauseTextAndLightbarHelpLine = DigDistMsgReader_SetMsgListPauseTextAndLightbarHelpLine;
	this.SetEnhancedReaderHelpLine = DigDistMsgReader_SetEnhancedReaderHelpLine;
	this.EditExistingMsg = DigDistMsgReader_EditExistingMsg;
	this.CanDelete = DigDistMsgReader_CanDelete;
	this.CanDeleteLastMsg = DigDistMsgReader_CanDeleteLastMsg;
	this.CanEdit = DigDistMsgReader_CanEdit;
	this.CanQuote = DigDistMsgReader_CanQuote;
	this.ReadConfigFile = DigDistMsgReader_ReadConfigFile;
	this.DisplaySyncMsgHeader = DigDistMsgReader_DisplaySyncMsgHeader;
	this.GetMsgHdrFilenameFull = DigDistMsgReader_GetMsgHdrFilenameFull;
	this.GetMsgHdrByIdx = DigDistMsgReader_GetMsgHdrByIdx;
	this.GetMsgHdrByMsgNum = DigDistMsgReader_GetMsgHdrByMsgNum;
	this.GetMsgHdrByAbsoluteNum = DigDistMsgReader_GetMsgHdrByAbsoluteNum;
	this.AbsMsgNumToIdx = DigDistMsgReader_AbsMsgNumToIdx;
	this.IdxToAbsMsgNum = DigDistMsgReader_IdxToAbsMsgNum;
	this.NumMessages = DigDistMsgReader_NumMessages;
	this.HighestMessageNum = DigDistMsgReader_HighestMessageNum;
	this.IsValidMessageNum = DigDistMsgReader_IsValidMessageNum;
	this.PromptForMsgNum = DigDistMsgReader_PromptForMsgNum;
	this.ParseMsgHdrLineAtCodes = DigDistMsgReader_ParseMsgHdrLineAtCodes;
	this.ReplaceSubjFormatStr = DigDistMsgReader_ReplaceSubjFormatStr;
	this.FindNextNonDeletedMsgIdx = DigDistMsgReader_FindNextNonDeletedMsgIdx;
	this.ChangeSubBoard = DigDistMsgReader_ChangeSubBoard;
	this.EnhancedReaderChangeSubBoard = DigDistMsgReader_EnhancedReaderChangeSubBoard;
	this.ReplyToMsg = DigDistMsgReader_ReplyToMsg;
	this.DoPrivateReply = DigDistMsgReader_DoPrivateReply;
	this.DisplayEnhancedReaderHelp = DigDistMsgReader_DisplayEnhancedReaderHelp;
	this.DisplayEnhancedMsgHdr = DigDistMsgReader_DisplayEnhancedMsgHdr;
	this.DisplayEnhancedReaderWholeScrollbar = DigDistMsgReader_DisplayEnhancedReaderWholeScrollbar;
	this.UpdateEnhancedReaderScollbar = DigDistMsgReader_UpdateEnhancedReaderScollbar;
	this.MessageIsDeleted = DigDistMsgReader_MessageIsDeleted;
	this.MessageIsLastFromUser = DigDistMsgReader_MessageIsLastFromUser;
	this.DisplayEnhReaderError = DigDistMsgReader_DisplayEnhReaderError;
	this.EnhReaderPromptYesNo = DigDistMsgReader_EnhReaderPromptYesNo;
	this.DeleteMessage = DigDistMsgReader_DeleteMessage;
	this.GetExtdMsgHdrInfo = DigDistMsgReader_GetExtdMsgHdrInfo;
	this.GetMsgInfoForEnhancedReader = DigDistMsgReader_GetMsgInfoForEnhancedReader;
	this.GetLastReadMsgIdx = DigDistMsgReader_GetLastReadMsgIdx;
	this.GetScanPtrMsgIdx = DigDistMsgReader_GetScanPtrMsgIdx;
	this.SearchingAndResultObjsDefinedForCurSub = DigDistMsgReader_SearchingAndResultObjsDefinedForCurSub;
	this.RemoveFromSearchResults = DigDistMsgReader_RemoveFromSearchResults;
	this.FindThreadNextOffset = DigDistMsgReader_FindThreadNextOffset;
	this.FindThreadPrevOffset = DigDistMsgReader_FindThreadPrevOffset;

	// These two variables keep track of whether we're doing a message scan that spans
	// multiple sub-boards so that the enhanced reader function can enable use of
	// the > key to go to the next sub-board.
	this.doingMultiSubBoardScan = false;

	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 = scriptArgVals["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";