Skip to content
Snippets Groups Projects
DDMsgReader.js 731 KiB
Newer Older
				// and it's not the same message number that was passed in.
				if ((msgNumInput > 0) && (msgNumInput-1 != pOffset))
				{
					// If the message is marked as deleted, then output an error
					if (this.MessageIsDeleted(msgNumInput-1))
					{
						writeWithPause(this.msgAreaLeft, console.screen_rows-1,
									   "\1n" + this.text.msgHasBeenDeletedText.replace("%d", msgNumInput) + "\1n",
									   ERROR_PAUSE_WAIT_MS, "\1n", true);
						// Confirm with the user whether to read the message
						var readMsg = true;
						if (this.promptToReadMessage)
							var sReadMsgConfirmText = this.colors["readMsgConfirmColor"]
													+ "Read message "
													+ this.colors["readMsgConfirmNumberColor"]
													+ msgNumInput + this.colors["readMsgConfirmColor"]
													+ ": Are you sure";
							console.gotoxy(promptPos);
							console.print("\1n");
							readMsg = console.yesno(sReadMsgConfirmText);
							continueOn = false;
							retObj.newMsgOffset = msgNumInput - 1;
							retObj.nextAction = ACTION_GO_SPECIFIC_MSG;
						else
							writeMessage = false; // Don't re-write the current message again
				}
				else // Message number invalid or the same as what was passed in
					writeMessage = false; // Don't re-write the current message again
				// If the user chose to continue reading messages, then refresh
				// the last 2 message lines in the last part of the message area
				// and then put the cursor back to its original position.
				if (continueOn)
				{
					this.DisplayEnhReaderError("", msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr);
					// Move the cursor back to its original position
					console.gotoxy(originalCurpos);
				}
				break;
			case this.enhReaderKeys.prevMsgByTitle: // Previous message by title
			case this.enhReaderKeys.prevMsgByAuthor: // Previous message by author
			case this.enhReaderKeys.prevMsgByToUser: // Previous message by 'to user'
			case this.enhReaderKeys.prevMsgByThreadID: // Previous message by thread ID
				// Only allow this if we aren't doing a message search.
				if (!this.SearchingAndResultObjsDefinedForCurSub())
				{
					var threadPrevMsgOffset = this.FindThreadPrevOffset(msgHeader,
					                                                    keypressToThreadType(retObj.lastKeypress, this.enhReaderKeys),
						retObj.newMsgOffset = threadPrevMsgOffset;
						retObj.nextAction = ACTION_GO_SPECIFIC_MSG;
						continueOn = false;
						// Refresh the help line at the bottom of the screen
						//this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
						writeMessage = false; // Don't re-write the current message again
					}
					// Make sure the help line on the bottom of the screen is
					// drawn.
					this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
				}
				else
					writeMessage = false; // Don't re-write the current message again
				break;
			case this.enhReaderKeys.nextMsgByTitle: // Next message by title (subject)
			case this.enhReaderKeys.nextMsgByAuthor: // Next message by author
			case this.enhReaderKeys.nextMsgByToUser: // Next message by 'to user'
			case this.enhReaderKeys.nextMsgByThreadID: // Next message by thread ID
				// Only allow this if we aren't doing a message search.
				if (!this.SearchingAndResultObjsDefinedForCurSub())
				{
					var threadPrevMsgOffset = this.FindThreadNextOffset(msgHeader,
																		keypressToThreadType(retObj.lastKeypress, this.enhReaderKeys),
						retObj.newMsgOffset = threadPrevMsgOffset;
						retObj.nextAction = ACTION_GO_SPECIFIC_MSG;
						continueOn = false;
					}
					else
						writeMessage = false; // Don't re-write the current message again
					// Make sure the help line on the bottom of the screen is
					// drawn.
					this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
				}
				else
					writeMessage = false; // Don't re-write the current message again
				break;
			case this.enhReaderKeys.previousMsg: // Previous message
				// Look for a prior message that isn't marked for deletion.  Even
				// if we don't find one, we'll still want to return from this
				// function (with message index -1) so that this script can go
				// onto the previous message sub-board/group.
				retObj.newMsgOffset = this.FindNextNonDeletedMsgIdx(pOffset, false);
				// As a screen redraw optimization: Only return if there is a valid new
				// message offset or the user is allowed to change to a different sub-board.
				// Otherwise, don't return, and don't refresh the message on the screen.
				var goToPrevMessage = false;
				if ((retObj.newMsgOffset > -1) || allowChgMsgArea)
				{
					if (retObj.newMsgOffset == -1 && !curMsgSubBoardIsLast())
						goToPrevMessage = this.EnhReaderPromptYesNo(this.text.goToPrevMsgAreaPromptText,
																	msgInfo.messageLines, topMsgLineIdx,
																	msgLineFormatStr, solidBlockStartRow,
																	numSolidScrollBlocks);
						// We're not at the beginning of the sub-board, so it's okay to exit this
						// method and go to the previous message.
						goToPrevMessage = true;
				}
				if (goToPrevMessage)
				{
					continueOn = false;
					retObj.nextAction = ACTION_GO_PREVIOUS_MSG;
				}
				else
					writeMessage = false; // No need to refresh the message
				break;
			case this.enhReaderKeys.nextMsg: // Next message
			case KEY_ENTER:
				// Look for a later message that isn't marked for deletion.  Even
				// if we don't find one, we'll still want to return from this
				// function (with message index -1) so that this script can go
				// onto the next message sub-board/group.
				retObj.newMsgOffset = this.FindNextNonDeletedMsgIdx(pOffset, true);
				// Note: Unlike the left arrow key, we want to exit this method when
				// navigating to the next message, regardless of whether or not the
				// user is allowed to change to a different sub-board, so that processes
				// that require continuation (such as new message scan) can continue.
				// Still, if there are no more readable messages in the current sub-board
				// (and thus the user would go onto the next message area), prompt the
				// user whether they want to continue onto the next message area.
				if (retObj.newMsgOffset == -1 && !curMsgSubBoardIsLast())
				{
					// For personal mail, don't do anything, and don't refresh the
					// message.  In a sub-board, ask the user if they want to go
					// to the next one.
					if (this.readingPersonalEmail)
						writeMessage = false;
						// If configured to allow the user to post in the sub-board
						// instead of going to the next message area and we're not
						// scanning, then do so.
						if (this.readingPostOnSubBoardInsteadOfGoToNext && !this.doingMsgScan)
						{
							console.print("\1n");
							console.crlf();
							// Ask the user if they want to post on the sub-board.
							// If they say yes, then do so before exiting.
							if (!console.noyes(format(this.text.postOnSubBoard, this.msgbase.cfg.grp_name, this.msgbase.cfg.description)))
								bbs.post_msg(this.subBoardCode);
							continueOn = false;
							retObj.nextAction = ACTION_QUIT;
						}
							// Prompt the user whether they want to go to the next message area
							if (this.EnhReaderPromptYesNo(this.text.goToNextMsgAreaPromptText, msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr, solidBlockStartRow, numSolidScrollBlocks))
								// Let this method exit and let the caller go to the next sub-board
								retObj.nextAction = ACTION_GO_NEXT_MSG;
								writeMessage = false; // No need to refresh the message
				}
				else
				{
					// We're not at the end of the sub-board, so it's okay to exit this
					// method and go to the next message.
					continueOn = false;
					retObj.nextAction = ACTION_GO_NEXT_MSG;
				}
				break;
				// First & last message: Quit out of this input loop and let the
				// calling function, this.ReadMessages(), handle the action.
			case this.enhReaderKeys.firstMsg: // First message
				// Only leave this function if we aren't already on the first message.
				if (pOffset > 0)
				{
					continueOn = false;
					retObj.nextAction = ACTION_GO_FIRST_MSG;
				}
				else
					writeMessage = false; // Don't re-write the current message again
				break;
			case this.enhReaderKeys.lastMsg: // Last message
				// Only leave this function if we aren't already on the last message.
				if (pOffset < this.NumMessages() - 1)
				{
					continueOn = false;
					retObj.nextAction = ACTION_GO_LAST_MSG;
				}
				else
					writeMessage = false; // Don't re-write the current message again
				break;
			case this.enhReaderKeys.prevSubBoard: // Go to the previous message area
				if (allowChgMsgArea)
				{
					continueOn = false;
					retObj.nextAction = ACTION_GO_PREV_MSG_AREA;
				}
				else
					writeMessage = false; // Don't re-write the current message again
				break;
			case this.enhReaderKeys.nextSubBoard: // Go to the next message area
				if (allowChgMsgArea || this.doingMultiSubBoardScan)
				{
					continueOn = false;
					retObj.nextAction = ACTION_GO_NEXT_MSG_AREA;
				}
				else
					writeMessage = false; // Don't re-write the current message again
				break;
				// H and K: Display the extended message header info/kludge lines
				// (for the sysop)
			case this.enhReaderKeys.showHdrInfo:
			case this.enhReaderKeys.showKludgeLines:
				if (gIsSysop)
				{
					// Save the original cursor position
					var originalCurPos = console.getxy();
					// Get an array of the extended header info/kludge lines and then
					// allow the user to scroll through them.
					var extdHdrInfoLines = this.GetExtdMsgHdrInfo(msgHeader, (retObj.lastKeypress == this.enhReaderKeys.showKludgeLines));
					if (extdHdrInfoLines.length > 0)
					{
						// Calculate information for the scrollbar for the kludge lines
						var infoFractionShown = this.msgAreaHeight / extdHdrInfoLines.length;
						if (infoFractionShown > 1)
							infoFractionShown = 1.0;
						var numInfoSolidScrollBlocks = Math.floor(this.msgAreaHeight * infoFractionShown);
						if (numInfoSolidScrollBlocks == 0)
							numInfoSolidScrollBlocks = 1;
						var numNonSolidInfoScrollBlocks = this.msgAreaHeight - numInfoSolidScrollBlocks;
						var lastInfoSolidBlockStartRow = this.msgAreaTop;
						// Define a scrollbar update function for the header info/kludge lines
						function msgInfoScrollbarUpdateFn(pFractionToLastPage)
							var infoSolidBlockStartRow = msgReaderObj.msgAreaTop + Math.floor(numNonSolidInfoScrollBlocks * pFractionToLastPage);
							if (infoSolidBlockStartRow != lastInfoSolidBlockStartRow)
								msgReaderObj.UpdateEnhancedReaderScollbar(infoSolidBlockStartRow, lastInfoSolidBlockStartRow, numInfoSolidScrollBlocks);
							lastInfoSolidBlockStartRow = infoSolidBlockStartRow;
							console.gotoxy(1, console.screen_rows);
						// Display the kludge lines and let the user scroll through them
						this.DisplayEnhancedReaderWholeScrollbar(this.msgAreaTop, numInfoSolidScrollBlocks);
						scrollTextLines(extdHdrInfoLines, 0, this.colors["msgBodyColor"], true,
						this.msgAreaLeft, this.msgAreaTop, this.msgAreaWidth,
						msgAreaHeight, 1, console.screen_rows,
						msgInfoScrollbarUpdateFn);
						// Display the scrollbar for the message to refresh it on the screen
						solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
						this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
						writeMessage = true; // We want to refresh the message on the screen
					else
					{
						// There are no kludge lines for this message
						this.DisplayEnhReaderError(this.text.noKludgeLinesForThisMsgText, msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr);
						console.gotoxy(originalCurPos);
					}
				}
				else // The user is not a sysop
					writeMessage = false;
				break;
				// Message list, change message area: Quit out of this input loop
				// and let the calling function, this.ReadMessages(), handle the
				// action.
			case this.enhReaderKeys.showMsgList: // Message list
				retObj.nextAction = ACTION_DISPLAY_MSG_LIST;
				continueOn = false;
				break;
			case this.enhReaderKeys.chgMsgArea: // Change message area, if allowed
				if (allowChgMsgArea)
				{
					retObj.nextAction = ACTION_CHG_MSG_AREA;
				}
				else
					writeMessage = false; // No need to refresh the message
				break;
			case this.enhReaderKeys.downloadAttachments: // Download attachments
				if (msgInfo.attachments.length > 0)
				{
					console.print("\1n");
					console.gotoxy(1, console.screen_rows);
					console.crlf();
					console.print("\1c- Download Attached Files -\1n");
					// Note: sendAttachedFiles() will output a CRLF at the beginning.
					sendAttachedFiles(msgInfo.attachments);

					// Refresh things on the screen
					console.clear("\1n");
					// Display the message header and key help line again
					this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
					this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
					// Display the scrollbar again to refresh it on the screen
					solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
					this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
					writeMessage = true; // We want to refresh the message on the screen
				}
				else
					writeMessage = false;
				break;
			case this.enhReaderKeys.saveToBBSMachine:
				// Save the message to the BBS machine - Only allow this
				// if the user is a sysop.
				if (gIsSysop)
				{
					// Prompt the user for a filename to save the message to the
					// BBS machine
					var promptPos = this.EnhReaderPrepLast2LinesForPrompt();
					console.print("\1n\1cFilename:\1h");
					var inputLen = console.screen_columns - 10; // 10 = "Filename:" length + 1
					var filename = console.getstr(inputLen, K_NOCRLF);
					console.print("\1n");
					if (filename.length > 0)
						//var saveMsgRetObj = this.SaveMsgToFile(msgHeader, filename, true, msgInfo.messageLines);
						var saveMsgRetObj = this.SaveMsgToFile(msgHeader, filename, true);
						console.gotoxy(promptPos);
						console.cleartoeol("\1n");
						console.gotoxy(promptPos);
						if (saveMsgRetObj.succeeded)
							console.print("\1n\1cThe message has been saved.\1n");
						else
							console.print("\1n\1y\1hFailed: " + saveMsgRetObj.errorMsg + "\1n");
						mswait(ERROR_PAUSE_WAIT_MS);
						console.gotoxy(promptPos);
						console.print("\1n\1y\1hMessage not exported\1n");
						mswait(ERROR_PAUSE_WAIT_MS);
					// Refresh the last 2 lines of the message on the screen to overwrite
					// the file save prompt
					this.DisplayEnhReaderError("", msgInfo.messageLines, topMsgLineIdx, msgLineFormatStr);
					writeMessage = false; // Don't write the whole message again
				}
				else
					writeMessage = false;
				break;
			case this.enhReaderKeys.userEdit: // Edit the user who wrote the message
				if (gIsSysop)
				{
					console.print("\1n");
					console.crlf();
					console.print("- Edit user " + msgHeader.from);
					console.crlf();
					var editObj = editUser(msgHeader.from);
					if (editObj.errorMsg.length != 0)
						console.crlf();
						console.print("\1y\1h" + editObj.errorMsg + "\1n");
						console.crlf();
						console.pause();

					// Refresh things on the screen
					console.clear("\1n");
					// Display the message header and key help line again
					this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
					this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
					// Display the scrollbar again to refresh it on the screen
					solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
					this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
					writeMessage = true; // We want to refresh the message on the screen
				}
				else // The user is not a sysop
			case this.enhReaderKeys.forwardMsg: // Forward the message
				console.print("\1n");
				console.crlf();
				console.print("\1c- Forward message\1n");
				console.crlf();
				var retStr = this.ForwardMessage(msgHeader, messageText);
				if (retStr.length > 0)
				{
					console.print("\1n\1h\1y* " + retStr + "\1n");
					console.crlf();
					console.pause();
				}

				// Refresh things on the screen
				console.clear("\1n");
				// Display the message header and key help line again
				this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
				this.DisplayEnhancedMsgReadHelpLine(console.screen_rows, allowChgMsgArea);
				// Display the scrollbar again to refresh it on the screen
				solidBlockStartRow = this.msgAreaTop + Math.floor(numNonSolidScrollBlocks * fractionToLastPage);
				this.DisplayEnhancedReaderWholeScrollbar(solidBlockStartRow, numSolidScrollBlocks);
				writeMessage = true; // We want to refresh the message on the screen
				break;
			case this.enhReaderKeys.quit: // Quit
				retObj.nextAction = ACTION_QUIT;
				continueOn = false;
				break;
			default:
				writeMessage = false;
				break;

	return retObj;
}
// Helper method for ReadMessageEnhanced() - Does the traditional (non-scrollable) reader interface
function DigDistMsgReader_ReadMessageEnhanced_Traditional(msgHeader, allowChgMsgArea, messageText, msgHasANSICodes, pOffset)
{
	var retObj = new Object();
	retObj.offsetValid = true;
	retObj.userReplied = false;
	retObj.lastKeypress = "";
	retObj.newMsgOffset = -1;
	retObj.nextAction = ACTION_NONE;
	retObj.refreshEnhancedRdrHelpLine = false;
	
	// Separate the message text from any attachments in the message.
	var msgAndAttachmentInfo = determineMsgAttachments(msgHeader, messageText, true);
	// Only interpret @-codes if the user is reading personal email.  There
	// are many @-codes that do some action such as move the cursor, execute a
	// script, etc., and I don't want users on message networks to do anything
	// malicious to users on other BBSes.
	if (this.readingPersonalEmail)
		msgAndAttachmentInfo.msgText = replaceAtCodesInStr(msgAndAttachmentInfo.msgText); // Or this.ParseMsgAtCodes(msgAndAttachmentInfo.msgText, msgHeader) to replace only some @ codes
	var msgTextWrapped = word_wrap(msgAndAttachmentInfo.msgText, console.screen_columns-1);

	// Generate the key help text
	var keyHelpText = "\1n\1c\1h#\1n\1b, \1c\1hLeft\1n\1b, \1c\1hRight\1n\1b, ";
	if (this.CanDelete() || this.CanDeleteLastMsg())
		keyHelpText += "\1c\1hDEL\1b, ";
	if (this.CanEdit())
		keyHelpText += "\1c\1hE\1y)\1n\1cdit\1b, ";
	keyHelpText += "\1c\1hF\1y)\1n\1cirst\1b, \1c\1hL\1y)\1n\1cast\1b, \1c\1hR\1y)\1n\1ceply\1b, ";
	// If the user is allowed to change to a different message area, then
	// include that option.
	if (allowChgMsgArea)
	{
		// If there's room for the private reply option, then include that
		// before the change area option.
		if (console.screen_columns >= 89)
			keyHelpText += "\1c\1hP\1y)\1n\1crivate reply\1b, ";
		keyHelpText += "\1c\1hC\1y)\1n\1chg area\1b, ";
	}
		// The user isn't allowed to change to a different message area.
		// Go ahead and include the private reply option.
		keyHelpText += "\1c\1hP\1y)\1n\1crivate reply\1b, ";
	}
	keyHelpText += "\1c\1hQ\1y)\1n\1cuit\1b, \1c\1h?\1g: \1c";
	// User input loop
	var writeMessage = true;
	var writePromptText = true;
	var continueOn = true;
	while (continueOn)
	{
		if (writeMessage)
			if (console.term_supports(USER_ANSI))
				console.clear("\1n");
			// Write the message header & message body to the screen
			this.DisplayEnhancedMsgHdr(msgHeader, pOffset+1, 1);
			console.print("\1n" + this.colors["msgBodyColor"]);
			console.putmsg(msgTextWrapped, P_NOATCODES);
		// Write the prompt text
		if (writePromptText)
			console.print(keyHelpText);
		// Default the writing of the message & input prompt to true for the
		// next iteration.
		writeMessage = true;
		writePromptText = true;
		// Input a key from the user and take action based on the keypress.
		//retObj.lastKeypress = getKeyWithESCChars(K_UPPER|K_NOCRLF|K_NOECHO|K_NOSPIN);
		retObj.lastKeypress = getKeyWithESCChars(K_UPPER);
		switch (retObj.lastKeypress)
			case this.enhReaderKeys.deleteMessage: // Delete message
				console.crlf();
				// Prompt the user for confirmation to delete the message.
				// Note: this.PromptAndDeleteMessage() will check to see if the user
				// is a sysop or the message was posted by the user.
				// If the message was deleted, then exit this read method
				// and return KEY_RIGHT as the last keypress so that the
				// calling method will go to the next message/sub-board.
				// Otherwise (if the message was not deleted), refresh the
				// last 2 lines of the message on the screen.
				// TODO: For the DeleteMessage() call, pass the array of file
				// attachments for it to delete (i.e., msgInfo.attachments)
				var msgWasDeleted = this.PromptAndDeleteMessage(pOffset);
				if (msgWasDeleted)
				{
					var msgSearchObj = this.LookForNextOrPriorNonDeletedMsg(pOffset);
					continueOn = msgSearchObj.continueInputLoop;
					retObj.newMsgOffset = msgSearchObj.newMsgOffset;
					retObj.nextAction = msgSearchObj.nextAction;
					if (msgSearchObj.promptGoToNextArea)
					{
						if (console.yesno(this.text.goToNextMsgAreaPromptText))
						{
							// Let this method exit and let the caller go to the next sub-board
							continueOn = false;
							retObj.nextAction = ACTION_GO_NEXT_MSG;
						}
						else
							writeMessage = false; // No need to refresh the message
					}
				}
				break;
			case this.enhReaderKeys.selectMessage: // Select message (for batch delete, etc.)
				console.crlf();
				var selectMessage = !console.noyes("Select this message");
				this.ToggleSelectedMessage(this.subBoardCode, pOffset, selectMessage);
				break;
				// TODO: Write this?  Not sure yet if it makes much sense to
				// have batch delete in the reader interface.
				// Prompt the user for confirmation, and use
				// this.DeleteSelectedMessages() to mark the selected messages
				// as deleted.
				// Returns an object with the following properties:
				//  deletedAll: Boolean - Whether or not all messages were successfully marked
				//              for deletion
				//  failureList: An object containing indexes of messages that failed to get
				//               marked for deletion, indexed by internal sub-board code, then
				//               containing messages indexes as properties.  Reasons for failing
				//               to mark messages deleted can include the user not having permission
				//               to delete in a sub-board, failure to open the sub-board, etc.
				writeMessage = false; // No need to refresh the message
				break;
			case this.enhReaderKeys.editMsg: // Edit the message
					// Let the user edit the message if they want to
					var editReturnObj = this.EditExistingMsg(pOffset);
					// If the user confirmed editing the message, then see if the
					// message was edited and refresh the screen accordingly.
					if (editReturnObj.userConfirmed)
					{
						// If the message was edited, then refresh the text lines
						// array and update the other message-related variables.
						if (editReturnObj.msgEdited && (editReturnObj.newMsgIdx > -1))
						{
							// When the message is edited, the old message will be
							// deleted and the edited message will be posted as a new
							// message.  So we should return to the caller and have it
							// go directly to that new message.
							continueOn = false;
							retObj.newMsgOffset = editReturnObj.newMsgIdx;
						}
					}
				}
				else
				{
					writeMessage = false;
					writePromptText = false;
				}
				break;
			case this.enhReaderKeys.showHelp: // Show help
				if (!console.term_supports(USER_ANSI))
				{
					console.crlf();
					console.crlf();
				}
				this.DisplayEnhancedReaderHelp(allowChgMsgArea, msgAndAttachmentInfo.attachments.length > 0);
				if (!console.term_supports(USER_ANSI))
				{
					console.crlf();
					console.crlf();
				}
				break;
			case this.enhReaderKeys.reply: // Reply to the message
			case this.enhReaderKeys.privateReply: // Private reply
				// If the user pressed the private reply key while reading private
				// mail, then do nothing (allow only the regular reply key to reply).
				// If not reading personal email, go ahead and let the user reply
				// with either the reply or private reply keypress.
				var privateReply = (retObj.lastKeypress == this.enhReaderKeys.privateReply);
				if (privateReply && this.readingPersonalEmail)
				{
					writeMessage = false; // Don't re-write the current message again
					writePromptText = false; // Don't write the prompt text again
				}
				else
				{
					console.crlf();
					// Get the message header with fields expanded so we can get the most info possible.
					//var extdMsgHdr = this.GetMsgHdrByAbsoluteNum(msgHeader.number, true);
					var extdMsgHdr = this.msgbase.get_msg_header(false, msgHeader.number, true);
					// Let the user reply to the message
					var replyRetObj = this.ReplyToMsg(extdMsgHdr, msgAndAttachmentInfo.msgText, privateReply, pOffset);
					retObj.userReplied = replyRetObj.postSucceeded;
					//retObj.msgNotReadable = replyRetObj.msgWasDeleted;
					var msgWasDeleted = replyRetObj.msgWasDeleted;
					if (msgWasDeleted)
					{
						var msgSearchObj = this.LookForNextOrPriorNonDeletedMsg(pOffset);
						continueOn = msgSearchObj.continueInputLoop;
						retObj.newMsgOffset = msgSearchObj.newMsgOffset;
						retObj.nextAction = msgSearchObj.nextAction;
						if (msgSearchObj.promptGoToNextArea)
						{
							if (console.yesno(this.text.goToNextMsgAreaPromptText))
							{
								// Let this method exit and let the caller go to the next sub-board
								continueOn = false;
								retObj.nextAction = ACTION_GO_NEXT_MSG;
							}
							else
								writeMessage = true; // We want to refresh the message on the screen
						// If the messagebase object was not successfully re-opened
						// after posting the message, then  we'll want to quit.
						if (!replyRetObj.msgbaseReOpened)
							retObj.nextAction = ACTION_QUIT;
							continueOn = false;
							// Display an error
							console.print("\1n");
							console.crlf();
							console.print("\1h\1yMessagebase error after replying.  Aborting.\1n");
							mswait(ERROR_PAUSE_WAIT_MS);
			case this.enhReaderKeys.postMsg: // Post a message
				if (!this.readingPersonalEmail)
				{
					// Let the user post a message.
					if (bbs.post_msg(this.subBoardCode))
						// TODO: If the user is doing a search, it might be
						// useful to search their new message and add it to
						// the search results if it's a match..  but maybe
						// not?

					console.pause();

					// We'll want to refresh the message & prompt text on the screen
					writeMessage = true;
					writePromptText = true;
				}
				else
				{
					// Don't write the current message or prompt text in the next iteration
					writeMessage = false;
					writePromptText = false;
				}
				break;
			// Numeric digit: The start of a number of a message to read
			case "0":
			case "1":
			case "2":
			case "3":
			case "4":
			case "5":
			case "6":
			case "7":
			case "8":
			case "9":
				console.crlf();
				// Put the user's input back in the input buffer to
				// be used for getting the rest of the message number.
				console.ungetstr(retObj.lastKeypress);
				// Prompt for the message number
				var msgNumInput = this.PromptForMsgNum(null, this.text.readMsgNumPromptText, false, ERROR_PAUSE_WAIT_MS, false);
				// Only allow reading the message if the message number is valid
				// and it's not the same message number that was passed in.
				if ((msgNumInput > 0) && (msgNumInput-1 != pOffset))
				{
					// If the message is marked as deleted, then output an error
					if (this.MessageIsDeleted(msgNumInput-1))
						console.print("\1n" + this.text.msgHasBeenDeletedText.replace("%d", msgNumInput) + "\1n");
						// Confirm with the user whether to read the message
						var readMsg = true;
						if (this.promptToReadMessage)
							readMsg = console.yesno("\1n" + this.colors["readMsgConfirmColor"]
													+ "Read message "
													+ this.colors["readMsgConfirmNumberColor"]
													+ msgNumInput + this.colors["readMsgConfirmColor"]
													+ ": Are you sure");
							retObj.newMsgOffset = msgNumInput - 1;
							retObj.nextAction = ACTION_GO_SPECIFIC_MSG;
						}
					}
			case this.enhReaderKeys.prevMsgByTitle: // Previous message by title
			case this.enhReaderKeys.prevMsgByAuthor: // Previous message by author
			case this.enhReaderKeys.prevMsgByToUser: // Previous message by 'to user'
			case this.enhReaderKeys.prevMsgByThreadID: // Previous message by thread ID
				// Only allow this if we aren't doing a message search.
				if (!this.SearchingAndResultObjsDefinedForCurSub())
				{
					console.crlf(); // For the "Searching..." text
					var threadPrevMsgOffset = this.FindThreadPrevOffset(msgHeader,
																		keypressToThreadType(retObj.lastKeypress, this.enhReaderKeys),
						retObj.newMsgOffset = threadPrevMsgOffset;
						retObj.nextAction = ACTION_GO_SPECIFIC_MSG;
			case this.enhReaderKeys.nextMsgByTitle: // Next message by title (subject)
			case this.enhReaderKeys.nextMsgByAuthor: // Next message by author
			case this.enhReaderKeys.nextMsgByToUser: // Next message by 'to user'
			case this.enhReaderKeys.nextMsgByThreadID: // Next message by thread ID
				// Only allow this if we aren't doing a message search.
				if (!this.SearchingAndResultObjsDefinedForCurSub())
				{
					console.crlf(); // For the "Searching..." text
					var threadNextMsgOffset = this.FindThreadNextOffset(msgHeader,
																		keypressToThreadType(retObj.lastKeypress, this.enhReaderKeys),
						retObj.newMsgOffset = threadNextMsgOffset;
						retObj.nextAction = ACTION_GO_SPECIFIC_MSG;
			case this.enhReaderKeys.previousMsg: // Previous message
				// TODO: Change the key for this?
				// Look for a prior message that isn't marked for deletion.  Even
				// if we don't find one, we'll still want to return from this
				// function (with message index -1) so that this script can go
				// onto the previous message sub-board/group.
				retObj.newMsgOffset = this.FindNextNonDeletedMsgIdx(pOffset, false);
				var goToPrevMessage = false;
				if ((retObj.newMsgOffset > -1) || allowChgMsgArea)
				{
					if (retObj.newMsgOffset == -1 && !curMsgSubBoardIsLast())
						console.crlf();
						goToPrevMessage = console.yesno(this.text.goToPrevMsgAreaPromptText);
						// We're not at the beginning of the sub-board, so it's okay to exit this
						// method and go to the previous message.
						goToPrevMessage = true;
				}
				if (goToPrevMessage)
				{
					continueOn = false;
					retObj.nextAction = ACTION_GO_PREVIOUS_MSG;
				}
				break;
			case this.enhReaderKeys.nextMsg: // Next message
			case KEY_ENTER:
				// Look for a later message that isn't marked for deletion.  Even
				// if we don't find one, we'll still want to return from this
				// function (with message index -1) so that this script can go
				// onto the next message sub-board/group.
				retObj.newMsgOffset = this.FindNextNonDeletedMsgIdx(pOffset, true);
				// Note: Unlike the left arrow key, we want to exit this method when
				// navigating to the next message, regardless of whether or not the
				// user is allowed to change to a different sub-board, so that processes
				// that require continuation (such as new message scan) can continue.
				// Still, if there are no more readable messages in the current sub-board
				// (and thus the user would go onto the next message area), prompt the
				// user whether they want to continue onto the next message area.
				if (retObj.newMsgOffset == -1 && !curMsgSubBoardIsLast())
				{
					console.print("\1n");
					console.crlf();
					// If configured to allow the user to post in the sub-board
					// instead of going to the next message area and we're not
					// scanning, then do so.
					if (this.readingPostOnSubBoardInsteadOfGoToNext && !this.doingMsgScan)
						// Ask the user if they want to post on the sub-board.
						// If they say yes, then do so before exiting.
						if (!console.noyes(format(this.text.postOnSubBoard, this.msgbase.cfg.grp_name, this.msgbase.cfg.description)))
							bbs.post_msg(this.subBoardCode);
						if (console.yesno(this.text.goToNextMsgAreaPromptText))
						{
							// Let this method exit and let the caller go to the next sub-board
							continueOn = false;
							retObj.nextAction = ACTION_GO_NEXT_MSG;
						}
				}
				else
				{
					// We're not at the end of the sub-board, so it's okay to exit this
					// method and go to the next message.
					continueOn = false;
					retObj.nextAction = ACTION_GO_NEXT_MSG;
				}
				break;
			case this.enhReaderKeys.firstMsg: // First message
				// Only leave this function if we aren't already on the first message.
				if (pOffset > 0)
				{
					continueOn = false;
					retObj.nextAction = ACTION_GO_FIRST_MSG;
				}
				else
				{
					writeMessage = false;
					writePromptText = false;
				}
				break;
			case this.enhReaderKeys.lastMsg: // Last message
				// Only leave this function if we aren't already on the last message.
				if (pOffset < this.NumMessages() - 1)
				{
					continueOn = false;
					retObj.nextAction = ACTION_GO_LAST_MSG;
				}
				else
				{
					writeMessage = false;
					writePromptText = false;
				}
				break;
			case "-": // Go to the previous message area
				if (allowChgMsgArea)
				{
					continueOn = false;
					retObj.nextAction = ACTION_GO_PREV_MSG_AREA;
				}
				else
				{
					writeMessage = false;
					writePromptText = false;
				}
				break;
			case "+": // Go to the next message area
				if (allowChgMsgArea || this.doingMultiSubBoardScan)
				{
					continueOn = false;
					retObj.nextAction = ACTION_GO_NEXT_MSG_AREA;
				}
				else
				{
					writeMessage = false;
					writePromptText = false;
				}
				break;
			// H and K: Display the extended message header info/kludge lines
			// (for the sysop)
			case this.enhReaderKeys.showHdrInfo:
			case this.enhReaderKeys.showKludgeLines:
				if (gIsSysop)
				{
					console.crlf();
					// Get an array of the extended header info/kludge lines and then
					// display them.
					var extdHdrInfoLines = this.GetExtdMsgHdrInfo(msgHeader, (retObj.lastKeypress == this.enhReaderKeys.showKludgeLines));
						for (var infoIter = 0; infoIter < extdHdrInfoLines.length; ++infoIter)
							console.print(extdHdrInfoLines[infoIter]);
						// There are no kludge lines for this message
						console.print(this.text.noKludgeLinesForThisMsgText);
						console.crlf();
						console.pause();
				}
				else // The user is not a sysop
				{
					writeMessage = false;
					writePromptText = false;
				}
				break;
				// Message list, change message area: Quit out of this input loop
				// and let the calling function, this.ReadMessages(), handle the
				// action.
			case this.enhReaderKeys.showMsgList: // Message list
				retObj.nextAction = ACTION_DISPLAY_MSG_LIST;
				continueOn = false;
				break;
			case this.enhReaderKeys.chgMsgArea: // Change message area, if allowed
				if (allowChgMsgArea)
				{
					retObj.nextAction = ACTION_CHG_MSG_AREA;
			case this.enhReaderKeys.downloadAttachments: // Download attachments
				if (msgAndAttachmentInfo.attachments.length > 0)
				{
					console.print("\1n");
					console.crlf();
					console.print("\1c- Download Attached Files -\1n");
					// Note: sendAttachedFiles() will output a CRLF at the beginning.
					sendAttachedFiles(msgAndAttachmentInfo.attachments);

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