diff --git a/src/sbbs3/con_out.cpp b/src/sbbs3/con_out.cpp
index 243b65f6d1fa60d9e68f049dfbd6fff257ed8862..1d03c03d918e26cf6efaadde7abeaf970affe200 100644
--- a/src/sbbs3/con_out.cpp
+++ b/src/sbbs3/con_out.cpp
@@ -677,8 +677,11 @@ int sbbs_t::outchar(char ch)
 		} else {
 			if(utf8[0] != 0)
 				putcom(utf8);
-			else
+			else {
+				if(ch == '\n' && line_delay)
+					SLEEP(line_delay);
 				outcom(ch);
+			}
 		}
 	}
 	if(outchar_esc == ansiState_none) {
diff --git a/src/sbbs3/js_console.cpp b/src/sbbs3/js_console.cpp
index 118f636d037f7c54276d7a75c7b13656f122d168..4b944deb555885136185cba1d8bba99242da267b 100644
--- a/src/sbbs3/js_console.cpp
+++ b/src/sbbs3/js_console.cpp
@@ -33,6 +33,7 @@ enum {
 	,CON_PROP_LNCNTR
 	,CON_PROP_COLUMN
 	,CON_PROP_LASTLINELEN
+	,CON_PROP_LINE_DELAY
 	,CON_PROP_ATTR
 	,CON_PROP_TOS
 	,CON_PROP_ROW
@@ -97,6 +98,9 @@ static JSBool js_console_get(JSContext *cx, JSObject *obj, jsid id, jsval *vp)
 		case CON_PROP_LASTLINELEN:
 			val=sbbs->lastlinelen;
 			break;
+		case CON_PROP_LINE_DELAY:
+			val = sbbs->line_delay;
+			break;
 		case CON_PROP_ATTR:
 			val=sbbs->curatr;
 			break;
@@ -244,6 +248,9 @@ static JSBool js_console_set(JSContext *cx, JSObject *obj, jsid id, JSBool stric
 		case CON_PROP_LASTLINELEN:
 			sbbs->lastlinelen=val;
 			break;
+		case CON_PROP_LINE_DELAY:
+			sbbs->line_delay = val;
+			break;
 		case CON_PROP_ATTR:
 			if(JSVAL_IS_STRING(*vp)) {
 				JSVALUE_TO_MSTRING(cx, *vp, sval, NULL);
@@ -355,6 +362,7 @@ static jsSyncPropertySpec js_console_properties[] = {
 	{	"current_row"		,CON_PROP_ROW				,CON_PROP_FLAGS ,31800},
 	{	"current_column"	,CON_PROP_COLUMN			,CON_PROP_FLAGS ,315},
 	{	"last_line_length"	,CON_PROP_LASTLINELEN		,CON_PROP_FLAGS	,317},
+	{	"line_delay"		,CON_PROP_LINE_DELAY		,CON_PROP_FLAGS	,320},
 	{	"attributes"		,CON_PROP_ATTR				,CON_PROP_FLAGS	,310},
 	{	"top_of_screen"		,CON_PROP_TOS				,JSPROP_ENUMERATE|JSPROP_READONLY	,310},
 	{	"screen_rows"		,CON_PROP_ROWS				,CON_PROP_FLAGS	,310},
@@ -395,6 +403,7 @@ static const char* con_prop_desc[] = {
 	,"current 0-based row counter"
 	,"current 0-based column counter (used to auto-increment <i>line_counter</i> when screen wraps)"
 	,"length of last line sent to terminal (before a carriage-return or line-wrap)"
+	,"duration of delay (in milliseconds) before each line-feed character is sent to the terminal"
 	,"current display attributes (set with number or string value)"
 	,"set to <i>true</i> if the terminal cursor is already at the top of the screen - <small>READ ONLY</small>"
 	,"number of remote terminal screen rows (in lines)"
diff --git a/src/sbbs3/prntfile.cpp b/src/sbbs3/prntfile.cpp
index 5a1de95822ff56f4740fbb5da4b2663aa1ad6396..e1d0147e1a182d5eba18e6618ee0ef1b3926523c 100644
--- a/src/sbbs3/prntfile.cpp
+++ b/src/sbbs3/prntfile.cpp
@@ -112,6 +112,7 @@ bool sbbs_t::printfile(const char* fname, long mode, long org_cols, JSObject* ob
 	} else {	// Line-at-a-time mode
 		ulong sys_status_sav = sys_status;
 		enum output_rate output_rate = cur_output_rate;
+		uint org_line_delay = line_delay;
 		uint tmpatr = curatr;
 		ulong orgcon = console;
 		attr_sp = 0;	/* clear any saved attributes */
@@ -149,6 +150,8 @@ bool sbbs_t::printfile(const char* fname, long mode, long org_cols, JSObject* ob
 		/* Restore original settings of Forced Pause On/Off */
 		sys_status &= ~(SS_PAUSEOFF|SS_PAUSEON);
 		sys_status |= (sys_status_sav&(SS_PAUSEOFF|SS_PAUSEON));
+
+		line_delay = org_line_delay;
 	}
 
 	if((mode&P_NOABORT || rip) && online==ON_REMOTE) {
diff --git a/src/sbbs3/putmsg.cpp b/src/sbbs3/putmsg.cpp
index f50be4a478c1b65d79fc9e823141a9bca9cc2743..18216bf9dc7214cdc0fc9000e65b754a80589071 100644
--- a/src/sbbs3/putmsg.cpp
+++ b/src/sbbs3/putmsg.cpp
@@ -1,7 +1,4 @@
 /* Synchronet message/menu display routine */
-// vi: tabstop=4
-
-/* $Id: putmsg.cpp,v 1.68 2020/05/11 05:03:47 rswindell Exp $ */
 
 /****************************************************************************
  * @format.tab-size 4		(Plain Text/Source Code File Header)			*
@@ -16,21 +13,9 @@
  * See the GNU General Public License for more details: gpl.txt or			*
  * http://www.fsf.org/copyleft/gpl.html										*
  *																			*
- * Anonymous FTP access to the most recent released source is available at	*
- * ftp://vert.synchro.net, ftp://cvs.synchro.net and ftp://ftp.synchro.net	*
- *																			*
- * Anonymous CVS access to the development source and modification history	*
- * is available at cvs.synchro.net:/cvsroot/sbbs, example:					*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs login			*
- *     (just hit return, no password is necessary)							*
- * cvs -d :pserver:anonymous@cvs.synchro.net:/cvsroot/sbbs checkout src		*
- *																			*
  * For Synchronet coding style and modification guidelines, see				*
  * http://www.synchro.net/source.html										*
  *																			*
- * You are encouraged to submit any modifications (preferably in Unix diff	*
- * format) via e-mail to mods@synchro.net									*
- *																			*
  * Note: If this box doesn't appear square, then you need to fix your tabs.	*
  ****************************************************************************/
 
@@ -53,6 +38,7 @@
 char sbbs_t::putmsg(const char *buf, long mode, long org_cols, JSObject* obj)
 {
 	uint 	tmpatr;
+	uint	org_line_delay = line_delay;
 	ulong 	orgcon=console;
 	ulong	sys_status_sav=sys_status;
 	enum output_rate output_rate = cur_output_rate;
@@ -80,6 +66,9 @@ char sbbs_t::putmsg(const char *buf, long mode, long org_cols, JSObject* obj)
 	/* Restore original settings of Forced Pause On/Off */
 	sys_status&=~(SS_PAUSEOFF|SS_PAUSEON);
 	sys_status|=(sys_status_sav&(SS_PAUSEOFF|SS_PAUSEON));
+
+	line_delay = org_line_delay;
+
 	return(ret);
 }
 
@@ -449,6 +438,23 @@ char sbbs_t::putmsgfrag(const char* buf, long& mode, long org_cols, JSObject* ob
 					mode |= P_NOABORT;
 					continue;
 				}
+				if(memcmp(str+l, "@LINEDELAY@", 11) == 0) {
+					l += 11;
+					line_delay = 100;
+					continue;
+				}
+				if(memcmp(str+l, "@LINEDELAY:", 11) == 0) {
+					char* p = str + l + 11;
+					SKIP_DIGIT(p);
+					if(*p == '@') {
+						l += 11;
+						line_delay = atoi(str + l) * 10;
+						while(str[l] != '@')
+							l++;
+						l++;
+						continue;
+					}
+				}
 				bool was_tos = (row == 0);
  				i=show_atcode((char *)str+l, obj);	/* returns 0 if not valid @ code */
 				l+=i;					/* i is length of code string */
diff --git a/src/sbbs3/sbbs.h b/src/sbbs3/sbbs.h
index 911ab3953e5c4047ce2ce591847b385027e90abc..f20155827cb1609f089826bc5e014f79a6db5e51 100644
--- a/src/sbbs3/sbbs.h
+++ b/src/sbbs3/sbbs.h
@@ -559,6 +559,7 @@ public:
 	char 	lbuf[LINE_BUFSIZE+1];/* Temp storage for each line output */
 	int		lbuflen;		/* Number of characters in line buffer */
 	uint	latr=0;			/* Starting attribute of line buffer */
+	uint	line_delay=0;	/* Delay duration (ms) after each line sent */
 	ulong	console;		/* Defines current Console settings */
 	char 	wordwrap[81];	/* Word wrap buffer */
 	time_t	now=0,			/* Used to store current time in Unix format */