Skip to content
Snippets Groups Projects
DDMsgReader.js 765 KiB
Newer Older
// Return value: A string describing the search type value
function searchTypeValToStr(pSearchType)
{
	if (typeof(pSearchType) != "number")
		return "Unknown (not a number)";

	var searchTypeStr = "";
	switch (pSearchType)
	{
		case SEARCH_NONE:
			searchTypeStr = "None (SEARCH_NONE)";
			break;
		case SEARCH_KEYWORD:
			searchTypeStr = "Keyword (SEARCH_KEYWORD)";
			break;
		case SEARCH_FROM_NAME:
			searchTypeStr = "'From' name (SEARCH_FROM_NAME)";
			break;
		case SEARCH_TO_NAME_CUR_MSG_AREA:
			searchTypeStr = "'To' name (SEARCH_TO_NAME_CUR_MSG_AREA)";
			break;
		case SEARCH_TO_USER_CUR_MSG_AREA:
			searchTypeStr = "To you (SEARCH_TO_USER_CUR_MSG_AREA)";
			break;
		case SEARCH_MSG_NEWSCAN:
			searchTypeStr = "New message scan (SEARCH_MSG_NEWSCAN)";
			break;
		case SEARCH_MSG_NEWSCAN_CUR_SUB:
			searchTypeStr = "New in current message area (SEARCH_MSG_NEWSCAN_CUR_SUB)";
			break;
		case SEARCH_MSG_NEWSCAN_CUR_GRP:
			searchTypeStr = "New in current message group (SEARCH_MSG_NEWSCAN_CUR_GRP)";
			break;
		case SEARCH_MSG_NEWSCAN_ALL:
			searchTypeStr = "Newscan - All (SEARCH_MSG_NEWSCAN_ALL)";
			break;
		case SEARCH_TO_USER_NEW_SCAN:
			searchTypeStr = "To You new scan (SEARCH_TO_USER_NEW_SCAN)";
			break;
		case SEARCH_TO_USER_NEW_SCAN_CUR_SUB:
			searchTypeStr = "To You new scan, current sub-board (SEARCH_TO_USER_NEW_SCAN_CUR_SUB)";
			break;
		case SEARCH_TO_USER_NEW_SCAN_CUR_GRP:
			searchTypeStr = "To You new scan, current group (SEARCH_TO_USER_NEW_SCAN_CUR_GRP)";
			break;
		case SEARCH_TO_USER_NEW_SCAN_ALL:
			searchTypeStr = "To You new scan, all sub-boards (SEARCH_TO_USER_NEW_SCAN_ALL)";
			break;
		case SEARCH_ALL_TO_USER_SCAN:
			searchTypeStr = "All To You scan (SEARCH_ALL_TO_USER_SCAN)";
			break;
		default:
			searchTypeStr = "Unknown (" + pSearchType + ")";
			break;
	}
	return searchTypeStr;
}

// This function converts a reader mode string to one of the defined reader mode
// value constants.  If the passed-in mode string is unknown, then the return value
// will be -1.
//
// Parameters:
//  pModeStr: A string describing a reader mode ("read", "reader", "list", "lister")
//
// Return value: An integer representing the reader mode value (READER_MODE_READ,
//               READER_MODE_LIST), or -1 if the passed-in mode string is unknown.
function readerModeStrToVal(pModeStr)
{
   if (typeof(pModeStr) != "string")
      return -1;

   var readerModeInt = -1;
   var modeStr = pModeStr.toLowerCase();
   if ((modeStr == "read") || (modeStr == "reader"))
      readerModeInt = READER_MODE_READ;
   else if ((modeStr == "list") || (modeStr == "lister"))
      readerModeInt = READER_MODE_LIST;
   return readerModeInt;
}

// This function returns a boolean to signify whether or not the user's
// terminal supports both high-ASCII characters and ANSI codes.
function canDoHighASCIIAndANSI()
{
	//return (console.term_supports(USER_ANSI) && (user.settings & USER_NO_EXASCII == 0));
	return (console.term_supports(USER_ANSI));
}

// Searches a given range in an open message base and returns an object with arrays
// containing the message headers (0-based indexed and indexed by message number)
// with the message headers of any found messages.
//
// Parameters:
//  pSubCode: The internal code of the message sub-board
//  pSearchType: The type of search to do (one of the SEARCH_ values)
//  pSearchString: The string to search for.
//  pListingPersonalEmailFromUser: Optional boolean - Whether or not we're listing
//                                 personal email sent by the user.  This defaults
//                                 to false.
//  pStartIndex: The starting message index (0-based).  Optional; defaults to 0.
//  pEndIndex: One past the last message index.  Optional; defaults to the total number
//             of messages.
//
// Return value: An object with the following arrays:
//               indexed: A 0-based indexed array of message headers
function searchMsgbase(pSubCode, pSearchType, pSearchString, pListingPersonalEmailFromUser, pStartIndex, pEndIndex)
	if ((pSubCode != "mail") && ((typeof(pSearchString) != "string") || !searchTypePopulatesSearchResults(pSearchType)))
		return msgHeaders;

	var msgbase = new MsgBase(pSubCode);
	if (!msgbase.open())
		return msgHeaders;

	var endMsgIndex = msgbase.total_msgs;
		if ((pStartIndex >= 0) && (pStartIndex < msgbase.total_msgs))
			startMsgIndex = pStartIndex;
	}
	if (typeof(pEndIndex) == "number")
	{
		if ((pEndIndex >= 0) && (pEndIndex > startMsgIndex) && (pEndIndex <= msgbase.total_msgs))
			endMsgIndex = pEndIndex;
	}

	// Define a search function for the message field we're going to search
	var readingPersonalEmailFromUser = (typeof(pListingPersonalEmailFromUser) == "boolean" ? pListingPersonalEmailFromUser : false);
	var matchFn = null;
	var useGetAllMsgHdrs = false;
	switch (pSearchType)
	{
		// It might seem odd to have SEARCH_NONE in here, but it's here because
		// when reading personal email, we need to search for messages only to
		// the current user.
		case SEARCH_NONE:
			if (pSubCode == "mail")
			{
				// Set up the match function slightly differently depending on whether
				// we're looking for mail from the current user or to the current user.
				if (readingPersonalEmailFromUser)
				{
					matchFn = function(pSearchStr, pMsgHdr, pMsgBase, pSubBoardCode) {
						var msgText = strip_ctrl(pMsgBase.get_msg_body(false, pMsgHdr.number, false, false, true, true));
						return gAllPersonalEmailOptSpecified || msgIsFromUser(pMsgHdr);
					// We're reading mail to the user
					matchFn = function(pSearchStr, pMsgHdr, pMsgBase, pSubBoardCode) {
						var msgText = strip_ctrl(pMsgBase.get_msg_body(false, pMsgHdr.number, false, false, true, true));
						var msgMatchesCriteria = (gAllPersonalEmailOptSpecified || msgIsToUserByNum(pMsgHdr));
						// If only new/unread personal email is to be displayed, then check
						// that the message has not been read.
						if (gCmdLineArgVals.onlynewpersonalemail)
							msgMatchesCriteria = (msgMatchesCriteria && ((pMsgHdr.attr & MSG_READ) == 0));
						return msgMatchesCriteria;
			useGetAllMsgHdrs = true;
			matchFn = function(pSearchStr, pMsgHdr, pMsgBase, pSubBoardCode) {
				var msgText = strip_ctrl(pMsgBase.get_msg_body(false, pMsgHdr.number, false, false, true, true));
				var keywordFound = ((pMsgHdr.subject.toUpperCase().indexOf(pSearchStr) > -1) || (msgText.toUpperCase().indexOf(pSearchStr) > -1));
				if (pSubBoardCode == "mail")
					return keywordFound && msgIsToUserByNum(pMsgHdr);
			useGetAllMsgHdrs = true;
			matchFn = function(pSearchStr, pMsgHdr, pMsgBase, pSubBoardCode) {
				var fromNameFound = (pMsgHdr.from.toUpperCase() == pSearchStr.toUpperCase());
				if (pSubBoardCode == "mail")
					return fromNameFound && (gAllPersonalEmailOptSpecified || msgIsToUserByNum(pMsgHdr));
				else
					return fromNameFound;
			}
			break;
		case SEARCH_TO_NAME_CUR_MSG_AREA:
			useGetAllMsgHdrs = true;
			matchFn = function(pSearchStr, pMsgHdr, pMsgBase, pSubBoardCode) {
				return (pMsgHdr.to.toUpperCase() == pSearchStr);
			}
			break;
		case SEARCH_TO_USER_CUR_MSG_AREA:
			useGetAllMsgHdrs = true;
		case SEARCH_ALL_TO_USER_SCAN:
			matchFn = function(pSearchStr, pMsgHdr, pMsgBase, pSubBoardCode) {
				// See if the message is not marked as deleted and the 'To' name
				// matches the user's handle, alias, and/or username.
				return (((pMsgHdr.attr & MSG_DELETE) == 0) && userNameHandleAliasMatch(pMsgHdr.to));
			}
			break;
		case SEARCH_TO_USER_NEW_SCAN:
		case SEARCH_TO_USER_NEW_SCAN_CUR_SUB:
		case SEARCH_TO_USER_NEW_SCAN_CUR_GRP:
		case SEARCH_TO_USER_NEW_SCAN_ALL:
			if (pSubCode != "mail")
			{
				// If pStartIndex or pEndIndex aren't specified, then set
				// startMsgIndex to the scan pointer and endMsgIndex to one
				// past the index of the last message in the sub-board
				if (typeof(pStartIndex) != "number")
				{
					// First, write some messages to the log if verbose logging is enabled
					if (gCmdLineArgVals.verboselogging)
					{
						writeToSysAndNodeLog("New-to-user scan for " +
						                     subBoardGrpAndName(pSubCode) + " -- Scan pointer: " +
						                     msg_area.sub[pSubCode].scan_ptr);
					//startMsgIndex = absMsgNumToIdx(msgbase, msg_area.sub[pSubCode].last_read);
					startMsgIndex = absMsgNumToIdx(msgbase, GetScanPtrOrLastMsgNum(pSubCode));
					if (startMsgIndex == -1)
					{
						msg_area.sub[pSubCode].scan_ptr = 0;
						startMsgIndex = 0;
					}
						// If this message has been read, then start at the next message.
						var startMsgHeader = msgbase.get_msg_header(true, startMsgIndex, false);
						else
						{
							if ((startMsgHeader.attr & MSG_READ) == MSG_READ)
								++startMsgIndex;
						}
					endMsgIndex = (msgbase.total_msgs > 0 ? msgbase.total_msgs : 0);
			}
			matchFn = function(pSearchStr, pMsgHdr, pMsgBase, pSubBoardCode) {
				// Note: This assumes pSubBoardCode is not "mail" (personal mail).
				// See if the message 'To' name matches the user's handle, alias,
				// and/or username and is not marked as deleted and is unread.
				return (((pMsgHdr.attr & MSG_DELETE) == 0) && ((pMsgHdr.attr & MSG_READ) == 0) && userNameHandleAliasMatch(pMsgHdr.to));
			}
			break;
		case SEARCH_MSG_NEWSCAN:
		case SEARCH_MSG_NEWSCAN_CUR_SUB:
		case SEARCH_MSG_NEWSCAN_CUR_GRP:
		case SEARCH_MSG_NEWSCAN_ALL:
			matchFn = function(pSearchStr, pMsgHdr, pMsgBase, pSubBoardCode) {
				// Note: This assumes pSubBoardCode is not "mail" (personal mail).
				// Get the offset of the last read message and compare it with the
				// offset of the given message header
				var lastReadMsgHdr = pMsgBase.get_msg_header(false, msg_area.sub[pSubBoardCode].last_read, false);
				var lastReadMsgOffset = 0;
				if (lastReadMsgHdr != null)
					lastReadMsgOffset = msgNumToIdxFromMsgbase(pSubBoardCode, lastReadMsgHdr.number);
				return (msgNumToIdxFromMsgbase(pSubBoardCode, pMsgHdr.number) > lastReadMsgOffset);
		// If get_all_msg_headers exists as a function, then use it.  Otherwise,
		// iterate through all message offsets and get the headers.  We want to
		// use get_all_msg_hdrs() if possible because that will include information
		// about how many votes each message got (up/downvotes for regular
		// messages or who voted for which options for poll messages).
		if (useGetAllMsgHdrs && (typeof(msgbase.get_all_msg_headers) === "function"))
			// Pass false to get_all_msg_headers() to tell it not to include vote messages
			// (the parameter was introduced in Synchronet 3.17+)
			var tmpHdrs = msgbase.get_all_msg_headers(false);
			// Re-do startMsgIndex and endMsgIndex based on the message headers we got
			startMsgIndex = 0;
			endMsgIndex = msgbase.total_msgs;
			if (typeof(pStartIndex) == "number")
				if ((pStartIndex >= 0) && (pStartIndex < tmpHdrs.length))
					startMsgIndex = pStartIndex;
			}
			if (typeof(pEndIndex) == "number")
			{
				if ((pEndIndex >= 0) && (pEndIndex > startMsgIndex) && (pEndIndex <= tmpHdrs.length))
					endMsgIndex = pEndIndex;
			}
			// Search the message headers
			var msgIdx = 0;
			for (var prop in tmpHdrs)
			{
				// Only add the message header if the message is readable to the user
				// and msgIdx is within bounds
				if ((msgIdx >= startMsgIndex) && (msgIdx < endMsgIndex) && isReadableMsgHdr(tmpHdrs[prop], pSubCode))
						if (matchFn(pSearchString, tmpHdrs[prop], msgbase, pSubCode))
							msgHeaders.indexed.push(tmpHdrs[prop]);
					}
				}
				++msgIdx;
			}
		}
		else
		{
			for (var msgIdx = startMsgIndex; msgIdx < endMsgIndex; ++msgIdx)
			{
				var msgHeader = msgbase.get_msg_header(true, msgIdx, false);
				// I've seen situations where the message header object is null for
				// some reason, so check that before running the search function.
				if (msgHeader != null)
				{
					if (matchFn(pSearchString, msgHeader, msgbase, pSubCode))
// Returns whether or not a message is to the current user (either the current
// logged-in user or the user specified by the userNum command-line argument)
// and is not deleted.
//
// Parameters:
//  pMsgHdr: A message header object
//
// Return value: Boolean - Whether or not the message is to the user and is not
//               deleted.
function msgIsToUserByNum(pMsgHdr)
	// Return false if  the message is marked as deleted
	if ((pMsgHdr.attr & MSG_DELETE) == MSG_DELETE)
		return false;

	var msgIsToUser = false;
	// If an alternate user number was specified on the command line, then use that
	// user information.  Otherwise, use the current logged-in user.
	if (gCmdLineArgVals.hasOwnProperty("altUserNum"))
		msgIsToUser = (pMsgHdr.to_ext == gCmdLineArgVals.altUserNum);
	else
		msgIsToUser = (pMsgHdr.to_ext == user.number);
	return msgIsToUser;
// Returns whether or not a message is from the current user (either the current
// logged-in user or the user specified by the userNum command-line argument)
// and is not deleted.
//
// Parameters:
//  pMsgHdr: A message header object
//
// Return value: Boolean - Whether or not the message is from the logged-in user
//               and is not deleted.
function msgIsFromUser(pMsgHdr)
{
	if (typeof(pMsgHdr) != "object")
		return false;
	// Return false if  the message is marked as deleted
	if ((pMsgHdr.attr & MSG_DELETE) == MSG_DELETE)
		return false;

	var isFromUser = false;

	// If an alternate user number was specified on the command line, then use that
	// user information.  Otherwise, use the current logged-in user.

	if (pMsgHdr.hasOwnProperty("from_ext"))
	{
		if (gCmdLineArgVals.hasOwnProperty("altUserNum"))
			isFromUser = (pMsgHdr.from_ext == gCmdLineArgVals.altUserNum);
		else
			isFromUser = (pMsgHdr.from_ext == user.number);
	}
	else
	{
		var hdrFromUpper = pMsgHdr.from.toUpperCase();
		if (gCmdLineArgVals.hasOwnProperty("altUserName") && gCmdLineArgVals.hasOwnProperty("altUserAlias"))
			isFromUser = ((hdrFromUpper == gCmdLineArgVals.altUserAlias.toUpperCase()) || (hdrFromUpper == gCmdLineArgVals.altUserName.toUpperCase()));
		else
			isFromUser = ((hdrFromUpper == user.alias.toUpperCase()) || (hdrFromUpper == user.name.toUpperCase()));
	}

	return isFromUser;
// Given some text, this converts ANSI color codes to Synchronet codes and
// removes unwanted ANSI codes (such as cursor movement codes, etc.).
//
// Parameters:
//  pText: A string to process
//
// Return value: A version of the string with Synchronet color codes converted to
//               Synchronet attribute codes and unwanted ANSI codes removed
function cvtANSIToSyncAndRemoveUnwantedANSI(pText)
{
	// Attributes
	var txt = pText.replace(/\[0[mM]/g, "\1n"); // All attributes off
	txt = txt.replace(/\[1[mM]/g, "\1h"); // Bold on (use high intensity)
	txt = txt.replace(/\[5[mM]/g, "\1i"); // Blink on
	// Foreground colors
	txt = txt.replace(/\[30[mM]/g, "\1k"); // Black foreground
	txt = txt.replace(/\[31[mM]/g, "\1r"); // Red foreground
	txt = txt.replace(/\[32[mM]/g, "\1g"); // Green foreground
	txt = txt.replace(/\[33[mM]/g, "\1y"); // Yellow foreground
	txt = txt.replace(/\[34[mM]/g, "\1b"); // Blue foreground
	txt = txt.replace(/\[35[mM]/g, "\1m"); // Magenta foreground
	txt = txt.replace(/\[36[mM]/g, "\1c"); // Cyan foreground
	txt = txt.replace(/\[37[mM]/g, "\1w"); // White foreground
	// Background colors
	txt = txt.replace(/\[40[mM]/g, "\1" + "0"); // Black background
	txt = txt.replace(/\[41[mM]/g, "\1" + "1"); // Red background
	txt = txt.replace(/\[42[mM]/g, "\1" + "2"); // Green background
	txt = txt.replace(/\[43[mM]/g, "\1" + "3"); // Yellow background
	txt = txt.replace(/\[44[mM]/g, "\1" + "4"); // Blue background
	txt = txt.replace(/\[45[mM]/g, "\1" + "5"); // Magenta background
	txt = txt.replace(/\[46[mM]/g, "\1" + "6"); // Cyan background
	txt = txt.replace(/\[47[mM]/g, "\1" + "7"); // White background
	// Convert ;-delimited modes (such as alue;...;Valuem)
	txt = ANSIMultiConvertToSyncCodes(txt);
	// Remove ANSI codes that are not wanted (such as moving the cursor, etc.)
	txt = txt.replace(/\[[0-9]+[aA]/g, ""); // Cursor up
	txt = txt.replace(/\[[0-9]+[bB]/g, ""); // Cursor down
	txt = txt.replace(/\[[0-9]+[cC]/g, ""); // Cursor forward
	txt = txt.replace(/\[[0-9]+[dD]/g, ""); // Cursor backward
	txt = txt.replace(/\[[0-9]+;[0-9]+[hH]/g, ""); // Cursor position
	txt = txt.replace(/\[[0-9]+;[0-9]+[fF]/g, ""); // Cursor position
	txt = txt.replace(/\[[sS]/g, ""); // Restore cursor position
	txt = txt.replace(/\[2[jJ]/g, ""); // Erase display
	txt = txt.replace(/\[[kK]/g, ""); // Erase line
	txt = txt.replace(/\[=[0-9]+[hH]/g, ""); // Set various screen modes
	txt = txt.replace(/\[=[0-9]+[lL]/g, ""); // Reset various screen modes
	return txt;
}

// Returns whether a given message group index & sub-board index (or the current ones,
// based on bbs.curgrp and bbs.cursub) are for the last message sub-board on the system.
//
// Parameters:
//  pGrpIdx: Optional - The index of the message group.  If not specified, this will
//           default to bbs.curgrp.  If bbs.curgrp is not defined in that case,
//           then this method will return false.
//  pSubIdx: Optional - The index of the message sub-board.  If not specified, this will
//           default to bbs.cursub.  If bbs.cursub is not defined in that case,
//           then this method will return false.
//
// Return value: Boolean - Whether or not the current/given message group index & sub-board
//               index are for the last message sub-board on the system.  If there
//               are any issues with any of the values (including bbs.curgrp or
//               bbs.cursub), this method will return false.
function curMsgSubBoardIsLast(pGrpIdx, pSubIdx)
{
   var curGrp = 0;
   if (typeof(pGrpIdx) == "number")
      curGrp = pGrpIdx;
   else if (typeof(bbs.curgrp) == "number")
      curGrp = bbs.curgrp;
   else
      return false;
   var curSub = 0;
   if (typeof(pSubIdx) == "number")
      curSub = pSubIdx;
   else if (typeof(bbs.cursub) == "number")
      curSub = bbs.cursub;
   else
      return false;

   return (curGrp == msg_area.grp_list.length-1) && (curSub == msg_area.grp_list[msg_area.grp_list.length-1].sub_list.length-1);
}

// Parses arguments, where each argument in the given array is in the format
// -arg=val.  If the value is the string "true" or "false", then the value will
// be a boolean.  Otherwise, the value will be a string.
//
// Parameters:
//  argv: An array of strings containing values in the format -arg=val
//
// Return value: An object containing the argument values.  The index will be
//               the argument names, converted to lowercase.  The values will
//               be either the string argument values or boolean values, depending
//               on the formats of the arguments passed in.
	var argVals = getDefaultArgParseObj();
	// Sanity checking for argv - Make sure it's an array
	if ((typeof(argv) != "object") || (typeof(argv.length) != "number"))
	// First, test the arguments to see if they're in a format as called by
	// Synchronet for loadable modules
	argVals = parseLoadableModuleArgs(argv);
	if (argVals.loadableModule)
		return argVals;

	// Go through argv looking for strings in the format -arg=val and parse them
	// into objects in the argVals array.
	var equalsIdx = 0;
	var argName = "";
	var argVal = "";
	var argValLower = ""; // For case-insensitive "true"/"false" matching
	var argValIsTrue = false;
	for (var i = 0; i < argv.length; ++i)
	{
		// We're looking for strings that start with "-", except strings that are
		// only "-".
		if ((typeof(argv[i]) != "string") || (argv[i].length == 0) ||
		    (argv[i].charAt(0) != "-") || (argv[i] == "-"))
		// Look for an = and if found, split the string on the =
		equalsIdx = argv[i].indexOf("=");
		// If a = is found, then split on it and add the argument name & value
		// to the array.  Otherwise (if the = is not found), then treat the
		// argument as a boolean and set it to true (to enable an option).
		if (equalsIdx > -1)
		{
			argName = argv[i].substring(1, equalsIdx).toLowerCase();
			argVal = argv[i].substr(equalsIdx+1);
			argValLower = argVal.toLowerCase();
			// If the argument value is the word "true" or "false", then add it as a
			// boolean.  Otherwise, add it as a string.
			argValIsTrue = (argValLower == "true");
			if (argValIsTrue || (argValLower == "false"))
				argVals[argName] = argValIsTrue;
			else
				argVals[argName] = argVal;
		}
		else // An equals sign (=) was not found.  Add as a boolean set to true to enable the option.
		{
			argName = argv[i].substr(1).toLowerCase();
			if ((argName == "chooseareafirst") || (argName == "personalemail") ||
			    (argName == "personalemailsent") || (argName == "allpersonalemail") ||
			    (argName == "verboselogging") || (argName == "suppresssearchtypetext") ||
			    (argName == "onlynewpersonalemail"))

	// Sanity checking
	// If the arguments include personalEmail and personalEmail is enabled,
	// then check to see if a search type was specified - If so, only allow
	// keyword search and from name search.
	if (argVals.hasOwnProperty("personalemail") && argVals.personalemail)
	{
		// If a search type is specified, only allow keyword search & from name
		// search
		if (argVals.hasOwnProperty("search"))
		{
			var searchValLower = argVals.search.toLowerCase();
			if ((searchValLower != "keyword_search") && (searchValLower != "from_name_search"))
				delete argVals.search;
		}
	}
	// If the arguments include userNum, make sure the value is all digits.  If so,
	// add altUserNum to the arguments as a number type for user matching when looking
	// for personal email to the user.
	if (argVals.hasOwnProperty("usernum"))
	{
		if (/^[0-9]+$/.test(argVals.usernum))
		{
			var specifiedUserNum = Number(argVals.usernum);
			// If the specified number is different than the current logged-in
			// user, then load the other user account and read their name and
			// alias and also store their user number in the arg vals as a
			// number.
			if (specifiedUserNum != user.number)
			{
				var theUser = new User(specifiedUserNum);
				argVals.altUserNum = theUser.number;
				argVals.altUserName = theUser.name;
				argVals.altUserAlias = theUser.alias;
			}
			else
				delete argVals.usernum;
		}
		else
			delete argVals.usernum;
	}
// Helper for parseArgs() - If we get loadable module arguments from Synchronet, this parses them.
//
// Parameters:
//  argv: An array of strings containing values in the format -arg=val
//
// Return value: An object containing the argument values.  The property "loadableModule"
//               in this object will be a boolean that specifies whether or not loadable
//               module arguments were specified.
function parseLoadableModuleArgs(argv)
{
	var argVals = getDefaultArgParseObj();

	var allDigitsRegex = /^[0-9]+$/; // To check if a string consists only of digits
	var arg1Lower = argv[0].toLowerCase();
	// 2 args, and the 1st arg is a sub-board code & the 2nd arg is numeric & is
	// the value of SCAN_INDEX: List messages in the specified sub-board (List Msgs module)
	if (argv.length == 2 && subBoardCodeIsValid(arg1Lower) && allDigitsRegex.test(argv[1]) && +(argv[1]) === SCAN_INDEX)
	{
		argVals.loadableModule = true;
		argVals.subboard = arg1Lower;
		argVals.startmode = "list";
	}
	// 2 parameters: Whether or not all subs are being scanned (0 or 1), and the scan mode (numeric)
	// (Scan Subs module)
	else if (argv.length == 2 && /^[0-1]$/.test(argv[0]) && allDigitsRegex.test(argv[1]) && isValidScanMode(+(argv[1])))
	{
		argVals.loadableModule = true;
		var scanAllSubs = (argv[0] == "1");
		var scanMode = +(argv[1]);
		if ((scanMode & SCAN_NEW) == SCAN_NEW)
		{
			// Newscan
			// TODO: SCAN_CONST and SCAN_BACK could be used along with SCAN_NEW
			// SCAN_CONST: Continuous message scanning
			// SCAN_BACK: Display most recent message if none new
			argVals.search = "new_msg_scan";
			argVals.suppresssearchtypetext = true;
			if (scanAllSubs)
				argVals.search = "new_msg_scan_all";
		}
		else if (((scanMode & SCAN_TOYOU) == SCAN_TOYOU) || ((scanMode & SCAN_UNREAD) == SCAN_UNREAD))
		{
			// Scan for messages posted to you/new messages posted to you
			argVals.startmode = "read";
			argVals.search = "to_user_new_scan";
			argVals.suppresssearchtypetext = true;
			if (scanAllSubs)
				argVals.search = "to_user_new_scan_all";
		}
		else if ((scanMode & SCAN_FIND) == SCAN_FIND)
		{
			argVals.search = "keyword_search";
			argVals.startmode = "list";
		}
		else
		{
			// Stock Synchronet functionality.  Includes SCAN_CONST and SCAN_BACK.
			bbs.scan_subs(scanMode, scanAllSubs);
			argVals.exitNow = true;
		}
	}
	// Scan Msgs loadable module support:
	// 1. The sub-board internal code
	// 2. The scan mode (numeric)
	// 3. Optional: Search text (if any)
	else if ((argv.length == 2 || argv.length == 3) && subBoardCodeIsValid(arg1Lower) && allDigitsRegex.test(argv[1]) && isValidScanMode(+(argv[1])))
	{
		argVals.loadableModule = true;
		var scanMode = +(argv[1]);
		if (scanMode == SCAN_READ)
		{
			argVals.subboard = arg1Lower;
			argVals.startmode = "read";
			// If a search string is specified (as the 3rd command-line argument),
			// then use it for a search scan.
			if (argv.length == 3 && argv[2] != "")
			{
				argVals.search = "keyword_search";
				argVals.searchtext = argv[2];
			}
		}
		else if (scanMode == SCAN_FIND)
		{
			argVals.subboard = arg1Lower;
			argVals.search = "keyword_search";
			argVals.startmode = "list";
			if (argv.length == 3 && argv[2] != "")
				argVals.searchtext = argv[2];
		}
		// Some modes that the Digital Distortion Message Reader doesn't handle yet: Use
		// Synchronet's stock behavior.
		else
		{
			if (argv.length == 3)
				bbs.scan_msgs(arg1Lower, scanMode, argv[2]);
			else
				bbs.scan_msgs(arg1Lower, scanMode);
			argVals.exitNow = true;
		}
	}
	// Reading personal email: 'Which' mailbox & user number (both numeric) (Read Mail module)
	else if ((argv.length == 2 || argv.length == 3) && allDigitsRegex.test(argv[0]) && allDigitsRegex.test(argv[1]) && isValidUserNum(+(argv[1])))
	{
		argVals.loadableModule = true;
		var whichMailbox = +(argv[0]);
		var userNum = +(argv[1]);
		// The optional 3rd argument in this case is mode bits.  See if we should only display
		// new (unread) personal email.
		var newMailOnly = false;
		if (argv.length >= 3)
		{
			var modeVal = +(argv[2]);
			newMailOnly = (((modeVal & SCAN_FIND) == SCAN_FIND) && ((modeVal & LM_UNREAD) == LM_UNREAD));
		}
		// Start in list mode
		argVals.startmode = "list"; // "read"
		// Note: MAIL_ANY won't be passed to this script.
		switch (whichMailbox)
		{
			case MAIL_YOUR: // Mail sent to you
				argVals.personalemail = true;
				argVals.usernum = argv[1];
				if (newMailOnly)
					argVals.onlynewpersonalemail = true;
				break;
			case MAIL_SENT: // Mail you have sent
				argVals.personalemailsent = true;
				argVals.usernum = argv[1];
				break;
			case MAIL_ALL:
				argVals.allpersonalemail = true;
				break;
			default:
				bbs.read_mail(whichMailbox);
				argVals.exitNow = true;
				break;
		}
	}
	return argVals;
}
// Returns an object with default settings for argument parsing
function getDefaultArgParseObj()
{
	return {
		chooseareafirst: false,
		personalemail: false,
		onlynewpersonalemail: false,
		personalemailsent: false,
		verboselogging: false,
		suppresssearchtypetext: false,
		loadableModule: false,
		exitNow: false
	};
}

// Returns a string describing all message attributes (main, auxiliary, and net).
//
// Parameters:
//  pMsgHdr: A message header object.  
//
// Return value: A string describing all of the message attributes
function makeAllMsgAttrStr(pMsgHdr)
{
	if ((pMsgHdr == null) || (typeof(pMsgHdr) != "object"))
		return "";
	var msgAttrStr = makeMainMsgAttrStr(pMsgHdr.attr);
	var auxAttrStr = makeAuxMsgAttrStr(pMsgHdr.auxattr);
	if (auxAttrStr.length > 0)
	{
		if (msgAttrStr.length > 0)
			msgAttrStr += ", ";
		msgAttrStr += auxAttrStr;
	}
	var netAttrStr = makeNetMsgAttrStr(pMsgHdr.netattr);
	if (netAttrStr.length > 0)
	{
		if (msgAttrStr.length > 0)
			msgAttrStr += ", ";
		msgAttrStr += netAttrStr;
	}
	return msgAttrStr;
}

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

// 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;
}

// Separates message text and any attachment data.
// This is for message headers generated in version 3.16 and earlier of Synchronet.
// In version 3.17 and later, Synchronet added auxiliary attributes (auxattr)
// MSG_FILEATTACH and MSG_MIMEATTACH as well as the function bbs.download_msg_attachments(msgHdr)
// which will allow a user to download attachments in a message.
//
// Parameters:
//  pMsgHdr: The message header object
//  pMsgText: The text of a message
//  pGetB64Data: Optional boolean - Whether or not to get the Base64-encoded
//               data for base64-encoded attachments (i.e., in multi-part MIME
//               emails).  Defaults to true.
//
// Return value: An object containing the following properties:
//               msgText: The text of the message, without any of the
//                        attachment base64-encoded data, etc.  If
//                        the message doesn't have any attachments, then
//                        this will likely be the same as pMsgText.
//               attachments: An array of objects containing the following properties
//                            for each attachment:
//                            B64Data: Base64-encoded file data - Only for attachments
//                                     that were attached as base64 in the message (i.e.,
//                                     in a multi-part MIME message).  If the attachment
//                                     was uploaded to the user's Synchronet mailbox,
//                                     then the object won't have the B64Data property.
//                            filename: The name of the attached file
//                            fullyPathedFilename: The full path & filename of the
//                                                 attached file saved on the BBS machine
//               errorMsg: An error message if anything went wrong.  If
//                         nothing went wrong, this will be an empty string.
function determineMsgAttachments(pMsgHdr, pMsgText, pGetB64Data)
{
	var retObj = {
		msgText: "",
		attachments: [],
		errorMsg: ""
	};

	// Keep track of the user's inbox directory:  sbbs/data/file/<userNum>.in
	var userInboxDir = backslash(backslash(system.data_dir + "file") + format("%04d.in", user.number));
	// If the message subject is a filename that exists in the user's