Skip to content
Snippets Groups Projects
DDMsgReader.js 790 KiB
Newer Older
					// messages in the sub-board.
					retObj.opSuccessful = 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.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("\x01cUser name/number/email address\x01h:\x01n");
	console.crlf();
	var msgDest = console.getstr(console.screen_columns - 1, K_LINE);
	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("\x01n\x01cFailed to write message to a file for editing\x01n");
					console.print("\x01n\x01cCouldn't create temporary directory\x01n");
					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("\x01n\x01cForwarding via email to " + msgDest + "\x01n");
					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("\x01h\x01y* Invalid user number (" + msgDest + ")\x01n");
						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("\x01n\x01cForwarding to " + destUser.netmail + "\x01n");
								console.crlf();
								destMsgHdr.to = destUser.name;
								destMsgHdr.to_net_addr = destUser.netmail;
								destMsgHdr.to_net_type = NET_INTERNET;
							}
						}
						if (!sendToNetEmail)
						{
							console.print("\x01n\x01cForwarding to " + destUser.alias + "\x01n");
							console.crlf();
							destMsgHdr.to_ext = destUser.number;
					console.print("\x01h\x01y* Unknown destination\x01n");
					console.crlf();
				}
			}
			var savedMsg = true;
			if (confirmedForwardMsg)
				savedMsg = tmpMsgbase.save_msg(destMsgHdr, newMsgBody);
			else
			{
				console.print("\x01n\x01cCanceled\x01n");
				console.print("\x01h\x01y* Failed to send the message!\x01n");
				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("\x01n\x01cCanceled\x01n");
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 = "\x01n" + 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", "");
	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)
		{
			retObj.errorMsg = "This poll is closed";
			return retObj;
		}
		else if (userVotedMaxVotes)
		{
			retObj.errorMsg = bbs.text(typeof(VotedAlready) != "undefined" ? VotedAlready : 780);
			if (removeNLsFromVoteText)
				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;
			return retObj;
		}
	}

	// If the user has voted on this message already, then set an error message and return.
	if (this.HasUserVotedOnMsg(pMsgHdr.number))
	{
		retObj.errorMsg = bbs.text(typeof(VotedAlready) != "undefined" ? VotedAlready : 780);
		if (removeNLsFromVoteText)
			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;

	// New MsgBase method: vote_msg(). it takes a message header object
	// (like save_msg), except you only need a few properties, in order of
	// importarnce:
	// attr: you need to have this set to MSG_UPVOTE, MSG_DOWNVOTE, or MSG_VOTE
	// thread_back or reply_id: either of these must be set to indicate msg to vote on
	// from: name of voter
	// from_net_type and from_net_addr: if applicable

	// Do some initial setup of the header for the vote message to be
	// saved to the messagebase
	var voteMsgHdr = {
		thread_back: pMsgHdr.number,
		reply_id: pMsgHdr.id,
		from: (msgbase.cfg.settings & SUB_NAME) == SUB_NAME ? user.name : user.alias
	};
	if (pMsgHdr.from.hasOwnProperty("from_net_type"))
	{
		voteMsgHdr.from_net_type = pMsgHdr.from_net_type;
		if (pMsgHdr.from_net_type != NET_NONE)
			voteMsgHdr.from_net_addr = user.email;
	}

	// Input vote options from the user differently depending on whether
	// the message is a poll or not
	if ((pMsgHdr.attr & MSG_POLL) == MSG_POLL)
	{
		if (pMsgHdr.hasOwnProperty("field_list"))
		{
			var selectHdr = bbs.text(typeof(BallotHdr) != "undefined" ? BallotHdr : 791);
			printf("\x01n" + selectHdr + "\x01n", pMsgHdr.subject);
			var optionFormatStr = "\x01n\x01c\x01h%2d\x01n\x01c: \x01h%s\x01n";
			var optionNum = 1;
			for (var fieldI = 0; fieldI < pMsgHdr.field_list.length; ++fieldI)
			{
				if (pMsgHdr.field_list[fieldI].type == SMB_POLL_ANSWER)
				{
					printf(optionFormatStr, optionNum++, pMsgHdr.field_list[fieldI].data);
					console.crlf();
				}
			}
			console.crlf();
			// Get & process the selection from the user
			var userInputNum = 0;
			if (pMsgHdr.votes > 1)
			{
				// Support multiple answers from the user
				console.print("\x01n\x01gYour vote numbers, separated by commas, up to \x01h" + pMsgHdr.votes + "\x01n\x01g (Blank/Q=Quit):");
				console.print("\x01c\x01h");
				var userInput = consoleGetStrWithValidKeys("0123456789,Q", null, false);
				if ((userInput.length > 0) && (userInput.toUpperCase() != "Q"))
				{
					var userAnswers = userInput.split(",");
					if (userAnswers.length > 0)
					{
						// Generate confirmation text and an array of numbers
						// representing the user's choices, up to the number
						// of responses allowed
						var confirmText = "Vote ";
						var voteNumbers = [];
						for (var i = 0; (i < userAnswers.length) && (i < pMsgHdr.votes); ++i)
						{
							// Trim any whitespace from the user's response
							userAnswers[i] = trimSpaces(userAnswers[i], true, true, true);
							if (/^[0-9]+$/.test(userAnswers[i]))
							{
								voteNumbers.push(+userAnswers[i]);
								confirmText += userAnswers[i] + ",";
							}
						}
						// If the confirmation text has a trailing comma, remove it
						if (/,$/.test(confirmText))
							confirmText = confirmText.substr(0, confirmText.length-1);
						// Confirm from the user and submit their vote if they say yes
						if (voteNumbers.length > 0)
						{
							if (console.yesno(confirmText))
							{
								userInputNum = 0;
								for (var i = 0; i < voteNumbers.length; ++i)
									userInputNum |= (1 << (voteNumbers[i]-1));
							}
							else
								retObj.userQuit = true;
						}
					}
				}
				else
					retObj.userQuit = true;
			}
				// Get the selection prompt text from text.dat and replace the %u or %d with
				// the number 1 (default option)
				var selectPromptText = bbs.text(SelectItemWhich);
				selectPromptText = selectPromptText.replace(/%[uU]/, 1).replace(/%[dD]/, 1);
				console.mnemonics(selectPromptText);
				var maxNum = optionNum - 1;
				userInputNum = console.getnum(maxNum);
				if (userInputNum == -1) // The user chose Q to quit
					retObj.userQuit = true;
		// The message is not a poll - Prompt for up/downvote
		if ((typeof(MSG_UPVOTE) != "undefined") && (typeof(MSG_DOWNVOTE) != "undefined"))
		{
			var voteAttr = 0;
			// Get text line 783 to prompt for voting
			var textDatText = bbs.text(typeof(VoteMsgUpDownOrQuit) != "undefined" ? VoteMsgUpDownOrQuit : 783);
			if (removeNLsFromVoteText)
				textDatText = textDatText.replace("\r\n", "").replace("\n", "").replace("\N", "").replace("\r", "").replace("\R", "").replace("\R\n", "").replace("\r\N", "").replace("\R\N", "");
			// Using getAllowedKeyWithMode() instead of console.getkeys() so we
			// can control the input mode better, so it doesn't output a CRLF
			switch (getAllowedKeyWithMode("UDQ" + KEY_UP + KEY_DOWN, K_NOCRLF|K_NOSPIN))
					voteAttr = MSG_DOWNVOTE;
					break;
				case "Q":
				default:
					retObj.userQuit = true;
					break;
			}
			// If the user voted, then save the user's vote in the attr property
			// in the header
			if (voteAttr != 0)
				voteMsgHdr.attr = voteAttr;
		}
		else
			retObj.errorMsg = "MSG_UPVOTE & MSG_DOWNVOTE are not defined";
	}

	// If the user hasn't quit and there is no error message, then save the vote
	// message header
	if (!retObj.userQuit && (retObj.errorMsg.length == 0))
	{
		retObj.savedVote = msgbase.vote_msg(voteMsgHdr);
		// If the save was successful, then update
		// this.hdrsForCurrentSubBoard with the updated
		// message header (for the message that was read)
		if (retObj.savedVote)
		{
			if (this.hdrsForCurrentSubBoardByMsgNum.hasOwnProperty(pMsgHdr.number))
			{
				var originalMsgIdx = this.hdrsForCurrentSubBoardByMsgNum[pMsgHdr.number];
				var tmpHdrs = msgbase.get_all_msg_headers(true);
				if (tmpHdrs.hasOwnProperty(pMsgHdr.number))
					this.hdrsForCurrentSubBoard[originalMsgIdx] = tmpHdrs[pMsgHdr.number];
					// Originally, this script assigned retObj.updatedHdr as follows:
					//retObj.updatedHdr = pMsgHdr;
					// However, after an update, there were a couple errors that total_votes and upvotes
					// were read-only, so it wuldn't assign to them, so now we copy pMsgHdr this way:
					retObj.updatedHdr = {};
					for (var prop in pMsgHdr)
						retObj.updatedHdr[prop] = pMsgHdr[prop];
					if (this.hdrsForCurrentSubBoard[originalMsgIdx].hasOwnProperty("total_votes"))
						retObj.updatedHdr.total_votes = this.hdrsForCurrentSubBoard[originalMsgIdx].total_votes;
					if (this.hdrsForCurrentSubBoard[originalMsgIdx].hasOwnProperty("upvotes"))
						retObj.updatedHdr.upvotes = this.hdrsForCurrentSubBoard[originalMsgIdx].upvotes;
					if (this.hdrsForCurrentSubBoard[originalMsgIdx].hasOwnProperty("tally"))
						retObj.updatedHdr.tally = this.hdrsForCurrentSubBoard[originalMsgIdx].tally;
		{
			// Failed to save the vote
			retObj.errorMsg = "Failed to save your vote";
		}
// For the DigDistMsgReader class: Checks to see whether a user has voted on a message.
// The message must belong to the currently-open sub-board.
//
// Parameters:
//  pMsgNum: The message number
//  pUser: Optional - A user account to check.  If omitted, the current logged-in
//         user will be used.
function DigDistMsgReader_HasUserVotedOnMsg(pMsgNum, pUser)
	// Don't do this for personal email
	if (this.subBoardCode == "mail")
		return false;

	// Thanks to echicken for explaining how to check this.  To check a user's
	// vote, use MsgBase.how_user_voted().
	/*
	The return value will be:
	0 - user hasn't voted
	1 - upvoted
	2 - downvoted
	Or, if the message was a poll, it's a bitfield:
	if (votes&(1<<2)) {
	 // User selected answer 2
	}
	*/
	var userHasVotedOnMsg = false;
	var msgbase = new MsgBase(this.subBoardCode);
	if (msgbase.open())
		if (typeof(msgbase.how_user_voted) === "function")
			var votes = 0;
			if (typeof(pUser) == "object")
				votes = msgbase.how_user_voted(pMsgNum, (msgbase.cfg.settings & SUB_NAME) == SUB_NAME ? pUser.name : pUser.alias);
				votes = msgbase.how_user_voted(pMsgNum, (msgbase.cfg.settings & SUB_NAME) == SUB_NAME ? user.name : user.alias);
// Gets information about the upvotes and downvotes for a message.
// If the user is a sysop, this will also get who voted on the message.
//
// Parameters:
//  pMsgHdr: A header of a message that has upvotes & downvotes
//
// Return value: An array of strings containing information about the upvotes,
//               downvotes, tally, and (if the user is a sysop) who submitted
//               votes on the message.
function DigDistMsgReader_GetUpvoteAndDownvoteInfo(pMsgHdr)
{
	// If the message header doesn't have the "total_votes" or "upvotes" properties,
	// then there's no vote information, so just return an empty array.
	if (!pMsgHdr.hasOwnProperty("total_votes") || !pMsgHdr.hasOwnProperty("upvotes"))
		return [];

	var msgVoteInfo = getMsgUpDownvotesAndScore(pMsgHdr);
	voteInfo.push("Upvotes: " + msgVoteInfo.upvotes);
	voteInfo.push("Downvotes: " + msgVoteInfo.downvotes);
	voteInfo.push("Score: " + msgVoteInfo.voteScore);
	if (pMsgHdr.hasOwnProperty("tally"))
		voteInfo.push("Tally: " + pMsgHdr.tally);

	// If the user is the sysop, then also add the names of people who
	// voted on the message.
	{
		// Check all the messages in the messagebase after the current one
		// to find response messages
		var msgbase = new MsgBase(this.subBoardCode);
		if (msgbase.open())
			// Pass true to get_all_msg_headers() to tell it to return vote messages
			// (the parameter was introduced in Synchronet 3.17+)
			var tmpHdrs = msgbase.get_all_msg_headers(true);
			for (var tmpProp in tmpHdrs)
				if (tmpHdrs[tmpProp] == null)
					continue;
				// If this header's thread_back or reply_id matches the poll message
				// number, then append the 'user voted' string to the message body.
				if ((tmpHdrs[tmpProp].thread_back == pMsgHdr.number) || (tmpHdrs[tmpProp].reply_id == pMsgHdr.id))
					var tmpMessageBody = msgbase.get_msg_body(false, tmpHdrs[tmpProp].number, false, false, true, true);
					if ((tmpHdrs[tmpProp].field_list.length == 0) && (tmpMessageBody.length == 0))
						var msgWrittenLocalTime = msgWrittenTimeToLocalBBSTime(tmpHdrs[tmpProp]);
						var voteDate = strftime("%a %b %d %Y %H:%M:%S", msgWrittenLocalTime);
						voteInfo.push("\x01n\x01c\x01h" + tmpHdrs[tmpProp].from + "\x01n\x01c voted on this message on " + voteDate + "\x01n");
// For the DigDistMsgReader class: Gets the body (text) of a message.  If it's
// a poll, this method will format the message body with poll results.  Otherwise,
// this method will simply get the message body.
//
// Parameters:
//  pMsgHeader: The message header
//
// Return value: The poll results, colorized.  If the message is not a
//               poll message, then an empty string will be returned.
function DigDistMsgReader_GetMsgBody(pMsgHdr)
	var msgbase = new MsgBase(this.subBoardCode);
	if (!msgbase.open())
		return "";
	var msgBody = "";
	if ((typeof(MSG_TYPE_POLL) != "undefined") && (pMsgHdr.type & MSG_TYPE_POLL) == MSG_TYPE_POLL)
	{
		// A poll is intended to be parsed (and displayed) using on the header data. The
		// (optional) comments are stored in the hdr.field_list[] with type values of
		// SMB_COMMENT (now defined in sbbsdefs.js) and the available answers are in the
		// field_list[] with type values of SMB_POLL_ANSWER.

		// The 'comments' and 'answers' are also a part of the message header, so you can
		// grab them separately, then format and display them however you want.  You can
		// find them in the header.field_list array; each element in that array should be
		// an object with a 'type' and a 'data' property.  Relevant types here are
		// SMB_COMMENT and SMB_POLL_ANSWER.  (This is what I'm doing on the web, and I
		// just ignore the message body for poll messages.)

		if (pMsgHdr.hasOwnProperty("field_list"))
		{
			// Figure out the longest length of the poll choices, with
			// a maximum of 22 characters less than the terminal width.
			// Use a minimum of 27 characters.
			// That length will be used for the formatting strings for
			// the poll results.
			var voteOptDescLen = 0;
			for (var fieldI = 0; fieldI < pMsgHdr.field_list.length; ++fieldI)
			{
				if (pMsgHdr.field_list[fieldI].type == SMB_POLL_ANSWER)
				{
					if (pMsgHdr.field_list[fieldI].data.length > voteOptDescLen)
						voteOptDescLen = pMsgHdr.field_list[fieldI].data.length;
				}
			}
			if (voteOptDescLen > console.screen_columns - 22)
				voteOptDescLen = console.screen_columns - 22;
			else if (voteOptDescLen < 27)
				voteOptDescLen = 27;

			// Format strings for outputting the voting option lines
			var unvotedOptionFormatStr = "\x01n\x01c\x01h%2d\x01n\x01c: \x01w\x01h%-" + voteOptDescLen + "s [%-4d %6.2f%]\x01n";
			var votedOptionFormatStr = "\x01n\x01c\x01h%2d\x01n\x01c: \x01" + "5\x01w\x01h%-" + voteOptDescLen + "s [%-4d %6.2f%]\x01n";
			// Add up the total number of votes so that we can
			// calculate vote percentages.
			var totalNumVotes = 0;
			if (pMsgHdr.hasOwnProperty("tally"))
			{
				for (var tallyI = 0; tallyI < pMsgHdr.tally.length; ++tallyI)
					totalNumVotes += pMsgHdr.tally[tallyI];
			}
			// Go through field_list and append the voting options and stats to
			// msgBody
			var optionNum = 1;
			var numVotes = 0;
			var votePercentage = 0;
			for (var fieldI = 0; fieldI < pMsgHdr.field_list.length; ++fieldI)
			{
				if (pMsgHdr.field_list[fieldI].type == SMB_COMMENT)
					pollComment += pMsgHdr.field_list[fieldI].data + "\r\n";
				else if (pMsgHdr.field_list[fieldI].type == SMB_POLL_ANSWER)
				{
					// Figure out the number of votes on this option and the
					// vote percentage
					if (pMsgHdr.hasOwnProperty("tally"))
					{
						if (tallyIdx < pMsgHdr.tally.length)
							numVotes = pMsgHdr.tally[tallyIdx];
							votePercentage = (numVotes / totalNumVotes) * 100;
						}
					}
					// Append to the message text
					msgBody += format(numVotes == 0 ? unvotedOptionFormatStr : votedOptionFormatStr,
					                  optionNum++, pMsgHdr.field_list[fieldI].data.substr(0, voteOptDescLen),
					                  numVotes, votePercentage);
					if (numVotes > 0)
						msgBody += " " + CHECK_CHAR;
					msgBody += "\r\n";
			if (pollComment.length > 0)
				msgBody = pollComment + "\r\n" + msgBody;
			// If voting is allowed in this sub-board and the current logged-in
			// user has not voted on this message, then append some text saying
			// how to vote.
			var votingAllowed = ((this.subBoardCode != "mail") && (((msg_area.sub[this.subBoardCode].settings & SUB_NOVOTING) == 0)));
			if (votingAllowed && !this.HasUserVotedOnMsg(pMsgHdr.number))
				msgBody += "\x01n\r\n\x01gTo vote in this poll, press \x01w\x01h" + this.enhReaderKeys.vote + "\x01n\x01g now.\r\n";

			// If the current logged-in user created this poll, then show the
			// users who have voted on it so far.
			var msgFromUpper = pMsgHdr.from.toUpperCase();
			if ((msgFromUpper == user.name.toUpperCase()) || (msgFromUpper == user.handle.toUpperCase()))
			{
				// Check all the messages in the messagebase after the current one
				// to find poll response messages
				// Get the line from text.dat for writing who voted & when.  It
				// is a format string and should look something like this:
				//"\r\n\x01n\x01hOn %s, in \x01c%s \x01n\x01c%s\r\n\x01h\x01m%s voted in your poll: \x01n\x01h%s\r\n" 787 PollVoteNotice
				var userVotedInYourPollText = bbs.text(typeof(PollVoteNotice) != "undefined" ? PollVoteNotice : 787);

				// Pass true to get_all_msg_headers() to tell it to return vote messages
				// (the parameter was introduced in Synchronet 3.17+)
				var tmpHdrs = msgbase.get_all_msg_headers(true);
				for (var tmpProp in tmpHdrs)
					if (tmpHdrs[tmpProp] == null)
						continue;
					// If this header's thread_back or reply_id matches the poll message
					// number, then append the 'user voted' string to the message body.
					if ((tmpHdrs[tmpProp].thread_back == pMsgHdr.number) || (tmpHdrs[tmpProp].reply_id == pMsgHdr.id))
						var msgWrittenLocalTime = msgWrittenTimeToLocalBBSTime(tmpHdrs[tmpProp]);
						var voteDate = strftime("%a %b %d %Y %H:%M:%S", msgWrittenLocalTime);
						var grpName = "";
						var msgbaseCfgName = "";
						var msgbase = new MsgBase(this.subBoardCode);
						if (msgbase.open())
							grpName = msgbase.cfg.grp_name;
							msgbaseCfgName = msgbase.cfg.name;
							msgbase.close();
						msgBody += format(userVotedInYourPollText, voteDate, grpName, msgbaseCfgName, tmpHdrs[tmpProp].from, pMsgHdr.subject);
	{
		// If the message is UTF8 and the terminal is not UTF8-capable, then convert
		// the text to cp437.
		msgBody = msgbase.get_msg_body(false, pMsgHdr.number, false, false, true, true);
		if (pMsgHdr.hasOwnProperty("is_utf8") && pMsgHdr.is_utf8)
		{
			var userConsoleSupportsUTF8 = false;
			if (typeof(USER_UTF8) != "undefined")
				userConsoleSupportsUTF8 = console.term_supports(USER_UTF8);
			if (!userConsoleSupportsUTF8)
				msgBody = utf8_cp437(msgBody);
		}
		// Remove any initial coloring from the message body, which can color the whole message
		msgBody = removeInitialColorFromMsgBody(msgBody);
		// For HTML-formatted messages, convert HTML entities
		if (pMsgHdr.hasOwnProperty("text_subtype") && pMsgHdr.text_subtype.toLowerCase() == "html")
		{
			msgBody = html2asc(msgBody);
			// Remove excessive blank lines after HTML-translation
			msgBody = msgBody.replace(/\r\n\r\n\r\n/g, '\r\n\r\n');
		}
	// Remove any Synchronet pause codes that might exist in the message
	msgBody = msgBody.replace("\x01p", "").replace("\x01P", "");

	// If the user is a sysop, this is a moderated message area, and the message
	// hasn't been validated, then prepend the message with a message to let the
	// sysop now know to validate it.
		if (user.is_sysop && msg_area.sub[this.subBoardCode].is_moderated && ((pMsgHdr.attr & MSG_VALIDATED) == 0))
			var validateNotice = "\x01n\x01h\x01yThis is an unvalidated message in a moderated area.  Press "
							   + this.enhReaderKeys.validateMsg + " to validate it.\r\n\x01g";
			for (var i = 0; i < 79; ++i)
				validateNotice += HORIZONTAL_SINGLE;
			validateNotice += "\x01n\r\n";
// For the DigDistMsgReader class: Refreshes a message header in one of the
// internal message arrays.
//
// Parameters:
//  pMsgNum: The number of the message to replace
function DigDistMsgReader_RefreshMsgHdrInArrays(pMsgNum)
{
	var msgbase = new MsgBase(this.subBoardCode);
	if (!msgbase.open())
		return;
	if (this.msgSearchHdrs.hasOwnProperty(this.subBoardCode) &&
	    (this.msgSearchHdrs[this.subBoardCode].indexed.length > 0))
	{
		for (var i = 0; i < this.msgSearchHdrs[this.subBoardCode].indexed.length; ++i)
		{
			if (this.msgSearchHdrs[this.subBoardCode].indexed[i].number == pMsgNum)
			{
				var newMsgHdr = msgbase.get_msg_header(false, pMsgNum, true);
				if (newMsgHdr != null)
					this.msgSearchHdrs[this.subBoardCode].indexed[i] = newMsgHdr;
				break;
			}
		}
	}
	else if (this.hdrsForCurrentSubBoard.length > 0)
	{
		if (this.hdrsForCurrentSubBoardByMsgNum.hasOwnProperty(pMsgNum))
		{
			if (msgHdrs.hasOwnProperty(pMsgNum))
			{
				var msgIdx = this.hdrsForCurrentSubBoardByMsgNum[pMsgNum];
				this.hdrsForCurrentSubBoard[msgIdx] = msgHdrs[pMsgNum];
			}
		}
	}
	msgbase.close();
}

// For the DigDistMessageReader class: Re-calculates the message list widths and
// format strings
//
// Parameters:
//  pMsgNumLen: Optional - Length to use for the message number field.  If not specified,
//              then this will get the number of messages in the sub-board and use that
//              length.
function DigDistMsgReader_RecalcMsgListWidthsAndFormatStrs(pMsgNumLen)
{
	// Note: Constructing these strings must be done after reading the configuration
	// file in order for the configured colors to be used

	this.sMsgListHdrFormatStr = "";
	this.sMsgInfoFormatStr = "";
	this.sMsgInfoToUserFormatStr = "";
	this.sMsgInfoFromUserFormatStr = "";