Skip to content
Snippets Groups Projects
DDMsgReader.js 727 KiB
Newer Older
		{
			retObj.errorMsg = "Unable to open the sub-board";
			return retObj;
		}
	}
	retObj.msgText = word_wrap(msgBody, console.screen_columns - 1, true);
	var msgTextAltered = retObj.msgText; // Will alter the message text, but not yet
	// Only interpret @-codes if the user is reading personal email.  There
	// are many @-codes that do some action such as move the cursor, execute a
	// script, etc., and I don't want users on message networks to do anything
	// malicious to users on other BBSes.
	if (this.readingPersonalEmail)
		msgTextAltered = replaceAtCodesInStr(msgTextAltered); // Or this.ParseMsgAtCodes(msgTextAltered, pMsgHdr) to replace only some @ codes
	msgTextAltered = msgTextAltered.replace(/\t/g, this.tabReplacementText);
	// Convert other BBS color codes to Synchronet attribute codes if the settings
	// to do so are enabled.
	msgTextAltered = convertAttrsToSyncPerSysCfg(msgTextAltered, false);

	// If this is a message with a "By: <name> to <name>" and a date, then
	// sometimes such a message might have enter characters (ASCII 13), which
	// can mess up the display of the message, so remove enter characters
	// from the beginning of the message.
	var msgTextWithoutAttrs = strip_ctrl(msgTextAltered);
	var fromToSearchStr = "By: " + pMsgHdr.from + " to " + pMsgHdr.to;
	var toFromSearchStr = "By: " + pMsgHdr.to + " to " + pMsgHdr.from;
	var fromToStrIdx = msgTextWithoutAttrs.indexOf(fromToSearchStr);
	var toFromStrIdx = msgTextWithoutAttrs.indexOf(toFromSearchStr);
	var strIdx = -1;
	if (fromToStrIdx > -1)
		strIdx = fromToStrIdx;
	else if (toFromStrIdx > -1)
		strIdx = toFromStrIdx;
	if (strIdx > -1)
	{
		// " on Mon Feb 13 2017 01:00 pm " // 29 characters long
		strIdx += toFromSearchStr.length + 29 + 37; // 37: Extra room for Synchronet attribute codes
		// Remove enter characters from the beginning of the message
		var tmpStr = msgTextAltered.substring(0, strIdx).replace(ascii(13), "");
		msgTextAltered = tmpStr + msgTextAltered.substr(strIdx);
		// To remove the "By: <name> to <name> on <date>" lines altogether:
		//msgTextAltered = msgTextAltered.substr(strIdx);
	}

	var wordWrapTheMsgText = true;
	if (typeof(pWordWrap) == "boolean")
		wordWrapTheMsgText = pWordWrap;
	if (wordWrapTheMsgText)
	{
		// Wrap the text to fit into the available message area.
		// Note: In Synchronet 3.15 (and some beta builds of 3.16), there seemed to
		// be a bug in the word_wrap() function where the word wrap length in Linux
		// was one less than Windows, so if the BBS is running 3.15 or earlier of
		// Synchronet, add 1 to the word wrap length if running in Linux.
		var textWrapLen = this.msgAreaWidth;
		if (system.version_num <= 31500)
			textWrapLen = gRunningInWindows ? this.msgAreaWidth : this.msgAreaWidth + 1;
		var msgTextWrapped = word_wrap(msgTextAltered, textWrapLen);
		retObj.messageLines = lfexpand(msgTextWrapped).split("\r\n");
		// Go through the message lines and trim them to ensure they'll easily fit
		// in the message display area without having to trim them later.  (Note:
		// this is okay to do since we're only using messageLines to display the
		// message on the screen; messageLines isn't used for quoting/replying).
		for (var msgLnIdx = 0; msgLnIdx < retObj.messageLines.length; ++msgLnIdx)
			retObj.messageLines[msgLnIdx] = shortenStrWithAttrCodes(retObj.messageLines[msgLnIdx], this.msgAreaWidth);
		// Set up some variables for displaying the message
		retObj.topMsgLineIdxForLastPage = retObj.messageLines.length - this.msgAreaHeight;
		if (retObj.topMsgLineIdxForLastPage < 0)
			retObj.topMsgLineIdxForLastPage = 0;
		// Variables for the scrollbar to show the fraction of the message shown
		retObj.msgFractionShown = this.msgAreaHeight / retObj.messageLines.length;
		if (retObj.msgFractionShown > 1)
			retObj.msgFractionShown = 1.0;
		retObj.numSolidScrollBlocks = Math.floor(this.msgAreaHeight * retObj.msgFractionShown);
		if (retObj.numSolidScrollBlocks == 0)
			retObj.numSolidScrollBlocks = 1;
	}
	else
	{
		retObj.messageLines = [];
		retObj.messageLines.push(msgTextAltered);
	}
	retObj.numNonSolidScrollBlocks = this.msgAreaHeight - retObj.numSolidScrollBlocks;
	retObj.solidBlockStartRow = this.msgAreaTop;

	return retObj;
}

// For the DigDistMsgReader class: Returns the index of the last read message in
// the current message area.  If reading personal email, this will look at the
// search results.  Otherwise, this will use the sub-board's last_read pointer.
// If there is no last read message or if there is a problem getting the last read
// message index, this method will return -1.
//
// Parameters:
//  pMailStartFromFirst: Optional boolean - Whether or not to start from the
//                       first message (rather than from the last message) if
//                       reading personal email.  Will stop looking at the first
//                       unread message.  Defaults to false.
//
// Return value: An object containing the following properties:
//               lastReadMsgIdx: The index of the last read message in the current message area
//               lastReadMsgNum: The number of the last read message in the current message area
function DigDistMsgReader_GetLastReadMsgIdxAndNum(pMailStartFromFirst)
	if (this.readingPersonalEmail)
	{
		if (this.SearchingAndResultObjsDefinedForCurSub())
		{
			var startFromFirst = (typeof(pMailStartFromFirst) == "boolean" ? pMailStartFromFirst : false);
			if (startFromFirst)
			{
				for (var idx = 0; idx < this.msgSearchHdrs[this.subBoardCode].indexed.length; ++idx)
				{
					if ((this.msgSearchHdrs[this.subBoardCode].indexed[idx].attr & MSG_READ) == MSG_READ)
					{
						retObj.lastReadMsgIdx = idx;
						retObj.lastReadMsgNum = this.msgSearchHdrs[this.subBoardCode].indexed[idx].number;
					}
					else
						break;
				}
			}
			else
			{
				for (var idx = this.msgSearchHdrs[this.subBoardCode].indexed.length-1; idx >= 0; --idx)
				{
					if ((this.msgSearchHdrs[this.subBoardCode].indexed[idx].attr & MSG_READ) == MSG_READ)
					{
						retObj.lastReadMsgIdx = idx;
						retObj.lastReadMsgNum = this.msgSearchHdrs[this.subBoardCode].indexed[idx].number;
			// Sanity checking for retObj.lastReadMsgIdx (note: this function should return -1 if
			if (retObj.lastReadMsgIdx >= this.msgSearchHdrs[this.subBoardCode].indexed.length)
			{
				retObj.lastReadMsgIdx = this.msgSearchHdrs[this.subBoardCode].indexed.length - 1;
				retObj.lastReadMsgNum = this.msgSearchHdrs[this.subBoardCode].indexed[retObj.lastReadMsgIdx].number;
			}
		//retObj.lastReadMsgIdx = this.AbsMsgNumToIdx(msg_area.sub[this.subBoardCode].last_read);
		retObj.lastReadMsgIdx = this.GetMsgIdx(msg_area.sub[this.subBoardCode].last_read);
		retObj.lastReadMsgNum = msg_area.sub[this.subBoardCode].last_read;
		// hdrsForCurrentSubBoardByMsgNum is an object that maps absolute message numbers
		// to their index to hdrsForCurrentSubBoard
		this.hdrsForCurrentSubBoardByMsgNum = {};
		// Sanity checking for retObj.lastReadMsgIdx (note: this function should return -1 if
		var msgbase = new MsgBase(this.subBoardCode);
		if (msgbase.open())
			// If retObj.lastReadMsgIdx is -1, as a result of GetMsgIdx(), then see what the last read
			// message index is according to the Synchronet message base.  If
			// this.hdrsForCurrentSubBoard.length has been populated, then if the last
			// message index according to Synchronet is greater than that, then set the
			// message index to the last index in this.hdrsForCurrentSubBoard.length.
				var msgIdxAccordingToMsgbase = absMsgNumToIdx(msgbase, msg_area.sub[this.subBoardCode].last_read);
				if ((this.hdrsForCurrentSubBoard.length > 0) && (msgIdxAccordingToMsgbase >= this.hdrsForCurrentSubBoard.length))
				{
					retObj.lastReadMsgIdx = this.hdrsForCurrentSubBoard.length - 1;
					retObj.lastReadMsgNum = this.hdrsForCurrentSubBoard[retObj.lastReadMsgIdx].number;
				}
			//if (retObj.lastReadMsgIdx >= msgbase.total_msgs)
			//	retObj.lastReadMsgIdx = msgbase.total_msgs - 1;
			// TODO: Is this code right?  Modified 3/24/2015 to replace
			// the above 2 commented lines.
			if ((retObj.lastReadMsgIdx < 0) || (retObj.lastReadMsgIdx >= msgbase.total_msgs))
			{
				// Look for the first message not marked as deleted
				var nonDeletedMsgIdx = this.FindNextNonDeletedMsgIdx(0, true);
				// If a non-deleted message was found, then set the last read
				// pointer to it.
				if (nonDeletedMsgIdx > -1)
				{
					var newLastRead = this.IdxToAbsMsgNum(nonDeletedMsgIdx);
					if (newLastRead > -1)
						msg_area.sub[this.subBoardCode].last_read = newLastRead;
					else
						msg_area.sub[this.subBoardCode].last_read = 0;
				}
				else
					msg_area.sub[this.subBoardCode].last_read = 0;
			}
		}
	}
}

// For the DigDistMsgReader class: Returns the index of the message pointed to
// by the scan pointer in the current sub-board.  If reading personal email or
// if the message base isn't open, this will return 0.  If the scan pointer is
// 0 or if the messagebase is open and the scan pointer is invalid, this will
// return -1.
function DigDistMsgReader_GetScanPtrMsgIdx()
{
	if (this.readingPersonalEmail)
		return 0;
	if (msg_area.sub[this.subBoardCode].scan_ptr == 0)
		return -1;

	// If the user's scan pointer is a crazy value, that could be because
	// the user hasn't read messages in the sub-board yet.  In that case,
	// just use 0.  Otherwise, get the user's scan pointer message index.
	var msgIdx = 0;
	// If pMsgNum is 4294967295 (0xffffffff, or ~0), that is a special value
	// for the user's scan_ptr meaning it should point to the latest message
	// in the messagebase.
	if (msg_area.sub[this.subBoardCode].scan_ptr != 0xffffffff)
		msgIdx = this.GetMsgIdx(msg_area.sub[this.subBoardCode].scan_ptr);
	var msgbase = new MsgBase(this.subBoardCode);
	if (msgbase.open())
		if ((msgIdx < 0) || (msgIdx >= msgbase.total_msgs) || (msg_area.sub[this.subBoardCode].scan_ptr == 0xffffffff))
			msgIdx = -1;
			// Look for the first message not marked as deleted
			var nonDeletedMsgIdx = this.FindNextNonDeletedMsgIdx(0, true);
			// If a non-deleted message was found, then set the scan pointer to it.
			if (nonDeletedMsgIdx > -1)
			{
				var newLastRead = this.IdxToAbsMsgNum(nonDeletedMsgIdx);
				if (newLastRead > -1)
					msg_area.sub[this.subBoardCode].scan_ptr = newLastRead;
				else
					msg_area.sub[this.subBoardCode].scan_ptr = 0;
			}
			else
				msg_area.sub[this.subBoardCode].scan_ptr = 0;
		}
	}
	return msgIdx;
}

// For the DigDistMsgReader class: Returns whether there is a search specified
// (according to this.searchType) and the search result objects are defined for
// the current sub-board (as specified by this.subBoardCode).
//
// Return value: Boolean - Whether or not there is a search specified and the
//               search result objects are defined for the current sub-board
//               (as specified by this.subBoardCode).
function DigDistMsgReader_SearchingAndResultObjsDefinedForCurSub()
{
	return (this.SearchTypePopulatesSearchResults() && (this.msgSearchHdrs != null) &&
	         this.msgSearchHdrs.hasOwnProperty(this.subBoardCode) &&
	         (typeof(this.msgSearchHdrs[this.subBoardCode]) == "object") &&
	         (typeof(this.msgSearchHdrs[this.subBoardCode].indexed) != "undefined"));
}

// For the DigDistMsgReader class: Removes a message header from the search
// results array for the current sub-board.
//
// Parameters:
//  pMsgIdx: The index of the message header to remove (in the indexed messages,
//           not necessarily the actual message offset in the messagebase)
function DigDistMsgReader_RemoveFromSearchResults(pMsgIdx)
{
	if (typeof(pMsgIdx) != "number")
		return;

	if ((typeof(this.msgSearchHdrs) == "object") &&
	    this.msgSearchHdrs.hasOwnProperty(this.subBoardCode) &&
	    (typeof(this.msgSearchHdrs[this.subBoardCode].indexed) != "undefined"))
	{
		if ((pMsgIdx >= 0) && (pMsgIdx < this.msgSearchHdrs[this.subBoardCode].indexed.length))
			this.msgSearchHdrs[this.subBoardCode].indexed.splice(pMsgIdx, 1);
	}
}

// For the DigDistMsgReader class: Looks for the next message in the thread of
// a given message (by its header).
//
// Paramters:
//  pMsgHdr: A message header object - The next message in the thread will be
//           searched starting from this message
//  pThreadType: The type of threading to use.  Can be THREAD_BY_ID, THREAD_BY_TITLE,
//               THREAD_BY_AUTHOR, or THREAD_BY_TO_USER.
//  pPositionCursorForStatus: Optional boolean - Whether or not to move the cursor
//                            to the bottom row before outputting status messages.
//                            Defaults to false.
//
// Return value: The offset (index) of the next message thread, or -1 if none
//               was found.
function DigDistMsgReader_FindThreadNextOffset(pMsgHdr, pThreadType, pPositionCursorForStatus)
{
	if ((pMsgHdr == null) || (typeof(pMsgHdr) != "object"))
		return -1;

	var newMsgOffset = -1;

			// The thread_id field was introduced in Synchronet 3.16.  So, if
			// the Synchronet version is 3.16 or higher and the message header
			// has a thread_id field, then look for the next message with the
			// same thread ID.  If the Synchronet version is below 3.16 or there
			// is no thread ID, then fall back to using the header's thread_next,
			// if it exists.
			if ((system.version_num >= 31600) && (typeof(pMsgHdr.thread_id) == "number"))
			{
				// Look for the next message with the same thread ID.
				// Write "Searching.."  in case searching takes a while.
				if (pPositionCursorForStatus)
				{
					console.gotoxy(1, console.screen_rows);
					console.cleartoeol();
					console.gotoxy(this.msgAreaLeft, console.screen_rows);
				}
				console.print("\x01h\x01ySearching\x01i...\x01n");
				// Look for the next message in the thread
				var nextMsgOffset = -1;
				var numOfMessages = this.NumMessages();
				if (pMsgHdr.offset < numOfMessages - 1)
				{
					var nextMsgHdr;
					for (var messageIdx = pMsgHdr.offset+1; (messageIdx < numOfMessages) && (nextMsgOffset == -1); ++messageIdx)
					{
						nextMsgHdr = this.GetMsgHdrByIdx(messageIdx);
						if (((nextMsgHdr.attr & MSG_DELETE) == 0) && (typeof(nextMsgHdr.thread_id) == "number") && (nextMsgHdr.thread_id == pMsgHdr.thread_id))
							nextMsgOffset = nextMsgHdr.offset;
					}
				}
				*/
				if (this.GetMsgIdx(pMsgHdr.number) < numOfMessages - 1)
				{
					var nextMsgHdr;
					for (var messageIdx = this.GetMsgIdx(pMsgHdr.number)+1; (messageIdx < numOfMessages) && (nextMsgOffset == -1); ++messageIdx)
					{
						nextMsgHdr = this.GetMsgHdrByIdx(messageIdx);
						if (((nextMsgHdr.attr & MSG_DELETE) == 0) && (typeof(nextMsgHdr.thread_id) == "number") && (nextMsgHdr.thread_id == pMsgHdr.thread_id))
						{
							//nextMsgOffset = nextMsgHdr.offset;
							nextMsgOffset = this.GetMsgIdx(nextMsgHdr.number);
						}
					}
				}
				if (nextMsgOffset > -1)
					newMsgOffset = nextMsgOffset;
			}
			// Fall back to thread_next if the Synchronet version is below 3.16 or there is
			// no thread_id field in the header
			else if ((typeof(pMsgHdr.thread_next) == "number") && (pMsgHdr.thread_next > 0))
			{
				//newMsgOffset = this.AbsMsgNumToIdx(pMsgHdr.thread_next);
				newMsgOffset = this.GetMsgIdx(pMsgHdr.thread_next);
			}
			break;
		case THREAD_BY_TITLE:
		case THREAD_BY_AUTHOR:
		case THREAD_BY_TO_USER:
			// Title (subject) searching will look for the subject anywhere in the
			// other messages' subjects (not a fully exact subject match), so if
			// the message subject is blank, we won't want to do the search.
			var doSearch = true;
			if ((pThreadType == THREAD_BY_TITLE) && (pMsgHdr.subject.length == 0))
				doSearch = false;
			if (doSearch)
				var subjUppercase = "";
				var fromNameUppercase = "";
				var toNameUppercase = "";

				// Set up a message header matching function, depending on
				// which field of the header we want to match
				var msgHdrMatch;
				if (pThreadType == THREAD_BY_TITLE)
				{
					subjUppercase = pMsgHdr.subject.toUpperCase();
					// Remove any leading instances of "RE:" from the subject
					while (/^RE:/.test(subjUppercase))
						subjUppercase = subjUppercase.substr(3);
					while (/^RE: /.test(subjUppercase))
						subjUppercase = subjUppercase.substr(4);
					// Remove any leading & trailing whitespace from the subject
					subjUppercase = trimSpaces(subjUppercase, true, true, true);
					msgHdrMatch = function(pMsgHdr) {
						return (((pMsgHdr.attr & MSG_DELETE) == 0) && (pMsgHdr.subject.toUpperCase().indexOf(subjUppercase, 0) > -1));
					};
				}
				else if (pThreadType == THREAD_BY_AUTHOR)
				{
					fromNameUppercase = pMsgHdr.from.toUpperCase();
					msgHdrMatch = function(pMsgHdr) {
						return (((pMsgHdr.attr & MSG_DELETE) == 0) && (pMsgHdr.from.toUpperCase() == fromNameUppercase));
					};
				}
				else if (pThreadType == THREAD_BY_TO_USER)
				{
					toNameUppercase = pMsgHdr.to.toUpperCase();
					msgHdrMatch = function(pMsgHdr) {
						return (((pMsgHdr.attr & MSG_DELETE) == 0) && (pMsgHdr.to.toUpperCase() == toNameUppercase));
					};
				}

				// Perform the search
				// Write "Searching.."  in case searching takes a while.
				if (pPositionCursorForStatus)
				{
					console.gotoxy(1, console.screen_rows);
					console.cleartoeol();
					console.gotoxy(this.msgAreaLeft, console.screen_rows);
				}
				console.print("\x01h\x01ySearching\x01i...\x01n");
				// Look for the next message that contains the given message's subject
				var nextMsgOffset = -1;
				var numOfMessages = this.NumMessages();
					var nextMsgHdr;
					for (var messageIdx = pMsgHdr.offset+1; (messageIdx < numOfMessages) && (nextMsgOffset == -1); ++messageIdx)
					{
						nextMsgHdr = this.GetMsgHdrByIdx(messageIdx);
						if (msgHdrMatch(nextMsgHdr))
							nextMsgOffset = nextMsgHdr.offset;
					}
				*/
				if (this.GetMsgIdx(pMsgHdr.number) < numOfMessages - 1)
				{
					var nextMsgHdr;
					for (var messageIdx = this.GetMsgIdx(pMsgHdr.number)+1; (messageIdx < numOfMessages) && (nextMsgOffset == -1); ++messageIdx)
					{
						nextMsgHdr = this.GetMsgHdrByIdx(messageIdx);
						if (msgHdrMatch(nextMsgHdr))
						{
							//nextMsgOffset = nextMsgHdr.offset;
							nextMsgOffset = this.GetMsgIdx(nextMsgHdr.number);
						}
					}
				}
				if (nextMsgOffset > -1)
					newMsgOffset = nextMsgOffset;
	// If no messages were found, then output a message to say so with a momentary pause.
	if (newMsgOffset == -1)
	{
		if (pPositionCursorForStatus)
		{
			console.gotoxy(1, console.screen_rows);
			console.cleartoeol();
			console.gotoxy(this.msgAreaLeft, console.screen_rows);
		}
		else
			console.crlf();
		console.print("\x01n\x01h\x01yNo messages found.\x01n");
	return newMsgOffset;
}

// For the DigDistMsgReader class: Looks for the previous message in the thread of
// a given message (by its header).
//
// Paramters:
//  pMsgHdr: A message header object - The previous message in the thread will be
//           searched starting from this message
//  pThreadType: The type of threading to use.  Can be THREAD_BY_ID, THREAD_BY_TITLE,
//               THREAD_BY_AUTHOR, or THREAD_BY_TO_USER.
//  pPositionCursorForStatus: Optional boolean - Whether or not to move the cursor
//                            to the bottom row before outputting status messages.
//                            Defaults to false.
//
// Return value: The offset (index) of the previous message thread, or -1 if
//               none was found.
function DigDistMsgReader_FindThreadPrevOffset(pMsgHdr, pThreadType, pPositionCursorForStatus)
{
	if ((pMsgHdr == null) || (typeof(pMsgHdr) != "object"))
		return -1;

	var newMsgOffset = -1;

			// The thread_id field was introduced in Synchronet 3.16.  So, if
			// the Synchronet version is 3.16 or higher and the message header
			// has a thread_id field, then look for the previous message with the
			// same thread ID.  If the Synchronet version is below 3.16 or there
			// is no thread ID, then fall back to using the header's thread_next,
			// if it exists.
			if ((system.version_num >= 31600) && (typeof(pMsgHdr.thread_id) == "number"))
			{
				// Look for the previous message with the same thread ID.
				// Write "Searching.." in case searching takes a while.
				if (pPositionCursorForStatus)
				{
					console.gotoxy(1, console.screen_rows);
					console.cleartoeol();
					console.gotoxy(this.msgAreaLeft, console.screen_rows);
				}
				console.print("\x01h\x01ySearching\x01i...\x01n");
				// Look for the previous message in the thread
				var nextMsgOffset = -1;
				if (pMsgHdr.offset > 0)
				{
					var prevMsgHdr;
					for (var messageIdx = pMsgHdr.offset-1; (messageIdx >= 0) && (nextMsgOffset == -1); --messageIdx)
					{
						prevMsgHdr = this.GetMsgHdrByIdx(messageIdx);
						if (((prevMsgHdr.attr & MSG_DELETE) == 0) && (typeof(prevMsgHdr.thread_id) == "number") && (prevMsgHdr.thread_id == pMsgHdr.thread_id))
							nextMsgOffset = prevMsgHdr.offset;
					}
				}
				*/
				if (this.GetMsgIdx(pMsgHdr.number) > 0)
				{
					var prevMsgHdr;
					for (var messageIdx = this.GetMsgIdx(pMsgHdr.number)-1; (messageIdx >= 0) && (nextMsgOffset == -1); --messageIdx)
					{
						prevMsgHdr = this.GetMsgHdrByIdx(messageIdx);
						if (((prevMsgHdr.attr & MSG_DELETE) == 0) && (typeof(prevMsgHdr.thread_id) == "number") && (prevMsgHdr.thread_id == pMsgHdr.thread_id))
						{
							//nextMsgOffset = prevMsgHdr.offset;
							nextMsgOffset = this.GetMsgIdx(prevMsgHdr.number);
				if (nextMsgOffset > -1)
					newMsgOffset = nextMsgOffset;
			}
			// Fall back to thread_next if the Synchronet version is below 3.16 or there is
			// no thread_id field in the header
			else if ((typeof(pMsgHdr.thread_back) == "number") && (pMsgHdr.thread_back > 0))
			{
				//newMsgOffset = this.AbsMsgNumToIdx(pMsgHdr.thread_back);
				newMsgOffset = this.GetMsgIdx(pMsgHdr.thread_back);
				if (newMsgOffset < 0)
					newMsgOffset = 0;
			}
			// If thread_back is valid for the message header, then use that.
			if ((typeof(pMsgHdr.thread_back) == "number") && (pMsgHdr.thread_back > 0))
			{
				//newMsgOffset = this.AbsMsgNumToIdx(pMsgHdr.thread_back);
				newMsgOffset = this.GetMsgIdx(pMsgHdr.thread_back);
			}
				// If thread_id is defined and the index of the first message
				// in the thread is before the current message, then search
				// backwards for messages with a matching thread_id.
				//var firstThreadMsgIdx = this.AbsMsgNumToIdx(pMsgHdr.thread_first);
				var firstThreadMsgIdx = this.GetMsgIdx(pMsgHdr.thread_first);
				if ((typeof(pMsgHdr.thread_id) == "number") && (firstThreadMsgIdx < pMsgHdr.offset))
				{
					// Note (2014-10-11): Digital Man said thread_id was
					// introduced in Synchronet version 3.16 and was still
					// a work in progress and isn't 100% accurate for
					// networked sub-boards.

					// Look for the previous message with the same thread ID.
					// Note: I'm not sure when thread_id was introduced in
					// Synchronet, so I'm not sure of the minimum version where
					// this will work.
					// Write "Searching.." in case searching takes a while.
					if (pPositionCursorForStatus)
					{
						console.gotoxy(1, console.screen_rows);
						console.cleartoeol();
						console.gotoxy(this.msgAreaLeft, console.screen_rows);
					}
					console.print("\x01h\x01ySearching\x01i...\x01n");
					// Look for the previous message in the thread
					var nextMsgOffset = -1;
					if (pMsgHdr.offset > 0)
					{
						for (var messageIdx = pMsgHdr.offset-1; (messageIdx >= 0) && (nextMsgOffset == -1); --messageIdx)
						{
							var prevMsgHdr = this.GetMsgHdrByIdx(messageIdx);
							if (((prevMsgHdr.attr & MSG_DELETE) == 0) && (typeof(prevMsgHdr.thread_id) == "number") && (prevMsgHdr.thread_id == pMsgHdr.thread_id))
								nextMsgOffset = prevMsgHdr.offset;
						}
					}
					if (nextMsgOffset > -1)
						newMsgOffset = nextMsgOffset;
				}
			break;
		case THREAD_BY_TITLE:
		case THREAD_BY_AUTHOR:
		case THREAD_BY_TO_USER:
			// Title (subject) searching will look for the subject anywhere in the
			// other messages' subjects (not a fully exact subject match), so if
			// the message subject is blank, we won't want to do the search.
			var doSearch = true;
			if ((pThreadType == THREAD_BY_TITLE) && (pMsgHdr.subject.length == 0))
				doSearch = false;
			if (doSearch)
				var subjUppercase = "";
				var fromNameUppercase = "";
				var toNameUppercase = "";

				// Set up a message header matching function, depending on
				// which field of the header we want to match
				var msgHdrMatch;
				if (pThreadType == THREAD_BY_TITLE)
				{
					subjUppercase = pMsgHdr.subject.toUpperCase();
					// Remove any leading instances of "RE:" from the subject
					while (/^RE:/.test(subjUppercase))
						subjUppercase = subjUppercase.substr(3);
					while (/^RE: /.test(subjUppercase))
						subjUppercase = subjUppercase.substr(4);
					// Remove any leading & trailing whitespace from the subject
					subjUppercase = trimSpaces(subjUppercase, true, true, true);
					msgHdrMatch = function(pMsgHdr) {
						return (((pMsgHdr.attr & MSG_DELETE) == 0) && (pMsgHdr.subject.toUpperCase().indexOf(subjUppercase, 0) > -1));
					};
				}
				else if (pThreadType == THREAD_BY_AUTHOR)
				{
					fromNameUppercase = pMsgHdr.from.toUpperCase();
					msgHdrMatch = function(pMsgHdr) {
						return (((pMsgHdr.attr & MSG_DELETE) == 0) && (pMsgHdr.from.toUpperCase() == fromNameUppercase));
					};
				}
				else if (pThreadType == THREAD_BY_TO_USER)
				{
					toNameUppercase = pMsgHdr.to.toUpperCase();
					msgHdrMatch = function(pMsgHdr) {
						return (((pMsgHdr.attr & MSG_DELETE) == 0) && (pMsgHdr.to.toUpperCase() == toNameUppercase));
					};
				}

				// Perform the search
				// Write "Searching.."  in case searching takes a while.
				if (pPositionCursorForStatus)
				{
					console.gotoxy(1, console.screen_rows);
					console.cleartoeol();
					console.gotoxy(this.msgAreaLeft, console.screen_rows);
				}
				console.print("\x01h\x01ySearching\x01i...\x01n");
				// Look for the next message that contains the given message's subject
				var nextMsgOffset = -1;
					var nextMsgHdr;
					for (var messageIdx = pMsgHdr.offset-1; (messageIdx >= 0) && (nextMsgOffset == -1); --messageIdx)
					{
						nextMsgHdr = this.GetMsgHdrByIdx(messageIdx);
						if (msgHdrMatch(nextMsgHdr))
							nextMsgOffset = nextMsgHdr.offset;
					}
				*/
				if (this.GetMsgIdx(pMsgHdr.number) > 0)
				{
					var nextMsgHdr;
					for (var messageIdx = this.GetMsgIdx(pMsgHdr.number)-1; (messageIdx >= 0) && (nextMsgOffset == -1); --messageIdx)
					{
						nextMsgHdr = this.GetMsgHdrByIdx(messageIdx);
						if (msgHdrMatch(nextMsgHdr))
						{
							//nextMsgOffset = nextMsgHdr.offset;
							nextMsgOffset = this.GetMsgIdx(nextMsgHdr.number);
						}
					}
				}
				if (nextMsgOffset > -1)
					newMsgOffset = nextMsgOffset;
	// If no messages were found, then output a message to say so with a momentary pause.
	if (newMsgOffset == -1)
	{
		if (pPositionCursorForStatus)
		{
			console.gotoxy(1, console.screen_rows);
			console.cleartoeol();
			console.gotoxy(this.msgAreaLeft, console.screen_rows);
		}
		else
			console.crlf();
		console.print("\x01n\x01h\x01yNo messages found.\x01n");
	return newMsgOffset;
}

// For the DigDistMsgReader class: Calculates the top message index for a page,
// for the traditional-style message list.
//
// Parameters:
//  pPageNum: A page number (1-based)
function DigDistMsgReader_CalcTraditionalMsgListTopIdx(pPageNum)
{
      this.tradListTopMsgIdx = this.NumMessages() - (this.tradMsgListNumLines * (pPageNum-1)) - 1;
   else
      this.tradListTopMsgIdx = (this.tradMsgListNumLines * (pPageNum-1));
}

// For the DigDistMsgReader class: Calculates the top message index for a page,
// for the lightbar message list.
//
// Parameters:
//  pPageNum: A page number (1-based)
function DigDistMsgReader_CalcLightbarMsgListTopIdx(pPageNum)
{
	if (this.reverseListOrder)
		this.lightbarListTopMsgIdx = this.NumMessages() - (this.lightbarMsgListNumLines * (pPageNum-1)) - 1;
	else
	{
		//this.lightbarListTopMsgIdx = (this.lightbarMsgListNumLines * (pPageNum-1));
		var pageIdx = pPageNum - 1;
		if (pageIdx < 0)
			pageIdx = 0;
		this.lightbarListTopMsgIdx = this.lightbarMsgListNumLines * pageIdx;
	}
}

// For the DigDistMsgReader class: Given a message number (1-based), this calculates
// the screen index veriables (stored in the object) for the message list.  This is
// used for the enhanced reader mode when we want the message list to be in the
// correct place for the message being read.
//
// Parameters:
//  pMsgNum: The message number (1-based)
function DigDistMsgReader_CalcMsgListScreenIdxVarsFromMsgNum(pMsgNum)
{
	// Calculate the message list variables
	var numItemsPerPage = this.tradMsgListNumLines;
	if (this.msgListUseLightbarListInterface && canDoHighASCIIAndANSI())
		numItemsPerPage = this.lightbarMsgListNumLines;
	var newPageNum = findPageNumOfItemNum(pMsgNum, numItemsPerPage, this.NumMessages(), this.reverseListOrder);
	this.CalcTraditionalMsgListTopIdx(newPageNum);
	this.CalcLightbarMsgListTopIdx(newPageNum);
	this.lightbarListSelectedMsgIdx = pMsgNum - 1;
	if (this.lightbarListCurPos == null)
		this.lightbarListCurPos = {};
	this.lightbarListCurPos.x = 1;
	this.lightbarListCurPos.y = this.lightbarMsgListStartScreenRow + ((pMsgNum-1) - this.lightbarListTopMsgIdx);
}

// For the DigDistMsgReader class: Validates a user's choice in message area.
// Returns a status/error message for the caller to display if there's an
// error.  This function outputs intermediate status messages (i.e.,
// "Searching..").
//
// Parameters:
//  pGrpIdx: The message group index (i.e., bbs.curgrp)
//  pSubIdx: The message sub-board index (i.e., bbs.cursub)
//  pCurPos: Optional - An object containing x and y properties representing
//           the cursor position.  Used for outputting intermediate status
//           messages, but not for outputting the error message.
//
// Return value: An object containing the following properties:
//               msgAreaGood: A boolean to indicate whether the message area
//                            can be selected
//               errorMsg: If the message area can't be selected, this string
//                         will contain an eror message.  Otherwise, this will
//                         be an empty string.
function DigDistMsgReader_ValidateMsgAreaChoice(pGrpIdx, pSubIdx, pCurPos)
{

	// Get the internal code of the sub-board from the given group & sub-board
	// indexes
	var subCode = msg_area.grp_list[pGrpIdx].sub_list[pSubIdx].code;

	// If a search is specified that would populate the search results, then do
	// a search in the given sub-board.
	if (this.SearchTypePopulatesSearchResults())
	{
		// See if we can use pCurPos to move the cursor before displaying messages
		var useCurPos = (console.term_supports(USER_ANSI) && (typeof(pCurPos) == "object") &&
		                 (typeof(pCurPos.x) == "number") && (typeof(pCurPos.y) == "number"));

		// TODO: In case new messages were posted in this sub-board, it might help
		// to check the current number of messages vs. the previous number of messages
		// and search the new messages if there are more.

		// Determine whether or not to search - If there are no search results for
		// the given sub-board already, then do a search in the sub-board.
		var doSearch = true;
		if (this.msgSearchHdrs.hasOwnProperty(subCode) &&
		    (typeof(this.msgSearchHdrs[subCode]) == "object") &&
		    (typeof(this.msgSearchHdrs[subCode].indexed) != "undefined"))
		{
			doSearch = (this.msgSearchHdrs[subCode].indexed.length == 0);
		}
		if (doSearch)
		{
			if (useCurPos)
			{
				console.gotoxy(pCurPos);
				console.cleartoeol("\x01n");
			console.print("\x01n\x01h\x01wSearching\x01i...\x01n");
			this.msgSearchHdrs[subCode] = searchMsgbase(subCode, this.searchType, this.searchString, this.readingPersonalEmailFromUser);
			// If there are no messages, then set the return object variables to indicate so.
			if (this.msgSearchHdrs[subCode].indexed.length == 0)
				retObj.errorMsg = "No search results found";
			}
		}
	}
	else
	{
		// No search is specified.  Just check to see if there are any messages
		// to read in the given sub-board.
		var msgBase = new MsgBase(subCode);
		if (msgBase.open())
		{
			if (msgBase.total_msgs == 0)
			{
				retObj.msgAreaGood = false;
				retObj.errorMsg = "No messages in that message area";
			}
			msgBase.close();
		}
	}

	return retObj;
}

// For the DigDistMsgReader class: Validates a message if the sub-board
// requires message validation.
//
// Parameters:
//  pSubBoardCode: The internal code of the sub-board
//  pMsgNum: The message number
//
// Return value: Boolean - Whether or not validating the message was successful
function DigDistMsgReader_ValidateMsg(pSubBoardCode, pMsgNum)
{
	if (!msg_area.sub[pSubBoardCode].is_moderated)
		return true;

	var validationSuccessful = false;
	var msgbase = new MsgBase(this.subBoardCode);
	if (msgbase.open())
		var msgHdr = msgbase.get_msg_header(false, pMsgNum, false);
		if (msgHdr != null)
			if ((msgHdr.attr & MSG_VALIDATED) == 0)
			{
				msgHdr.attr |= MSG_VALIDATED;
				validationSuccessful = msgbase.put_msg_header(false, msgHdr.number, msgHdr);
			}
			else
				validationSuccessful = true;
// For the DigDistMsgReader class: Gets the current sub-board's group name and description.
//
// Return value: An object with the following properties:
//               grpName: The group name
//               grpDesc: The group description
function DigDistMsgReader_GetGroupNameAndDesc()
{
	var retObj = {
		grpName: "",
		grpDesc: ""
	}
	var msgbase = new MsgBase(this.subBoardCode);
	if (msgbase.open())
	{
		retObj.grpName = msgbase.cfg.grp_name;
		retObj.grpDesc = msgbase.cfg.description;
		msgbase.close();
	}
	return retObj;
}

// For the DigDistMsgReader class: Writes message lines to a file on the BBS
// machine.
//
// Parameters:
//  pMsgHdr: The header object for the message
//  pFilename: The name of the file to write the message to
//
// Return value: An object containing the following properties:
//               succeeded: Boolean - Whether or not the file was successfully written
//               errorMsg: String - On failure, will contain the reason it failed
function DigDistMsgReader_SaveMsgToFile(pMsgHdr, pFilename)
	if (typeof(pMsgHdr) !== "object")
		return({ succeeded: false, errorMsg: "Header object not given"});
	if (typeof(pFilename) != "string")
		return({ succeeded: false, errorMsg: "Filename parameter not a string"});
	if (pFilename.length == 0)
		return({ succeeded: false, errorMsg: "Empty filename given"});

	// Get the message text and save it
	// Note: GetMsgInfoForEnhancedReader() can expand @-codes in the message,
	// but for now we're saving the message basically as-is.
	//var msgInfo = this.GetMsgInfoForEnhancedReader(pMsgHdr, false, false, false);
	var msgbase = new MsgBase(this.subBoardCode);
	if (msgbase.open())
		var msgBody = msgbase.get_msg_body(false, pMsgHdr.number, false, false, true, true);
		var messageSaveFile = new File(pFilename);
		if (messageSaveFile.open("w"))
			// Write some header information to the file
			if (pMsgHdr.hasOwnProperty("from"))
				messageSaveFile.writeln("From: " + pMsgHdr.from);
			if (pMsgHdr.hasOwnProperty("to"))
				messageSaveFile.writeln("  To: " + pMsgHdr.to);
			if (pMsgHdr.hasOwnProperty("subject"))
				messageSaveFile.writeln("Subj: " + pMsgHdr.subject);
			/*
			if (pMsgHdr.hasOwnProperty("when_written_time"))
				messageSaveFile.writeln(strftime("Date: %Y-%m-%d %H:%M:%S", msgHeader.when_written_time));
			*/
			if (pMsgHdr.hasOwnProperty("date"))
				messageSaveFile.writeln("Date: " + pMsgHdr.date);
			if (pMsgHdr.hasOwnProperty("from_net_addr"))
				messageSaveFile.writeln("From net address: " + pMsgHdr.from_net_addr);
			if (pMsgHdr.hasOwnProperty("to_net_addr"))
				messageSaveFile.writeln("To net address: " + pMsgHdr.to_net_addr);
			if (pMsgHdr.hasOwnProperty("id"))
				messageSaveFile.writeln("ID: " + pMsgHdr.id);
			if (pMsgHdr.hasOwnProperty("reply_id"))
				messageSaveFile.writeln("Reply ID: " + pMsgHdr.reply_id);
			messageSaveFile.writeln("===============================");

			// If the message body has ANSI, then use the Graphic object to strip it
			// of any cursor movement codes
			var msgHasANSICodes = msgBody.indexOf("\x1b[") >= 0;
			if (msgHasANSICodes)
			{
				if (textHasDrawingChars(msgBody))
				{
					var graphic = new Graphic(this.msgAreaWidth, this.msgAreaHeight);
					graphic.auto_extend = true;
					graphic.ANSI = ansiterm.expand_ctrl_a(msgBody);
					msgBody = syncAttrCodesToANSI(graphic.MSG);
				}
				else
					msgBody = syncAttrCodesToANSI(msgBody);
			}

			// Write the message body to the file
			messageSaveFile.write(msgBody);
			messageSaveFile.close();