diff --git a/xtrn/DDMsgReader/DDMsgReader.cfg b/xtrn/DDMsgReader/DDMsgReader.cfg
index 030858168f6f7a58b68942a89b18c987a94b93d1..5c69c7d33ccc993170014e166ccb555a0b300cb0 100644
--- a/xtrn/DDMsgReader/DDMsgReader.cfg
+++ b/xtrn/DDMsgReader/DDMsgReader.cfg
@@ -41,5 +41,10 @@ displayAvatars=true
 ; Whether or not to right-justify avatars.  False means left-justify.
 rightJustifyAvatars=true
 
+; How to sort the message list:
+; Received: By date/time received (fastest)
+; Written: By date/time written (takes time due to sorting)
+msgListSort=Received
+
 ; The theme file name (for colors, strings, etc.)
 themeFilename=DefaultTheme.cfg
\ No newline at end of file
diff --git a/xtrn/DDMsgReader/DDMsgReader.js b/xtrn/DDMsgReader/DDMsgReader.js
index 9a5e2f013305cae5082084a5d856bf06e0b04138..e39e1c06f109810bcb1ebda364d112da527a7944 100644
--- a/xtrn/DDMsgReader/DDMsgReader.js
+++ b/xtrn/DDMsgReader/DDMsgReader.js
@@ -87,6 +87,15 @@
  *                              message area chooser.  In that scenario, the current
  *                              highlighted sub-board in the other group will be
  *                              the first one.
+ * 2021-03-15 Eric Oulashin     Version 1.42 Beta
+ *                              Started working on converting HTML entities in
+ *                              HTML-formatted messages.
+ * 2021-08-02                   Added the ability to sort the message list by date
+ *                              & time written rather than the import date/time.
+ *                              This is specified in the configuration file via the
+ *                              msgListSort option.
+ * 2022-01-13 Eric Oulashin     Version 1.42
+ *                              Fixed attachment downloading.
  */
 
 
@@ -171,6 +180,7 @@ if (requireFnExists)
 	require("userdefs.js", "USER_UTF8");
 	require("dd_lightbar_menu.js", "DDLightbarMenu");
 	require("mouse_getkey.js", "mouse_getkey");
+	require("html2asc.js", 'html2asc');
 }
 else
 {
@@ -180,8 +190,10 @@ else
 	load("userdefs.js");
 	load("dd_lightbar_menu.js");
 	load("mouse_getkey.js");
+	load("html2asc.js");
 }
 
+
 // This script requires Synchronet version 3.15 or higher.
 // Exit if the Synchronet version is below the minimum.
 if (system.version_num < 31500)
@@ -198,8 +210,8 @@ if (system.version_num < 31500)
 }
 
 // Reader version information
-var READER_VERSION = "1.41";
-var READER_DATE = "2021-02-12";
+var READER_VERSION = "1.42";
+var READER_DATE = "2022-01-13";
 
 // Keyboard key codes for displaying on the screen
 var UP_ARROW = ascii(24);
@@ -357,6 +369,10 @@ const ACTION_QUIT = 29;
 // Definitions for help line refresh parameters for error functions
 const REFRESH_MSG_AREA_CHG_LIGHTBAR_HELP_LINE = 0;
 
+// Message list sort types
+const MSG_LIST_SORT_DATETIME_RECEIVED = 0;
+const MSG_LIST_SORT_DATETIME_WRITTEN = 1;
+
 // Misc. defines
 var ERROR_WAIT_MS = 1500;
 var SEARCH_TIMEOUT_MS = 10000;
@@ -1053,6 +1069,9 @@ function DigDistMsgReader(pSubBoardCode, pScriptArgs)
 	this.displayAvatars = true;
 	this.rightJustifyAvatar = true;
 
+	// Message list sort option
+	this.msgListSort = MSG_LIST_SORT_DATETIME_RECEIVED;
+
 	this.cfgFilename = "DDMsgReader.cfg";
 	// Check the command-line arguments for a custom configuration file name
 	// before reading the configuration file.
@@ -1507,15 +1526,31 @@ function DigDistMsgReader_FilterMsgHdrsIntoHdrsForCurrentSubBoard(pMsgHdrs, pCle
 		this.hdrsForCurrentSubBoard = [];
 		this.hdrsForCurrentSubBoardByMsgNum = {};
 	}
+
 	for (var prop in pMsgHdrs)
 	{
-		// Only add the message header if the message is readable to the user
+		// Only add the message header if the message is readable to the user.
+		// this.hdrsForCurrentSubBoardByMsgNum also has to be populated, but
+		// that's done later in this function, in case this.hdrsForCurrentSubBoard
+		// needs to be sorted.
 		if (isReadableMsgHdr(pMsgHdrs[prop], this.subBoardCode))
 		{
 			this.hdrsForCurrentSubBoard.push(pMsgHdrs[prop]);
-			this.hdrsForCurrentSubBoardByMsgNum[pMsgHdrs[prop].number] = this.hdrsForCurrentSubBoard.length - 1;
+			// This isn't done right here anymore due to the possibility of
+			// this.hdrsForCurrentSubBoard being sorted
+			//this.hdrsForCurrentSubBoardByMsgNum[pMsgHdrs[prop].number] = this.hdrsForCurrentSubBoard.length - 1;
 		}
 	}
+
+	// If the sort type is date/time written, then sort the message header
+	// array as such
+	if (this.msgListSort == MSG_LIST_SORT_DATETIME_WRITTEN)
+		this.hdrsForCurrentSubBoard.sort(sortMessageHdrsByDateTime);
+
+	// Populate this.hdrsForCurrentSubBoardByMsgNum (this needs to be done here
+	// based on the order of this.hdrsForCurrentSubBoard)
+	for (var idx = 0; idx < this.hdrsForCurrentSubBoard.length; ++idx)
+		this.hdrsForCurrentSubBoardByMsgNum[this.hdrsForCurrentSubBoard[idx].number] = idx;
 }
 
 // For the DigDistMsgReader class: Gets the message offset (index) for a message, given
@@ -3681,6 +3716,22 @@ function DigDistMsgReader_ListMessages_Lightbar(pAllowChgSubBoard)
 				DisplayHelpLine(this.msgListLightbarModeHelpLine);
 			}
 		}
+		// S: Sorting options
+		else if (lastUserInputUpper == "S")
+		{
+			if (gIsSysop) // Temporary
+			{
+				console.gotoxy(1, console.screen_rows);
+				console.cleartoeol("\1n");
+				console.gotoxy(1, console.screen_rows);
+				console.print("\1gSort\1n");
+				mswait(ERROR_PAUSE_WAIT_MS);
+			}
+			
+
+			// Refresh the help line
+			DisplayHelpLine(this.msgListLightbarModeHelpLine);
+		}
 	}
 	this.lightbarListSelectedMsgIdx = msgListMenu.selectedItemIdx;
 	this.lightbarListTopMsgIdx = msgListMenu.topItemIdx;
@@ -3749,7 +3800,7 @@ function DigDistMsgReader_CreateLightbarMsgListMenu()
 
 	// Add additional keypresses for quitting the menu's input loop so we can
 	// respond to these keys
-	var additionalQuitKeys = "EeqQgGcC ?0123456789" + CTRL_A + CTRL_D;
+	var additionalQuitKeys = "EeqQgGcCsS ?0123456789" + CTRL_A + CTRL_D;
 	if (this.CanDelete() || this.CanDeleteLastMsg())
 		additionalQuitKeys += KEY_DEL;
 	if (this.CanEdit())
@@ -4081,6 +4132,8 @@ function DigDistMsgReader_PrintMessageInfo(pMsgHeader, pHighlight, pMsgNum, pRet
 			msgIndicatorChar = "\1n\1r\1h\1i" + this.colors.msgListHighlightBkgColor + "*\1n";
 		else if (this.MessageIsSelected(this.subBoardCode, msgNum-1))
 			msgIndicatorChar = "\1n" + this.colors.selectedMsgMarkColor + this.colors.msgListHighlightBkgColor + CHECK_CHAR + "\1n";
+		else if (msgHdrHasAttachmentFlag(pMsgHeader))
+			msgIndicatorChar = "\1n" + this.colors.selectedMsgMarkColor + this.colors.msgListHighlightBkgColor + "A\1n";
 		var fromName = pMsgHeader.from;
 		// If the message was posted anonymously and the logged-in user is
 		// not the sysop, then show "Anonymous" for the 'from' name.
@@ -4109,6 +4162,8 @@ function DigDistMsgReader_PrintMessageInfo(pMsgHeader, pHighlight, pMsgNum, pRet
 			msgIndicatorChar = "\1n\1r\1h\1i*\1n";
 		else if (this.MessageIsSelected(this.subBoardCode, msgNum-1))
 			msgIndicatorChar = "\1n" +  this.colors.selectedMsgMarkColor + CHECK_CHAR + "\1n";
+		else if (msgHdrHasAttachmentFlag(pMsgHeader))
+			msgIndicatorChar = "\1n" +  this.colors.selectedMsgMarkColor + "A\1n";
 
 		// Determine whether to use the normal, "to-user", or "from-user" format string.
 		// The differences are the colors.  Then, output the message information line.
@@ -4673,7 +4728,7 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 					writeMessage = false; // Don't write the current message again
 				break;
 			case this.enhReaderKeys.showHelp: // Show the help screen
-				this.DisplayEnhancedReaderHelp(allowChgMsgArea, msgInfo.attachments.length > 0);
+				this.DisplayEnhancedReaderHelp(allowChgMsgArea, msgInfo.hasAttachments);
 				// If the enhanced message header width is less than the console
 				// width, then clear the screen to remove anything left on the
 				// screen from the help screen.
@@ -5109,14 +5164,21 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 					writeMessage = false; // No need to refresh the message
 				break;
 			case this.enhReaderKeys.downloadAttachments: // Download attachments
-				if (msgInfo.attachments.length > 0)
+				if (msgInfo.hasAttachments)
 				{
 					console.print("\1n");
 					console.gotoxy(1, console.screen_rows);
 					console.crlf();
-					console.print("\1c- Download Attached Files -\1n");
-					// Note: sendAttachedFiles() will output a CRLF at the beginning.
-					sendAttachedFiles(msgInfo.attachments);
+					// If bbs.download_msg_attachments() exists (Synchronet 3.17+), use
+					// the new method.  Otherwise, use the older method.
+					if (typeof(bbs.download_msg_attachments) === "function")
+						allowUserToDownloadMessage_NewInterface(msgHeader, this.subBoardCode);
+					else
+					{
+						console.print("\1c- Download Attached Files -\1n");
+						// Note: sendAttachedFiles() will output a CRLF at the beginning.
+						sendAttachedFiles(msgInfo.attachments);
+					}
 
 					// Refresh things on the screen
 					console.clear("\1n");
@@ -5680,7 +5742,7 @@ function DigDistMsgReader_ReadMessageEnhanced_Traditional(msgHeader, allowChgMsg
 					console.crlf();
 					console.crlf();
 				}
-				this.DisplayEnhancedReaderHelp(allowChgMsgArea, msgAndAttachmentInfo.attachments.length > 0);
+				this.DisplayEnhancedReaderHelp(allowChgMsgArea, msgAndAttachmentInfo.hasAttachments);
 				if (!console.term_supports(USER_ANSI))
 				{
 					console.crlf();
@@ -6049,13 +6111,20 @@ function DigDistMsgReader_ReadMessageEnhanced_Traditional(msgHeader, allowChgMsg
 				}
 				break;
 			case this.enhReaderKeys.downloadAttachments: // Download attachments
-				if (msgAndAttachmentInfo.attachments.length > 0)
+				if (msgAndAttachmentInfo.hasAttachments)
 				{
 					console.print("\1n");
 					console.crlf();
 					console.print("\1c- Download Attached Files -\1n");
-					// Note: sendAttachedFiles() will output a CRLF at the beginning.
-					sendAttachedFiles(msgAndAttachmentInfo.attachments);
+					// If bbs.download_msg_attachments() exists (Synchronet 3.17+), use
+					// the new method.  Otherwise, use the older method.
+					if (typeof(bbs.download_msg_attachments) === "function")
+						allowUserToDownloadMessage_NewInterface(msgHeader, this.subBoardCode);
+					else
+					{
+						// Note: sendAttachedFiles() will output a CRLF at the beginning.
+						sendAttachedFiles(msgAndAttachmentInfo.attachments);
+					}
 
 					// Ensure the message is refreshed on the screen
 					writeMessage = true;
@@ -7589,6 +7658,11 @@ function DigDistMsgReader_ReadConfigFile()
 					this.displayAvatars = (valueUpper == "TRUE");
 				else if (settingUpper == "RIGHTJUSTIFYAVATARS")
 					this.rightJustifyAvatar = (valueUpper == "TRUE");
+				else if (settingUpper == "MSGLISTSORT")
+				{
+					if (valueUpper == "WRITTEN")
+						this.msgListSort = MSG_LIST_SORT_DATETIME_WRITTEN;
+				}
 			}
 		}
 
@@ -7817,14 +7891,14 @@ function DigDistMsgReader_EditExistingMsg(pMsgIndex)
 	}
 
 	// Dump the message body to a temporary file in the node dir
-	//var originalMsgBody = msgbase.get_msg_body(true, pMsgIndex);
+	//var originalMsgBody = msgbase.get_msg_body(true, pMsgIndex, false, false, true, true);
 	var originalMsgBody;
 	var tmpMsgHdr = this.GetMsgHdrByIdx(pMsgIndex, false, msgbase);
 	var msgHdrIsBogus = (tmpMsgHdr.hasOwnProperty("isBogus") ? tmpMsgHdr.isBogus : false);
 	if (msgHdrIsBogus)
-		originalMsgBody = msgbase.get_msg_body(true, pMsgIndex);
+		originalMsgBody = msgbase.get_msg_body(true, pMsgIndex, false, false, true, true);
 	else
-		originalMsgBody = msgbase.get_msg_body(false, tmpMsgHdr.number);
+		originalMsgBody = msgbase.get_msg_body(false, tmpMsgHdr.number, false, false, true, true);
 	var tempFilename = system.node_dir + "DDMsgLister_message.txt";
 	var tmpFile = new File(tempFilename);
 	if (tmpFile.open("w"))
@@ -8256,7 +8330,9 @@ function DigDistMsgReader_GetMsgHdrByIdx(pMsgIdx, pExpandFields, pMsgbase)
 		}
 	}
 	else
+	{
 		msgHdr = getHdrFromMsgbase(pMsgbase, this.subBoardCode, true, pMsgIdx, pExpandFields);
+	}
 	if (msgHdr == null)
 		msgHdr = getBogusMsgHdr();
 	return msgHdr;
@@ -9123,7 +9199,7 @@ function DigDistMsgReader_ReplyToMsg(pMsgHdr, pMsgText, pPrivate, pMsgIdx)
 				}
 				else
 				{
-					var msgText = msgbase.get_msg_body(false, msgHeader.number);
+					var msgText = msgbase.get_msg_body(false, msgHeader.number, false, false, true, true);
 					//quoteFile.write(word_wrap(msgText, 80/*79*/));
 					quoteFile.write(msgText);
 				}
@@ -11842,6 +11918,7 @@ function DigDistMsgReader_GetExtdMsgHdrInfo(pMsgHdr, pKludgeOnly)
 //               numSolidScrollBlocks: The number of solid scrollbar blocks
 //               numNonSolidScrollBlocks: The number of non-solid scrollbar blocks
 //               solidBlockStartRow: The starting row on the screen for the scrollbar blocks
+//               hasAttachments: Boolean - Whether or not the message has attachments
 //               attachments: An array of the attached filenames (as strings)
 //               displayFrame: A Frame object for displaying the message with
 //                             a scrollable interface.  Used when the message
@@ -11864,6 +11941,7 @@ function DigDistMsgReader_GetMsgInfoForEnhancedReader(pMsgHdr, pWordWrap, pDeter
 		numSolidScrollBlocks: 0,
 		numNonSolidScrollBlocks: 0,
 		solidBlockStartRow: 0,
+		hasAttachments: false,
 		attachments: [],
 		displayFrame: null,
 		displayFrameScrollbar: null,
@@ -11880,7 +11958,7 @@ function DigDistMsgReader_GetMsgInfoForEnhancedReader(pMsgHdr, pWordWrap, pDeter
 		var msgbase = new MsgBase(this.subBoardCode);
 		if (msgbase.open())
 		{
-			msgBody = msgbase.get_msg_body(false, pMsgHdr.number);
+			msgBody = msgbase.get_msg_body(false, pMsgHdr.number, false, false, true, true);
 			msgbase.close();
 		}
 		else
@@ -12081,6 +12159,10 @@ function DigDistMsgReader_GetMsgInfoForEnhancedReader(pMsgHdr, pWordWrap, pDeter
 	retObj.numNonSolidScrollBlocks = this.msgAreaHeight - retObj.numSolidScrollBlocks;
 	retObj.solidBlockStartRow = this.msgAreaTop;
 
+	// Set the hasAttachments attribute of retObj.  For Synchronet 3.17 and newer,
+	// the header might have an attachment attribute set, so we can use that.
+	retObj.hasAttachments = (msgHdrHasAttachmentFlag(pMsgHdr) || retObj.attachments.length > 0);
+
 	return retObj;
 }
 
@@ -13335,7 +13417,7 @@ function DigDistMsgReader_ForwardMessage(pMsgHdr, pMsgBody)
 				var msgbase = new MsgBase(this.subBoardCode);
 				if (msgbase.open())
 				{
-					pMsgBody = msgbase.get_msg_body(false, pMsgHdr.number);
+					pMsgBody = msgbase.get_msg_body(false, pMsgHdr.number, false, false, true, true);
 					msgbase.close();
 				}
 				else
@@ -14004,7 +14086,7 @@ function DigDistMsgReader_GetUpvoteAndDownvoteInfo(pMsgHdr)
 					// number, then append the 'user voted' string to the message body.
 					if ((tmpHdrs[tmpProp].thread_back == pMsgHdr.number) || (tmpHdrs[tmpProp].reply_id == pMsgHdr.id))
 					{
-						var tmpMessageBody = msgbase.get_msg_body(false, tmpHdrs[tmpProp].number);
+						var tmpMessageBody = msgbase.get_msg_body(false, tmpHdrs[tmpProp].number, false, false, true, true);
 						if ((tmpHdrs[tmpProp].field_list.length == 0) && (tmpMessageBody.length == 0))
 						{
 							var msgWrittenLocalTime = msgWrittenTimeToLocalBBSTime(tmpHdrs[tmpProp]);
@@ -14172,7 +14254,7 @@ function DigDistMsgReader_GetMsgBody(pMsgHdr)
 	{
 		// If the message is UTF8 and the terminal is not UTF8-capable, then convert
 		// the text to cp437.
-		msgBody = msgbase.get_msg_body(false, pMsgHdr.number);
+		msgBody = msgbase.get_msg_body(false, pMsgHdr.number, false, false, true, true);
 		if (pMsgHdr.hasOwnProperty("is_utf8") && pMsgHdr.is_utf8)
 		{
 			var userConsoleSupportsUTF8 = false;
@@ -14183,6 +14265,14 @@ function DigDistMsgReader_GetMsgBody(pMsgHdr)
 		}
 		// Remove any initial coloring from the message body, which can color the whole message
 		msgBody = removeInitialColorFromMsgBody(msgBody);
+		// For HTML-formatted messages, convert HTML entities
+		//console.print("\1n\r\nSubtype: " + pMsgHdr.text_subtype + "\r\n\1p"); // Temporary
+		if (pMsgHdr.hasOwnProperty("text_subtype") && pMsgHdr.text_subtype.toLowerCase() == "html")
+		{
+			msgBody = html2asc(msgBody);
+			// Remove excessive blank lines after HTML-translation
+			msgBody = msgBody.replace(/\r\n\r\n\r\n/g, '\r\n\r\n');
+		}
 	}
 	msgbase.close();
 
@@ -15835,7 +15925,7 @@ function searchMsgbase(pSubCode, pSearchType, pSearchString, pListingPersonalEma
 				if (readingPersonalEmailFromUser)
 				{
 					matchFn = function(pSearchStr, pMsgHdr, pMsgBase, pSubBoardCode) {
-						var msgText = strip_ctrl(pMsgBase.get_msg_body(false, pMsgHdr.number));
+						var msgText = strip_ctrl(pMsgBase.get_msg_body(false, pMsgHdr.number, false, false, true, true));
 						return gAllPersonalEmailOptSpecified || msgIsFromUser(pMsgHdr);
 						
 					}
@@ -15844,7 +15934,7 @@ function searchMsgbase(pSubCode, pSearchType, pSearchString, pListingPersonalEma
 				{
 					// We're reading mail to the user
 					matchFn = function(pSearchStr, pMsgHdr, pMsgBase, pSubBoardCode) {
-						var msgText = strip_ctrl(pMsgBase.get_msg_body(false, pMsgHdr.number));
+						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.
@@ -15858,7 +15948,7 @@ function searchMsgbase(pSubCode, pSearchType, pSearchString, pListingPersonalEma
 		case SEARCH_KEYWORD:
 			useGetAllMsgHdrs = true;
 			matchFn = function(pSearchStr, pMsgHdr, pMsgBase, pSubBoardCode) {
-				var msgText = strip_ctrl(pMsgBase.get_msg_body(false, pMsgHdr.number));
+				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);
@@ -17706,6 +17796,10 @@ function getSubBoardCodeFromNum(pSubBoardNum)
 }
 
 // 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
@@ -17760,7 +17854,7 @@ function determineMsgAttachments(pMsgHdr, pMsgText, pGetB64Data)
 	{
 		// If there are any attachments, prepend the message text with a message
 		// saying that the message contains attachments.
-		if (retObj.attachments.length > 0)
+		if (msgHdrHasAttachmentFlag(pMsgHdr) || retObj.attachments.length > 0)
 			retObj.msgText = msgHasAttachmentsTxt + retObj.msgText;
 		return retObj;
 	}
@@ -17777,7 +17871,7 @@ function determineMsgAttachments(pMsgHdr, pMsgText, pGetB64Data)
 		//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)
+		if (msgHdrHasAttachmentFlag(pMsgHdr) || retObj.attachments.length > 0)
 			retObj.msgText = msgHasAttachmentsTxt + pMsgText;
 		else
 			retObj.msgText = pMsgText;
@@ -17975,14 +18069,14 @@ function determineMsgAttachments(pMsgHdr, pMsgText, pGetB64Data)
 
 	// If there are any attachments, prepend the message text with a message
 	// saying that the message contains attachments.
-	if (retObj.attachments.length > 0)
+	if (msgHdrHasAttachmentFlag(pMsgHdr) || 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))
+	if ((msgHdrHasAttachmentFlag(pMsgHdr) || (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-A to download.\1n";
@@ -19778,6 +19872,152 @@ function GetScanPtrOrLastMsgNum(pSubCode)
 	return msgNumToReturn;
 }
 
+// Returns whether a message header has one of the attachment flags
+// enabled (for Synchtonet 3.17 or newer).
+//
+// Parameters:
+//  pMsgHdr: A message header (returned from MsgBase.get_msg_header())
+//
+// Return value: Boolean - Whether or not the message has one of the attachment flags
+function msgHdrHasAttachmentFlag(pMsgHdr)
+{
+	if (typeof(pMsgHdr) !== "object" || typeof(pMsgHdr.auxattr) === "undefined")
+		return false;
+
+	var attachmentFlag = false;
+	if (typeof(MSG_FILEATTACH) !== "undefined" && typeof(MSG_MIMEATTACH) !== "undefined")
+		attachmentFlag = (pMsgHdr.auxattr & (MSG_FILEATTACH|MSG_MIMEATTACH)) > 0;
+	return attachmentFlag;
+}
+
+// Allows the user to download a message and its attachments, using the newer
+// Synchronet interface (the function bbs.download_msg_attachments() must exist).
+//
+// Parameters:
+//  pMsgHdr: The message header
+//  pSubCode: The sub-board code that the message is in
+function allowUserToDownloadMessage_NewInterface(pMsgHdr, pSubCode)
+{
+	if (typeof(bbs.download_msg_attachments) !== "function")
+		return;
+	if (typeof(pSubCode) !== "string")
+		return;
+	if (typeof(pMsgHdr) !== "object" || typeof(pMsgHdr.number) == "undefined")
+		return;
+
+	var msgBase = new MsgBase(pSubCode);
+	if (msgBase.open())
+	{
+		// bbs.download_msg_attachments() requires a message header returned
+		// by MsgBase.get_msg_header()
+		var msgHdrForDownloading = msgBase.get_msg_header(false, pMsgHdr.number, false);
+		// Allow the user to download the message
+		if (!console.noyes("Download message", P_NOCRLF))
+		{
+			if (!download_msg(msgHdrForDownloading, msgBase, console.yesno("Plain-text only")))
+				console.print("\1n\r\nFailed\r\n");
+		}
+		// Allow the user to download the attachments
+		console.creturn();
+		bbs.download_msg_attachments(msgHdrForDownloading);
+		msgBase.close();
+		delete msgBase; // Free some memory?
+	}
+}
+
+// From msglist.js - Prompts the user if they want to download the message text
+function download_msg(msg, msgbase, plain_text)
+{
+	var fname = system.temp_dir + "msg_" + msg.number + ".txt";
+	var f = new File(fname);
+	if(!f.open("wb"))
+		return false;
+	var text = msgbase.get_msg_body(msg
+				,/* strip ctrl-a */false
+				,/* dot-stuffing */false
+				,/* tails */true
+				,plain_text);
+	f.write(msg.get_rfc822_header(/* force_update: */false, /* unfold: */false
+		,/* default_content_type */!plain_text));
+	f.writeln(text);
+	f.close();
+	return bbs.send_file(fname);
+}
+
+
+////////// Message list sort functions
+
+// For sorting message headers by date & time
+//
+// Parameters:
+//  msgHdrA: The first message header
+//  msgHdrB: The second message header
+//
+// Return value: -1, 0, or 1, depending on whether header A comes before,
+//               is equal to, or comes after header B
+function sortMessageHdrsByDateTime(msgHdrA, msgHdrB)
+{
+	// Return -1, 0, or 1, depending on whether msgHdrA's date & time comes
+	// before, is equal to, or comes after msgHdrB's date & time
+	// Convert when_written_time to local time before comparing the times
+	var localWrittenTimeA = msgWrittenTimeToLocalBBSTime(msgHdrA);
+	var localWrittenTimeB = msgWrittenTimeToLocalBBSTime(msgHdrB);
+	var yearA = +strftime("%Y", localWrittenTimeA);
+	var monthA = +strftime("%m", localWrittenTimeA);
+	var dayA = +strftime("%d", localWrittenTimeA);
+	var hourA = +strftime("%H", localWrittenTimeA);
+	var minuteA = +strftime("%M", localWrittenTimeA);
+	var secondA = +strftime("%S", localWrittenTimeA);
+	var yearB = +strftime("%Y", localWrittenTimeB);
+	var monthB = +strftime("%m", localWrittenTimeB);
+	var dayB = +strftime("%d", localWrittenTimeB);
+	var hourB = +strftime("%H", localWrittenTimeB);
+	var minuteB = +strftime("%M", localWrittenTimeB);
+	var secondB = +strftime("%S", localWrittenTimeB);
+	if (yearA < yearB)
+		return -1;
+	else if (yearA > yearB)
+		return 1;
+	else
+	{
+		if (monthA < monthB)
+			return -1;
+		else if (monthA > monthB)
+			return 1;
+		else
+		{
+			if (dayA < dayB)
+				return -1;
+			else if (dayA > dayB)
+				return 1;
+			else
+			{
+				if (hourA < hourB)
+					return -1;
+				else if (hourA > hourB)
+					return 1;
+				else
+				{
+					if (minuteA < minuteB)
+						return -1;
+					else if (minuteA > minuteB)
+						return 1;
+					else
+					{
+						if (secondA < secondB)
+							return -1;
+						else if (secondA > secondB)
+							return 1;
+						else
+							return 0;
+					}
+				}
+			}
+		}
+	}
+}
+
+
 // For debugging: Writes some text on the screen at a given location with a given pause.
 //
 // Parameters:
diff --git a/xtrn/DDMsgReader/readme.txt b/xtrn/DDMsgReader/readme.txt
index 1d9fecc41bfa8944d2144b3d6baecfed288dc03a..0419a8558593102f94cfc6e9b8f047e548e68c77 100644
--- a/xtrn/DDMsgReader/readme.txt
+++ b/xtrn/DDMsgReader/readme.txt
@@ -1,6 +1,6 @@
                       Digital Distortion Message Reader
-                                 Version 1.41
-                           Release date: 2021-02-12
+                                 Version 1.42
+                           Release date: 2022-01-13
 
                                      by
 
@@ -99,7 +99,9 @@ The following is a list of features:
 - Allows the user to delete and edit existing messages that they've written, if the
   sub-board supports those operations.
 - Allows the user to download file attachments, whether uploaded to their
-  mailbox on Synchronet or attached to internet emails
+  mailbox on Synchronet or attached to internet emails.  When a message has
+  attachments, it will appear in the message list with an "A" between the
+  message number and sender name.
 - Allows the user to forward a message to an email address or another user
   (using the O key).  This can be useful, for instance, if the user wants to
   send a message in a public sub-board to their personal email for future
@@ -446,11 +448,11 @@ the help screen
 - Text # 662 (i.e., "Download attached file: xyz.txt (500 bytes)"): Used to
 confirm downloading an attached file
 
-the user chooses to downloaded attached files, the reader will prompt to
-confirm downloading of each file.  For the prompt text, the reader will use
-text number 662 from text.dat (in the sbbs/ctrl directory).  If you want to
-customize the text for that confirmation prompt, you would need to open
-text.dat and modify text number 662.
+When the user chooses to downloaded attached files, the reader will prompt to
+confirm downloading.  With Synchronet versions prior to 3.17, for the prompt
+text, the reader will use text number 662 from text.dat (in the sbbs/ctrl
+directory).  If you want to customize the text for that confirmation prompt,
+you would need to open text.dat and modify text number 662.
 
 4. Header ANSI/asc file
 =======================
@@ -667,6 +669,14 @@ rightJustifyAvatars                   Whether or not to right-justify avatars.
                                       Valid values are true and false.  Flase
                                       means to left-justify avatars.
 
+msgListSort                           How to sort the message list.  This can
+                                      be either Received (to sort by date/time
+                                      received) or Written (to sort by
+                                      date/time written).  Received is the
+                                      fastest, as it does not sort the list;
+                                      Written adds time since sorting is
+                                      required.
+
 themeFilename                         The name of the configuration file to
                                       use for colors & string settings
 
diff --git a/xtrn/DDMsgReader/revision_history.txt b/xtrn/DDMsgReader/revision_history.txt
index 1376ef0aa3738b6e2f33f2bfeecba87746b9e87a..b321a94171911dee9a68d603588655c7b15ed124 100644
--- a/xtrn/DDMsgReader/revision_history.txt
+++ b/xtrn/DDMsgReader/revision_history.txt
@@ -5,6 +5,13 @@ Revision History (change log)
 =============================
 Version  Date         Description
 -------  ----         -----------
+1.42     2022-01-13   Fixed attachment downloading.
+                      Also, the first attempt at converting HTML entities in
+                      HTML-formatted messages.
+                      Also, added the ability to sort the message list by date
+                      & time written rather than the import date/time. This is
+                      specified in the configuration file via the msgListSort
+                      option.
 1.41     2021-02-12   Bug fix: When changing to another area with the lightbar
                       interface, if the user's current sub-board is a
                       high-numbered sub-board and they select a message group