Skip to content
Snippets Groups Projects
DDMsgReader.js 635 KiB
Newer Older
	var argVals = new Object();
	// Set default values for parameters that are just true/false values
	argVals.chooseareafirst = false;
	argVals.personalemail = false;
	argVals.personalemailsent = false;
	argVals.verboselogging = false;
	argVals.suppresssearchtypetext = false;

	// Sanity checking for pArgArr - Make sure it's an array
	if ((typeof(pArgArr) != "object") || (typeof(pArgArr.length) != "number"))
		return argVals;

	// Go through pArgArr 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 < pArgArr.length; ++i)
	{
		// We're looking for strings that start with "-", except strings that are
		// only "-".
		if ((typeof(pArgArr[i]) != "string") || (pArgArr[i].length == 0) ||
		    (pArgArr[i].charAt(0) != "-") || (pArgArr[i] == "-"))
		{
			continue;
		}
		// Look for an = and if found, split the string on the =
		equalsIdx = pArgArr[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 = pArgArr[i].substring(1, equalsIdx).toLowerCase();
			argVal = pArgArr[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 = pArgArr[i].substr(1).toLowerCase();
			if ((argName == "chooseareafirst") || (argName == "personalemail") ||
			    (argName == "personalemailsent") || (argName == "allpersonalemail") ||
				(argName == "verboselogging") || (argName == "suppresssearchtypetext"))

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

	return argVals;
}

// 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)
//
// Return value: A string describing the main message attributes
function makeMainMsgAttrStr(pMainMsgAttrs)
{
   var msgAttrStr = "";
   if (typeof(pMainMsgAttrs) == "number")
   {
      for (var prop in gMainMsgAttrStrs)
      {
         if ((pMainMsgAttrs & prop) == prop)
         {
            if (msgAttrStr.length > 0)
               msgAttrStr += ", ";
            msgAttrStr += gMainMsgAttrStrs[prop];
         }
      }
   }
   return msgAttrStr;
}

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

// Returns a string describing network message attributes.  Makes use of the
// gNetMsgAttrStrs object for the network message attributes and description
// strings.
//
// Parameters:
//  pMainMsgAttrs: The bit field for the network message attributes
//                 (normally, the 'netattr' property of a header object)
//
// Return value: A string describing the network message attributes
function makeNetMsgAttrStr(pMainMsgAttrs)
{
   var msgAttrStr = "";
   if (typeof(pMainMsgAttrs) == "number")
   {
      for (var prop in gNetMsgAttrStrs)
      {
         if ((pMainMsgAttrs & prop) == prop)
         {
            if (msgAttrStr.length > 0)
               msgAttrStr += ", ";
            msgAttrStr += gNetMsgAttrStrs[prop];
         }
      }
   }
   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
function writeToSysAndNodeLog(pMessage)
{
	if (typeof(pMessage) != "string")
		return;

	var logMessage = "Digital Distortion Message Reader (" +  user.alias + "): " + pMessage;
	log(LOG_INFO, logMessage);
	bbs.log_str(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;
}

14299 14300 14301 14302 14303 14304 14305 14306 14307 14308 14309 14310 14311 14312 14313 14314 14315 14316 14317 14318 14319 14320 14321 14322 14323 14324 14325 14326 14327 14328 14329 14330 14331 14332 14333 14334 14335 14336 14337 14338 14339 14340 14341 14342 14343 14344 14345 14346 14347 14348 14349 14350 14351 14352 14353 14354 14355 14356 14357 14358 14359 14360 14361 14362 14363 14364 14365 14366 14367 14368 14369 14370 14371 14372 14373 14374 14375 14376 14377 14378 14379 14380 14381 14382 14383 14384 14385 14386 14387 14388 14389 14390 14391 14392 14393 14394 14395 14396 14397 14398 14399 14400 14401 14402 14403 14404 14405 14406 14407 14408 14409 14410 14411 14412 14413 14414 14415 14416 14417 14418 14419 14420 14421 14422 14423 14424 14425 14426 14427 14428 14429 14430 14431 14432 14433 14434 14435 14436 14437 14438 14439 14440 14441 14442 14443 14444 14445 14446 14447 14448 14449 14450 14451 14452 14453 14454 14455 14456 14457 14458 14459 14460 14461 14462 14463 14464 14465 14466 14467 14468 14469 14470 14471 14472 14473 14474 14475 14476 14477 14478 14479 14480 14481 14482 14483 14484 14485 14486 14487 14488 14489 14490 14491 14492 14493 14494 14495 14496 14497 14498 14499 14500 14501 14502 14503 14504 14505 14506 14507 14508 14509 14510 14511 14512 14513 14514 14515 14516 14517 14518 14519 14520 14521 14522 14523 14524 14525 14526 14527 14528 14529 14530 14531 14532 14533 14534 14535 14536 14537 14538 14539 14540 14541 14542 14543 14544 14545 14546 14547 14548 14549 14550 14551 14552 14553 14554 14555 14556 14557 14558 14559 14560 14561 14562 14563 14564 14565 14566 14567 14568 14569 14570 14571 14572 14573 14574 14575 14576 14577 14578 14579 14580 14581 14582 14583 14584 14585 14586 14587 14588 14589 14590 14591 14592 14593 14594 14595 14596 14597 14598 14599 14600 14601 14602 14603 14604 14605 14606 14607 14608 14609 14610 14611 14612 14613 14614 14615 14616 14617 14618 14619 14620 14621 14622 14623 14624 14625 14626 14627 14628 14629 14630 14631 14632 14633 14634 14635 14636 14637 14638 14639 14640 14641 14642 14643 14644 14645 14646 14647 14648 14649 14650 14651 14652 14653 14654 14655 14656 14657 14658 14659 14660 14661 14662 14663 14664 14665 14666 14667 14668 14669 14670 14671 14672 14673 14674 14675 14676 14677 14678 14679 14680 14681 14682 14683 14684 14685 14686 14687 14688 14689 14690 14691 14692 14693 14694 14695 14696 14697 14698 14699 14700 14701 14702 14703 14704 14705 14706 14707 14708 14709 14710 14711 14712 14713 14714 14715 14716 14717 14718 14719 14720 14721 14722 14723 14724 14725 14726 14727 14728 14729 14730 14731 14732 14733 14734 14735 14736 14737 14738 14739 14740 14741 14742 14743 14744 14745 14746 14747 14748 14749 14750 14751 14752 14753 14754 14755 14756 14757 14758 14759 14760 14761 14762 14763 14764 14765 14766 14767 14768 14769 14770 14771 14772 14773 14774 14775 14776 14777 14778 14779 14780 14781 14782 14783 14784 14785 14786 14787 14788 14789 14790 14791 14792 14793 14794 14795 14796 14797 14798 14799 14800 14801 14802 14803 14804 14805 14806 14807 14808 14809 14810 14811 14812 14813 14814 14815 14816 14817 14818 14819 14820
// Separates message text and any attachment data.
//
// 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 = new Object();
	retObj.msgText = "";
	retObj.attachments = [];
	retObj.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
	// inbox directory, then add its filename to the list of attached
	// filenames that will be returned
	var fullyPathedAttachmentFilename = userInboxDir + pMsgHdr.subject;
	if (file_exists(fullyPathedAttachmentFilename))
	{
		retObj.attachments.push({ filename: pMsgHdr.subject,
		                          fullyPathedFilename: fullyPathedAttachmentFilename });
	}

	// The message to prepend onto the message text if the message has attachments
	var msgHasAttachmentsTxt = "\1n\1g\1h- This message contains one or more attachments. Press CTRL-D to download.\1n\r\n"
	                         + "\1n\1g\1h--------------------------------------------------------------------------\1n\r\n";

	// Sanity checking
	if (typeof(pMsgText) != "string")
	{
		// If there are any attachments, prepend the message text with a message
		// saying that the message contains attachments.
		if (retObj.attachments.length > 0)
			retObj.msgText = msgHasAttachmentsTxt + retObj.msgText;
		return retObj;
	}

	// If the message text doesn't include a line starting with -- and a
	// line starting with "Content-type:", then then just return the
	// the same text in retObj.
	//var hasMultiParts = /--\S+\s*Content-Type:/.test(pMsgText);
	//var hasMultiParts = ((dashDashIdx > -1) && (/Content-Type/.test(pMsgText)));
	var dashDashIdx = pMsgText.indexOf("--");
	var hasMultiParts = ((dashDashIdx > -1) && (pMsgText.indexOf("Content-Type", dashDashIdx+1) > dashDashIdx));
	if (!hasMultiParts)
	{
		//retObj.msgText = pMsgText;
		// If there are any attachments, prepend the message text with a message
		// saying that the message contains attachments.
		if (retObj.attachments.length > 0)
			retObj.msgText = msgHasAttachmentsTxt + pMsgText;
		else
			retObj.msgText = pMsgText;
		return retObj;
	}

	var getB64Data = true;
	if (typeof(pGetB64Data) == "boolean")
		getB64Data = pGetB64Data;

	// Look in the message text for a line starting with -- followed by some characters,
	// then whitespace
	var sepMatches = /--\S+\s/.exec(pMsgText);
	var msgSeparator = sepMatches[0];
	// If the last character in msgSeparator is a whitepsace character, then
	// remove it.
	if (/\s/.test(msgSeparator.substr(msgSeparator.length-1, 1)))
		msgSeparator = msgSeparator.substr(0, msgSeparator.length-1);
	var contentType = ""; // The content type of the current section
	var lastContentType = ""; // The content type of the last section
	var contentEncodingType = "";
	var sepIdx = 0;
	var lastSepIdx = -1;
	var lastContentTypeIdx = -1;
	var lastContentEncodingTypeIdx = -1;
	var startIdx = 0;
	var gotMessageText = false; // In case the message has both text/plain & text/html
	while ((sepIdx = pMsgText.indexOf(msgSeparator, startIdx)) >= 0)
	{
		var contentEncodingTypeIdx = -1;
		// Look for a "Content-Type:" from the starting index
		var contentTypeIdx = pMsgText.indexOf("Content-Type: ", startIdx+msgSeparator.length);
		if (contentTypeIdx > -1)
		{
			// Extract the content-type string up to a newline or 15 characters
			// if there's no newline
			var newlineIdx = pMsgText.indexOf("\n", contentTypeIdx+14);
			contentType = pMsgText.substring(contentTypeIdx+14, newlineIdx > -1 ? newlineIdx : contentTypeIdx+29);
			// If the last character is whitespace (i.e., a newline), then remove it.
			if (/\s/.test(contentType.substr(contentType.length-1, 1)))
				contentType = contentType.substr(0, contentType.length-1);

			// Update the start index for looking for the next message separator string
			// - This should be after the "Content-type:" value.
			startIdx = contentTypeIdx + contentType.length;
		}
		else
		{
			// No "Content-Type:" string was found
			// Update the start index for looking for the next message separator string
			startIdx = sepIdx + msgSeparator.length;
		}

		if ((lastSepIdx > -1) && (lastContentTypeIdx > -1))
		{
			// msgTextSearchStartIdx stores the index of where to start looking
			// for the message text.  It could be lastContentTypeIdx, or it could
			// be the content encoding type index if the "Content encoding type"
			// text is found for the current message part.
			var msgTextSearchStartIdx = lastContentTypeIdx;

			// Look for "Content-Transfer-Encoding:" right after the content type
			// and extract the content encoding type string
			contentEncodingTypeIdx = pMsgText.indexOf("Content-Transfer-Encoding:", lastContentTypeIdx);
			// If "Content-Transfer-Encoding:" wasn't found after the content type,
			// then look just before the content type, but after the last separator
			// string.
			if (contentEncodingTypeIdx == -1)
				contentEncodingTypeIdx = pMsgText.indexOf("Content-Transfer-Encoding:", lastSepIdx);
			// If the next "Content-Encoding-Type" is after the current section,
			// then this section doesn't have a content type, so blank it out.
			if (contentEncodingTypeIdx > sepIdx)
			{
				contentEncodingTypeIdx = -1;
				contentEncodingType = "";
			}
			else
			{
				msgTextSearchStartIdx = contentEncodingTypeIdx;
				// Extract the content encoding type
				var newlineIdx = pMsgText.indexOf("\n", contentEncodingTypeIdx+26);
				contentEncodingType = pMsgText.substring(contentEncodingTypeIdx, newlineIdx);
				// If the last character is whitespace (i.e., a newline), then remove it.
				if (/\s/.test(contentEncodingType.substr(contentEncodingType.length-1, 1)))
					contentEncodingType = contentEncodingType.substr(0, contentEncodingType.length-1);
				// Update startIdx based on the length of the "content encoding type" string
				startIdx += contentEncodingType.length;
				// Now, store just the content type in contentEncodingType (i.e., "base64").
				contentEncodingType = contentEncodingType.substr(27).toLowerCase();
			}

			// Look for the message text
			var contentTypeSearchIdx = -1;
			//if ((contentTypeSearchIdx = lastContentType.indexOf("text/plain")) > -1)
			if ((contentTypeSearchIdx = lastContentType.indexOf("text/")) > -1)
			{
				if (!gotMessageText)
				{
					var newlineIdx = pMsgText.indexOf("\n", msgTextSearchStartIdx); // Used to be lastContentTypeIdx
					if (newlineIdx > -1)
						retObj.msgText = pMsgText.substring(newlineIdx+1, sepIdx);
					else
						retObj.msgText = pMsgText.substring(lastSepIdx, sepIdx);
					gotMessageText = true;
				}
			}
			else
			{
				// Look for a filename in the content-type specification
				// If it doesn't contain the filename, then we'll have to look on the
				// next line for the filename.
				var attachmentFilename = "";
				var matches = /name="(.*)"/.exec(lastContentType);
				if (matches != null)
				{
					if (matches.length >= 2)
						attachmentFilename = matches[1];
				}
				if (attachmentFilename.length == 0)
				{
					// Look for the filename on the next line
					var newlineIdx = pMsgText.indexOf("\n", lastContentTypeIdx);
					if (newlineIdx > -1)
					{
						// 1000 chars should be enough
						var nextLine = pMsgText.substr(newlineIdx+1, 1000);
						var matches = /name="(.*)"/.exec(nextLine);
						if (matches != null)
						{
							if (matches.length >= 2)
								attachmentFilename = matches[1];
						}
					}
				}
				// If we got a filename, then extract the base64-encoded file data.
				if (attachmentFilename.length > 0)
				{
					var fileInfo = { filename: attachmentFilename,
					                 fullyPathedFilename: gFileAttachDir + attachmentFilename };
					// Only extract the base64-encoded data if getB64Data is true
					// and the current section's encoding type was actually specified
					// as base64.
					if (getB64Data && (contentEncodingType == "base64"))
					{
						// There should be 2 newlines before the base64 data
						// TODO: There's a bug here where sometimes it isn't getting
						// the correct section for base64 data.  The code later that
						// looks for an existing filename in the attachments is sort
						// of a way around that though.
						var lineSeparator = ascii(13) + ascii(10);
						var twoNLIdx = pMsgText.indexOf(lineSeparator + lineSeparator, lastContentTypeIdx);
						if (twoNLIdx > -1)
						{
							// Get the base64-encoded data for the current file from the message,
							// and remove the newline & carriage return characters and whitespace
							// from it.
							fileInfo.B64Data = pMsgText.substring(twoNLIdx+2, sepIdx);
							fileInfo.B64Data = fileInfo.B64Data.replace(new RegExp(ascii(13) + "|" + ascii(10), "g"), "").trim();

							// Update the start index for looking for the next message separator
							// string
							startIdx = twoNLIdx;
						}
					}
					// Add the file attachment information to the return object.
					// If there is already an entry with the filename, then replace
					// that one; otherwise, append it.
					var fileExists = false;
					for (var fileIdx = 0; (fileIdx < retObj.attachments.length) && !fileExists; ++fileIdx)
					{
						if (retObj.attachments[fileIdx].filename == fileInfo.filename)
						{
							fileExists = true;
							if (getB64Data && fileInfo.hasOwnProperty("B64Data"))
								retObj.attachments[fileIdx].B64Data = fileInfo.B64Data;
						}
					}
					if (!fileExists)
						retObj.attachments.push(fileInfo);
				}
			}
		}

		lastContentType = contentType;
		lastSepIdx = sepIdx;
		lastContentTypeIdx = contentTypeIdx;
		lastContentEncodingTypeIdx = contentEncodingTypeIdx;

		// The end of the message will have the message separator string with
		// "--" appended to it.  If we've reached that point, then we know we
		// can stop.
		if (pMsgText.substr(sepIdx, msgSeparator.length+2) == msgSeparator + "--")
			break;
	}

	// If there are any attachments, prepend the message text with a message
	// saying that the message contains attachments.
	if (retObj.attachments.length > 0)
		retObj.msgText = msgHasAttachmentsTxt + retObj.msgText;

	// If there are attachments and the message text is more than will fit on the
	// screen (75% of the console height to account for the ), then append text at
	// the end to say there are attachments.
	var maxNumCharsOnScreen = 79 * Math.floor(console.screen_rows * 0.75);
	if ((retObj.attachments.length > 0) && (retObj.msgText.length > maxNumCharsOnScreen))
	{
		retObj.msgText += "\1n\r\n\1g\1h--------------------------------------------------------------------------\1n\r\n";
		retObj.msgText += "\1g\1h- This message contains one or more attachments. Press CTRL-D to download.\1n";
	}

	return retObj;
}

// Allows the user to download files that were attached to a message.  Takes an
// array of file information given by determineMsgAttachments().
//
// Parameters:
//  pAttachments: An array of file attachment information returned by
//                determineMsgAttachments()
//                            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
function sendAttachedFiles(pAttachments)
{
	if (Object.prototype.toString.call(pAttachments) !== "[object Array]")
		return;

	// Synchronet doesn't allow batch downloading of files that aren't in the
	// file database, so we have to send each one at a time. :(

	// Get the file download confirmation text from text.dat
	// 662: "\r\nDownload attached file: \1w%s\1b (%s bytes)"
	var DLPromptTextOrig = bbs.text(DownloadAttachedFileQ);

	var anyErrors = false;
	// For each item in the array, allow the user to download the attachment.
	var fileNum = 1;
	pAttachments.forEach(function(fileInfo) {
		console.print("\1n");
		console.crlf();

		// If the file doesn't exist and base64 data is available for the file,
		// then save it to the temporary attachments directory.
		// Note that we need to save the file first in order to get the file's size
		// to display in the confirmation prompt to download the file.
		// errorMsg will contain an error if something went wrong creating the
		// temporary attachments directory, etc.
		var errorMsg = "";
		var savedFileToBBS = false; // If we base64-decoded the file, we'll want to delete it after it's sent.
		if (!file_exists(fileInfo.fullyPathedFilename))
		{
			if (fileInfo.hasOwnProperty("B64Data"))
			{
				// If the temporary attachments directory doesn't exist,
				// then create it.
				var attachmentDirExists = true; // Will be false if it couldn't be created
				if (!file_isdir(gFileAttachDir))
				{
					// If it's a file rather than a directory, then remove it
					// before creating it as a directory.
					if (file_exists(gFileAttachDir))
						file_remove(gFileAttachDir);
					attachmentDirExists = mkdir(gFileAttachDir);
				}

				// Write the file to the BBS machine
				if (attachmentDirExists)
				{
					var attachedFile = new File(fileInfo.fullyPathedFilename);
					if (attachedFile.open("wb"))
					{
						attachedFile.base64 = true;
						if (!attachedFile.write(fileInfo.B64Data))
							errorMsg = "\1h\1g* \1n\1cCan't send " + quoteStrWithSpaces(fileInfo.filename) + " - Failed to save it to the BBS!";
						attachedFile.close();
						// Saved the file to the temporary attachments directory (even if it failed
						// to write, there's probably still an empty file there).
						savedFileToBBS = true;
					}
					else
						errorMsg = "\1h\1g* \1n\1cFailed to save " + quoteStrWithSpaces(fileInfo.filename) + "!";
				}
				else
					errorMsg = "\1h\1g* \1n\1cFailed to create temporary directory on the BBS!";
			}
			else
				errorMsg = "\1h\1g* \1n\1cCan't send " + quoteStrWithSpaces(fileInfo.filename) + " because it doesn't exist or wasn't encoded in a known format";
		}
		// If we can send the file, then prompt the user for confirmation, and if they
		// answer yes, then send it.
		// Note that we needed to save the file first in order to get the file's size
		// to display in the confirmation prompt.
		if (errorMsg.length == 0)
		{
			// Print the file number
			console.print("\1n\1cFile \1g" + fileNum + "\1c of \1g" + pAttachments.length + "\1n");
			console.crlf();
			// Prompt the user to confirm whether they want to download the
			// file.  If the user chooses yes, then send it.
			var fileSize = Math.round(file_size(fileInfo.fullyPathedFilename));
			var DLPromptText = format(DLPromptTextOrig, fileInfo.filename, fileSize);
			if (console.yesno(DLPromptText))
				bbs.send_file(fileInfo.fullyPathedFilename);

			// If the file was base64-decoded and saved to the BBS machine (as opposed to
			// being in the user's mailbox), then delete the file.
			if (savedFileToBBS)
				file_remove(fileInfo.fullyPathedFilename);
		}
		else
		{
			// There was an error creating the temporary attachment directory, etc., so
			// display the error and pause to let the user read it.
			//console.print(errorMsg);
			//console.putmsg(word_wrap(errorMsg, console.screen_columns-1, errorMsg.length, false));
			//console.crlf();
			var errMsgLines = lfexpand(word_wrap(errorMsg, console.screen_columns-1, errorMsg.length, false)).split("\r\n");
			console.print("\1n");
			for (var errorIdx = 0; errorIdx < errMsgLines.length; ++errorIdx)
			{
				console.print(errMsgLines[errorIdx]);
				console.crlf();
			}
			console.pause();
		}

		++fileNum;
	});

	// If the temporary attachments directory exists, then delete it.
	if (file_exists(gFileAttachDir))
		deltree(gFileAttachDir);
}

// 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 = "";
	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;
		case 37: // reply To position
			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;
		// TODO: Try to figure out what other types there are
		default:
			fieldTypeLabel = pFieldListType.toString();
			break;
	}

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

	return fieldTypeLabel;
}

/////////////////////////////////////////////////////////////////////////
// Debug helper function

// Writes some text on the screen at a given location with a given pause.
//
// Parameters:
//  pX: The column number on the screen at which to write the message
//  pY: The row number on the screen at which to write the message
//  pText: The text to write
//  pPauseMS: The pause time, in milliseconds
//  pClearLineAttrib: Optional - The color/attribute to clear the line with.
//                    If not specified or null is specified, defaults to normal attribute.
//  pClearLineAfter: Whether or not to clear the line again after the message is dispayed and
//                   the pause occurred.  This is optional.
function writeWithPause(pX, pY, pText, pPauseMS, pClearLineAttrib, pClearLineAfter)
{
   var clearLineAttrib = "\1n";
   if ((pClearLineAttrib != null) && (typeof(pClearLineAttrib) == "string"))
      clearLineAttrib = pClearLineAttrib;
   console.gotoxy(pX, pY);
   console.cleartoeol(clearLineAttrib);
   console.print(pText);
	if (pPauseMS > 0)
		mswait(pPauseMS);
   if (pClearLineAfter)
   {
      console.gotoxy(pX, pY);
      console.cleartoeol(clearLineAttrib);
   }
}