From ae0cfd9ad89a91162788e547aef8f6fb18bd2967 Mon Sep 17 00:00:00 2001
From: "Rob Swindell (on Debian Linux)" <rob@synchro.net>
Date: Sun, 4 May 2025 16:23:23 -0700
Subject: [PATCH] Add support to SMBLIB for "Fixed" format message text

By default, messages are assumed to be of "Flowed" format (CRLF-delimited
paragraphs). When posting pre-formatted text, this aux attribute flag
should be set to indicate to message viewers that the message text should
be displayed as-is, without applying word-wrapping or parsing/expanding
markup tags.

This adds a new key/value field to QWK HEADERS.DAT and FTN Kludge:
"Format: fixed" and "Fixed: flowed" as appropriate.

This flag is now automatically set when posting messages in raw input mode,
when using postmeme.js, or postmsg.js with the -P option.

Not done: adding or parsing the "Format" parameter in MIME content-type
message header fields per RFC-3676.
---
 exec/load/smbdefs.js   |  1 +
 exec/postmeme.js       |  2 +-
 exec/postmsg.js        |  4 ++++
 src/sbbs3/atcodes.cpp  |  3 ++-
 src/sbbs3/getmsg.cpp   |  5 ++++-
 src/sbbs3/msgtoqwk.cpp |  1 +
 src/sbbs3/postmsg.cpp  |  2 ++
 src/sbbs3/qwktomsg.cpp |  4 ++++
 src/sbbs3/sbbsecho.c   | 11 +++++++++++
 src/sbbs3/sbbsecho.h   |  2 +-
 src/smblib/smbdefs.h   |  1 +
 src/smblib/smbstr.c    |  1 +
 12 files changed, 33 insertions(+), 4 deletions(-)

diff --git a/exec/load/smbdefs.js b/exec/load/smbdefs.js
index 87074140ae..c138918499 100644
--- a/exec/load/smbdefs.js
+++ b/exec/load/smbdefs.js
@@ -69,6 +69,7 @@ const MSG_KILLFILE			= (1<<3);	// Delete file(s) when sent
 const MSG_RECEIPTREQ		= (1<<4);	// Return receipt requested
 const MSG_CONFIRMREQ		= (1<<5);	// Confirmation receipt requested
 const MSG_NODISP			= (1<<6);	// Msg may not be displayed to user
+const MSG_FIXED_FORMAT		= (1<<7);   // Pre-formatted message body text
 const MSG_HFIELDS_UTF8		= (1<<13);	// Message header fields are UTF-8 encoded
 const POLL_CLOSED			= (1<<24);	// Closed to voting
 const POLL_RESULTS_MASK		= (3<<30);	// 4 possible values:
diff --git a/exec/postmeme.js b/exec/postmeme.js
index 7ec8b8c158..7c29f88254 100755
--- a/exec/postmeme.js
+++ b/exec/postmeme.js
@@ -27,7 +27,7 @@ if (!msg)
 var sub = msg_area.sub[bbs.cursub_code];
 console.print(format(bbs.text("Posting"), sub.grp_name, sub.description));
 
-var hdr = { from_ext: user.number, from:  sub.settings & SUB_NAME ? user.name : user.alias };
+var hdr = { auxattr: MSG_FIXED_FORMAT, from_ext: user.number, from:  sub.settings & SUB_NAME ? user.name : user.alias };
 console.print(bbs.text("PostTo"));
 hdr.to = console.getstr("All", LEN_ALIAS, K_EDIT | K_LINE | K_AUTODEL);
 if (console.aborted || !hdr.to)
diff --git a/exec/postmsg.js b/exec/postmsg.js
index 9dec58235c..0085dbd549 100644
--- a/exec/postmsg.js
+++ b/exec/postmsg.js
@@ -25,6 +25,7 @@ function usage()
 	print("\t-T<date/time> set message date and time in string format supported by JS Date()");
 	print("\t-d            use default values (no prompt) for to, from, and subject");
 	print("\t-F            set file request attribute flag");
+	print("\t-P            set message format to 'Fixed' (pre-formatted text)");
 	print();
 	print("Note: You may need to enclose multi-word options in quotes (e.g. \"-fMy Name\")");
 	print();
@@ -53,6 +54,9 @@ for(var i in argv) {
 				case 'F':
 					hdrs.auxattr = MSG_FILEREQUEST;
 					break;
+				case 'P':
+					hdrs.auxattr = MSG_FIXED_FORMAT;
+					break;
 				case 'e':
 					if(val.length)
 						hdrs.from_ext = val;
diff --git a/src/sbbs3/atcodes.cpp b/src/sbbs3/atcodes.cpp
index c469525289..99acd66346 100644
--- a/src/sbbs3/atcodes.cpp
+++ b/src/sbbs3/atcodes.cpp
@@ -2109,7 +2109,7 @@ const char* sbbs_t::atcode(const char* sp, char* str, size_t maxlen, int* pmode,
 		return str;
 	}
 	if (!strcmp(sp, "MSG_AUXATTR") && current_msg != NULL) {
-		safe_snprintf(str, maxlen, "%s%s%s%s%s%s%s"
+		safe_snprintf(str, maxlen, "%s%s%s%s%s%s%s%s"
 		              , current_msg->hdr.auxattr & MSG_FILEREQUEST   ? "FileRequest  "   :nulstr
 		              , current_msg->hdr.auxattr & MSG_FILEATTACH    ? "FileAttach  "    :nulstr
 		              , current_msg->hdr.auxattr & MSG_MIMEATTACH    ? "MimeAttach  "    :nulstr
@@ -2117,6 +2117,7 @@ const char* sbbs_t::atcode(const char* sp, char* str, size_t maxlen, int* pmode,
 		              , current_msg->hdr.auxattr & MSG_RECEIPTREQ    ? "ReceiptReq  "    :nulstr
 		              , current_msg->hdr.auxattr & MSG_CONFIRMREQ    ? "ConfirmReq  "    :nulstr
 		              , current_msg->hdr.auxattr & MSG_NODISP        ? "DontDisplay  "   :nulstr
+		              , current_msg->hdr.auxattr & MSG_FIXED_FORMAT  ? "FixedFormat "    :nulstr
 		              );
 		return str;
 	}
diff --git a/src/sbbs3/getmsg.cpp b/src/sbbs3/getmsg.cpp
index 06930d68ae..3d82178926 100644
--- a/src/sbbs3/getmsg.cpp
+++ b/src/sbbs3/getmsg.cpp
@@ -101,7 +101,7 @@ void sbbs_t::show_msgattr(const smbmsg_t* msg)
 	              );
 
 	char auxattr_str[64];
-	safe_snprintf(auxattr_str, sizeof(auxattr_str), "%s%s%s%s%s%s%s"
+	safe_snprintf(auxattr_str, sizeof(auxattr_str), "%s%s%s%s%s%s%s%s"
 	              , auxattr & MSG_FILEREQUEST? "FileRequest  "   :nulstr
 	              , auxattr & MSG_FILEATTACH ? "FileAttach  "    :nulstr
 	              , auxattr & MSG_MIMEATTACH ? "MimeAttach  "    :nulstr
@@ -109,6 +109,7 @@ void sbbs_t::show_msgattr(const smbmsg_t* msg)
 	              , auxattr & MSG_RECEIPTREQ ? "ReceiptReq  "    :nulstr
 	              , auxattr & MSG_CONFIRMREQ ? "ConfirmReq  "    :nulstr
 	              , auxattr & MSG_NODISP     ? "DontDisplay  "   :nulstr
+	              , auxattr & MSG_FIXED_FORMAT ? "FixedFormat "  :nulstr
 	              );
 
 	char netattr_str[64];
@@ -347,6 +348,8 @@ bool sbbs_t::show_msg(smb_t* smb, smbmsg_t* msg, int p_mode, post_t* post)
 	}
 	if (console & CON_RAW_IN)
 		p_mode = P_NOATCODES;
+	else if (msg->hdr.auxattr & MSG_FIXED_FORMAT)
+		p_mode &= ~(P_WORDWRAP | P_MARKUP);
 	putmsg(p, p_mode, msg->columns);
 	smb_freemsgtxt(txt);
 	if (term->column)
diff --git a/src/sbbs3/msgtoqwk.cpp b/src/sbbs3/msgtoqwk.cpp
index 3a9194015a..34383fed3c 100644
--- a/src/sbbs3/msgtoqwk.cpp
+++ b/src/sbbs3/msgtoqwk.cpp
@@ -123,6 +123,7 @@ int sbbs_t::msgtoqwk(smbmsg_t* msg, FILE *qwk_fp, int mode, smb_t* smb
 		fprintf(hdrs, "Utf8 = %s\n"
 		        , ((smb_msg_is_utf8(msg) || (msg->hdr.auxattr & MSG_HFIELDS_UTF8)) && (mode & QM_UTF8))
 		        ? "true" : "false");
+		fprintf(hdrs, "Format = %s\n", (msg->hdr.auxattr & MSG_FIXED_FORMAT) ? "fixed" : "flowed");
 
 		/* Message-IDs */
 		fprintf(hdrs, "%s: %s\n", smb_hfieldtype(RFC822MSGID), msgid);
diff --git a/src/sbbs3/postmsg.cpp b/src/sbbs3/postmsg.cpp
index 23624007bb..eb2cca5e5c 100644
--- a/src/sbbs3/postmsg.cpp
+++ b/src/sbbs3/postmsg.cpp
@@ -78,6 +78,7 @@ bool sbbs_t::postmsg(int subnum, int wm_mode, smb_t* resmb, smbmsg_t* remsg)
 	char*       msgbuf = NULL;
 	uint16_t    xlat;
 	ushort      msgattr = 0;
+	uint32_t    auxattr = (console & CON_RAW_IN) ? MSG_FIXED_FORMAT : 0;
 	int         i, storage;
 	int         dupechk_hashes;
 	int         length;
@@ -285,6 +286,7 @@ bool sbbs_t::postmsg(int subnum, int wm_mode, smb_t* resmb, smbmsg_t* remsg)
 
 	memset(&msg, 0, sizeof(msg));
 	msg.hdr.attr = msgattr;
+	msg.hdr.auxattr = auxattr;
 	msg.hdr.when_written = smb_when(time(NULL), sys_timezone(&cfg));
 	msg.hdr.when_imported.time = time32(NULL);
 	msg.hdr.when_imported.zone = msg.hdr.when_written.zone;
diff --git a/src/sbbs3/qwktomsg.cpp b/src/sbbs3/qwktomsg.cpp
index 5533e85210..657d3910c2 100644
--- a/src/sbbs3/qwktomsg.cpp
+++ b/src/sbbs3/qwktomsg.cpp
@@ -42,6 +42,10 @@ static bool qwk_parse_header_list(sbbs_t* sbbs, uint confnum, smbmsg_t* msg, str
 		if (stricmp(value, "true") == 0)
 			msg->hdr.auxattr |= MSG_HFIELDS_UTF8;
 	}
+	if ((p = iniPopKey(headers, ROOT_SECTION, "format", value)) != NULL) {
+		if (stricmp(value, "fixed") == 0)
+			msg->hdr.auxattr |= MSG_FIXED_FORMAT;
+	}
 
 	if ((p = iniPopKey(headers, ROOT_SECTION, "WhenWritten", value)) != NULL) {
 		xpDateTime_t dt = isoDateTimeStr_parse(p);
diff --git a/src/sbbs3/sbbsecho.c b/src/sbbs3/sbbsecho.c
index 318c9068ac..637f1ede31 100644
--- a/src/sbbs3/sbbsecho.c
+++ b/src/sbbs3/sbbsecho.c
@@ -1215,6 +1215,7 @@ int create_netmail(const char *to, const smbmsg_t* msg, const char *subject, con
 				charset = FIDO_CHARSET_CP437;
 		}
 		fprintf(fp, "\1CHRS: %s\r", charset);
+		fprintf(fp, "\1FORMAT: %s\r", (msg->hdr.auxattr & MSG_FIXED_FORMAT) ? "fixed" : "flowed");
 		if (msg->editor != NULL)
 			fprintf(fp, "\1NOTE: %s\r", msg->editor);
 		if (subject != msg->subj)
@@ -3643,6 +3644,15 @@ int fmsgtosmsg(char* fbuf, fmsghdr_t* hdr, uint usernumber, uint subnum)
 					smb_hfield_bin(&msg, SMB_COLUMNS, columns);
 			}
 
+			else if (!strncmp(fbuf + l + 1, "FORMAT:", 7)) {   /* SBBSecho */
+				l += 8;
+				while (l < length && fbuf[l] == ' ') l++;
+				m = l;
+				while (m < length && fbuf[m] != '\r') m++;
+				if (strnicmp(fbuf + l, "fixed", m - l) == 0)
+					msg.hdr.auxattr |= MSG_FIXED_FORMAT;
+			}
+
 			else if (!strncmp(fbuf + l + 1, "BBSID:", 6)) {
 				l += 7;
 				while (l < length && fbuf[l] <= ' ' && fbuf[l] >= 0) l++;
@@ -5158,6 +5168,7 @@ ulong export_echomail(const char* sub_code, const nodecfg_t* nodecfg, bool resca
 					charset = FIDO_CHARSET_CP437;
 			}
 			f += sprintf(fmsgbuf + f, "\1CHRS: %s\r", charset);
+			f += sprintf(fmsgbuf + f, "\1FORMAT: %s\r", (msg.hdr.auxattr & MSG_FIXED_FORMAT) ? "fixed" : "flowed");
 			if (msg.editor != NULL)
 				f += sprintf(fmsgbuf + f, "\1NOTE: %s\r", msg.editor);
 
diff --git a/src/sbbs3/sbbsecho.h b/src/sbbs3/sbbsecho.h
index c52ea28e33..48590fe0ea 100644
--- a/src/sbbs3/sbbsecho.h
+++ b/src/sbbs3/sbbsecho.h
@@ -28,7 +28,7 @@
 #include "ini_file.h"
 
 #define SBBSECHO_VERSION_MAJOR      3
-#define SBBSECHO_VERSION_MINOR      24
+#define SBBSECHO_VERSION_MINOR      25
 
 #define SBBSECHO_PRODUCT_CODE       0x12FF  /* from http://ftsc.org/docs/ftscprod.013 */
 
diff --git a/src/smblib/smbdefs.h b/src/smblib/smbdefs.h
index 14cac00324..d1f5808ea1 100644
--- a/src/smblib/smbdefs.h
+++ b/src/smblib/smbdefs.h
@@ -304,6 +304,7 @@ typedef unsigned char uchar;
 #define MSG_RECEIPTREQ      (1 << 4)      /* Return receipt requested */
 #define MSG_CONFIRMREQ      (1 << 5)      /* Confirmation receipt requested */
 #define MSG_NODISP          (1 << 6)      /* Msg may not be displayed to user */
+#define MSG_FIXED_FORMAT    (1 << 7)      /* Preformatted message body text */
 #define MSG_HFIELDS_UTF8    (1 << 13)     /* Message header fields are UTF-8 encoded */
 #define POLL_CLOSED         (1 << 24)     /* Closed to voting */
 #define POLL_RESULTS_MASK   (3U << 30)    /* 4 possible values: */
diff --git a/src/smblib/smbstr.c b/src/smblib/smbstr.c
index d1fcc70296..577e6dd7fa 100644
--- a/src/smblib/smbstr.c
+++ b/src/smblib/smbstr.c
@@ -483,6 +483,7 @@ char* smb_auxattrstr(int32_t attr, char* outstr, size_t maxlen)
 	MSG_ATTR_CHECK(attr, RECEIPTREQ);
 	MSG_ATTR_CHECK(attr, CONFIRMREQ);
 	MSG_ATTR_CHECK(attr, NODISP);
+	MSG_ATTR_CHECK(attr, FIXED_FORMAT);
 	MSG_ATTR_CHECK(attr, HFIELDS_UTF8);
 	if (attr & POLL_CLOSED)
 		sprintf(str + strlen(str), "%sPOLL-CLOSED", str[0] == 0 ? "" : ", ");
-- 
GitLab