Skip to content
Snippets Groups Projects
DDMsgReader.js 765 KiB
Newer Older
	// 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("\1n\1h\1yNo messages found.\1n");
		mswait(ERROR_PAUSE_WAIT_MS);
	}

	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("\1n");
				console.gotoxy(pCurPos);
			}
			console.print("\1n\1h\1wSearching\1i...\1n");
			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
//  pStripCtrl: Boolean - Whether or not to remove Synchronet control
//              codes from the message lines
//  pMsgLines: An array containing the message lines
//  pAttachments: An array containing attachment information (as returned by determineMsgAttachments())
//
// 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, pStripCtrl, pMsgLines, pAttachments)
{
	// Sanity checking
	if (typeof(pFilename) != "string")
		return({ succeeded: false, errorMsg: "Filename parameter not a string"});
	if (pFilename.length == 0)
		return({ succeeded: false, errorMsg: "Empty filename given"});

	// If no message lines are passed in, then get the message lines now.
	var msgLines = pMsgLines;
	var attachments = pAttachments;
	if ((pMsgLines == null) || (typeof(pMsgLines) != "object"))
	{
		if (typeof(pMsgHdr) == "object")
		{
			// Get the message text, interpret any @-codes in it, replace tabs with spaces
			// to prevent weirdness when displaying the message lines, and word-wrap the
			// text so that it looks good on the screen,
			//GetMsgInfoForEnhancedReader(pMsgHdr, pWordWrap, pDetermineAttachments, pGetB64Data)
			//var msgInfo = this.GetMsgInfoForEnhancedReader(pMsgHdr, false, false, false);
			var msgInfo = this.GetMsgInfoForEnhancedReader(pMsgHdr, true, true, true);
			msgLines = msgInfo.messageLines;
			if (msgInfo.hasOwnProperty("attachments"))
				attachments = msgInfo.attachments;
		}
		else
			return({ succeeded: false, errorMsg: "No message lines and null header object"});
	}


	// If there are message attachments, then treat pFilename as a directory and
	// create the directory for saving both the message text & attachments.
	// Then, save the attachments to that directory.
	var msgTextFilename = pFilename;
	if ((attachments != null) && (attachments.length > 0))
	{
		if (file_isdir(pFilename))
		{
			if (file_exists(pFilename))
				return({ succeeded: false, errorMsg: "Can't make directory: File with that name exists"});
		}
		else
		{
			if (!mkdir(pFilename))
				return({ succeeded: false, errorMsg: "Failed to create directory"});
		}

		// The name of the file to save the message text will be called "messageText.txt"
		// in the save directory.
		var savePathWithTrailingSlash = backslash(pFilename);
		msgTextFilename = savePathWithTrailingSlash + "messageText.txt";

		// Save the attachments to the directory
		var saveFileError = "";
		for (var attachIdx = 0; (attachIdx < attachments.length) && (saveFileError.length == 0); ++attachIdx)
		{
			var destFilename = savePathWithTrailingSlash + attachments[attachIdx].filename;
			// If the file info has base64 data, then decode & save it to the directory.
			// Otherwise, the file was probably uploaded to the user's mailbox in Synchronet,
			// so copy the file to the save directory.
			if (attachments[attachIdx].hasOwnProperty("B64Data"))
			{
				var attachedFile = new File(destFilename);
				if (attachedFile.open("wb"))
				{
					attachedFile.base64 = true;
					if (!attachedFile.write(attachments[attachIdx].B64Data))
						saveFileError = "\1n\1cFailed to save " + attachments[attachIdx].filename;
					attachedFile.close();
				}
			}
			else
			{
				// There is no base64 data for the file, so it's probably in the
				// user's mailbox in Synchronet, so copy it to the save directory.
				if (file_exists(attachments[attachIdx].fullyPathedFilename))
				{
					if (!file_copy(attachments[attachIdx].fullyPathedFilename, destFilename))
						saveFileError = "Failed to copy " + attachments[attachIdx].filename;
				}
				else
					saveFileError = "File " + attachments[attachIdx].fullyPathedAttachmentFilename + " doesn't exist";
			}
		}
		if (saveFileError.length > 0)
			return({ succeeded: false, errorMsg: saveFileError });
	}

	var messageSaveFile = new File(msgTextFilename);
	if (messageSaveFile.open("w"))
	{
		// Write some header information to the file
		if (typeof(pMsgHdr) == "object")
		{
			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("===============================");
		}
		// Write the message body to the file
		if (pStripCtrl)
		{
			for (var msgLineIdx = 0; msgLineIdx < msgLines.length; ++msgLineIdx)
				messageSaveFile.writeln(strip_ctrl(msgLines[msgLineIdx]));
		}
		else
		{
			for (var msgLineIdx = 0; msgLineIdx < msgLines.length; ++msgLineIdx)
				messageSaveFile.writeln(msgLines[msgLineIdx]);
		}
		messageSaveFile.close();
	}
	else
	{
		retObj.succeeded = false;
		retObj.errorMsg = "Unable to open the message file for writing";
	}

	return retObj;
}

// For the DigDistMsgReader class: Toggles whether a message has been 'selected'
// (i.e., for things like batch delete, etc.)
//
// Parameters:
//  pSubCode: The internal sub-board code of the message sub-board where the
//            message resides
//  pMsgIdx: The index of the message to toggle
//  pSelected: Optional boolean to explictly specify whether the message should
//             be selected.  If this is not provided (or is null), then this
//             message will simply toggle the selection state of the message.
function DigDistMsgReader_ToggleSelectedMessage(pSubCode, pMsgIdx, pSelected)
{
	// Sanity checking
	if (typeof(pSubCode) != "string") return;
	if (typeof(pMsgIdx) != "number") return;

	// If the 'selected message' object doesn't have the sub code index,
	// then add it.
	if (!this.selectedMessages.hasOwnProperty(pSubCode))
		this.selectedMessages[pSubCode] = {};

	// If pSelected is a boolean, then it specifies the specific selection
	// state of the message (true = selected, false = not selected).
	if (typeof(pSelected) == "boolean")
	{
		if (pSelected)
		{
			if (!this.selectedMessages[pSubCode].hasOwnProperty(pMsgIdx))
				this.selectedMessages[pSubCode][pMsgIdx] = true;
		}
		else
		{
			if (this.selectedMessages[pSubCode].hasOwnProperty(pMsgIdx))
				delete this.selectedMessages[pSubCode][pMsgIdx];
		}
	}
	else
	{
		// pSelected is not a boolean, so simply toggle the selection state of
		// the message.
		// If the object for the given sub-board code contains the message
		// index, then remove it.  Otherwise, add it.
		if (this.selectedMessages[pSubCode].hasOwnProperty(pMsgIdx))
			delete this.selectedMessages[pSubCode][pMsgIdx];
		else
			this.selectedMessages[pSubCode][pMsgIdx] = true;
	}
}

// For the DigDistMsgReader class: Returns whether a message (by sub-board code & index)
// is selected (i.e., for batch delete, etc.).
//
// Parameters:
//  pSubCode: The internal sub-board code of the message sub-board where the
//            message resides
//  pMsgIdx: The index of the message to toggle
//
// Return value: Boolean - Whether or not the given message has been selected
function DigDistMsgReader_MessageIsSelected(pSubCode, pMsgIdx)
{
	return (this.selectedMessages.hasOwnProperty(pSubCode) && this.selectedMessages[pSubCode].hasOwnProperty(pMsgIdx));
}

// For the DigDistMsgReader class: Checks to see if all selected messages can
// be deleted (i.e., whether the user has permission to delete all of them).
function DigDistMsgReader_AllSelectedMessagesCanBeDeleted()
{
	// If the user has sysop access, then they should be able to delete messages.
		return true;

	var userCanDeleteAllSelectedMessages = true;

	var msgBase = null;
	for (var subBoardCode in this.selectedMessages)
	{
		// If the current sub-board is personal mail, then the user can delete
		// those messages.  Otherwise, check the sub-board configuration to see
		// if the user can delete messages in the sub-board.
		if (subBoardCode != "mail")
		{
			msgBase = new MsgBase(subBoardCode);
			if (msgBase.open())
			{
				userCanDeleteAllSelectedMessages = userCanDeleteAllSelectedMessages && ((msgBase.cfg.settings & SUB_DEL) == SUB_DEL);
				msgBase.close();
			}
		}
		if (!userCanDeleteAllSelectedMessages)
			break;
	}
	
	return userCanDeleteAllSelectedMessages;
}

// For the DigDistMsgReader class: Marks the 'selected messages' (in
// this.selecteMessages) 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.
function DigDistMsgReader_DeleteSelectedMessages()
{

	var msgBase = null;
	var msgHdr = null;
	var msgWasDeleted = false;
	for (var subBoardCode in this.selectedMessages)
	{
		msgBase = new MsgBase(subBoardCode);
		if (msgBase.open())
		{
			// Allow the user to delete the messages if they're the sysop, they're
			// reading their personal mail, or the sub-board allows deleting messages.
			if (user.is_sysop || (subBoardCode == "mail") || ((msgBase.cfg.settings & SUB_DEL) == SUB_DEL))
			{
				for (var msgIdx in this.selectedMessages[subBoardCode])
				{
					// It seems that msgIdx is a string, so make sure we have a
					// numeric version.
					var msgIdxNumber = +msgIdx;
					// If doing a search (this.msgSearchHdrs has the sub-board code),
					// then get the message header by index from there.  Otherwise,
					// use the message base object to get the message by index.
					if (this.msgSearchHdrs.hasOwnProperty(subBoardCode) &&
					    (this.msgSearchHdrs[subBoardCode].indexed.length > 0))
					{
						if ((msgIdxNumber >= 0) && (msgIdxNumber < this.msgSearchHdrs[subBoardCode].indexed.length))
							msgHdr = this.msgSearchHdrs[subBoardCode].indexed[msgIdxNumber];
					}
					else if (this.hdrsForCurrentSubBoard.length > 0)
					{
						if ((msgIdxNumber >= 0) && (msgIdxNumber < this.hdrsForCurrentSubBoard.length))
							msgHdr = this.hdrsForCurrentSubBoard[msgIdxNumber];
					}
					else
					{
						if ((msgIdxNumber >= 0) && (msgIdxNumber < msgBase.total_msgs))
							msgHdr = msgBase.get_msg_header(true, msgIdxNumber, false);
					}
					// If we got the message header, then mark it for deletion.
					// If the message header wasn't marked as deleted, then add
					// the message index to the return object.
					if (msgHdr != null)
					{
						//msgWasDeleted = msgBase.remove_msg(true, msgHdr.offset);
						msgWasDeleted = msgBase.remove_msg(false, msgHdr.number);
					}
						// Refresh the message header in the header arrays (if it
						// exists there) and remove the message index from the
						// selectedMessages object
						this.RefreshHdrInSavedArrays(msgIdxNumber, MSG_DELETE, subBoardCode);
						delete this.selectedMessages[subBoardCode][msgIdx];

						// Delete any vote response messages that may exist for this message
						var voteDelRetObj = deleteVoteMsgs(msgBase, msgHdr.number, msgHdr.id, (subBoardCode == "mail"));
						// TODO: If the main messages was deleted, does it matter if vote response messages
						// are deleted?
						if (!voteDelRetObj.allVoteMsgsDeleted)
						{
							retObj.deletedAll = false;
							if (!retObj.failureList.hasOwnProperty(subBoardCode))
								retObj.failureList[subBoardCode] = [];
							retObj.failureList[subBoardCode].push(msgIdxNumber);
						}
					}
					else
					{
						retObj.deletedAll = false;
						if (!retObj.failureList.hasOwnProperty(subBoardCode))
							retObj.failureList[subBoardCode] = [];
						retObj.failureList[subBoardCode].push(msgIdxNumber);
					}
				}
				// If the sub-board index array no longer has any properties (i.e.,
				// all messages in the sub-board were marked as deleted), then remove
				// the sub-board property from this.selectedMessages
				if (Object.keys(this.selectedMessages[subBoardCode]).length == 0)
					delete this.selectedMessages[subBoardCode];
			}
			else
			{
				// The user doesn't have permission to delete messages
				// in this sub-board.
				// Create an entry in retObj.failureList indexed by the
				// sub-board code to indicate failure to delete all
				// messages in the sub-board.
				retObj.deletedAll = false;
				retObj.failureList[subBoardCode] = [];
			}

			msgBase.close();
		}
		else
		{
			// Failure to open the sub-board.
			// Create an entry in retObj.failureList indexed by the
			// sub-board code to indicate failure to delete all messages
			// in the sub-board.
			retObj.deletedAll = false;
			retObj.failureList[subBoardCode] = [];
		}
	}

	return retObj;
}

// For the DigDistMsgReader class: Returns the number of selected messages
function DigDistMsgReader_NumSelectedMessages()
{
	var numSelectedMsgs = 0;

	for (var subBoardCode in this.selectedMessages)
		numSelectedMsgs += Object.keys(this.selectedMessages[subBoardCode]).length;

	return numSelectedMsgs;
}

// Allows the user to forward a message to an email address or
// another user.  This function is interactive with the user.
//
// Parameters:
//  pMsgHeader: The header of the message being forwarded
//  pMsgBody: The body text of the message
//
// Return value: A blank string on success or a string containing a
//               message upon failure.
function DigDistMsgReader_ForwardMessage(pMsgHdr, pMsgBody)
{
	if (typeof(pMsgHdr) != "object")
		return "Invalid message header given";

	var retStr = "";
	console.print("\1n");
	console.crlf();
	console.print("\1cUser name/number/email address\1h:\1n");
	console.crlf();
	var msgDest = console.getstr(console.screen_columns - 1, K_LINE);
	console.print("\1n");
	console.crlf();
	if (msgDest.length > 0)
	{
		var tmpMsgbase = new MsgBase("mail");
		if (tmpMsgbase.open())
		{
			// If the given message body is not a string, then get the
			// message body from the messagebase.
			if (typeof(pMsgBody) != "string")
			{
				var msgbase = new MsgBase(this.subBoardCode);
				if (msgbase.open())
				{
					pMsgBody = msgbase.get_msg_body(false, pMsgHdr.number, false, false, true, true);
					msgbase.close();
				}
				else
					return "Unable to open the sub-board to get the message body";
			}
			// Prepend some lines to the message body to describe where
			// the message came from originally.
			var newMsgBody = "This is a forwarded message from " + system.name + "\n";
			newMsgBody += "Forwarded by: " + user.alias;
			if (user.alias != user.name)
				newMsgBody += " (" + user.name + ")";
			newMsgBody += "\n";
			if (this.subBoardCode == "mail")
				newMsgBody += "From " + user.name + "'s personal email\n";
			else
			{
				newMsgBody += "From sub-board: "
				           + msg_area.grp_list[msg_area.sub[this.subBoardCode].grp_index].description
				           + ", " + msg_area.sub[this.subBoardCode].description + "\n";
			}
			newMsgBody += "From: " + pMsgHdr.from + "\n";
			newMsgBody += "To: " + pMsgHdr.to + "\n";
			newMsgBody += "Subject: " + pMsgHdr.subject + "\n";
			newMsgBody += "==================================\n\n";
			newMsgBody += pMsgBody;

Eric Oulashin's avatar
Eric Oulashin committed
			// Ask whether to edit the message before forwarding it,
			// and use console.editfile(filename) to edit it.
			if (!console.noyes("Edit the message before sending"))
			{
				var baseWorkDir = system.node_dir + "DDMsgReader_Temp";
				deltree(baseWorkDir + "/");
				if (mkdir(baseWorkDir))
				{
					// TODO: Let the user edit the message, then read it
					// and set newMsgBody to it
					var tmpMsgFilename = baseWorkDir + "/message.txt";
					// Write the current message to the file
					var wroteMsgToTmpFile = false;
					var outFile = new File(tmpMsgFilename);
					if (outFile.open("w"))
					{
						wroteMsgToTmpFile = outFile.write(newMsgBody, newMsgBody.length);
						outFile.close();
					}
					if (wroteMsgToTmpFile)
					{
						// Let the user edit the file, and if successful,
						// read it in to newMsgBody
						if (console.editfile(tmpMsgFilename))
						{
							var inFile = new File(tmpMsgFilename);
							if (inFile.open("r"))
							{
								newMsgBody = inFile.read(inFile.length);
								inFile.close();
							}
						}
					}
					else
					{
						console.print("\1n\1cFailed to write message to a file for editing\1n");
						console.crlf();
						console.pause();
					}
				}
				else
				{
					console.print("\1n\1cCouldn't create temporary directory\1n");
					console.crlf();
					console.pause();
				}
			}
			// End New (editing message)

			// Create part of a header object which will be used when saving/sending
			// the message.  The destination ("to" informatoin) will be filled in
			// according to the destination type.
			var destMsgHdr = { to_net_type: NET_NONE, from: user.name,
							   replyto: user.name, subject: "Fwd: " + pMsgHdr.subject };
			if (user.netmail.length > 0)
				destMsgHdr.replyto_net_addr = user.netmail;
				destMsgHdr.replyto_net_addr = user.email;
			//destMsgHdr.when_written_time = 
			//destMsgHdr.when_written_zone = system.timezone;
			//destMsgHdr.when_written_zone_offset = 

			var confirmedForwardMsg = true;

			// If the destination is in the format anything@anything, then
			// accept it as the message destination.  It could be an Internet
			// address (someone@somewhere.com), FidoNet address (sysop@1:327/4),
			// or a QWK address (someone@HOST).
			// We could specifically use gEmailRegex and gFTNEmailregex to test
			// msgDest, but just using those would be too restrictive.
			if (/^.*@.*$/.test(msgDest))
			{
				confirmedForwardMsg = console.yesno("Forward via email to " + msgDest);
				if (confirmedForwardMsg)
				{
					console.print("\1n\1cForwarding via email to " + msgDest + "\1n");
					console.crlf();
					destMsgHdr.to = msgDest;
					destMsgHdr.to_net_addr = msgDest;
					destMsgHdr.to_net_type = netaddr_type(msgDest);
				}
			}
			else
			{
				// See if what the user entered is a user number/name/alias
				var userNum = 0;
				if (/^[0-9]+$/.test(msgDest))
				{
					userNum = +msgDest;
					// Determine if the user entered a valid user number
					var lastUserNum = (system.lastuser == undefined ? system.stats.total_users : system.lastuser + 1);
					if ((userNum < 1) || (userNum >= lastUserNum))
					{
						userNum = 0;
						console.print("\1h\1y* Invalid user number (" + msgDest + ")\1n");
						console.crlf();
					}
				}
				else // Try to get a user number assuming msgDest is a username/alias
					userNum = system.matchuser(msgDest, true);
				// If we have a valid user number, then we can forward the message.
				if (userNum > 0)
				{
					var destUser = new User(userNum);
					confirmedForwardMsg = console.yesno("Forward to " + destUser.alias + " (user " + destUser.number + ")");
					if (confirmedForwardMsg)
					{
						destMsgHdr.to = destUser.alias;
						// If the destination user has an Internet email address,
						// ask the user if they want to send to the destination
						// user's Internet email address
						var sendToNetEmail = false;
						if (destUser.netmail.length > 0)
						{
							sendToNetEmail = !console.noyes("Send to the user's Internet email (" + destUser.netmail + ")");
							if (sendToNetEmail)
							{
								console.print("\1n\1cForwarding to " + destUser.netmail + "\1n");
								console.crlf();
								destMsgHdr.to = destUser.name;
								destMsgHdr.to_net_addr = destUser.netmail;
								destMsgHdr.to_net_type = NET_INTERNET;
							}
						}
						if (!sendToNetEmail)
						{
							console.print("\1n\1cForwarding to " + destUser.alias + "\1n");
							console.crlf();
							destMsgHdr.to_ext = destUser.number;
						}
					}
				}
				else
				{
					confirmedForwardMsg = false;
					console.print("\1h\1y* Unknown destination\1n");
					console.crlf();
				}
			}
			var savedMsg = true;
			if (confirmedForwardMsg)
				savedMsg = tmpMsgbase.save_msg(destMsgHdr, newMsgBody);
			else
			{
				console.print("\1n\1cCanceled\1n");
				console.crlf();
			}
			tmpMsgbase.close();

			if (!savedMsg)
			{
				console.print("\1h\1y* Failed to send the message!\1n");
				console.crlf();
			}

			// Pause for user input so the user can see the messages written
			console.pause();
		}
		else
			retStr = "Failed to open email messagebase";
	}
	else
	{
		console.print("\1n\1cCanceled\1n");
		console.crlf();
		console.pause();
	}

	return retStr;
}

function printMsgHdrInfo(pMsgHdr)
{
	if (typeof(pMsgHdr) != "object")
		return;

	for (var prop in pMsgHdr)
	{
		if (prop == "to_net_type")
			print(prop + ": " + toNetTypeToStr(pMsgHdr[prop]));
		else
			console.print(prop + ": " + pMsgHdr[prop]);
		console.crlf();
	}
}

function toNetTypeToStr(toNetType)
{
	var toNetTypeStr = "Unknown";
	if (typeof(toNetType) == "number")
	{
		switch (toNetType)
		{
			case NET_NONE:
				toNetTypeStr = "Local";
				break;
			case NET_UNKNOWN:
				toNetTypeStr = "Unknown networked";
				break;
			case NET_FIDO:
				toNetTypeStr = "FidoNet";
				break;
			case NET_POSTLINK:
				toNetTypeStr = "PostLink";
				break;
			case NET_QWK:
				toNetTypeStr = "QWK";
				break;
			case NET_INTERNET:
				toNetTypeStr = "Internet";
				break;
			default:
				toNetTypeStr = "Unknown";
				break;
		}
	}
	return toNetTypeStr;
}

// For the DigDistMsgReader class: Lets the user vote on a message
//
// Parameters:
//  pMsgHdr: The header of the mesasge being voted on
//  pRemoveNLsFromVoteText: Optional boolean - Whether or not to remove newlines
//                          (and carriage returns) from the voting text from
//                          text.dat.  Defaults to false.
//
// Return value: An object with the following properties:
//               BBSHasVoteFunction: Boolean - Whether or not the system has
//                                   the vote_msg function
//               savedVote: Boolean - Whether or not the vote was saved
//               userQuit: Boolean - Whether or not the user quit and didn't vote
//               errorMsg: String - An error message, if something went wrong
//               mnemonicsRequiredForErrorMsg: Boolean - Whether or not mnemonics is required to print the error message
//               updatedHdr: The updated message header containing vote information.
//                           If something went wrong, this will be null.
function DigDistMsgReader_VoteOnMessage(pMsgHdr, pRemoveNLsFromVoteText)
	var retObj = {
		BBSHasVoteFunction: false,
		savedVote: false,
		userQuit: false,
		errorMsg: "",
		mnemonicsRequiredForErrorMsg: false,
		updatedHdr: null
	};
	// Don't allow voting for personal email
	if (this.subBoardCode == "mail")
	{
		retObj.errorMsg = "Can not vote on personal email";
		return retObj;
	}
	
	// Check whether the user has the voting restiction
	if ((user.security.restrictions & UFLAG_V) == UFLAG_V)
	{
		// Use the line from text.dat that says the user is not allowed to vote,
		// and remove newlines from it.
		retObj.errorMsg = "\1n" + bbs.text(typeof(R_Voting) != "undefined" ? R_Voting : 781).replace("\r\n", "").replace("\n", "").replace("\N", "").replace("\r", "").replace("\R", "").replace("\R\n", "").replace("\r\N", "").replace("\R\N", "");
		return retObj;
	}
	var msgbase = new MsgBase(this.subBoardCode);
	if (!msgbase.open())
	{
		return retObj;
	}

	// If the message vote function is not defined in the running verison of Synchronet,
	// then just return.
	retObj.BBSHasVoteFunction = (typeof(msgbase.vote_msg) == "function");
	var removeNLsFromVoteText = (typeof(pRemoveNLsFromVoteText) === "boolean" ? pRemoveNLsFromVoteText : false)

	// See if voting is allowed in the current sub-board
	if ((msg_area.sub[this.subBoardCode].settings & SUB_NOVOTING) == SUB_NOVOTING)
	{
		retObj.errorMsg = bbs.text(typeof(VotingNotAllowed) != "undefined" ? VotingNotAllowed : 779);
			retObj.errorMsg = retObj.errorMsg.replace("\r\n", "").replace("\n", "").replace("\N", "").replace("\r", "").replace("\R", "").replace("\R\n", "").replace("\r\N", "").replace("\R\N", "");
		retObj.mnemonicsRequiredForErrorMsg = true;

	// If the message is a poll question and has the maximum number of votes
	// already or is closed for voting, then don't let the user vote on it.
	if ((pMsgHdr.attr & MSG_POLL) == MSG_POLL)
	{
		var userVotedMaxVotes = false;
		var numVotes = (pMsgHdr.hasOwnProperty("votes") ? pMsgHdr.votes : 0);
		if (typeof(msgbase.how_user_voted) === "function")
			var votes = msgbase.how_user_voted(pMsgHdr.number, (msgbase.cfg.settings & SUB_NAME) == SUB_NAME ? user.name : user.alias);
			// TODO: I'm not sure if this 'if' section is correct anymore for
			// the latest 3.17 build of Synchronet (August 14, 2017)
			// Digital Man said:
			// In a poll message, the "votes" property specifies the maximum number of
			// answers/votes per ballot (0 is the equivalent of 1).
			// Max votes testing? :
			// userVotedMaxVotes = (votes == pMsgHdr.votes);
			if (votes >= 0)
			{
				if ((votes == 0) || (votes == 1))
					userVotedMaxVotes = (votes == 3); // (1 << 0) | (1 << 1);
				else
				{
					userVotedMaxVotes = true;
					for (var voteIdx = 0; voteIdx <= numVotes; ++voteIdx)
					{
						if (votes && (1 << voteIdx) == 0)
						{
							userVotedMaxVotes = false;
							break;
						}
					}
				}
			}
		}
		var pollIsClosed = ((pMsgHdr.auxattr & POLL_CLOSED) == POLL_CLOSED);
		if (pollIsClosed)
		{