diff --git a/exec/load/attr_conv.js b/exec/load/attr_conv.js
index 2c47200e36ab7f7885fdd71cd35af66a782375a4..12f403991974e69b971c72b5413dfa5ae076d9d0 100644
--- a/exec/load/attr_conv.js
+++ b/exec/load/attr_conv.js
@@ -1135,4 +1135,15 @@ function syncAttrCodesToANSI(pText)
 	}
 	else
 		return pText; // No Synchronet-style attribute codes found, so just return the text.
+}
+
+// Returns whether a string has any Synchronet attribute codes
+//
+// Parameters:
+//  pStr: the string to check
+//
+// Return value: Boolean - Whether or not the string has any Synchronet attribute codes
+function hasSyncAttrCodes(pStr)
+{
+	return (pStr.search(/\1[krgybmcwhifn\-_01234567]/i) > -1);
 }
\ No newline at end of file
diff --git a/xtrn/DDMsgReader/DDMsgReader.js b/xtrn/DDMsgReader/DDMsgReader.js
index 8ea091d0030d6e75e11da7988f0d355bda63174d..070dfbc74cc28d6a300e0cb8251552cfd2ab9247 100644
--- a/xtrn/DDMsgReader/DDMsgReader.js
+++ b/xtrn/DDMsgReader/DDMsgReader.js
@@ -27,8 +27,13 @@
  * 2022-03-23 Eric Oulashin     Version 1.47a
  *                              Now calls bbs.edit_msg() to edit an existing message (if
  *                              that function exists - It was added in Synchronet 3.18).
- * 2022-03-23 Eric Oulashin     Version 1.48
+ * 2022-06-12 Eric Oulashin     Version 1.48
  *                              Improved display of ANSI messages via the use of the Graphic object
+ * 2022-06-13 Eric Oulashin     Version 1.49
+ *                              Refactor: Simplified saving a message to BBS machine for sysop
+ *                              (as-is, less processing); removed attachment stuff for pre-Synchronet
+ *                              3.17; moved hasSyncAttrCodes() to attr_conv.js because that's where it
+ *                              needs to be.
  */
 
 // TODO: In the message list, add the ability to search with / similar to my area chooser
@@ -132,8 +137,8 @@ var ansiterm = require("ansiterm_lib.js", 'expand_ctrl_a');
 
 
 // Reader version information
-var READER_VERSION = "1.48";
-var READER_DATE = "2022-06-12";
+var READER_VERSION = "1.49";
+var READER_DATE = "2022-06-13";
 
 // Keyboard key codes for displaying on the screen
 var UP_ARROW = ascii(24);
@@ -4516,6 +4521,12 @@ function DigDistMsgReader_ReadMessageEnhanced(pOffset, pAllowChgArea)
 		graphic.width = this.msgAreaWidth;
 		messageText = graphic.MSG;
 	}
+	if (msgHdrHasAttachmentFlag(msgHeader))
+	{
+		messageText = "\1n\1g\1h- This message contains one or more attachments. Press CTRL-A to download.\1n\r\n"
+		            + "\1n\1g\1h--------------------------------------------------------------------------\1n\r\n"
+		            + messageText;
+	}
 	var useScrollingInterface = this.scrollingReaderInterface && console.term_supports(USER_ANSI);
 	// If we switch to the non-scrolling interface here, then the calling method should
 	// refresh the enhanced reader help line on the screen.
@@ -4529,9 +4540,7 @@ function DigDistMsgReader_ReadMessageEnhanced(pOffset, pAllowChgArea)
 	{
 		// If the message has ANSI codes, remove any ANSI clear screen codes from the message text
 		if (msgHasANSICodes)
-		{
 			messageText = messageText.replace(/\u001b\[[012]J/gi, "");
-		}
 		retObj = this.ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgArea, messageText, msgHasANSICodes, pOffset);
 	}
 	else
@@ -4580,7 +4589,6 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 	var msgInfo = this.GetMsgInfoForEnhancedReader(msgHeader, true, true, true, messageText, msgHasANSICodes);
 
 	var topMsgLineIdxForLastPage = msgInfo.topMsgLineIdxForLastPage;
-	var msgFractionShown = msgInfo.msgFractionShown;
 	var numSolidScrollBlocks = msgInfo.numSolidScrollBlocks;
 	var numNonSolidScrollBlocks = msgInfo.numNonSolidScrollBlocks;
 	var solidBlockStartRow = msgInfo.solidBlockStartRow;
@@ -4614,6 +4622,8 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 		solidBlockLastStartRow = solidBlockStartRow;
 		console.gotoxy(1, console.screen_rows);
 	}
+
+	var msgHasAttachments = msgHdrHasAttachmentFlag(msgHeader);
 	// User input loop
 	var continueOn = true;
 	while (continueOn)
@@ -4664,8 +4674,7 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 				// calling method will go to the next message/sub-board.
 				// Otherwise (if the message was not deleted), refresh the
 				// last 2 lines of the message on the screen.
-				var msgWasDeleted = this.PromptAndDeleteMessage(pOffset, promptPos, true, this.msgAreaWidth,
-																true, msgInfo.attachments);
+				var msgWasDeleted = this.PromptAndDeleteMessage(pOffset, promptPos, true, this.msgAreaWidth, true);
 				if (msgWasDeleted)
 				{
 					var msgSearchObj = this.LookForNextOrPriorNonDeletedMsg(pOffset);
@@ -4788,7 +4797,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.hasAttachments);
+				this.DisplayEnhancedReaderHelp(allowChgMsgArea, msgHasAttachments);
 				// 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.
@@ -4819,8 +4828,8 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 						var extdMsgHdr = msgbase.get_msg_header(false, msgHeader.number, true);
 						msgbase.close();
 						// Let the user reply to the message.
-						var replyRetObj = this.ReplyToMsg(extdMsgHdr, msgInfo.msgText, privateReply, pOffset);
-														  retObj.userReplied = replyRetObj.postSucceeded;
+						var replyRetObj = this.ReplyToMsg(extdMsgHdr, messageText, privateReply, pOffset);
+						retObj.userReplied = replyRetObj.postSucceeded;
 						//retObj.msgNotReadable = replyRetObj.msgWasDeleted;
 						var msgWasDeleted = replyRetObj.msgWasDeleted;
 						//if (retObj.msgNotReadable)
@@ -5224,21 +5233,12 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 					writeMessage = false; // No need to refresh the message
 				break;
 			case this.enhReaderKeys.downloadAttachments: // Download attachments
-				if (msgInfo.hasAttachments)
+				if (msgHasAttachments)
 				{
 					console.print("\1n");
 					console.gotoxy(1, console.screen_rows);
 					console.crlf();
-					// 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);
-					}
+					allowUserToDownloadMessage_NewInterface(msgHeader, this.subBoardCode);
 
 					// Refresh things on the screen
 					console.clear("\1n");
@@ -5267,13 +5267,18 @@ function DigDistMsgReader_ReadMessageEnhanced_Scrollable(msgHeader, allowChgMsgA
 					console.print("\1n");
 					if (filename.length > 0)
 					{
-						//var saveMsgRetObj = this.SaveMsgToFile(msgHeader, filename, true, msgInfo.messageLines);
-						var saveMsgRetObj = this.SaveMsgToFile(msgHeader, filename, true);
+						var saveMsgRetObj = this.SaveMsgToFile(msgHeader, filename);
 						console.gotoxy(promptPos);
 						console.cleartoeol("\1n");
 						console.gotoxy(promptPos);
 						if (saveMsgRetObj.succeeded)
-							console.print("\1n\1cThe message has been saved.\1n");
+						{
+							var statusMsg = "\1n\1cThe message has been saved.";
+							if (msgHdrHasAttachmentFlag(msgHeader))
+								statusMsg += " Attachments not saved.";
+							statusMsg += "\1n";
+							console.print(statusMsg);
+						}
 						else
 							console.print("\1n\1y\1hFailed: " + saveMsgRetObj.errorMsg + "\1n");
 						mswait(ERROR_PAUSE_WAIT_MS);
@@ -5651,16 +5656,16 @@ function DigDistMsgReader_ReadMessageEnhanced_Traditional(msgHeader, allowChgMsg
 		nextAction: ACTION_NONE,
 		refreshEnhancedRdrHelpLine: false
 	};
-	
-	// Separate the message text from any attachments in the message.
-	var msgAndAttachmentInfo = determineMsgAttachments(msgHeader, messageText, true);
+
+	var msgHasAttachments = msgHdrHasAttachmentFlag(msgHeader);
+
 	// Only interpret @-codes if the user is reading personal email.  There
 	// are many @-codes that do some action such as move the cursor, execute a
 	// script, etc., and I don't want users on message networks to do anything
 	// malicious to users on other BBSes.
 	if (this.readingPersonalEmail)
-		msgAndAttachmentInfo.msgText = replaceAtCodesInStr(msgAndAttachmentInfo.msgText); // Or this.ParseMsgAtCodes(msgAndAttachmentInfo.msgText, msgHeader) to replace only some @ codes
-	var msgTextWrapped = word_wrap(msgAndAttachmentInfo.msgText, console.screen_columns-1);
+		messageText = replaceAtCodesInStr(messageText); // Or this.ParseMsgAtCodes(messageText, msgHeader) to replace only some @ codes
+	var msgTextWrapped = (msgHasANSICodes ? messageText : word_wrap(messageText, console.screen_columns-1));
 
 	// Generate the key help text
 	var keyHelpText = "\1n\1c\1h#\1n\1b, \1c\1hLeft\1n\1b, \1c\1hRight\1n\1b, ";
@@ -5724,8 +5729,6 @@ function DigDistMsgReader_ReadMessageEnhanced_Traditional(msgHeader, allowChgMsg
 				// calling method will go to the next message/sub-board.
 				// Otherwise (if the message was not deleted), refresh the
 				// last 2 lines of the message on the screen.
-				// TODO: For the DeleteMessage() call, pass the array of file
-				// attachments for it to delete (i.e., msgInfo.attachments)
 				var msgWasDeleted = this.PromptAndDeleteMessage(pOffset);
 				if (msgWasDeleted)
 				{
@@ -5802,7 +5805,7 @@ function DigDistMsgReader_ReadMessageEnhanced_Traditional(msgHeader, allowChgMsg
 					console.crlf();
 					console.crlf();
 				}
-				this.DisplayEnhancedReaderHelp(allowChgMsgArea, msgAndAttachmentInfo.hasAttachments);
+				this.DisplayEnhancedReaderHelp(allowChgMsgArea, msgHasAttachments);
 				if (!console.term_supports(USER_ANSI))
 				{
 					console.crlf();
@@ -5832,7 +5835,7 @@ function DigDistMsgReader_ReadMessageEnhanced_Traditional(msgHeader, allowChgMsg
 						var extdMsgHdr = msgbase.get_msg_header(false, msgHeader.number, true);
 						msgbase.close();
 						// Let the user reply to the message
-						var replyRetObj = this.ReplyToMsg(extdMsgHdr, msgAndAttachmentInfo.msgText, privateReply, pOffset);
+						var replyRetObj = this.ReplyToMsg(extdMsgHdr, messageText, privateReply, pOffset);
 						retObj.userReplied = replyRetObj.postSucceeded;
 						//retObj.msgNotReadable = replyRetObj.msgWasDeleted;
 						var msgWasDeleted = replyRetObj.msgWasDeleted;
@@ -6171,20 +6174,12 @@ function DigDistMsgReader_ReadMessageEnhanced_Traditional(msgHeader, allowChgMsg
 				}
 				break;
 			case this.enhReaderKeys.downloadAttachments: // Download attachments
-				if (msgAndAttachmentInfo.hasAttachments)
+				if (msgHasAttachments)
 				{
 					console.print("\1n");
 					console.crlf();
 					console.print("\1c- Download Attached Files -\1n");
-					// 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);
-					}
+					allowUserToDownloadMessage_NewInterface(msgHeader, this.subBoardCode);
 
 					// Ensure the message is refreshed on the screen
 					writeMessage = true;
@@ -6209,9 +6204,14 @@ function DigDistMsgReader_ReadMessageEnhanced_Traditional(msgHeader, allowChgMsg
 					console.crlf();
 					if (filename.length > 0)
 					{
-						var saveMsgRetObj = this.SaveMsgToFile(msgHeader, filename, true);
+						var saveMsgRetObj = this.SaveMsgToFile(msgHeader, filename);
 						if (saveMsgRetObj.succeeded)
+						{
 							console.print("\1n\1cThe message has been saved.\1n");
+							if (msgHdrHasAttachmentFlag(msgHeader))
+								console.print(" Attachments not saved.");
+							console.print("\1n");
+						}
 						else
 							console.print("\1n\1y\1hFailed: " + saveMsgRetObj.errorMsg + "\1n");
 						mswait(ERROR_PAUSE_WAIT_MS);
@@ -8212,17 +8212,17 @@ function DigDistMsgReader_DisplaySyncMsgHeader(pMsgHdr)
 		// Generate a string describing the message attributes, then output the default
 		// header.
 		var allMsgAttrStr = makeAllMsgAttrStr(pMsgHdr);
-		console.print("\1n\1w���������������������������������������������������������������������������۲��");
+		console.print("\1n\1w���������������������������������������������������������������������������۲��");
 		console.crlf();
-		console.print("\1n\1w�����\1cFrom\1w\1h: \1b" + pMsgHdr["from"].substr(0, console.screen_columns-12));
+		console.print("\1n\1w�����\1cFrom\1w\1h: \1b" + pMsgHdr["from"].substr(0, console.screen_columns-12));
 		console.crlf();
-		console.print("\1n\1w�����\1cTo  \1w\1h: \1b" + pMsgHdr["to"].substr(0, console.screen_columns-12));
+		console.print("\1n\1w�����\1cTo  \1w\1h: \1b" + pMsgHdr["to"].substr(0, console.screen_columns-12));
 		console.crlf();
-		console.print("\1n\1w�����\1cSubj\1w\1h: \1b" + pMsgHdr["subject"].substr(0, console.screen_columns-12));
+		console.print("\1n\1w�����\1cSubj\1w\1h: \1b" + pMsgHdr["subject"].substr(0, console.screen_columns-12));
 		console.crlf();
-		console.print("\1n\1w�����\1cDate\1w\1h: \1b" + dateTimeStr.substr(0, console.screen_columns-12));
+		console.print("\1n\1w�����\1cDate\1w\1h: \1b" + dateTimeStr.substr(0, console.screen_columns-12));
 		console.crlf();
-		console.print("\1n\1w�����\1cAttr\1w\1h: \1b" + allMsgAttrStr.substr(0, console.screen_columns-12));
+		console.print("\1n\1w�����\1cAttr\1w\1h: \1b" + allMsgAttrStr.substr(0, console.screen_columns-12));
 		console.crlf();
 	}
 }
@@ -10308,12 +10308,10 @@ function DigDistMsgReader_EnhReaderPromptYesNo(pQuestion, pMessageLines, pTopLin
 //  pPromptRowWidth: Optional - The width of the prompt row (if pProptLoc is valid)
 //  pConfirmDelete: Optional boolean - Whether or not to confirm deleting the
 //                  message.  Defaults to true.
-//  pAttachments: Optional - An array of file attachment information returned by
-//                determineMsgAttachments()
 //
 // Return value: Boolean - Whether or not the message was deleted
 function DigDistMsgReader_PromptAndDeleteMessage(pOffset, pPromptLoc, pClearPromptRowAtFirstUse,
-                                                 pPromptRowWidth, pConfirmDelete, pAttachments)
+                                                 pPromptRowWidth, pConfirmDelete)
 {
 	// Sanity checking
 	if ((pOffset == null) || (typeof(pOffset) != "number"))
@@ -10396,18 +10394,6 @@ function DigDistMsgReader_PromptAndDeleteMessage(pOffset, pPromptLoc, pClearProm
 			msgWasDeleted = msgbase.remove_msg(false, msgHeader.number);
 			if (msgWasDeleted)
 			{
-				// If there are attachments, then delete them.
-				if (Object.prototype.toString.call(pAttachments) === "[object Array]")
-				{
-					if (pAttachments.length > 0)
-					{
-						for (var attachIdx = 0; attachIdx < pAttachments.length; ++attachIdx)
-						{
-							if (file_exists(pAttachments[attachIdx].fullyPathedFilename))
-								file_remove(pAttachments[attachIdx].fullyPathedFilename);
-						}
-					}
-				}
 				// Delete any vote response messages for this message
 				var voteDelRetObj = deleteVoteMsgs(msgbase, msgHeader.number, msgHeader.id, (this.subBoardCode == "mail"));
 				if (!voteDelRetObj.allVoteMsgsDeleted)
@@ -10969,7 +10955,7 @@ function DigDistMsgReader_SelectMsgArea_Traditional()
 		//console.crlf();
 		this.ListMsgGrps(grpSearchText);
 		console.crlf();
-		console.print("\1n\1b\1h� \1n\1cWhich, \1h/\1n\1c or \1hCTRL-F\1n\1c, \1hQ\1n\1cuit, or [\1h" +
+		console.print("\1n\1b\1h� \1n\1cWhich, \1h/\1n\1c or \1hCTRL-F\1n\1c, \1hQ\1n\1cuit, or [\1h" +
 		              +(msg_area.sub[this.subBoardCode].grp_index+1) + "\1n\1c]: \1h");
 		// Accept Q (quit), / or CTRL_F (Search) or a file library number
 		selectedGrp = console.getkeys("Q/" + CTRL_F, msg_area.grp_list.length);
@@ -11024,7 +11010,7 @@ function DigDistMsgReader_SelectMsgArea_Traditional()
 					this.DisplayAreaChgHdr();
 					this.ListSubBoardsInMsgGroup(selectedGrp-1, defaultSubBoard-1, null, subSearchText);
 					console.crlf();
-					console.print("\1n\1b\1h� \1n\1cWhich, \1h/\1n\1c or \1hCTRL-F\1n\1c, \1hQ\1n\1cuit, or [\1h" +
+					console.print("\1n\1b\1h� \1n\1cWhich, \1h/\1n\1c or \1hCTRL-F\1n\1c, \1hQ\1n\1cuit, or [\1h" +
 					              defaultSubBoard + "\1n\1c]: \1h");
 					// Accept Q (quit), / or CTRL_F (Search) or a sub-board number
 					selectedSubBoard = console.getkeys("Q/" + CTRL_F, msg_area.grp_list[selectedGrp - 1].sub_list.length);
@@ -11524,7 +11510,7 @@ function DigDistMsgReader_showChooseMsgAreaHelpScreen(pLightbar, pClearScreen)
 	console.crlf();
 	console.print("\1n\1c\1hMessage area (sub-board) chooser");
 	console.crlf();
-	console.print("\1k��������������������������������\1n");
+	console.print("\1k��������������������������������\1n");
 	console.crlf();
 	console.print("\1cFirst, a listing of message groups is displayed.  One can be chosen by typing");
 	console.crlf();
@@ -11536,7 +11522,7 @@ function DigDistMsgReader_showChooseMsgAreaHelpScreen(pLightbar, pClearScreen)
 	console.crlf();
 	console.print("Keyboard commands:");
 	console.crlf();
-	console.print("\1k\1h�����������������\1n");
+	console.print("\1k\1h�����������������\1n");
 	console.crlf();
 	console.print("\1n\1c\1h/\1n\1c or \1hCTRL-F\1n\1c: Find group/sub-board");
 	console.crlf();
@@ -11550,7 +11536,7 @@ function DigDistMsgReader_showChooseMsgAreaHelpScreen(pLightbar, pClearScreen)
 		console.crlf();
 		console.print("\1n\1cThe lightbar interface also allows up & down navigation through the lists:");
 		console.crlf();
-		console.print("\1k\1h��������������������������������������������������������������������������");
+		console.print("\1k\1h��������������������������������������������������������������������������");
 		console.crlf();
 		console.print("\1n\1c\1hUp\1n\1c/\1hdown arrow\1n\1c: Move the cursor up/down one line");
 		console.crlf();
@@ -11998,7 +11984,7 @@ function DigDistMsgReader_GetExtdMsgHdrInfo(pMsgHdr, pKludgeOnly)
 //               attachments: An array of the attached filenames (as strings)
 //               errorMsg: An error message, if something bad happened
 function DigDistMsgReader_GetMsgInfoForEnhancedReader(pMsgHdr, pWordWrap, pDetermineAttachments,
-                                                      pGetB64Data, pMsgBody, pMsgHasANSICodes)
+                                                      pGetB64Data, pMsgBody)
 {
 	var retObj = {
 		msgText: "",
@@ -12032,18 +12018,7 @@ function DigDistMsgReader_GetMsgInfoForEnhancedReader(pMsgHdr, pWordWrap, pDeter
 			return retObj;
 		}
 	}
-	if (determineAttachments)
-	{
-		var msgInfo = determineMsgAttachments(pMsgHdr, msgBody, getB64Data);
-		retObj.msgText = msgInfo.msgText;
-		retObj.attachments = msgInfo.attachments;
-	}
-	else
-	{
-		retObj.msgText = msgBody;
-		retObj.msgText = word_wrap(msgBody, console.screen_columns - 1, true);
-		retObj.attachments = [];
-	}
+	retObj.msgText = word_wrap(msgBody, console.screen_columns - 1, true);
 
 	var msgTextAltered = retObj.msgText; // Will alter the message text, but not yet
 	// Only interpret @-codes if the user is reading personal email.  There
@@ -12062,7 +12037,6 @@ function DigDistMsgReader_GetMsgInfoForEnhancedReader(pMsgHdr, pWordWrap, pDeter
 	// can mess up the display of the message, so remove enter characters
 	// from the beginning of the message.
 	var msgTextWithoutAttrs = strip_ctrl(msgTextAltered);
-	var first240Chars = msgTextWithoutAttrs.substr(0, 240);
 	var fromToSearchStr = "By: " + pMsgHdr.from + " to " + pMsgHdr.to;
 	var toFromSearchStr = "By: " + pMsgHdr.to + " to " + pMsgHdr.from;
 	var fromToStrIdx = msgTextWithoutAttrs.indexOf(fromToSearchStr);
@@ -12124,10 +12098,6 @@ 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;
 }
 
@@ -12967,112 +12937,39 @@ function DigDistMsgReader_GetGroupNameAndDesc()
 // Parameters:
 //  pMsgHdr: The header object for the message
 //  pFilename: The name of the file to write the message to
-//  pStripCtrl: Boolean - Whether or not to remove Synchronet control
-//              codes from the message lines
-//  pMsgLines: An array containing the message lines
-//  pAttachments: An array containing attachment information (as returned by determineMsgAttachments())
 //
 // Return value: An object containing the following properties:
 //               succeeded: Boolean - Whether or not the file was successfully written
 //               errorMsg: String - On failure, will contain the reason it failed
-function DigDistMsgReader_SaveMsgToFile(pMsgHdr, pFilename, pStripCtrl, pMsgLines, pAttachments)
+function DigDistMsgReader_SaveMsgToFile(pMsgHdr, pFilename)
 {
 	// Sanity checking
+	if (typeof(pMsgHdr) !== "object")
+		return({ succeeded: false, errorMsg: "Header object not given"});
 	if (typeof(pFilename) != "string")
 		return({ succeeded: false, errorMsg: "Filename parameter not a string"});
 	if (pFilename.length == 0)
 		return({ succeeded: false, errorMsg: "Empty filename given"});
 
-	// If no message lines are passed in, then get the message lines now.
-	var msgLines = pMsgLines;
-	var attachments = pAttachments;
-	if ((pMsgLines == null) || (typeof(pMsgLines) != "object"))
-	{
-		if (typeof(pMsgHdr) == "object")
-		{
-			// Get the message text, interpret any @-codes in it, replace tabs with spaces
-			// to prevent weirdness when displaying the message lines, and word-wrap the
-			// text so that it looks good on the screen,
-			//GetMsgInfoForEnhancedReader(pMsgHdr, pWordWrap, pDetermineAttachments, pGetB64Data)
-			//var msgInfo = this.GetMsgInfoForEnhancedReader(pMsgHdr, false, false, false);
-			var msgInfo = this.GetMsgInfoForEnhancedReader(pMsgHdr, true, true, true);
-			msgLines = msgInfo.messageLines;
-			if (msgInfo.hasOwnProperty("attachments"))
-				attachments = msgInfo.attachments;
-		}
-		else
-			return({ succeeded: false, errorMsg: "No message lines and null header object"});
-	}
-
 	var retObj = {
 		succeeded: true,
 		errorMsg: ""
 	};
 
-	// If there are message attachments, then treat pFilename as a directory and
-	// create the directory for saving both the message text & attachments.
-	// Then, save the attachments to that directory.
-	var msgTextFilename = pFilename;
-	if ((attachments != null) && (attachments.length > 0))
+	// Get the message text and save it
+	// Note: GetMsgInfoForEnhancedReader() can expand @-codes in the message,
+	// but for now we're saving the message basically as-is.
+	//var msgInfo = this.GetMsgInfoForEnhancedReader(pMsgHdr, false, false, false);
+	var msgbase = new MsgBase(this.subBoardCode);
+	if (msgbase.open())
 	{
-		if (file_isdir(pFilename))
-		{
-			if (file_exists(pFilename))
-				return({ succeeded: false, errorMsg: "Can't make directory: File with that name exists"});
-		}
-		else
-		{
-			if (!mkdir(pFilename))
-				return({ succeeded: false, errorMsg: "Failed to create directory"});
-		}
-
-		// The name of the file to save the message text will be called "messageText.txt"
-		// in the save directory.
-		var savePathWithTrailingSlash = backslash(pFilename);
-		msgTextFilename = savePathWithTrailingSlash + "messageText.txt";
-
-		// Save the attachments to the directory
-		var saveFileError = "";
-		for (var attachIdx = 0; (attachIdx < attachments.length) && (saveFileError.length == 0); ++attachIdx)
-		{
-			var destFilename = savePathWithTrailingSlash + attachments[attachIdx].filename;
-			// If the file info has base64 data, then decode & save it to the directory.
-			// Otherwise, the file was probably uploaded to the user's mailbox in Synchronet,
-			// so copy the file to the save directory.
-			if (attachments[attachIdx].hasOwnProperty("B64Data"))
-			{
-				var attachedFile = new File(destFilename);
-				if (attachedFile.open("wb"))
-				{
-					attachedFile.base64 = true;
-					if (!attachedFile.write(attachments[attachIdx].B64Data))
-						saveFileError = "\1n\1cFailed to save " + attachments[attachIdx].filename;
-					attachedFile.close();
-				}
-			}
-			else
-			{
-				// There is no base64 data for the file, so it's probably in the
-				// user's mailbox in Synchronet, so copy it to the save directory.
-				if (file_exists(attachments[attachIdx].fullyPathedFilename))
-				{
-					if (!file_copy(attachments[attachIdx].fullyPathedFilename, destFilename))
-						saveFileError = "Failed to copy " + attachments[attachIdx].filename;
-				}
-				else
-					saveFileError = "File " + attachments[attachIdx].fullyPathedAttachmentFilename + " doesn't exist";
-			}
-		}
-		if (saveFileError.length > 0)
-			return({ succeeded: false, errorMsg: saveFileError });
-	}
+		msgBody = msgbase.get_msg_body(false, pMsgHdr.number, false, false, true, true);
+		msgbase.close();
 
-	var messageSaveFile = new File(msgTextFilename);
-	if (messageSaveFile.open("w"))
-	{
-		// Write some header information to the file
-		if (typeof(pMsgHdr) == "object")
+		var messageSaveFile = new File(pFilename);
+		if (messageSaveFile.open("w"))
 		{
+			// Write some header information to the file
 			if (pMsgHdr.hasOwnProperty("from"))
 				messageSaveFile.writeln("From: " + pMsgHdr.from);
 			if (pMsgHdr.hasOwnProperty("to"))
@@ -13094,24 +12991,32 @@ function DigDistMsgReader_SaveMsgToFile(pMsgHdr, pFilename, pStripCtrl, pMsgLine
 			if (pMsgHdr.hasOwnProperty("reply_id"))
 				messageSaveFile.writeln("Reply ID: " + pMsgHdr.reply_id);
 			messageSaveFile.writeln("===============================");
-		}
-		// Write the message body to the file
-		if (pStripCtrl)
-		{
-			for (var msgLineIdx = 0; msgLineIdx < msgLines.length; ++msgLineIdx)
-				messageSaveFile.writeln(strip_ctrl(msgLines[msgLineIdx]));
+
+			// If the message body has ANSI, then use the Graphic object to strip it
+			// of any cursor movement codes
+			var msgHasANSICodes = msgBody.indexOf("\x1b[") >= 0;
+			if (msgHasANSICodes)
+			{
+				var graphic = new Graphic(this.msgAreaWidth, this.msgAreaHeight);
+				graphic.auto_extend = true;
+				graphic.ANSI = ansiterm.expand_ctrl_a(msgBody);
+				msgBody = syncAttrCodesToANSI(graphic.MSG);
+			}
+
+			// Write the message body to the file
+			messageSaveFile.write(msgBody);
+			messageSaveFile.close();
 		}
 		else
 		{
-			for (var msgLineIdx = 0; msgLineIdx < msgLines.length; ++msgLineIdx)
-				messageSaveFile.writeln(msgLines[msgLineIdx]);
+			retObj.succeeded = false;
+			retObj.errorMsg = "Failed to open the file for writing";
 		}
-		messageSaveFile.close();
 	}
 	else
 	{
 		retObj.succeeded = false;
-		retObj.errorMsg = "Unable to open the message file for writing";
+		retObj.errorMsg = "Unable to open the messagebase";
 	}
 
 	return retObj;
@@ -14740,7 +14645,7 @@ function displayTextWithLineBelow(pText, pCenter, pTextColor, pLineColor)
 		var solidLine = "";
 		var textLength = console.strlen(pText);
 		for (var i = 0; i < textLength; ++i)
-			solidLine += "�";
+			solidLine += HORIZONTAL_SINGLE;
 		console.center(lineColor + solidLine);
 	}
 	else
@@ -14750,7 +14655,7 @@ function displayTextWithLineBelow(pText, pCenter, pTextColor, pLineColor)
 		console.print(lineColor);
 		var textLength = console.strlen(pText);
 		for (var i = 0; i < textLength; ++i)
-			console.print("�");
+			console.print(HORIZONTAL_SINGLE);
 		console.crlf();
 	}
 }
@@ -16559,423 +16464,6 @@ function getSubBoardCodeFromNum(pSubBoardNum)
 	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
-	// 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-A 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 (msgHdrHasAttachmentFlag(pMsgHdr) || 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 (msgHdrHasAttachmentFlag(pMsgHdr) || 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 (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 ((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";
-	}
-
-	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.
 //
@@ -18602,17 +18090,6 @@ function strWithToUserColor(pStr, pToUserColor)
 	*/
 }
 
-// Returns whether a string has any Synchronet attribute codes
-//
-// Parameters:
-//  pStr: the string to check
-//
-// Return value: Boolean - Whether or not the string has any Synchronet attribute codes
-function hasSyncAttrCodes(pStr)
-{
-	return (pStr.search(/\1[krgybmcwhifn\-_01234567]/i) > -1);
-}
-
 // Gets the value of the user's current scan_ptr in a sub-board, or if it's
 // 0xffffffff, returns the message number of the last readable message in
 // the sub-board (this is the message number, not the index).
@@ -18702,7 +18179,6 @@ function allowUserToDownloadMessage_NewInterface(pMsgHdr, pSubCode)
 		console.creturn();
 		bbs.download_msg_attachments(msgHdrForDownloading);
 		msgBase.close();
-		delete msgBase; // Free some memory?
 	}
 }
 
diff --git a/xtrn/DDMsgReader/readme.txt b/xtrn/DDMsgReader/readme.txt
index 77df3221afb006b21406391bc61abfaa876a0aaf..930137772bceb4ef911bf62bb9877ed95c4309e0 100644
--- a/xtrn/DDMsgReader/readme.txt
+++ b/xtrn/DDMsgReader/readme.txt
@@ -1,6 +1,6 @@
                       Digital Distortion Message Reader
-                                 Version 1.48
-                           Release date: 2022-06-12
+                                 Version 1.49
+                           Release date: 2022-06-13
 
                                      by
 
diff --git a/xtrn/DDMsgReader/revision_history.txt b/xtrn/DDMsgReader/revision_history.txt
index fe3a5377f9b25c0ba18a5ffb72ed933a17b929dd..98bc97b544a0fe033168c1504ced81548c980746 100644
--- a/xtrn/DDMsgReader/revision_history.txt
+++ b/xtrn/DDMsgReader/revision_history.txt
@@ -5,6 +5,10 @@ Revision History (change log)
 =============================
 Version  Date         Description
 -------  ----         -----------
+1.49     2022-06-13   Refactor: Simplified saving a message to BBS machine for
+                      sysop (as-is, less processing); removed attachment stuff
+                      for pre-Synchronet 3.17; moved hasSyncAttrCodes() to
+                      attr_conv.js because that's where it needs to be.
 1.48     2022-06-12   Improved display of ANSI messages
 1.47a    2022-03-23   Internal change: Now calls bbs.edit_msg() for editing an
                       existing message (for Synchronet 3.18 and up).