Skip to content
Snippets Groups Projects
DDMsgReader.js 790 KiB
Newer Older

// Returns a string describing auxiliary message attributes.  Makes use of the
// gAuxMsgAttrStrs object for the auxiliary message attributes and description
// strings.
//
// Parameters:
//  pAuxMsgAttrs: The bit field for the auxiliary message attributes
//                (normally, the 'auxattr' property of a header object)
//  pIfEmptyString: Optional - A string to use if there are no attributes set
//
// Return value: A string describing the auxiliary message attributes
function makeAuxMsgAttrStr(pAuxMsgAttrs, pIfEmptyString)
   if (typeof(pAuxMsgAttrs) == "number")
         {
            if (msgAttrStr.length > 0)
               msgAttrStr += ", ";
            msgAttrStr += gAuxMsgAttrStrs[prop];
         }
      }
   }
   if ((msgAttrStr.length == 0) && (typeof(pIfEmptyString) == "string"))
	   msgAttrStr = pIfEmptyString;
   return msgAttrStr;
}

// Returns a string describing network message attributes.  Makes use of the
// gNetMsgAttrStrs object for the network message attributes and description
// strings.
//
// Parameters:
//  pNetMsgAttrs: The bit field for the network message attributes
//                (normally, the 'netattr' property of a header object)
//  pIfEmptyString: Optional - A string to use if there are no attributes set
//
// Return value: A string describing the network message attributes
function makeNetMsgAttrStr(pNetMsgAttrs, pIfEmptyString)
   if (typeof(pNetMsgAttrs) == "number")
         {
            if (msgAttrStr.length > 0)
               msgAttrStr += ", ";
            msgAttrStr += gNetMsgAttrStrs[prop];
         }
      }
   }
   if ((msgAttrStr.length == 0) && (typeof(pIfEmptyString) == "string"))
	   msgAttrStr = pIfEmptyString;
   return msgAttrStr;
}

// Given a sub-board code, this function returns a sub-board's group and name.
// If the given sub-board code is "mail", then this will return "Personal mail".
//
// Parameters:
//  pSubBoardCode: An internal sub-board code
//
// Return value: A string containing the sub-board code group & name, or
//               "Personal email" if it's the personal email sub-board
function subBoardGrpAndName(pSubBoardCode)
{
	if (typeof(pSubBoardCode) != "string")
		return "";

	var subBoardGrpAndName = "";
	if (pSubBoardCode == "mail")
		subBoardGrpAndName = "Personal mail";
	else
	{
		subBoardGrpAndName = msg_area.sub[pSubBoardCode].grp_name + " - "
                         + msg_area.sub[pSubBoardCode].name;
	}

	return subBoardGrpAndName;
}

// Returns whether a given string matches the current user's name, handle, or alias.
// Does a case-insensitive match.
//
// Parameters:
//  pStr: The string to match against the user's name/handle/alias
//
// Return value: Boolean - Whether or not the string matches the current user's name,
//               handle, or alias
function userNameHandleAliasMatch(pStr)
{
	if (typeof(pStr) != "string")
		return false;
	var strUpper = pStr.toUpperCase();
	return ((strUpper == user.name.toUpperCase()) || (strUpper == user.handle.toUpperCase()) || (strUpper == user.alias.toUpperCase()));
}

// Writes a log message to the system log (using LOG_INFO log level) and to the
// node log.  This will prepend the text "Digital Distortion Message Reader ("
// + user.alias + "): " to the log message.
// 
// Parameters:
//  pMessage: The message to log
//  pLogLevel: The log level.  Optional - Defaults to LOG_INFO.
function writeToSysAndNodeLog(pMessage, pLogLevel)
{
	if (typeof(pMessage) != "string")
		return;

	var logMessage = "Digital Distortion Message Reader (" +  user.alias + "): " + pMessage;
	var logLevel = (typeof(pLogLevel) == "number" ? pLogLevel : LOG_INFO);
	log(logLevel, logMessage);
// This function looks up and returns a sub-board code from the sub-board number.
// If no matching sub-board is found, this will return an empty string.
//
// Parameters:
//  pSubBoardNum: A sub-board number
//
// Return value: The sub-board code.  If no matching sub-board is found, an empty
//               string will be returned.
function getSubBoardCodeFromNum(pSubBoardNum)
{
	// Ensure we're using a numeric type for the sub-board number
	// (in case pSubBoardNum is a string rather than a number)
	var subNum = Number(pSubBoardNum);

	var subBoardCode = "";
	for (var subCode in msg_area.sub)
	{
		if (msg_area.sub[subCode].number == subNum)
		{
			subBoardCode = subCode;
			break;
		}
	}
	return subBoardCode;
}

// This function recursively removes a directory and all of its contents.  Returns
// whether or not the directory was removed.
//
// Parameters:
//  pDir: The directory to remove (with trailing slash).
//
// Return value: Boolean - Whether or not the directory was removed.
function deltree(pDir)
{
	if ((pDir == null) || (pDir == undefined))
		return false;
	if (typeof(pDir) != "string")
		return false;
	if (pDir.length == 0)
		return false;
	// Make sure pDir actually specifies a directory.
	if (!file_isdir(pDir))
		return false;
	// Don't wipe out a root directory.
	if ((pDir == "/") || (pDir == "\\") || (/:\\$/.test(pDir)) || (/:\/$/.test(pDir)) || (/:$/.test(pDir)))
		return false;

	// If we're on Windows, then use the "RD /S /Q" command to delete
	// the directory.  Otherwise, assume *nix and use "rm -rf" to
	// delete the directory.
	if (deltree.inWindows == undefined)
		deltree.inWindows = (/^WIN/.test(system.platform.toUpperCase()));
	if (deltree.inWindows)
		system.exec("RD " + withoutTrailingSlash(pDir) + " /s /q");
	else
		system.exec("rm -rf " + withoutTrailingSlash(pDir));
	// The directory should be gone, so we should return true.  I'd like to verify that the
	// directory really is gone, but file_exists() seems to return false for directories,
	// even if the directory does exist.  So I test to make sure no files are seen in the dir.
	return (directory(pDir + "*").length == 0);

	/*
	// Recursively deleting each file & dir using JavaScript:
	var retval = true;

	// Open the directory and delete each entry.
	var files = directory(pDir + "*");
	for (var i = 0; i < files.length; ++i)
	{
		// If the entry is a directory, then deltree it (Note: The entry
		// should have a trailing slash).  Otherwise, delete the file.
		// If the directory/file couldn't be removed, then break out
		// of the loop.
		if (file_isdir(files[i]))
		{
			retval = deltree(files[i]);
			if (!retval)
				break;
		}
		else
		{
			retval = file_remove(files[i]);
			if (!retval)
				break;
		}
	}

	// Delete the directory specified by pDir.
	if (retval)
		retval = rmdir(pDir);

	return retval;
*/
}

// Removes a trailing (back)slash from a path.
//
// Parameters:
//  pPath: A directory path
//
// Return value: The path without a trailing (back)slash.
function withoutTrailingSlash(pPath)
{
	if ((pPath == null) || (pPath == undefined))
		return "";

	var retval = pPath;
	if (retval.length > 0)
	{
		var lastIndex = retval.length - 1;
		var lastChar = retval.charAt(lastIndex);
		if ((lastChar == "\\") || (lastChar == "/"))
			retval = retval.substr(0, lastIndex);
	}
	return retval;
}

// Adds double-quotes around a string if the string contains spaces.
//
// Parameters:
//  pStr: A string to add double-quotes around if it has spaces
//
// Return value: The string with double-quotes if it contains spaces.  If the
//               string doesn't contain spaces, then the same string will be
//               returned.
function quoteStrWithSpaces(pStr)
{
	if (typeof(pStr) != "string")
		return "";
	var strCopy = pStr;
	if (pStr.indexOf(" ") > -1)
		strCopy = "\"" + pStr + "\"";
	return strCopy;
}

// Given a message header field list type number (i.e., the 'type' property for an
// entry in the field_list array in a message header), this returns a text label
// to be used for outputting the field.
//
// Parameters:
//  pFieldListType: A field_list entry type (numeric)
//  pIncludeTrailingColon: Optional boolean - Whether or not to include a trailing ":"
//                         at the end of the returned string.  Defaults to true.
//
// Return value: A text label for the field (a string)
function msgHdrFieldListTypeToLabel(pFieldListType, pIncludeTrailingColon)
{
	// The page at this URL lists the header field types:
	// http://synchro.net/docs/smb.html#Header Field Types:

	var fieldTypeLabel = "Unknown (" + pFieldListType.toString() + ")";
	switch (pFieldListType)
	{
		case 0: // Sender
			fieldTypeLabel = "Sender";
			break;
		case 1: // Sender Agent
			fieldTypeLabel = "Sender Agent";
			break;
		case 2: // Sender net type
			fieldTypeLabel = "Sender Net Type";
			break;
		case 3: // Sender Net Address
			fieldTypeLabel = "Sender Net Address";
			break;
		case 4: // Sender Agent Extension
			fieldTypeLabel = "Sender Agent Extension";
			break;
		case 5: // Sending agent (Sender POS)
			fieldTypeLabel = "Sender Agent";
			break;
		case 6: // Sender organization
			fieldTypeLabel = "Sender Organization";
			break;
		case 16: // Author
			fieldTypeLabel = "Author";
			break;
		case 17: // Author Agent
			fieldTypeLabel = "Author Agent";
			break;
		case 18: // Author Net Type
			fieldTypeLabel = "Author Net Type";
			break;
		case 19: // Author Net Address
			fieldTypeLabel = "Author Net Address";
			break;
		case 20: // Author Extension
			fieldTypeLabel = "Author Extension";
			break;
		case 21: // Author Agent (Author POS)
			fieldTypeLabel = "Author Agent";
			break;
		case 22: // Author Organization
			fieldTypeLabel = "Author Organization";
			break;
		case 32: // Reply To
			fieldTypeLabel = "Reply To";
			break;
		case 33: // Reply To agent
			fieldTypeLabel = "Reply To Agent";
			break;
		case 34: // Reply To net type
			fieldTypeLabel = "Reply To net type";
			break;
		case 35: // Reply To net address
			fieldTypeLabel = "Reply To net address";
			break;
		case 36: // Reply To extension
			fieldTypeLabel = "Reply To (extended)";
			break;
			fieldTypeLabel = "Reply To position";
			break;
		case 38: // Reply To organization (0x26 hex)
			fieldTypeLabel = "Reply To organization";
			break;
		case 48: // Recipient (0x30 hex)
			fieldTypeLabel = "Recipient";
			break;
		case 162: // Seen-by
			fieldTypeLabel = "Seen-by";
			break;
		case 163: // Path
			fieldTypeLabel = "Path";
			break;
		case 176: // RFCC822 Header
			fieldTypeLabel = "RFCC822 Header";
			break;
		case 177: // RFC822 MSGID
			fieldTypeLabel = "RFC822 MSGID";
			break;
		case 178: // RFC822 REPLYID
			fieldTypeLabel = "RFC822 REPLYID";
			break;
		case 240: // UNKNOWN
			fieldTypeLabel = "UNKNOWN";
			break;
		case 241: // UNKNOWNASCII
			fieldTypeLabel = "UNKNOWN (ASCII)";
			break;
		case 255:
			fieldTypeLabel = "UNUSED";
			break;
			fieldTypeLabel = "Unknown (" + pFieldListType.toString() + ")";
			break;
	}

	var includeTrailingColon = (typeof(pIncludeTrailingColon) == "boolean" ? pIncludeTrailingColon : true);
	if (includeTrailingColon)
		fieldTypeLabel += ":";

	return fieldTypeLabel;
}

// Capitalizes the first character of a string.
//
// Parameters:
//  pStr: The string to capitalize
//
// Return value: A version of the sting with the first character capitalized
function capitalizeFirstChar(pStr)
{
	var retStr = "";
	if (typeof(pStr) == "string")
	{
		if (pStr.length > 0)
			retStr = pStr.charAt(0).toUpperCase() + pStr.slice(1);
	}
	return retStr;
}

// Parses a list of numbers (separated by commas or spaces), which may contain
// ranges separated by dashes.  Returns an array of the individual numbers.
//
// Parameters:
//  pList: A comma-separated list of numbers, some which may contain
//         2 numbers separated by a dash denoting a range of numbers.
//
// Return value: An array of the individual numbers from the list
function parseNumberList(pList)
{
	if (typeof(pList) != "string")
		return [];

	var numberList = [];

	// Split pList on commas or spaces
	var commaOrSpaceSepArray = pList.split(/[\s,]+/);
	if (commaOrSpaceSepArray.length > 0)
	{
		// Go through the comma-separated array - If the element is a
		// single number, then append it to the number list to be returned.
		// If there is a range (2 numbers separated by a dash), then
		// append each number in the range individually to the array to be
		// returned.
		for (var i = 0; i < commaOrSpaceSepArray.length; ++i)
		{
			// If it's a single number, append it to numberList.
			if (/^[0-9]+$/.test(commaOrSpaceSepArray[i]))
				numberList.push(+commaOrSpaceSepArray[i]);
			// If there are 2 numbers separated by a dash, then split it on the
			// dash and generate the intermediate numbers.
			else if (/^[0-9]+-[0-9]+$/.test(commaOrSpaceSepArray[i]))
			{
				var twoNumbers = commaOrSpaceSepArray[i].split("-");
				if (twoNumbers.length == 2)
				{
					var num1 = +twoNumbers[0];
					var num2 = +twoNumbers[1];
					// If the 1st number is bigger than the 2nd, then swap them.
					if (num1 > num2)
					{
						var temp = num1;
						num1 = num2;
						num2 = temp;
					}
					// Append each individual number in the range to numberList.
					for (var number = num1; number <= num2; ++number)
						numberList.push(number);
				}
			}
		}
	}

	return numberList;
}

// Inputs a single keypress from the user from a list of valid keys, allowing
// input modes (see K_* in sbbsdefs.js for mode bits).  This is similar to
// console.getkeys(), except that this allows mode bits (such as K_NOCRLF, etc.).
//
// Parameters:
//  pAllowedKeys: A list of allowed keys (string)
//  pMode: Mode bits (see K_* in sbbsdefs.js)
//
// Return value: The user's inputted keypress
function getAllowedKeyWithMode(pAllowedKeys, pMode)
{
	var userInput = "";

	var keypress = "";
	var i = 0;
	var matchedKeypress = false;
	while (!matchedKeypress)
	{
		keypress = console.getkey(K_NOECHO|pMode);
		// Check to see if the keypress is one of the allowed keys
		for (i = 0; i < pAllowedKeys.length; ++i)
		{
			if (keypress == pAllowedKeys[i])
				userInput = keypress;
			else if (keypress.toUpperCase() == pAllowedKeys[i])
				userInput = keypress.toUpperCase();
			else if (keypress.toLowerCase() == pAllowedKeys[i])
				userInput = keypress.toLowerCase();
			if (userInput.length > 0)
			{
				matchedKeypress = true;
				// If K_NOECHO is not in pMode, then output the user's keypress
				if ((pMode & K_NOECHO) == 0)
					console.print(userInput);
				// If K_NOCRLF is not in pMode, then output a CRLF
				if ((pMode & K_NOCRLF) == 0)
					console.crlf();
				break;
			}
		}
	}

	return userInput;
}

// Loads a text file (an .ans or .asc) into an array.  This will first look for
// an .ans version, and if exists, convert to Synchronet colors before loading
// it.  If an .ans doesn't exist, this will look for an .asc version.
//
// Parameters:
//  pFilenameBase: The filename without the extension
//  pMaxNumLines: Optional - The maximum number of lines to load from the text file
//
// Return value: An array containing the lines from the text file
function loadTextFileIntoArray(pFilenameBase, pMaxNumLines)
{
	if (typeof(pFilenameBase) != "string")

	var maxNumLines = (typeof(pMaxNumLines) == "number" ? pMaxNumLines : -1);

	// See if there is a header file that is made for the user's terminal
	// width (areaChgHeader-<width>.ans/asc).  If not, then just go with
	// msgHeader.ans/asc.
	var txtFileExists = true;
	var txtFilenameFullPath = gStartupPath + pFilenameBase;
	var txtFileFilename = "";
	if (file_exists(txtFilenameFullPath + "-" + console.screen_columns + ".ans"))
		txtFileFilename = txtFilenameFullPath + "-" + console.screen_columns + ".ans";
	else if (file_exists(txtFilenameFullPath + "-" + console.screen_columns + ".asc"))
		txtFileFilename = txtFilenameFullPath + "-" + console.screen_columns + ".asc";
	else if (file_exists(txtFilenameFullPath + ".ans"))
		txtFileFilename = txtFilenameFullPath + ".ans";
	else if (file_exists(txtFilenameFullPath + ".asc"))
		txtFileFilename = txtFilenameFullPath + ".asc";
	else
		txtFileExists = false;
	if (txtFileExists)
	{
		var syncConvertedHdrFilename = txtFileFilename;
		// If the user's console doesn't support ANSI and the header file is ANSI,
		// then convert it to Synchronet attribute codes and read that file instead.
		if (!console.term_supports(USER_ANSI) && (getStrAfterPeriod(txtFileFilename).toUpperCase() == "ANS"))
			syncConvertedHdrFilename = txtFilenameFullPath + "_converted.asc";
			if (!file_exists(syncConvertedHdrFilename))
				if (getStrAfterPeriod(txtFileFilename).toUpperCase() == "ANS")
					var dotIdx = txtFileFilename.lastIndexOf(".");
					if (dotIdx >= 0)
					{
						var filenameBase = txtFileFilename.substr(0, dotIdx);
						var cmdLine = system.exec_dir + "ans2asc \"" + txtFileFilename + "\" \""
									+ syncConvertedHdrFilename + "\"";
						// Note: Both system.exec(cmdLine) and
						// bbs.exec(cmdLine, EX_NATIVE, gStartupPath) could be used to
						// execute the command, but system.exec() seems noticeably faster.
						system.exec(cmdLine);
					}
		/*
		// If the header file is ANSI, then convert it to Synchronet attribute
		// codes and read that file instead.  This is done so that this script can
		// accurately get the file line lengths using console.strlen().
		var syncConvertedHdrFilename = txtFilenameFullPath + "_converted.asc";
		if (!file_exists(syncConvertedHdrFilename))
		{
			if (getStrAfterPeriod(txtFileFilename).toUpperCase() == "ANS")
			{
				var filenameBase = txtFileFilename.substr(0, dotIdx);
				var cmdLine = system.exec_dir + "ans2asc \"" + txtFileFilename + "\" \""
				            + syncConvertedHdrFilename + "\"";
				// Note: Both system.exec(cmdLine) and
				// bbs.exec(cmdLine, EX_NATIVE, gStartupPath) could be used to
				// execute the command, but system.exec() seems noticeably faster.
				system.exec(cmdLine);
			}
			else
				syncConvertedHdrFilename = txtFileFilename;
		}
		*/
		// Read the header file into txtFileLines
		var hdrFile = new File(syncConvertedHdrFilename);
		if (hdrFile.open("r"))
		{
			var fileLine = null;
			while (!hdrFile.eof)
			{
				// Read the next line from the header file.
				fileLine = hdrFile.readln(2048);
				// fileLine should be a string, but I've seen some cases
				// where it isn't, so check its type.
				if (typeof(fileLine) != "string")
					continue;

				// Make sure the line isn't longer than the user's terminal
				//if (fileLine.length > console.screen_columns)
				//   fileLine = fileLine.substr(0, console.screen_columns);
				txtFileLines.push(fileLine);

				// If the header array now has the maximum number of lines, then
				// stop reading the header file.
				if (txtFileLines.length == maxNumLines)
					break;
			}
			hdrFile.close();
		}
	}
	return txtFileLines;
}

// Returns the portion (if any) of a string after the period.
//
// Parameters:
//  pStr: The string to extract from
//
// Return value: The portion of the string after the dot, if there is one.  If
//               not, then an empty string will be returned.
function getStrAfterPeriod(pStr)
{
	var strAfterPeriod = "";
	var dotIdx = pStr.lastIndexOf(".");
	if (dotIdx > -1)
		strAfterPeriod = pStr.substr(dotIdx+1);
	return strAfterPeriod;
}

// Adjusts a message's when-written time to the BBS's local time.
//
// Parameters:
//  pMsgHdr: A message header object
//
// Return value: The message's when_written_time adjusted to the BBS's local time.
//               If the message header doesn't have a when_written_time or
//               when_written_zone property, then this function will return -1.
function msgWrittenTimeToLocalBBSTime(pMsgHdr)
{
	if (!pMsgHdr.hasOwnProperty("when_written_time") || !pMsgHdr.hasOwnProperty("when_written_zone_offset") || !pMsgHdr.hasOwnProperty("when_imported_zone_offset"))
	var timeZoneDiffMinutes = pMsgHdr.when_imported_zone_offset - pMsgHdr.when_written_zone_offset;
	//var timeZoneDiffMinutes = pMsgHdr.when_written_zone - system.timezone;
	var timeZoneDiffSeconds = timeZoneDiffMinutes * 60;
	var msgWrittenTimeAdjusted = pMsgHdr.when_written_time + timeZoneDiffSeconds;
	return msgWrittenTimeAdjusted;
}

// Returns a string containing the message group & sub-board numbers and
// descriptions.
//
// Parameters:
//  pMsgbase: A MsgBase object
//
// Return value: A string containing the message group & sub-board numbers and
// descriptions
function getMsgAreaDescStr(pMsgbase)
{
	if (typeof(pMsgbase) != "object")
		return "";
	if (!pMsgbase.is_open)
		return "";

	var descStr = "";
	if (pMsgbase.cfg != null)
	{
		descStr = format("Group/sub-board num: %d, %d; %s - %s", pMsgbase.cfg.grp_number,
		                 pMsgbase.subnum, msg_area.grp_list[pMsgbase.cfg.grp_number].description,
		                 pMsgbase.cfg.description);
	}
	else
	{
		if ((pMsgbase.subnum == -1) || (pMsgbase.subnum == 65535))
			descStr = "Electronic Mail";
		else
			descStr = "Unspecified";
	}
	return descStr;
}

// Lets the sysop edit a user.
//
// Parameters:
//  pUsername: The name of the user to edit
//
// Return value: A function containing the following properties:
//               errorMsg: An error message on failure, or a blank string on success
function editUser(pUsername)
{

	if (typeof(pUsername) != "string")
	{
		retObj.errorMsg = "Given username is not a string";
		return retObj;
	}

	// If the logged-in user is not a sysop, then just return.
	{
		retObj.errorMsg = "Only a sysop can edit a user";
		return retObj;
	}

	// If the user exists, then let the sysop edit the user.
	var userNum = system.matchuser(pUsername);
	if (userNum != 0)
		bbs.exec("*str_cmds uedit " + userNum);
	else
		retObj.errorMsg = "User \"" + pUsername + "\" not found";
	
	return retObj;
}

// Returns an object containing bare minimum properties necessary to
// display an invalid message header.  Additionally, an object returned
// by this function will have an extra property, isBogus, that will be
// a boolean set to true.
//
// Parameters:
//  pSubject: Optional - A string to use as the subject in the bogus message
//            header object
function getBogusMsgHdr(pSubject)
{
	var msgHdr = {
		subject: (typeof(pSubject) == "string" ? pSubject : ""),
		when_imported_time: 0,
		when_written_time: 0,
		when_written_zone: 0,
		date: "Fri, 1 Jan 1960 00:00:00 -0000",
		attr: 0,
		to: "Nobody",
		from: "Nobody",
		number: 0,
		offset: 0,
		isBogus: true
	};
// Returns whether a message is readable to the user, based on its
// header and the sub-board code.
//
// Parameters:
//  pMsgHdr: The header object for the message
//  pSubBoardCode: The internal code for the sub-board the message is in
//
// Return value: Boolean - Whether or not the message is readable for the user
function isReadableMsgHdr(pMsgHdr, pSubBoardCode)
{
	if (pMsgHdr === null)
		return false;
	// Let the sysop see unvalidated messages and private messages but not other users.
			if ((msg_area.sub[pSubBoardCode].is_moderated && ((pMsgHdr.attr & MSG_VALIDATED) == 0)) ||
			    (((pMsgHdr.attr & MSG_PRIVATE) == MSG_PRIVATE) && !userHandleAliasNameMatch(pMsgHdr.to)))
			{
				return false;
			}
		}
	}
	// If the message is deleted, determine whether it should be viewable, based
	// on the system settings.
	if ((pMsgHdr.attr & MSG_DELETE) == MSG_DELETE)
	{
		// If the user is a sysop, check whether sysops can view deleted messages.
		// Otherwise, check whether users can view deleted messages.
		{
			if ((system.settings & SYS_SYSVDELM) == 0)
				return false;
		}
		else
		{
			if ((system.settings & SYS_USRVDELM) == 0)
				return false;
		}
	}
	// The message voting and poll variables were added in sbbsdefs.js for
	// Synchronet 3.17.  Make sure they're defined before referencing them.
	if (typeof(MSG_UPVOTE) != "undefined")
	{
		if ((pMsgHdr.attr & MSG_UPVOTE) == MSG_UPVOTE)
			return false;
	}
	if (typeof(MSG_DOWNVOTE) != "undefined")
	{
		if ((pMsgHdr.attr & MSG_DOWNVOTE) == MSG_DOWNVOTE)
			return false;
	}
	// Don't include polls as being unreadable messages - They just need to have
	// their answer selections read from the header instead of the message body
	/*
	if (typeof(MSG_POLL) != "undefined")
	{
		if ((pMsgHdr.attr & MSG_POLL) == MSG_POLL)
			return false;
	}
// Returns the number of readable messages in a sub-board.
//
// Parameters:
//  pMsgbase: The MsgBase object representing the sub-board
//  pSubBoardCode: The internal code of the sub-board
//
// Return value: The number of readable messages in the sub-board
function numReadableMsgs(pMsgbase, pSubBoardCode)
{
	if ((pMsgbase === null) || !pMsgbase.is_open)
		return 0;

	var numMsgs = 0;
	if (typeof(pMsgbase.get_all_msg_headers) === "function")
	{
		var msgHdrs = pMsgbase.get_all_msg_headers(true);
		for (var msgHdrsProp in msgHdrs)
		{
			if (msgHdrs[msgHdrsProp] == null)
				continue;
			else if (isReadableMsgHdr(msgHdrs[msgHdrsProp], pSubBoardCode))
				++numMsgs;
		}
	}
	else
	{
		var msgHeader;
		for (var i = 0; i < pMsgbase.total_msgs; ++i)
		{
			msgHeader = msgBase.get_msg_header(true, i, false);
			if (msgHeader == null)
				continue;
			else if (isReadableMsgHdr(msgHeader, pSubBoardCode))
				++numMsgs;
		}
	}
	return numMsgs;
}

// Marks or unmarks vote messages as deleted (messages that have voting response data for a message with
// a given message number).
//
// Parameters:
//  pMsgbase: A MessageBase object containing the messages to be deleted
//  pMsgNum: The number of the message for which vote messages should be deleted
//  pMsgID: The ID of the message for which vote messages should be deleted
//  pDoDelete: Boolean: If true, then mark for deletion.  If false, then remove the deleted attribute.
//  pIsMailSub: Boolean - Whether or not it's the personal email area
//
// Return value: An object containing the following properties:
//               numVoteMsgs: The number of vote messages for the given message number
//               toggleVoteMsgsAffected: The number of vote messages that were deleted/undeleted
//               allVoteMsgsAffected: Boolean - Whether or not all vote messages were deleted/undeleted
function toggleVoteMsgsDeleted(pMsgbase, pMsgNum, pMsgID, pDoDelete, pIsEmailSub)
		toggleVoteMsgsAffected: 0,
		allVoteMsgsAffected: true
	};

	if ((pMsgbase === null) || !pMsgbase.is_open)
		return retObj;
	if (typeof(pMsgNum) != "number")
		return retObj;
	if (pIsEmailSub)
		return retObj;

	// This relies on get_all_msg_headers() returning vote messages.  The get_all_msg_headers()
	// function was added in Synchronet 3.16, and the 'true' parameter to get vote headers was
	// added in Synchronet 3.17.
	if (typeof(pMsgbase.get_all_msg_headers) === "function")
	{
		var msgHdrs = pMsgbase.get_all_msg_headers(true);
		for (var msgHdrsProp in msgHdrs)
		{
			if (msgHdrs[msgHdrsProp] == null)
				continue;
			// If this header is a vote header and its thread_back or reply_id matches the given message,
			// then we can delete this message.
			var isVoteMsg = (((msgHdrs[msgHdrsProp].attr & MSG_VOTE) == MSG_VOTE) || ((msgHdrs[msgHdrsProp].attr & MSG_UPVOTE) == MSG_UPVOTE) || ((msgHdrs[msgHdrsProp].attr & MSG_DOWNVOTE) == MSG_DOWNVOTE));
			if (isVoteMsg && (msgHdrs[msgHdrsProp].thread_back == pMsgNum) || (msgHdrs[msgHdrsProp].reply_id == pMsgID))
				var msgWasAffected = false;
				if (pDoDelete)
					msgWasAffected = pMsgbase.remove_msg(false, msgHdrs[msgHdrsProp].number);
				else
				{
					var tmpMsgHdr = pMsgbase.get_msg_header(false, msgHdrs[msgHdrsProp].number, false);
					if (tmpMsgHdr != null)
					{
						if ((tmpMsgHdr.attr & MSG_DELETE) == MSG_DELETE)
						{
							tmpMsgHdr.attr = tmpMsgHdr.attr ^ MSG_DELETE;
							msgWasAffected = pMsgbase.put_msg_header(false, msgHdrs[msgHdrsProp].number, tmpMsgHdr);
						}
						else
							msgWasAffected = true; // No action needed
					}
				}
				retObj.allVoteMsgsAffected = (retObj.allVoteMsgsAffected && msgWasAffected);
				if (msgWasAffected)
					++retObj.toggleVoteMsgsAffected;
/////////////////////////////////////////////////////////////////////////
// Debug helper & error output functions

// Prints information from a message header on the screen, for debugging purpurposes.
//
// Parameters:
//  pMsgHdr: A message header object
function printMsgHdr(pMsgHdr)
{
	for (var prop in pMsgHdr)
	{
		if ((prop == "field_list") && (typeof(pMsgHdr[prop]) == "object"))
		{
			console.print(prop + ":\r\n");
			for (var objI = 0; objI < pMsgHdr[prop].length; ++objI)
			{
				console.print(" " + objI + ":\r\n");
				for (var innerProp in pMsgHdr[prop][objI])
					console.print("  " + innerProp + ": " + pMsgHdr[prop][objI][innerProp] + "\r\n");
			}
		}
		else
			console.print(prop + ": " + pMsgHdr[prop] + "\r\n");
	}
	console.pause();
}
// Closes a poll, using an existing MessageBase object.
//
// Parameters:
//  pMsgbase: A MessageBase object representing the current sub-board.  It
//            must be open.
//  pMsgNum: The message number (not the index)
//
// Return value: Boolean - Whether or not closing the poll succeeded
function closePollWithOpenMsgbase(pMsgbase, pMsgNum)
{
	var pollClosed = false;
	if ((pMsgbase !== null) && pMsgbase.is_open)
	{
		var userNameOrAlias = user.alias;
		// See if the poll was posted using the user's real name instead of
		// their alias
		var msgHdr = pMsgbase.get_msg_header(false, pMsgNum, false);
		if ((msgHdr != null) && ((msgHdr.attr & MSG_POLL) == MSG_POLL))
		{
			if (msgHdr.from.toUpperCase() == user.name.toUpperCase())
				userNameOrAlias = msgHdr.from;
		// Close the poll (the close_poll() method was added in the Synchronet
		// 3.17 build on August 19, 2017)
		pollClosed = pMsgbase.close_poll(pMsgNum, userNameOrAlias);
	}
	return pollClosed;
}

// Closes a poll.
//
// Parameters:
//  pSubBoardCode: The internal code of the sub-board
//  pMsgNum: The message number (not the index)
//
// Return value: Boolean - Whether or not closing the poll succeeded
function closePoll(pSubBoardCode, pMsgNum)
{
	var pollClosed = false;
	var msgbase = new MsgBase(pSubBoardCode);
	if (msgbase.open())
	{
		pollClosed = closePollWithOpenMsgbase(msgbase, pMsgNum);
		msgbase.close();
	}
	return pollClosed;
}

// Gets a message header from the messagebase, either by index (offset) or number.
//
// Parameters:
//  pMsgbase: Optional messagebase object.  If this is provided, then pSubBoardCode is not used.
//  pSubBoardCode: The messagebase sub-board code
//  pByIdx: Boolean - Whether or not to get the message header by index (if false, then by number)
//  pMsgIdxOrNum: The message index or number of the message header to retrieve
//  pExpandFields: Boolean - Whether or not to expand fields for the message header
function getHdrFromMsgbase(pMsgbase, pSubBoardCode, pByIdx, pMsgIdxOrNum, pExpandFields)
{
	var msgbaseIsOpen = false;
	var msgbase = null;
	var msgHdr = null;