Skip to content
Snippets Groups Projects
DDMsgReader.js 718 KiB
Newer Older
					returnValue = 1;
				return returnValue;
			});
		}
		else if (pSortType == "dateDesc")
		{
			subBoardArray.sort(function(pA, pB)
			{
				// Return -1, 0, or 1, depending on whether pA's date comes
				// after, is equal to, or comes before pB's date.
				var returnValue = 0;
				if (pA.newestPostDate > pB.newestPostDate)
					returnValue = -1;
				else if (pA.newestPostDate < pB.newestPostDate)
					returnValue = 1;
				return returnValue;
			});
		}
		else if (pSortType == "description")
		{
			// Binary safe string comparison  
			// 
			// version: 909.322
			// discuss at: http://phpjs.org/functions/strcmp    // +   original by: Waldo Malqui Silva
			// +      input by: Steve Hilder
			// +   improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
			// +    revised by: gorthaur
			// *     example 1: strcmp( 'waldo', 'owald' );    // *     returns 1: 1
			// *     example 2: strcmp( 'owald', 'waldo' );
			// *     returns 2: -1
			subBoardArray.sort(function(pA, pB)
			{
				return ((pA.description == pB.description) ? 0 : ((pA.description > pB.description) ? 1 : -1));
			});
		}
		// Display the sub-board list.
		for (var i = 0; i < subBoardArray.length; ++i)
		{
			console.crlf();
			console.print((subBoardArray[i].subBoardNum == highlightIndex) ? "\1n" +
			              this.colors.areaChooserMsgAreaMarkColor + "*" : " ");
			printf(this.subBoardListPrintfInfo[grpIndex].printfStr, +(subBoardArray[i].subBoardNum+1),
			       subBoardArray[i].description.substr(0, this.subBoardNameLen),
			       subBoardArray[i].numPosts, strftime("%Y-%m-%d", subBoardArray[i].newestPostDate),
			       strftime("%H:%M:%S", subBoardArray[i].newestPostDate));
		}
	}
	// If no sort type is specified, then output the sub-board information in
	// order of sub-board number.
	else
	{
		for (var arrSubBoardNum in msg_area.grp_list[grpIndex].sub_list)
		{
			// Open the current sub-board with the msgBase object.
			msgBase = new MsgBase(msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].code);
			if (msgBase.open())
			{
				// Get the date & time when the last message was imported.
				if (msgBase.total_msgs > 0)
				{
					msgHeader = msgBase.get_msg_header(true, msgBase.total_msgs-1, false);
					// Construct the date & time strings of the latest post
					if (this.msgAreaList_lastImportedMsg_showImportTime)
					{
						newestDate.date = strftime("%Y-%m-%d", msgHeader.when_imported_time);
						newestDate.time = strftime("%H:%M:%S", msgHeader.when_imported_time);
					}
					else
					{
						//newestDate.date = strftime("%Y-%m-%d", msgHeader.when_written_time);
						//newestDate.time = strftime("%H:%M:%S", msgHeader.when_written_time);
						var msgWrittenLocalTime = msgWrittenTimeToLocalBBSTime(msgHeader);
						if (msgWrittenLocalTime != -1)
						{
							newestDate.date = strftime("%Y-%m-%d", msgWrittenLocalTime);
							newestDate.time = strftime("%H:%M:%S", msgWrittenLocalTime);
						}
						else
						{
							newestDate.date = strftime("%Y-%m-%d", msgHeader.when_written_time);
							newestDate.time = strftime("%H:%M:%S", msgHeader.when_written_time);
						}
				// Print the sub-board information
				subBoardNum = +(arrSubBoardNum);
				console.crlf();
				console.print((subBoardNum == highlightIndex) ? "\1n" +
				              this.colors.areaChooserMsgAreaMarkColor + "*" : " ");
				printf(this.subBoardListPrintfInfo[grpIndex].printfStr, +(subBoardNum+1),
				       msg_area.grp_list[grpIndex].sub_list[arrSubBoardNum].description.substr(0, this.subBoardListPrintfInfo[grpIndex].nameLen),
				       msgBase.total_msgs, newestDate.date, newestDate.time);
}

//////////////////////////////////////////////
// Message group list stuff (lightbar mode) //
//////////////////////////////////////////////

// Displays a screenful of message groups, for the lightbar interface.
//
// Parameters:
//  pStartIndex: The message group index to start at (0-based)
//  pStartScreenRow: The row on the screen to start at (1-based)
//  pEndScreenRow: The row on the screen to end at (1-based)
//  pClearScreenFirst: Boolean - Whether or not to clear the screen first
//  pBlankToEndRow: Boolean - Whether or not to write blank lines to the end
//                  screen row if there aren't enough message groups to fill
//                  the screen.
function DigDistMsgReader_listScreenfulOfMsgGrps(pStartIndex, pStartScreenRow,
                                                  pEndScreenRow, pClearScreenFirst,
                                                  pBlankToEndRow)
{
	// Check the parameters; If they're bad, then just return.
	if ((typeof(pStartIndex) != "number") ||
	    (typeof(pStartScreenRow) != "number") ||
	    (typeof(pEndScreenRow) != "number"))
	{
		return;
	}
	if ((pStartIndex < 0) || (pStartIndex >= msg_area.grp_list.length))
		return;
	if ((pStartScreenRow < 1) || (pStartScreenRow > console.screen_rows))
		return;
	if ((pEndScreenRow < 1) || (pEndScreenRow > console.screen_rows))
		return;
	// If pStartScreenRow is greather than pEndScreenRow, then swap them.
	if (pStartScreenRow > pEndScreenRow)
	{
		var temp = pStartScreenRow;
		pStartScreenRow = pEndScreenRow;
		pEndScreenRow = temp;
	}
	// Calculate the ending index to use for the message groups array.
	var endIndex = pStartIndex + (pEndScreenRow-pStartScreenRow);
	if (endIndex >= msg_area.grp_list.length)
		endIndex = msg_area.grp_list.length - 1;
	var onePastEndIndex = endIndex + 1;
	// Clear the screen, go to the specified screen row, and display the message
	// group information.
	if (pClearScreenFirst)
		console.clear("\1n");
	console.gotoxy(1, pStartScreenRow);
	var grpIndex = pStartIndex;
	for (; grpIndex < onePastEndIndex; ++grpIndex)
	{
		this.WriteMsgGroupLine(grpIndex, false);
		if (grpIndex < endIndex)
			console.crlf();
	}
	// If pBlankToEndRow is true and we're not at the end row yet, then
	// write blank lines to the end row.
	if (pBlankToEndRow)
	{
		var screenRow = pStartScreenRow + (endIndex - pStartIndex) + 1;
		if (screenRow <= pEndScreenRow)
		{
			for (; screenRow <= pEndScreenRow; ++screenRow)
			{
				console.gotoxy(1, screenRow);
				console.clearline("\1n");
			}
		}
	}
}

// For the DigDistMsgReader class - Writes a message group information line.
//
// Parameters:
//  pGrpIndex: The index of the message group to write (assumed to be valid)
//  pHighlight: Boolean - Whether or not to write the line highlighted.
function DigDistMsgReader_writeMsgGroupLine(pGrpIndex, pHighlight)
{
	// TODO: If pHighlight is true, that causes the screen to be cleared
	// and the line is written on the first row of the console.
	console.print("\1n");
	// Write the highlight background color if pHighlight is true.
	if (pHighlight)
	console.print(this.colors.areaChooserMsgAreaBkgHighlightColor);
	// Write the message group information line
	console.print(((typeof(bbs.curgrp) == "number") && (pGrpIndex == msg_area.sub[this.subBoardCode].grp_index)) ? this.colors.areaChooserMsgAreaMarkColor + "*" : " ");
	printf((pHighlight ? this.msgGrpListHilightPrintfStr : this.msgGrpListPrintfStr),
	       +(pGrpIndex+1),
	       msg_area.grp_list[pGrpIndex].description.substr(0, this.msgGrpDescLen),
	       msg_area.grp_list[pGrpIndex].sub_list.length);
	console.cleartoeol("\1n");
}

//////////////////////////////////////////////////
// Message sub-board list stuff (lightbar mode) //
//////////////////////////////////////////////////

// Updates the page number text in the group list header line on the screen.
//
// Parameters:
//  pPageNum: The page number
//  pNumPages: The total number of pages
//  pGroup: Boolean - Whether or not this is for the group header.  If so,
//          then this will go to the right location for the group page text
//          and use this.colors.areaChooserMsgAreaHeaderColor for the text.
//          Otherwise, this will go to the right place for the sub-board page
//          text and use the sub-board header color.
//  pRestoreCurPos: Optional - Boolean - If true, then move the cursor back
//                  to the position where it was before this function was called
function DigDistMsgReader_updateMsgAreaPageNumInHeader(pPageNum, pNumPages, pGroup, pRestoreCurPos)
{
	var originalCurPos = null;
	if (pRestoreCurPos)
		originalCurPos = console.getxy();
nightfox's avatar
nightfox committed
		console.gotoxy(29, 1+this.areaChangeHdrLines.length);
		console.print("\1n" + this.colors.areaChooserMsgAreaHeaderColor + pPageNum + " of " +
		              pNumPages + ")   ");
	}
	else
	{
nightfox's avatar
nightfox committed
		console.gotoxy(51, 1+this.areaChangeHdrLines.length);
		console.print("\1n" + this.colors.areaChooserSubBoardHeaderColor + pPageNum + " of " +
		              pNumPages + ")   ");
	}
	if (pRestoreCurPos)
		console.gotoxy(originalCurPos);
}

// Displays a screenful of message sub-boards, for the lightbar interface.
//
// Parameters:
//  pGrpIndex: The index of the message group (0-based)
//  pStartSubIndex: The message sub-board index to start at (0-based)
//  pStartScreenRow: The row on the screen to start at (1-based)
//  pEndScreenRow: The row on the screen to end at (1-based)
//  pClearScreenFirst: Boolean - Whether or not to clear the screen first
//  pBlankToEndRow: Boolean - Whether or not to write blank lines to the end
//                  screen row if there aren't enough message groups to fill
//                  the screen.
function DigDistMsgReader_ListScreenfulOfSubBrds(pGrpIndex, pStartSubIndex,
                                                  pStartScreenRow, pEndScreenRow,
                                                  pClearScreenFirst, pBlankToEndRow)
{
	// Check the parameters; If they're bad, then just return.
	if ((typeof(pGrpIndex) != "number") ||
	    (typeof(pStartSubIndex) != "number") ||
	    (typeof(pStartScreenRow) != "number") ||
	    (typeof(pEndScreenRow) != "number"))
	{
		return;
	}
	if ((pGrpIndex < 0) || (pGrpIndex >= msg_area.grp_list.length))
		return;
	if ((pStartSubIndex < 0) ||
	    (pStartSubIndex >= msg_area.grp_list[pGrpIndex].sub_list.length))
	{
		return;
	}
	if ((pStartScreenRow < 1) || (pStartScreenRow > console.screen_rows))
		return;
	if ((pEndScreenRow < 1) || (pEndScreenRow > console.screen_rows))
		return;
	// If pStartScreenRow is greather than pEndScreenRow, then swap them.
	if (pStartScreenRow > pEndScreenRow)
	{
		var temp = pStartScreenRow;
		pStartScreenRow = pEndScreenRow;
		pEndScreenRow = temp;
	}
	// Calculate the ending index to use for the sub-board array.
	var endIndex = pStartSubIndex + (pEndScreenRow-pStartScreenRow);
	if (endIndex >= msg_area.grp_list[pGrpIndex].sub_list.length)
		endIndex = msg_area.grp_list[pGrpIndex].sub_list.length - 1;
	var onePastEndIndex = endIndex + 1;
	// Clear the screen and go to the specified screen row.
	if (pClearScreenFirst)
		console.clear("\1n");
	console.gotoxy(1, pStartScreenRow);
	var subIndex = pStartSubIndex;
	for (; subIndex < onePastEndIndex; ++subIndex)
	{
		this.WriteMsgSubBoardLine(pGrpIndex, subIndex, false);
		if (subIndex < endIndex)
			console.crlf();
	}
	// If pBlankToEndRow is true and we're not at the end row yet, then
	// write blank lines to the end row.
	if (pBlankToEndRow)
	{
		var screenRow = pStartScreenRow + (endIndex - pStartSubIndex) + 1;
		if (screenRow <= pEndScreenRow)
		{
			for (; screenRow <= pEndScreenRow; ++screenRow)
			{
				console.gotoxy(1, screenRow);
				console.clearline("\1n");
			}
		}
	}
}

// For the DigDistMsgReader class: Writes a message sub-board information line for
// the message area chooser functionality.
//
// Parameters:
//  pGrpIndex: The index of the message group (assumed to be valid)
//  pSubIndex: The index of the sub-board within the message group to write (assumed to be valid)
//  pHighlight: Boolean - Whether or not to write the line highlighted.
function DigDistMsgReader_WriteMsgSubBrdLine(pGrpIndex, pSubIndex, pHighlight)
	console.print("\1n");
	// Write the highlight background color if pHighlight is true.
	if (pHighlight)
		console.print(this.colors.areaChooserMsgAreaBkgHighlightColor);

	// Determine if pGrpIndex and pSubIndex specify the user's
	// currently-selected group and sub-board.
	var currentSub = false;
	if ((typeof(bbs.curgrp) == "number") && (typeof(bbs.cursub) == "number"))
		currentSub = ((pGrpIndex == msg_area.sub[this.subBoardCode].grp_index) && (pSubIndex == msg_area.sub[this.subBoardCode].index));

	// Open the current sub-board with the msgBase object (so that we can get
	// the date & time of the last imporeted message).
	var msgBase = new MsgBase(msg_area.grp_list[pGrpIndex].sub_list[pSubIndex].code);
	if (msgBase.open())
	{
		var newestDate = new Object(); // For storing the date of the newest post
		// Get the date & time when the last message was imported.
		if (msgBase.total_msgs > 0)
		{
			msgHeader = msgBase.get_msg_header(true, msgBase.total_msgs-1, false);
			// Construct the date & time strings of the latest post
			if (this.msgAreaList_lastImportedMsg_showImportTime)
			{
				newestDate.date = strftime("%Y-%m-%d", msgHeader.when_imported_time);
				newestDate.time = strftime("%H:%M:%S", msgHeader.when_imported_time);
			}
			else
			{
				//newestDate.date = strftime("%Y-%m-%d", msgHeader.when_written_time);
				//newestDate.time = strftime("%H:%M:%S", msgHeader.when_written_time);
				var msgWrittenLocalTime = msgWrittenTimeToLocalBBSTime(msgHeader);
				if (msgWrittenLocalTime != -1)
				{
					newestDate.date = strftime("%Y-%m-%d", msgWrittenLocalTime);
					newestDate.time = strftime("%H:%M:%S", msgWrittenLocalTime);
				}
				else
				{
					newestDate.date = strftime("%Y-%m-%d", msgHeader.when_written_time);
					newestDate.time = strftime("%H:%M:%S", msgHeader.when_written_time);
				}
		// Print the sub-board information line.
		console.print(currentSub ? this.colors.areaChooserMsgAreaMarkColor + "*" : " ");
		printf((pHighlight ? this.subBoardListPrintfInfo[pGrpIndex].highlightPrintfStr : this.subBoardListPrintfInfo[pGrpIndex].printfStr),
		       +(pSubIndex+1),
		msg_area.grp_list[pGrpIndex].sub_list[pSubIndex].description.substr(0, this.subBoardListPrintfInfo[pGrpIndex].nameLen),
		                                                                    msgBase.total_msgs, newestDate.date, newestDate.time);
		msgBase.close();

		delete msgBase;
	}
}

///////////////////////////////////////////////
// Other functions for the msg. area chooser //
///////////////////////////////////////////////

// For the DigDistMsgReader class: Shows the help screen
//
// Parameters:
//  pLightbar: Boolean - Whether or not to show lightbar help.  If
//             false, then this function will show regular help.
//  pClearScreen: Boolean - Whether or not to clear the screen first
function DigDistMsgReader_showChooseMsgAreaHelpScreen(pLightbar, pClearScreen)
{
   if (pClearScreen && console.term_supports(USER_ANSI))
      console.clear("\1n");
   else
      console.print("\1n");
   DisplayProgramInfo();
   console.crlf();
   console.print("\1n\1c\1hMessage area (sub-board) chooser");
   console.crlf();
   console.print("\1kÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ\1n");
   console.crlf();
   console.print("\1cFirst, a listing of message groups is displayed.  One can be chosen by typing");
   console.crlf();
   console.print("its number.  Then, a listing of sub-boards within that message group will be");
   console.crlf();
   console.print("shown, and one can be chosen by typing its number.");
   console.crlf();

   if (pLightbar)
   {
      console.crlf();
      console.print("\1n\1cThe lightbar interface also allows up & down navigation through the lists:");
      console.crlf();
      console.print("\1k\1hÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ");
      console.crlf();
      console.print("\1n\1c\1hUp arrow\1n\1c: Move the cursor up one line");
      console.crlf();
      console.print("\1hDown arrow\1n\1c: Move the cursor down one line");
      console.crlf();
      console.print("\1hENTER\1n\1c: Select the current group/sub-board");
      console.crlf();
      console.print("\1hHOME\1n\1c: Go to the first item on the screen");
      console.crlf();
      console.print("\1hEND\1n\1c: Go to the last item on the screen");
      console.crlf();
      console.print("\1hF\1n\1c: Go to the first page");
      console.crlf();
      console.print("\1hL\1n\1c: Go to the last page");
      console.crlf();
   }

   console.crlf();
   console.print("Additional keyboard commands:");
   console.crlf();
   console.print("\1k\1hÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄÄ");
   console.crlf();
   console.print("\1n\1c\1h?\1n\1c: Show this help screen");
   console.crlf();
   console.print("\1hQ\1n\1c: Quit");
   console.crlf();
}

// Builds sub-board printf format information for a message group.
// The widths of the description & # messages columns are calculated
// based on the greatest number of messages in a sub-board for the
// message group.
//
// Parameters:
//  pGrpIndex: The index of the message group
function DigDistMsgReader_BuildSubBoardPrintfInfoForGrp(pGrpIndex)
{
   // If the array of sub-board printf strings doesn't contain the printf
   // strings for this message group, then figure out the largest number
   // of messages in the message group and add the printf strings.
   if (typeof(this.subBoardListPrintfInfo[pGrpIndex]) == "undefined")
   {
      var greatestNumMsgs = getGreatestNumMsgs(pGrpIndex);

      this.subBoardListPrintfInfo[pGrpIndex] = new Object();
      this.subBoardListPrintfInfo[pGrpIndex].numMsgsLen = greatestNumMsgs.toString().length;
      // Sub-board name length: With a # items length of 4, this should be
      // 47 for an 80-column display.
      this.subBoardListPrintfInfo[pGrpIndex].nameLen = console.screen_columns -
                                   this.areaNumLen -
                                   this.subBoardListPrintfInfo[pGrpIndex].numMsgsLen -
                                   this.dateLen - this.timeLen - 7;
      // Create the printf strings
      this.subBoardListPrintfInfo[pGrpIndex].printfStr =
               " " + this.colors.areaChooserMsgAreaNumColor
               + "%" + this.areaNumLen + "d "
               + this.colors.areaChooserMsgAreaDescColor + "%-"
               + this.subBoardListPrintfInfo[pGrpIndex].nameLen + "s "
               + this.colors.areaChooserMsgAreaNumItemsColor + "%"
               + this.subBoardListPrintfInfo[pGrpIndex].numMsgsLen + "d "
               + this.colors.areaChooserMsgAreaLatestDateColor + "%" + this.dateLen + "s "
               + this.colors.areaChooserMsgAreaLatestTimeColor + "%" + this.timeLen + "s";
      this.subBoardListPrintfInfo[pGrpIndex].highlightPrintfStr =
                              "\1n" + this.colors.areaChooserMsgAreaBkgHighlightColor + " "
                              + "\1n" + this.colors.areaChooserMsgAreaBkgHighlightColor
                              + this.colors.areaChooserMsgAreaNumHighlightColor
                              + "%" + this.areaNumLen + "d \1n"
                              + this.colors.areaChooserMsgAreaBkgHighlightColor
                              + this.colors.areaChooserMsgAreaDescHighlightColor + "%-"
                              + this.subBoardListPrintfInfo[pGrpIndex].nameLen + "s \1n"
                              + this.colors.areaChooserMsgAreaBkgHighlightColor
                              + this.colors.areaChooserMsgAreaNumItemsHighlightColor + "%"
                              + this.subBoardListPrintfInfo[pGrpIndex].numMsgsLen + "d \1n"
                              + this.colors.areaChooserMsgAreaBkgHighlightColor
                              + this.colors.areaChooserMsgAreaDateHighlightColor + "%" + this.dateLen + "s \1n"
                              + this.colors.areaChooserMsgAreaBkgHighlightColor
                              + this.colors.areaChooserMsgAreaTimeHighlightColor + "%" + this.timeLen + "s\1n";
   }
}

// Returns an array of strings containing extended message header information,
// such as the kludge lines (for FidoNet-style networks), etc.
// For each kludge line, there will be a label string for the info line, the
// info line itself (wrapped to fit the message area width), then a blank
// line (except for the last kludge line).  The info lines that this method
// retrieves will only be retrieved if they exist in the given message header.
//
// Parameters:
//  pMsgHdr: A message header
//  pKludgeOnly: Boolean - Whether or not to only get the kludge lines.  If false,
//               then all header fields will be retrieved.
//
// Return value: An array of strings containing the extended message header information
function DigDistMsgReader_GetExtdMsgHdrInfo(pMsgHdr, pKludgeOnly)
{
	// If pMsgHdr is not valid, then just return an empty array.
	if (typeof(pMsgHdr) != "object")
		return new Array();

	// Get the message header with fields expanded so we can get the most info possible.
	var msgHdr = this.GetMsgHdrByAbsoluteNum(pMsgHdr.number, true);
	if (msgHdr == null)
		return new Array();
	var msgHdrInfoLines = new Array();
	var kludgeOnly = (typeof(pKludgeOnly) == "boolean" ? pKludgeOnly : false);
	if (kludgeOnly)
	{
		hdrInfoLineFields.push({ field: "ftn_msgid", label: "MSG ID:" });
		hdrInfoLineFields.push({ field: "X-FTN-MSGID", label: "MSG ID:" });
		hdrInfoLineFields.push({ field: "ftn_reply", label: "Reply ID:" });
		hdrInfoLineFields.push({ field: "X-FTN-REPLY", label: "Reply ID:" });
		hdrInfoLineFields.push({ field: "ftn_area", label: "Area tag:" });
		hdrInfoLineFields.push({ field: "X-FTN-AREA", label: "Area tag:" });
		hdrInfoLineFields.push({ field: "ftn_flags", label: "Flags:" });
		hdrInfoLineFields.push({ field: "ftn_pid", label: "Program ID:" });
		hdrInfoLineFields.push({ field: "ftn_tid", label: "Tosser ID:" });
		hdrInfoLineFields.push({ field: "X-FTN-TID", label: "Tosser ID:" });
		hdrInfoLineFields.push({ field: "X-FTN-Kludge", label: "Kludge:" });
		hdrInfoLineFields.push({ field: "X-FTN-SEEN-BY", label: "Seen-By:" });
		hdrInfoLineFields.push({ field: "X-FTN-PATH", label: "Path:" });
		hdrInfoLineFields.push({ field: "when_written_time", label: "When written time:" });
		hdrInfoLineFields.push({ field: "when_imported_time", label: "When imported time:" });
	}
	// This function returns the number of non-blank lines in a header info array.
	//
	// Return value: An object with the following properties:
	//               numNonBlankLines: The number of non-blank lines in the array
	//               firstNonBlankLineIdx: The index of the first non-blank line
	//               lastNonBlankLineIdx: The index of the last non-blank line
	function findHdrFieldDataArrayNonBlankLines(pHdrArray)
	{
		var retObj = new Object();
		retObj.numNonBlankLines = 0;
		retObj.firstNonBlankLineIdx = -1;
		retObj.lastNonBlankLineIdx = -1;

		for (var lineIdx = 0; lineIdx < pHdrArray.length; ++lineIdx)
				++retObj.numNonBlankLines;
				if (retObj.firstNonBlankLineIdx == -1)
					retObj.firstNonBlankLineIdx = lineIdx;
				retObj.lastNonBlankLineIdx = lineIdx;

		return retObj;
	}

	// Counts the number of elements in a header field_list array with the
	// same type, starting at a given index.
	//
	// Parameters:
	//  pFieldList: The field_list array in a message header
	//  pStartIdx: The index of the starting element to start counting at
	//
	// Return value: The number of elements with the same type as the start index element
	function fieldListCountSameTypes(pFieldList, pStartIdx)
	{
		if (typeof(pFieldList) == "undefined")
			return 0;
		if (typeof(pStartIdx) != "number")
			return 0;
		if ((pStartIdx < 0) || (pStartIdx >= pFieldList.length))
			return 0;

		var itemCount = 1;
		for (var idx = pStartIdx+1; idx < pFieldList.length; ++idx)
			if (pFieldList[idx].type == pFieldList[pStartIdx].type)
				++itemCount;
			else
				break;
		}
		return itemCount;
	}

	// Get the header fields
	var addTheField = true;
	var customFieldLabel = "";
	var propCounter = 1;
	{
		addTheField = true;
		customFieldLabel = "";

		if (prop == "field_list")
		{
			var hdrFieldLabel = "";
			var lastHdrFieldLabel = null;
			var addBlankLineAfterIdx = -1;
			for (var fieldI = 0; fieldI < msgHdr.field_list.length; ++fieldI)
			{
				// TODO: Some field types can be in the array multiple times but only
				// the last is valid.  For those, only get the last one:
				//  32 (Reply To)
				//  33 (Reply To agent)
				//  34 (Reply To net type)
				//  35 (Reply To net address)
				//  36 (Reply To extended)
				//  37 (Reply To position)
				//  38 (Reply To Organization)
				hdrFieldLabel = "\1n" + this.colors.hdrLineLabelColor + msgHdrFieldListTypeToLabel(msgHdr.field_list[fieldI].type) + "\1n";
				var infoLineWrapped = msgHdr.field_list[fieldI].data;
				var infoLineWrappedArray = lfexpand(infoLineWrapped).split("\r\n");
				var hdrArrayNonBlankLines = findHdrFieldDataArrayNonBlankLines(infoLineWrappedArray);
				if (hdrArrayNonBlankLines.numNonBlankLines > 0)
				{
					if (hdrArrayNonBlankLines.numNonBlankLines == 1)
					{
						var addExtraBlankLineAtEnd = false;
						var hdrItem = "\1n" + this.colors.hdrLineValueColor + infoLineWrappedArray[hdrArrayNonBlankLines.firstNonBlankLineIdx] + "\1n";
						// If the header field label is different, then add it to the
						// header info lines
						if ((lastHdrFieldLabel == null) || (hdrFieldLabel != lastHdrFieldLabel))
						{
							var numFieldItemsWithSameType = fieldListCountSameTypes(msgHdr.field_list, fieldI);
							if (numFieldItemsWithSameType > 1)
							{
								msgHdrInfoLines.push("");
								msgHdrInfoLines.push(hdrFieldLabel);
								addExtraBlankLineAtEnd = true;
								addBlankLineAfterIdx = fieldI + numFieldItemsWithSameType - 1;
							}
							else
							{
								hdrItem = hdrFieldLabel + " " + hdrItem;
								numFieldItemsWithSameType = -1;
							}
						}
						if (strip_ctrl(hdrItem).length < this.msgAreaWidth)
							msgHdrInfoLines.push(hdrItem);
						else
						{
							// If the header field label is different, then add a blank line
							// to the header info lines
							if ((lastHdrFieldLabel == null) || (hdrFieldLabel != lastHdrFieldLabel))
								msgHdrInfoLines.push("");
							msgHdrInfoLines.push(hdrFieldLabel);
							msgHdrInfoLines.push(infoLineWrappedArray[hdrArrayNonBlankLines.firstNonBlankLineIdx]);
							if ((lastHdrFieldLabel == null) || (hdrFieldLabel != lastHdrFieldLabel))
								msgHdrInfoLines.push("");
						}
					}
					else
					{
						// If the header field label is different, then add it to the
						// header info lines
						if ((lastHdrFieldLabel == null) || (hdrFieldLabel != lastHdrFieldLabel))
						{
							msgHdrInfoLines.push("");
							msgHdrInfoLines.push(hdrFieldLabel + "!");
						}
						var infoLineWrapped = msgHdr.field_list[fieldI].data;
						var infoLineWrappedArray = lfexpand(infoLineWrapped).split("\r\n");
						for (var lineIdx = 0; lineIdx < infoLineWrappedArray.length; ++lineIdx)
						{
							if (infoLineWrappedArray[lineIdx].length > 0)
								msgHdrInfoLines.push(infoLineWrappedArray[lineIdx]);
						}
						// If the header field label is different, then add a blank line to the
						// header info lines
						if ((lastHdrFieldLabel == null) || (hdrFieldLabel != lastHdrFieldLabel))
							msgHdrInfoLines.push("");
					}
					if (addBlankLineAfterIdx == fieldI)
						msgHdrInfoLines.push("");
				}
			// See if we should add this field
			addTheField = true;
			if (hdrInfoLineFields.length > 0)
				addTheField = false;
				for (var infoLineFieldIdx = 0; infoLineFieldIdx < hdrInfoLineFields.length; ++infoLineFieldIdx)
					if (prop == hdrInfoLineFields[infoLineFieldIdx].field)
						addTheField = true;
						customFieldLabel = hdrInfoLineFields[infoLineFieldIdx].label;
						break;
			if (!addTheField)
				continue;

			var propLabel = "";
			if (customFieldLabel.length > 0)
				propLabel = "\1n" + this.colors.hdrLineLabelColor + customFieldLabel + "\1n";
				// Remove underscores from the property for the label
				// Apply good-looking capitalization to the property label
				if ((propLabel == "id") || (propLabel == "ftn tid"))
					propLabel = propLabel.toUpperCase();
				else if (propLabel == "ftn area")
					propLabel = "FTN Area";
				else if (propLabel == "ftn pid")
					propLabel = "Program ID";
				else if (propLabel == "thread id")
					propLabel = "Thread ID";
				else if (propLabel == "attr")
					propLabel = "Attributes";
				else if (propLabel == "auxattr")
					propLabel = "Auxiliary attributes";
				else if (propLabel == "netattr")
					propLabel = "Network attributes";
					propLabel = capitalizeFirstChar(propLabel);
				// Add the label color and trailing colon to the label text
				propLabel = "\1n" + this.colors.hdrLineLabelColor + propLabel + ":\1n";
			var infoLineWrapped = word_wrap(msgHdr[prop], this.msgAreaWidth);
			var infoLineWrappedArray = lfexpand(infoLineWrapped).split("\r\n");
			var itemValue = "";
			for (var lineIdx = 0; lineIdx < infoLineWrappedArray.length; ++lineIdx)
			{
				if (infoLineWrappedArray[lineIdx].length > 0)
					// Set itemValue to the value that should be displayed
					if (prop == "when_written_time") //itemValue = system.timestr(msgHdr.when_written_time);
						itemValue = system.timestr(msgHdr.when_written_time) + " " + system.zonestr(msgHdr.when_written_zone);
					else if (prop == "when_imported_time") //itemValue = system.timestr(msgHdr.when_imported_time);
						itemValue = system.timestr(msgHdr.when_imported_time) + " " + system.zonestr(msgHdr.when_imported_zone);
					else if ((prop == "when_imported_zone") || (prop == "when_written_zone"))
						itemValue = system.zonestr(msgHdr[prop]);
						itemValue = makeMainMsgAttrStr(msgHdr[prop], "None");
						itemValue = makeAuxMsgAttrStr(msgHdr[prop], "None");
						itemValue = makeNetMsgAttrStr(msgHdr[prop], "None");
					else
						itemValue = infoLineWrappedArray[lineIdx];
					// Add the value color to the value text
					itemValue = "\1n" + this.colors.hdrLineValueColor + itemValue + "\1n";
					if (strip_ctrl(hdrItem).length < this.msgAreaWidth)
						msgHdrInfoLines.push(hdrItem);
					else
					{
						// If this isn't the first header property, then add an empty
						// line to the info lines array for spacing
						if (propCounter > 1)
							msgHdrInfoLines.push("");
						msgHdrInfoLines.push(propLabel);
						// In case the item value is too long to fit into the
						// message reading area, split it and add all the split lines.
						var itemValueWrapped = word_wrap(itemValue, this.msgAreaWidth);
						var itemValueWrappedArray = lfexpand(itemValueWrapped).split("\r\n");
						for (var lineIdx = 0; lineIdx < itemValueWrappedArray.length; ++lineIdx)
						{
							if (itemValueWrappedArray[lineIdx].length > 0)
								msgHdrInfoLines.push(itemValueWrappedArray[lineIdx]);
						}

						// If this isn't the first header property, then add an empty
						// line to the info lines array for spacing
						if (propCounter > 1)
							msgHdrInfoLines.push("");
					}
		++propCounter;
	}

	// If some info lines were added, then insert a header line & blank line to
	// the beginning of the array, and remove the last empty line from the array.
	if (msgHdrInfoLines.length > 0)
	{
		if (kludgeOnly)
		{
			msgHdrInfoLines.splice(0, 0, "\1n\1c\1hMessage Information/Kludge Lines\1n");
			msgHdrInfoLines.splice(1, 0, "\1n\1g\1h--------------------------------\1n");
		}
		else
		{
			msgHdrInfoLines.splice(0, 0, "\1n\1c\1hMessage Headers\1n");
			msgHdrInfoLines.splice(1, 0, "\1n\1g\1h---------------\1n");
		if (msgHdrInfoLines[msgHdrInfoLines.length-1].length == 0)
			msgHdrInfoLines.pop();
	return msgHdrInfoLines;
}

// For the DigDistMsgReader class: Gets & prepares message information for
// the enhanced reader.
//
// Parameters:
//  pMsgHdr: The message header
//  pWordWrap: Boolean - Whether or not to word-wrap the message to fit into the
//             display area.  This is optional and defaults to true.  This should
//             be true for normal use; the only time this should be false is when
//             saving the message to a file.
//  pDetermineAttachments: Boolean - Whether or not to parse the message text to
//                         get attachments from it.  This is optional and defaults
//                         to true.  If false, then the message text will be left
//                         intact with any base64-encoded attachments that may be
//                         in the message text (for multi-part MIME messages).
//  pGetB64Data: Boolean - Whether or not to get the Base64-encoded data for
//               base64-encoded attachments (i.e., in multi-part MIME emails).
//               This is optional and defaults to true.  This is only used when
//               pDetermineAttachments is true.
//  pMsgBody: Optional - A string containing the message body.  If this is not included
//            or is not a string, then this method will retrieve the message body.
//  pMsgHasANSICodes: Optional boolean - If the caller already knows whether the
//                    message text has ANSI codes, the caller can pass this parameter.
//
// Return value: An object with the following properties:
//               msgText: The unaltered message text
//               messageLines: An array containing the message lines, wrapped to
//                             the message area width
//               topMsgLineIdxForLastPage: The top message line index for the last page
//               msgFractionShown: The fraction of the message shown
//               numSolidScrollBlocks: The number of solid scrollbar blocks
//               numNonSolidScrollBlocks: The number of non-solid scrollbar blocks
//               solidBlockStartRow: The starting row on the screen for the scrollbar blocks
//               attachments: An array of the attached filenames (as strings)
//               displayFrame: A Frame object for displaying the message with
//                             a scrollable interface.  Used when the message
//                             contains ANSI, for instance.  If this object is
//                             null, then the reader should use its own scrolling
//                             interface.  Also, this will only be available if
//                             the BBS machine has frame.js in sbbs/exec/load.
//               displayFrameScrollbar: A ScrollBar object to work with displayFrame.
//                                  If scrollbar.js is not available on the BBS machine,
//                                  this will be null.
function DigDistMsgReader_GetMsgInfoForEnhancedReader(pMsgHdr, pWordWrap, pDetermineAttachments,
                                                      pGetB64Data, pMsgBody, pMsgHasANSICodes)
	var determineAttachments = true;
	if (typeof(pDetermineAttachments) == "boolean")
		determineAttachments = pDetermineAttachments;
	var getB64Data = true;
	if (typeof(pGetB64Data) == "boolean")
		getB64Data = pGetB64Data;
	var msgBody = (typeof(pMsgBody) == "string" ? pMsgBody : this.msgbase.get_msg_body(true, pMsgHdr.offset));
		var msgInfo = determineMsgAttachments(pMsgHdr, msgBody, getB64Data);
		retObj.msgText = msgInfo.msgText;
		retObj.attachments = msgInfo.attachments;
	}
	else
	{
		retObj.msgText = msgBody;
	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, pMsgHdr); // 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.
	if ((system.settings & SYS_RENEGADE) == SYS_RENEGADE)
		msgTextAltered = renegadeAttrsToSyncAttrs(msgTextAltered);
	if ((system.settings & SYS_WWIV) == SYS_WWIV)
		msgTextAltered = WWIVAttrsToSyncAttrs(msgTextAltered);
	if ((system.settings & SYS_CELERITY) == SYS_CELERITY)
		msgTextAltered = celerityAttrsToSyncAttrs(msgTextAltered);
	if ((system.settings & SYS_PCBOARD) == SYS_PCBOARD)
		msgTextAltered = PCBoardAttrsToSyncAttrs(msgTextAltered);
	if ((system.settings & SYS_WILDCAT) == SYS_WILDCAT)
		msgTextAltered = wildcatAttrsToSyncAttrs(msgTextAltered);
	// If the message contains ANSI codes, then if frame.js is available and
	// the user's terminal support ANSI, set up a Frame object for reading the
	// message with a scrollable interface.
	retObj.displayFrame = null;
	retObj.displayFrameScrollbar = null;
	var msgHasANSICodes = (typeof(pMsgHasANSICodes) == "boolean" ? pMsgHasANSICodes : textHasANSICodes(retObj.msgText));
	if (msgHasANSICodes)
	{
		if (gFrameJSAvailable && console.term_supports(USER_ANSI))
		{
			// Write the message to a file in a temporary directory,
			// have the frame object read it, then delete the temporary
			// directory.
			var ANSITempDirExists = false;
			var readerTmpOutputDir = backslash(system.node_dir + "DDMsgReaderANSIMsgTemp");
			if (!file_exists(readerTmpOutputDir))
				ANSITempDirExists = mkdir(readerTmpOutputDir);
			if (ANSITempDirExists)
			{
				var tmpANSIFileName = backslash(readerTmpOutputDir) + "tmpMsg.ans";
				var tmpANSIFile = new File(tmpANSIFileName);
				if (tmpANSIFile.open("w"))
				{
					var tmpANSIFileWritten = tmpANSIFile.write(msgTextAltered);
					tmpANSIFile.close();
					// If the file was successfully written, then create the
					// Frame object and have it read the file.
					if (tmpANSIFileWritten)
					{
						// Create the Frame object and 
						// TODO: Should this use 79 or 80 columns?
						retObj.displayFrame = new Frame(1, // x: Horizontal coordinate of top left
						                                this.enhMsgHeaderLines.length+1, // y: Vertical coordinate of top left
						                                80, // Width
						                                // Height: Allow for the message header
						                                // and enhanced reader help line
						                                console.screen_rows-this.enhMsgHeaderLines.length-1,
						                                BG_BLACK);
						retObj.displayFrame.v_scroll = true;
						retObj.displayFrame.h_scroll = false;
						retObj.displayFrame.scrollbars = true;
						// Load the message file into the Frame object
						retObj.displayFrame.load(tmpANSIFileName);
						// If scrollbar.js is available, then set up a vertical
						// scrollbar for the Frame object
						if (gScrollbarJSAvailable)
							retObj.displayFrameScrollbar = new ScrollBar(retObj.displayFrame, {bg: BG_BLACK, fg: LIGHTGRAY, orientation: "vertical", autohide: false});
				// Cleanup: Remove the temporary directory
				deltree(readerTmpOutputDir);
			}
		}
		else
		{
			// frame.js is not available.  Just convert the ANSI to
			// Synchronet attributes.  It might not look the best, but
			// at least we can convert some of the ANSI codes.
			msgTextAltered = ANSIAttrsToSyncAttrs(msgTextAltered);
		}
	}
	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;