diff --git a/exec/msglist.js b/exec/msglist.js
new file mode 100644
index 0000000000000000000000000000000000000000..6fb802d68ade2007df380934ac8ed44d578c84f8
--- /dev/null
+++ b/exec/msglist.js
@@ -0,0 +1,1450 @@
+// $Id$
+// vi: tabstop=4
+
+// Message Listing Module
+
+/* To install manually into a Baja command shell (*.src) file,
+   e.g. at the main or message menu, add a command key handler, like so:
+
+   cmdkey L
+		exec_bin "msglist"
+		end_cmd
+		
+   ... and then recompile with baja (e.g. "baja default.src").
+ */
+ 
+/* To install as mail reading module and message list module, run:
+   jsexec msglist -install
+ */
+
+"use strict";
+
+function install()
+{
+	var cnflib = load({}, "cnflib.js");
+	var main_cnf = cnflib.read("main.cnf");
+	if(!main_cnf)
+		return "Failed to read main.cnf";
+	main_cnf.readmail_mod = "msglist mail -preview";
+	main_cnf.listmsgs_mod = "msglist";
+	if(!cnflib.write("main.cnf", undefined, main_cnf))
+		return "Failed to write main.cnf";
+	file_touch(system.ctrl_dir + "recycle");
+	return true;
+}
+
+if(argv.indexOf('-install') >= 0)
+{
+	var result = install();
+	if(result !== true)
+		alert(result);
+	exit(result === true ? 0 : 1);
+}
+
+require('sbbsdefs.js', 'LEN_ALIAS');
+require("utf8_cp437.js", 'utf8_cp437');
+require("file_size.js", 'file_size_str');
+require("html2asc.js", 'html2asc');
+load('822header.js');
+var hexdump = load('hexdump_lib.js');
+var mimehdr = load('mimehdr.js');
+
+const age = load('age.js');
+
+var color_cfg = {
+	selection: BG_BLUE,
+	column: [ WHITE ],
+	sorted: BG_RED,
+	preview_separator_active: LIGHTGRAY,
+	preview_separator_inactive: LIGHTCYAN,
+	preview_active: LIGHTCYAN,
+	preview_inactive: LIGHTGRAY,
+};
+
+String.prototype.capitalize = function(){
+   return this.replace( /(^|\s)([a-z])/g , function(m,p1,p2){ return p1+p2.toUpperCase(); } );
+};
+
+function columnHeading(name)
+{
+	var heading_option = 'heading_' + name;
+	if(options[heading_option])
+		return options[heading_option];
+	switch(name) {
+		case 'date_time':			return "Date             Time";
+		case 'from_net_addr':		return "Net Address";
+		case 'id':					return "Message-ID";
+		case 'ftn_pid':				return "Program-ID";
+		case 'ftn_msgid':			return "FidoNet Message-ID";
+		case 'ftn_reply':			return "FidoNet Reply-ID";
+		case 'ftn_tid':				return "FidoNet Tosser-ID";
+		case 'priority':			return "Prio";
+	}
+	return name.replace(/_/g, ' ').capitalize();
+}
+
+/* Supported list formats */
+var list_format = 0;
+var list_formats = [];
+
+const sub_list_formats = [
+	[ "to", "subject" ],
+];
+
+const local_sub_list_formats = [
+	[ "attr", "score",  "age", "date_time"],
+];
+
+const net_sub_list_formats = [
+	[ "from_net_addr", "date_time", "zone"],
+	[ "attr", "score", "subject" ],
+	[ "id" ],
+	[ "ftn_pid" ],
+	[ "received", "age" ],
+	[ "tags" ],
+];
+
+const fido_sub_list_formats = [
+	[ "ftn_msgid" ],
+	[ "ftn_reply" ],
+	[ "ftn_tid" ],
+];
+
+const mail_sent_list_formats = [
+	[ "attributes", "to_list" ]
+];
+
+const mail_list_formats = [
+	[ "attributes", "subject" ],
+	[ "priority", "date_time", "zone" ],
+	[ "to_list" , "to_ext"],
+	[ "cc_list", "to_net_addr" ],
+	[ "from_net_addr" ],
+	[ "received" ],
+	[ "replyto_net_addr" ],
+];
+
+const extra_list_formats = [
+	[ "size", "text_length", "lines" ],
+];
+
+function max_len(name)
+{
+	switch(name /*.toLowerCase().replace(/\s+/g, '') */) {
+		case 'spam':
+		case 'read':
+			return 5;
+		case 'age':
+		case 'attr':
+			return 8;
+		case 'attributes':
+			return 16;
+		case 'date':
+			return 10;
+		case 'data_length':
+		case 'text_length':
+			return 11;
+		case 'from':
+		case 'to':
+			return 25;
+		case 'date_time':
+			return 24;		// Date and time
+		case 'zone':
+			return 10;
+		case 'received':
+			return 34;		// Date, time and zone
+		case 'score':
+			return 5;
+		case 'from_net_addr':
+			return 15;		// 333:6404/1384.1253
+		case 'priority':
+			return 4;
+		case 'size':
+			return 8;
+	}
+	return 39;
+}
+
+var cmd_prompt_fmt = "\x01n\x01c\xfe \x01h%s \x01n\x01c\xfe ";
+if(js.global.console==undefined || !console.term_supports(USER_ANSI))
+	cmd_prompt_fmt = "%s: ";
+
+function cp437(msg, prop)
+{
+	if(!msg[prop])
+		return null;
+	var xlatprop = prop + '_cp437';
+	if(msg[xlatprop])
+		return msg[xlatprop];
+	if((msg.auxattr&MSG_HFIELDS_UTF8) /* && !console.term_supports(USER_UTF8) */)
+		msg[xlatprop] = utf8_decode(msg[prop]);
+//	log('decoding ' + prop + ' : ' + msg[prop]);
+	if(msg[xlatprop] == undefined)
+		msg[xlatprop] = mimehdr.to_cp437(msg[prop]);
+	return msg[xlatprop];
+}
+
+function property_value(msg, prop)
+{
+	var val;
+	
+	switch(prop) {
+		case 'to_list':
+			return cp437(msg, prop) || msg.to;
+		case 'attr':
+			return msg_attributes(msg, msgbase, /* short: */true);
+		case 'spam':
+			if(msg.attr & MSG_SPAM)
+				val = "SPAM";
+			break;
+		case 'age':
+			return age.string(msg.when_written_time - (msg.when_written_zone_offset * 60)
+							, /* adjust_for_zone: */true);
+		case 'date':
+			return options.date_fmt ? strftime(options.date_fmt, msg.when_written_time) 
+									: system.datestr(msg.when_written_time);
+		case 'date_time':
+			return options.date_time_fmt ? strftime(options.date_time_fmt, msg.when_written_time)
+										: system.timestr(msg.when_written_time);
+		case 'zone':
+			return system.zonestr(msg.when_written_zone);
+		case 'received':
+			val = options.date_time_fmt ? strftime(options.date_time_fmt, msg.when_imported_time)
+										: system.timestr(msg.when_imported_time);
+			return format("%s %s", val, system.zonestr(msg.when_imported_zone));
+		case 'id':
+			if(msg.id) {
+				val = msg.id.match(/[^<>]+/);
+			}
+			break;
+		case 'size':
+			return file_size_str(msg.data_length);
+		case 'from':
+		case 'subject':
+			return cp437(msg, prop);
+		default:
+			val = msg[prop];
+			break;
+	}
+	if(val == undefined)
+		return '';
+	return val;
+}
+
+function property_sort_value(msg, prop)
+{
+	prop = prop; //.toLowerCase().replace(/\s+/g, '');
+	switch(prop) {
+		case 'attr':
+			return msg.attr;
+		case 'age':
+			return age.seconds(msg.when_written_time - (msg.when_written_zone_offset * 60)
+				, /* adjust_for_zone: */true);
+			break;
+		case 'date':
+		case 'date_time':
+			return msg.when_written_time;
+		case 'zone':
+			return msg.when_written_zone_offset;
+		case 'received':
+			return msg.when_imported_time;
+		case 'to_ext':
+			return parseInt(msg.to_ext, 10);
+		case 'priority':
+			return msg.priority ? msg.priority : SMB_PRIORITY_NORMAL;
+		case 'size':
+			return msg.data_length;
+	}
+	return property_value(msg, prop);
+}
+
+var sort_property = 'num';
+var sort_reversed = false;
+function sort_compare(a, b)
+{
+	var val1="";
+	var val2="";
+	
+	if(sort_reversed) {
+		var tmp = a;
+		a = b;
+		b = tmp;
+	}
+
+	val1 = property_sort_value(a, sort_property);
+	val2 = property_sort_value(b, sort_property);
+
+	if(typeof(val1) == "string")
+		val1=val1.toLowerCase();
+	if(typeof(val2) == "string")
+		val2=val2.toLowerCase();
+	else { /* Sort numbers backwards on purpose */
+		var tmp = val1;
+		val1 = val2;
+		val2 = tmp;
+	}
+	if(val1>val2) return 1;
+	if(val1<val2) return -1;
+	return 0;
+}
+
+function console_color(arg, selected)
+{
+	if(selected)
+		arg |= BG_BLUE|BG_HIGH;
+	if(js.global.console != undefined)
+		console.attributes = arg;
+}
+
+function console_beep()
+{
+	if(js.global.console != undefined && options.beep !== false)
+		console.beep();
+}
+
+function help()
+{
+	console.clear();
+	bbs.menu("msglist");
+	console.pause();
+}
+
+function list_msg(msg, digits, selected, sort, msg_ctrl, exclude)
+{
+	var color = color_cfg.column[0];
+	var color_mask = msg_ctrl ? 7 : 0xff;
+	
+	console_color(color&color_mask, selected);
+	printf("%-*u%c", digits, msg.num, msg.flagged ? 251 : ' ');
+	color = LIGHTMAGENTA;
+	if(color_cfg.column[1] != undefined)
+		color = color_cfg.column[1];
+	if(sort == "from")
+		color |= color_cfg.sorted;
+	color &= color_mask;
+	console_color(color++, selected);
+	printf("%-*.*s%c", LEN_ALIAS, LEN_ALIAS, property_value(msg, 'from'), selected ? '<' : ' ');
+		
+	if(!js.global.console || console.screen_columns >= 80) {
+		for(var i = 0; i < list_formats[list_format].length; i++) {
+			var prop = list_formats[list_format][i];
+			if(exclude.indexOf(prop) >= 0)
+				continue;
+			var heading = columnHeading(prop);
+			var fmt = "%-*.*s";
+			var len = Math.max(heading.length, max_len(prop));
+			if(i > 0)
+				len++;
+			var last_column = (i == list_formats[list_format].length - 1);
+			if(js.global.console) {
+				if(i > 0) {
+					if(last_column)
+						fmt = "%*.*s";
+					else
+						console.print(" ");
+				}
+				var cols_remain = console.screen_columns - console.current_column - 1;
+				if(last_column)
+					len = cols_remain;
+				else
+					len = Math.min(len, cols_remain);
+			}
+			if(color > WHITE)
+				color = DARKGRAY;
+			var custom_color = color_cfg.column[i+2];
+			if(custom_color != undefined)
+				color = custom_color;
+			if(prop == sort)
+				color |= color_cfg.sorted;
+			color &= color_mask;
+			console_color(color++, selected);
+			if(typeof msg[prop] == 'number')	// Right-justify numbers
+				fmt = "%*.*s";
+			printf(fmt, len, len, property_value(msg, prop));
+		}
+	}
+}
+
+function view_msg(msg, lines, total_msgs, grp_name, sub_name)
+{
+	var show_hdr = true;
+	var line_num = 0;
+	var hdr_len;
+//	console.clear();
+	msg.lines = lines.length;
+	
+	while(!js.terminated) {
+		if(show_hdr) {
+			console.home();
+//			console.status |= CON_CR_CLREOL;
+			bbs.show_msg_header(msg
+				, property_value(msg, 'subject')
+				, property_value(msg, 'from')
+				, msg.forward_path || msg.to_list || msg.to);
+//			console.status &= ~CON_CR_CLREOL;
+			hdr_len = console.line_counter;
+			if(console.term_supports(USER_ANSI))
+				show_hdr = false;
+		} else
+			console.gotoxy(1, hdr_len + 1);
+		var pagesize = console.screen_rows - (hdr_len + 2);
+		if(line_num + pagesize >= lines.length)
+			line_num = lines.length - pagesize;
+		else if(lines.length > pagesize && line_num >= lines.length - pagesize)
+			line_num = lines.length - (pagesize + 1);
+		if(line_num < 0)
+			line_num = 0;
+		var i = line_num;
+		var row = hdr_len;
+		while(row < (console.screen_rows - 2)) {
+			console.line_counter = 0;
+			if(i < lines.length)
+				console.putmsg(lines[i++].trimRight(), msg.is_utf8 ? P_UTF8 : 0);
+			console.cleartoeol();
+			console.crlf();
+			row++;
+		}
+//		if(line_num == 0)
+//			bbs.download_msg_attachments(msg);
+		var line_range = "no content";
+		if(lines && lines.length) {
+			line_range =	format(options.view_lines_fmt || "%slines %u-%u"
+				,content_description(msg), line_num + 1, i);
+			line_range += format(options.view_total_lines_fmt || " of %u", lines.length);
+		}
+		right_justify(format(options.view_line_range_fmt || "\x01n\x01h\x01k[%s]", line_range));
+		console.crlf();
+		console.print(format("\x01n\x01c\%s \x01h\x01bReading \x01n\x01c\%s %s \x01h%s " +
+			"\x01n\x01c(\x01h?\x01n\x01c=Menu) (\x01h%u\x01n\x01c of \x01h%u\x01n\x01c): \x01n\x01>"
+			, line_num ? "\x18" : "\xfe"
+			, line_num + pagesize < lines.length ? "\x19" : "\xfe"
+			, grp_name
+			, sub_name
+			, msg.num
+			, total_msgs));
+		// Only message text nav keys are handled here
+		var key = console.getkeys(total_msgs, K_UPPER|K_NOCRLF);
+		switch(key) {
+			case KEY_DOWN:
+				if(i < lines.length)
+					line_num++;
+				break;
+			case KEY_UP:
+				if(line_num)
+					line_num--;
+				break;
+			case KEY_HOME:
+				line_num = 0;
+				break;
+			case KEY_END:
+				line_num = lines.length;
+				break;
+			case KEY_PAGEUP:
+				line_num -= pagesize;
+				break;
+			case KEY_PAGEDN:
+			case ' ':
+				line_num += pagesize;
+				break;
+			/* TODO: support F5? */
+			case ascii(ctrl('R')):	/* refresh */
+				show_hdr = true;
+				break;
+			default:
+				return key;
+		}
+	}
+}
+
+function right_justify(text)
+{
+	console.print(format("%*s%s", console.screen_columns - console.current_column - (console.strlen(text) + 1), "", text));
+}
+
+function find(msgbase, search_list, text)
+{
+	var new_list=[];
+	var text=text.toUpperCase();
+	for(var i = 0; i < search_list.length; i++) {
+		var msg = search_list[i];
+		var msgtxt = msg.text;
+		if(msgtxt === undefined)
+			msgtxt = msgbase.get_msg_body(msg);
+		if(msgtxt.toUpperCase().indexOf(text) >= 0)
+			new_list.push(msg);
+		else {
+			for(var p in msg) {
+				if(typeof msg[p] != 'string')
+					continue;
+				if(msg[p].toUpperCase().indexOf(text) >= 0) {
+					new_list.push(msg);
+					break;
+				}
+			}
+		}
+	}
+	return new_list;
+}
+
+function line_split(text, chop)
+{
+	var maxlen = console.screen_columns - 1;
+	text = text.split('\n');
+	for(var i = 0; i < text.length; i++) {
+		// truncate trailing white-space and replace tabs w/spaces
+		text[i] = text[i].trimRight().replace(/\x09/g, '    ');
+		if(console.strlen(text[i]) > maxlen) {
+			if(chop !== true)
+				text.splice(i + 1, 0, text[i].substring(maxlen));
+			text[i] = text[i].substring(0, maxlen);
+		}
+	}
+	return text;
+}
+
+// Return an array of lines of text from the message body/tails/header fields
+function get_msg_lines(msgbase, msg, source, hex, wrap, chop)
+{
+	if(msg.text && msg.source === source && msg.hex == hex && msg.wrapped == wrap) {
+		msg.lines = msg.text.length;
+		return msg.text;
+	}
+	const preparing_fmt = options.preparing_preview_fmt || "%25s";
+	msg.hex = hex;
+	msg.source = (source===true && !hex);
+	msg.wrapped = false;
+	msg.html = false;
+	console.print(format(preparing_fmt, options.reading_message_text || "\x01[Reading message text ..."));
+	var text = msgbase.get_msg_body(msg
+				,/* strip ctrl-a */msg.source
+				,/* dot-stuffing */false
+				,/* tails */true
+				,/* plain-text */!msg.source);
+	if(!text) {
+		console.clearline();
+		return [];
+	}
+	if(text.length <= options.large_msg_threshold)
+		msg.wrapped = (wrap!==false && !hex);
+	if(hex) {
+		console.print(format(preparing_fmt, options.preparing_hex_dump || "\x01[Preparing hex-dump ..."));
+		text = text.slice(0, options.large_msg_threshold);
+		text = hexdump.generate(undefined, text, /* ASCII: */true, /* offsets: */true);
+	} else {
+		if(msg.source)
+			text = msg.get_rfc822_header(/* force_update: */false, /* unfold: */false) + text;
+		else {
+			if(msg.is_utf8 && !console.term_supports(USER_UTF8)) {
+				console.print(format(preparing_fmt, options.translating_charset || "\x01[Translating charset ..."));
+				text = utf8_cp437(text);
+			}
+			if((msg.text_subtype && msg.text_subtype.toLowerCase() == 'html')
+				|| (msg.content_type && msg.content_type.toLowerCase().indexOf("text/html") == 0)) {
+				console.print(format(preparing_fmt, options.decoding_html || "\x01[Decoding HTML ..."));
+				text = html2asc(text);
+				msg.html = true;
+				// remove excessive blank lines after HTML-translation
+				text = text.replace(/\r\n\r\n\r\n/g, '\r\n\r\n');
+			}
+		}
+		text = text.replace(/\xff/g, ' ');	// Use a regular old space for nbsp
+		if(msg.wrapped) {
+			console.print(format(preparing_fmt, options.wrapping_lines || "\x01[Wrapping lines ..."));
+			text = word_wrap(text, console.screen_columns - 1, (msg.columns || 80) - 1).split('\n');
+		} else {
+			console.print(format(preparing_fmt, options.splitting_lines || "\x01[Splitting lines ..."));
+			text = line_split(text, chop);
+		}
+		while(text.length && !text[0].trim().length)
+			text.shift(); // Remove initial blank lines
+		while(text.length && !text[text.length - 1].trim())
+			text.pop();	// Remove trailing blank lines
+	}
+	msg.lines = text.length;
+	if(options.cache_msg_text && text.length < options.large_msg_threshold)
+		msg.text = text;
+	console.clearline();
+	return text;
+}
+
+function next_msg(list, current, prop)
+{
+	var next = current + 1;
+	while(next < list.length) {
+		if(list[next][prop].toLowerCase() == list[current][prop].toLowerCase())
+			break;
+		next++;
+	}
+	if(next < list.length)
+		return next;
+	console_beep();
+	return current;
+}
+
+function prev_msg(list, current, prop)
+{
+	var prev = current - 1;
+	while(prev >= 0) {
+		if(list[prev][prop].toLowerCase() == list[current][prop].toLowerCase())
+			break;
+		prev--;
+	}
+	if(prev >= 0)
+		return prev;
+	console_beep();
+	return current;
+}
+
+function mail_reply(msg)
+{
+	console.clear();
+	var success = false;
+	if(msg.from_net_type || msg.replyto_net_type) {
+		var addr;
+		if(msg.replyto_net_type)
+			addr = msg.replyto_net_addr;
+		else if(msg.from_net_addr.indexOf('@') < 0)
+			addr = msg.from + '@' + msg.from_net_addr;
+		else
+			addr = msg.from_net_addr;
+		write(bbs.text(EnterNetMailAddress));
+		addr = console.getstr(addr, 128, K_EDIT|K_AUTODEL|K_LINE);
+		if(!addr || console.aborted)
+			return;
+		success = bbs.netmail(addr, msg);
+	} else if(msg.from_ext)
+		success = bbs.email(parseInt(msg.from_ext, 10), msg);
+	if(!success) {
+		alert("Failed to send");
+		console.pause();
+	}
+}
+
+function download_msg_source(msg)
+{
+	var fname = system.temp_dir + "msg_" + msg.number + ".txt";
+	var f = new File(fname);
+	if(!f.open("w"))
+		return false;
+	var text = msgbase.get_msg_body(msg
+				,/* strip ctrl-a */false
+				,/* dot-stuffing */false
+				,/* tails */true
+				,/* plain-text */false);	
+	f.write(msg.get_rfc822_header(/* force_update: */false, /* unfold: */false));
+	f.writeln(text);
+	f.close();
+	return bbs.send_file(fname);
+}
+
+function content_description(msg)
+{
+	var desc = [];
+	if(msg.hex)
+		desc.push('hex-dumpped');
+	else {
+		if(msg.wrapped)
+			desc.push('wrapped');
+		if(msg.source)
+			desc.push('source');
+		else {
+			if(msg.text_charset)
+				desc.push(msg.text_charset.toUpperCase());
+			else if(msg.is_utf8)
+				desc.push("UTF-8");
+			if(msg.html)
+				desc.push('HTML');
+		}
+	}
+	var retval = desc.join(' ');
+	if(retval) retval += ' ';
+	return retval;
+}
+
+function remove_extra_blank_lines(list)
+{
+	for(var i = 0; i < list.length; i++) {
+		if(truncsp(list[i]).length < 1 
+			&& list[i + 1] !== undefined
+			&& truncsp(list[i + 1]).length < 1)
+			list.splice(i + 1, 1);
+	}
+}
+
+function list_msgs(msgbase, list, current, preview, grp_name, sub_name)
+{
+	var mail = (msgbase.attributes & SMB_EMAIL);	
+	if(!current)
+		current = 0;
+	else {
+		for(var i = 0; i <= list.length; i++) {
+			if(list[i] && list[i].number == current) {
+				current = i;
+				break;
+			}
+		}
+	}
+	var top = current;
+	var sort;
+	var reversed = false;
+	var orglist = list.slice();
+	var digits = format("%u", list.length).length;
+	if(!preview)
+		preview = false;
+	var msg_line = 0;
+	var msg_ctrl = false;
+	var spam_visible = true;
+	var view_hex = false;
+	var view_wrapped = true;
+	var view_source = false;
+	var area_name = format(options.area_name_fmt || "\x01n\x01k\x017%s / %s\x01h"
+						,grp_name, sub_name);
+
+	console.line_counter = 0;
+	while(!js.terminated) {
+		var pagesize = console.screen_rows - 3;
+		if(preview)
+			pagesize = Math.floor(pagesize / 2);
+		console.home();
+		console.current_column = 0;
+		console.print(area_name);
+
+		/* Bounds checking: */
+		if(current < 0) {
+			console_beep();
+			current = 0;
+		} else if(current >= list.length) {
+			console_beep();
+			current = list.length-1;
+		}
+
+		if(console.screen_columns >= 80)
+			right_justify(format(options.area_nums_fmt || "[message %u of %u]"
+				, current + 1, list.length));
+		console.cleartoeol();
+		console.crlf();
+		if(list_format >= list_formats.length)
+			list_format = 0;
+		else if(list_format < 0)
+			list_format = list_formats.length-1;
+		
+		var exclude_heading = [];
+		if(!spam_visible)
+			exclude_heading.push("spam");
+		
+		/* Column headings */
+		printf("%-*s %-*s ", digits, "#", LEN_ALIAS, sort=="from" ? "FROM" : "From");
+		for(var i in list_formats[list_format]) {
+			var prop = list_formats[list_format][i];
+			if(exclude_heading.indexOf(prop) >= 0)
+				continue;
+			var fmt = "%-*.*s";
+			var heading = columnHeading(prop);
+			var last_column = (i == (list_formats[list_format].length - 1));
+			if(last_column) {
+				if(i > 0)
+					fmt = "%*.*s";
+			} else {
+				if(i > 0 && max_len(prop) >= heading.length)
+					console.print(" ");
+			}
+			var cols_remain = console.screen_columns - console.current_column - 1;
+			var len = cols_remain;
+			if(!last_column) {
+				len = Math.min(max_len(prop), cols_remain);
+				len = Math.max(heading.length, len);
+			}
+			printf(fmt
+				,len
+				,len
+				,sort==prop ? heading.toUpperCase() : heading);
+		}
+		console.cleartoeol();
+		console.crlf();
+
+		if(top > current)
+			top = current;
+		if(top && (top + pagesize) > list.length)
+			top = list.length-pagesize;
+		if(top + pagesize <= current)
+			top = (current+1) - pagesize;
+		if(top < 0)
+			top = 0;
+
+		for(var i = top; i - top < pagesize; i++) {
+			console.line_counter = 0;
+			console.attributes = LIGHTGRAY;
+			if(list[i] !== undefined && list[i] !== null) {
+				list_msg(list[i], digits, i == current, sort, msg_ctrl, exclude_heading);
+			}
+			console.cleartoeol();
+			console.crlf();
+		}
+		if(preview) {
+			var msg = list[current];
+			var text = get_msg_lines(msgbase, msg);
+			remove_extra_blank_lines(text);
+			var default_separator = "\xc4";
+			console.attributes = msg_ctrl ? color_cfg.preview_separator_active : color_cfg.preview_separator_inactive;
+			while(console.current_column < digits - 1)
+				write(options.preview_separator || default_separator);
+			write(options.preview_label || "\xd9 Preview");
+			var offset = pagesize + 4;
+			if(text) {
+				if(text.length) {
+					if(msg_line < 0 || !msg_ctrl)
+						msg_line = 0;
+					else {
+						var max = Math.max(text.length - 1, text.length - offset);
+						if(msg_line > max)
+							msg_line = max;
+					}
+	//				log(format("text lines(%s): %d\n", typeof text, text.length));
+					var max = Math.min(console.screen_rows - offset, text.length);
+					if(msg_line + max > text.length)
+						msg_line = text.length - max;
+					write(format(options.preview_lines_fmt || " lines %u-%u"
+						, msg_line + 1, msg_line + max));
+					if(max < text.length)
+						write(format(options.preview_total_lines_fmt || " of %u", text.length));
+				}
+				write(options.preview_label_terminator || " \xc0");
+				if(options.preview_properties) {
+					var array = options.preview_properties.split(',');
+					var propval = [];
+					for(var i in array) {
+						if(options.hide_redundant_properties !== false
+							&& list_formats[list_format].indexOf(array[i]) >= 0)
+							continue;
+						var val = property_value(msg, array[i]);
+						if(val)
+							propval.push(format("%.*s", options.preivew_properties_maxlen || 10, val));
+					}
+					propval = propval.join(options.preview_properties_separator || ', ').trim();
+					if(propval.length) {
+						while(console.current_column < console.screen_columns - (4 + propval.length + digits))
+							write(options.preview_separator || default_separator);
+						write(format(options.preview_properties_fmt || "\xd9 %s \xc0", propval));
+					}
+				}
+				while(console.current_column < console.screen_columns - 1)
+					write(options.preview_separator || default_separator);
+			} else {
+				write("!Error getting msg text: " + msgbase.error);
+				msg_line = 0;
+			}
+			console.cleartoeol();
+			console.crlf();
+			console.attributes = msg_ctrl ? color_cfg.preview_active : color_cfg.preview_inactive;
+			for(var i = offset; i < console.screen_rows; i++) {
+				if(text !== null && text[msg_line + (i - offset)])
+					console.write(strip_ctrl(text[msg_line + (i - offset)]));
+				console.cleartoeol();
+				console.crlf();
+			}
+		}
+		
+		var cmds = [];
+		if(!preview)
+			cmds.push("~Preview");
+		if(!(list[current].attr&MSG_NOREPLY)) {
+			if(mail)
+				cmds.push("~Reply");
+			else
+				cmds.push("~Reply/~Mail");
+		}
+		if(list[current].auxattr&(MSG_FILEATTACH|MSG_MIMEATTACH))
+			cmds.push("~Dload");
+		cmds.push("~Goto");
+		cmds.push("~Find");
+		var fmt = ", fmt:0-%u"
+		if(console.term_supports(USER_ANSI))
+			fmt += ", Hm/End/\x18\x19/PgUpDn";
+		fmt += ", ~Quit or [View] ~?";
+		console.mnemonics(cmds.join(", ") + format(fmt, list_formats.length-1));
+		console.cleartoeol();
+		bbs.nodesync(/* clearline: */false);
+		var key = console.getkey();
+		switch(key.toUpperCase()) {
+			case ctrl('A'):
+				var flagged = true;
+				if(list[0] && list[0].flagged)
+					flagged = false;
+				for(var i in list)
+					list[i].flagged = flagged;
+				break;
+			case KEY_DEL:
+				if(msgbase.cfg && !msg_area.sub[msgbase.cfg.code].is_operator)
+					break;
+				flagged = 0;
+				for(var i in list)
+					if(list[i].flagged) {
+						list[i].attr ^= MSG_DELETE;
+						list[i].attributes = msg_attributes(list[i], msgbase);
+						flagged++;
+					}
+				if(!flagged) {
+					list[current].attr ^= MSG_DELETE;
+					list[current].attributes = msg_attributes(list[current], msgbase);
+					current++;
+				}
+				break;
+			case 'V':
+			case '\r':
+				console.clear();
+				var viewed_msg = current;
+				while(!js.terminated && list[current]
+					&& (key = view_msg(list[current]
+						,get_msg_lines(msgbase, list[current], view_source, view_hex, view_wrapped)
+						,list.length
+						,grp_name, sub_name
+						)) != 'Q') {
+					switch(key) {
+						case '?':
+							console.clear();
+							bbs.menu("msgview");
+							console.pause();
+							break;
+						case '\r':
+						case '\n':
+							current++;
+							break;
+						case '\b':
+						case '-':
+							current--;
+							break;
+						case KEY_DEL:
+							if(msgbase.cfg && !msg_area.sub[msgbase.cfg.code].is_operator)
+								break;
+							list[current].attr ^= MSG_DELETE;
+							list[current].attributes = msg_attributes(list[current], msgbase);
+							if(!update_msg_attr(msgbase, list[current]))
+								alert("Delete failed");
+							break;
+						case KEY_LEFT:
+							if(!list[current].columns)
+								list[current].columns = 79;
+							else
+								list[current].columns--;
+							break;
+						case KEY_RIGHT:
+							if(!list[current].columns)
+								list[current].columns = 81;
+							else
+								list[current].columns++;
+							break;							
+						case '>':
+							current = next_msg(list, current, 'subject');
+							break;
+						case '<':
+							current = prev_msg(list, current, 'subject');
+							break;
+						case '}':
+							current = next_msg(list, current, 'from');
+							break;
+						case '{':
+							current = prev_msg(list, current, 'from');
+							break;
+						case ']':
+							current = next_msg(list, current, 'to');
+							break;
+						case '[':
+							current = prev_msg(list, current, 'to');
+							break;
+						case 'A':
+						case 'R':
+							if(mail)
+								mail_reply(list[current]);
+							else {
+								console.clear();
+								bbs.post_msg(msgbase.subnum, list[current]);
+							}
+							break;
+						case 'M':
+							mail_reply(list[current]);
+							break;
+						case 'S':
+							view_source = !view_source;
+							view_hex = false;
+							list[current].text = null;
+							break;
+						case 'H':
+							view_hex = !view_hex;
+							list[current].text = null;
+							break;
+						case 'W':
+							view_wrapped = !view_wrapped;
+							list[current].text = null;
+							break;
+						default:
+							if(typeof key == "number") {
+								current = key -1;
+								break;
+							}
+							break;
+					}
+					if(viewed_msg != current) {
+						console.clear();
+						viewed_msg = current;
+					}
+				}
+				
+				if(list[current]) {
+					var msg = list[current];
+					if(mail && msg.to_ext == user.number) {
+						msg.attr |= MSG_READ;
+						if(update_msg_attr(msgbase, msg))
+							msg.attributes = msg_attributes(msg, msgbase);
+					}
+				}
+				break;
+			case 'D':
+				console.clearline();
+				if(!console.noyes("Download message source", P_NOCRLF)) {
+					if(!download_msg_source(list[current], msgbase))
+						alert("failed");
+					continue;
+				}
+				console.creturn();
+				bbs.download_msg_attachments(list[current]);
+				continue;
+			case 'Q':
+				console.clear();
+				return false;
+			case KEY_HOME:
+				if(msg_ctrl)
+					msg_line = 0;
+				else
+					current=top=0;
+				continue;
+			case KEY_END:
+				if(msg_ctrl)
+					msg_line = 9999999;
+				else
+					current=list.length-1;
+				continue;
+			case KEY_UP:
+				if(msg_ctrl)
+					msg_line--;
+				else
+					current--;
+				break;
+			case KEY_DOWN:
+				if(msg_ctrl)
+					msg_line++;
+				else
+					current++;
+				break;
+			case ' ':
+				if(list[current].flagged)
+					list[current].flagged = false;
+				else
+					list[current].flagged = true;
+				if(!msg_ctrl)
+					current++;
+				break;
+			case KEY_PAGEDN:
+			case 'N':
+				current += pagesize;
+				top += pagesize;
+				break;
+			case KEY_PAGEUP:
+				current -= pagesize;
+				top -= pagesize;
+				break;
+			case 'P':
+				preview = !preview;
+				break;
+			case '\t':
+				preview = true;
+				msg_ctrl = !msg_ctrl;
+				break;
+			case '/':
+			case 'F':
+			{
+				console.clearline();
+				console.print("\x01n\x01y\x01hFind: ");
+				var search = console.getstr(60,K_LINE|K_UPPER|K_NOCRLF);
+				console.clearline(LIGHTGRAY);
+				if(search && search.length) {
+					console.print("Searching \x01i...\x01n");
+					var found = find(msgbase, orglist, search);
+					if(found.length) {
+						list = found;
+						current = 0;
+					} else {
+						console.print("\x01[Text not found: " + search);
+						sleep(1500);
+					}
+				}
+				break;
+			}
+			case 'G':
+				console.clearline();
+				console.print("\x01n\x01y\x01hGo to Message: ");
+				var num = console.getstr(digits,K_LINE|K_NUMBER|K_NOCRLF);
+				if(num)
+					top = current = num - 1;
+				console.clearline(LIGHTGRAY);
+				break;
+			case 'A':
+			case 'R':
+				if(mail)
+					mail_reply(list[current]);
+				else {
+					console.clear();
+					bbs.post_msg(msgbase.subnum, list[current]);
+				}
+				break;
+			case 'M':
+				mail_reply(list[current]);
+				break;
+			case 'S':
+				if(sort == undefined)
+					sort = (key == 'S') ? list_formats[list_format][list_formats[list_format].length - 1] : "from";
+				else {
+					var sort_field = list_formats[list_format].indexOf(sort);
+					if(sort_field >= list_formats[list_format].length)
+						sort = undefined;
+					else if(key == 'S')	{ /* capital 'S', move backwards through sort fields */
+						if(sort == "from")
+							sort = undefined;
+						else if(sort_field)
+							sort = list_formats[list_format][sort_field-1];
+						else
+							sort = "from";
+					} else {
+						if(sort_field < 0)
+							sort = list_formats[list_format][0];
+						else
+							sort = list_formats[list_format][sort_field+1];
+					}
+				}
+				if(sort == undefined)
+					list = orglist.slice();
+				else {
+					sort_reversed = reversed;
+					sort_property = sort;
+					write("\x01[\x01iSorting...\x01n\x01>");
+					list.sort(sort_compare);
+				}
+				break;
+			case 'T':
+				list = orglist.slice();
+				spam_visible = !spam_visible;
+				if(!spam_visible) {
+					list = [];
+					for(i in orglist) {
+						if(!(orglist[i].attr & MSG_SPAM))
+							list.push(orglist[i]);
+					}
+				}
+				break;
+			case '!':
+				list.reverse();
+				reversed = !reversed;
+				break;
+			case KEY_LEFT:
+				list_format--;
+				break;
+			case KEY_RIGHT:
+				list_format++;
+				break;
+			case '?':
+			case 'H':
+				help();
+				break;
+			default:
+				if(key>='0' && key <='9')
+					list_format = parseInt(key);
+				break;
+		}
+	}
+}
+
+function msg_attributes(msg, msgbase, short)
+{
+	var result = [];
+	var str = '';
+	if(msg.attr&MSG_SPAM)							result.push(options.attr_spam || "Sp"), str += 'S';
+	if(msg.auxattr&(MSG_FILEATTACH|MSG_MIMEATTACH))	result.push(options.attr_attach || "Att"), str += 'A';
+	if(msg.attr&MSG_DELETE)							result.push(options.attr_delete || "Del"), str += 'D';
+	if(msgbase.cfg 
+		&& msg.number > msg_area.sub[msgbase.cfg.code].scan_ptr)
+													result.push(options.attr_new || "New"), str += 'N';
+	if(msg.attr&MSG_REPLIED)						result.push(options.attr_replied || "Re"), str += 'r';
+	if(msg.attr&MSG_READ)							result.push(options.attr_read ||"Rd"), str += 'R';
+	if(msg.attr&MSG_PERMANENT)						result.push(options.attr_perm ||"Perm"), str += 'P';
+	if(msg.attr&MSG_ANONYMOUS)						result.push(options.attr_anon || "Anon"), str += 'A';
+	if(msg.attr&MSG_LOCKED)							result.push(options.attr_locked || "Lck"), str += 'L';
+	if(msg.attr&MSG_KILLREAD)						result.push(options.attr_kill || "Kill"), str += 'K';
+	if(msg.attr&MSG_NOREPLY)						result.push(options.attr_noreply || "NoRe"), str += 'n';
+	if(msg.attr&MSG_MODERATED)						result.push(options.attr_mod || "Mod"), str += 'M';
+	if(msg.attr&MSG_VALIDATED)						result.push(options.attr_valid || "Valid"), str += 'V';
+	if(msg.attr&MSG_PRIVATE)						result.push(options.attr_private || "Priv"), str += 'p';
+	if(msg.attr&MSG_POLL)							result.push(options.attr_poll || "Poll"), str += '?';
+	if(msg.netattr&MSG_SENT)						result.push(options.attr_intransit || "Sent"), str += 's';
+	if(msg.netattr&MSG_INTRANSIT)					result.push(options.attr_intransit || "InTransit"), str += 'T';
+	/*
+	if(sub_op(subnum) && msg->hdr.attr&MSG_ANONYMOUS)	return 'A';
+	*/
+	if(short) {
+		return str;
+	}
+	return result.join(options.attr_sep || ' ');
+}
+
+// Update a message header's attributes (only)
+function update_msg_attr(msgbase, msg)
+{
+	return true;
+	var hdr = msgbase.get_msg_header(msg.number, /* expand: */false);
+	if(hdr == null) {
+		alert("get_msg_header(" + msg.number +") failed");
+		return false;
+	}
+	hdr.attr = msg.attr;
+	var result =  msgbase.put_msg_header(hdr);
+	if(!result)
+		alert("put_msg_header(" + msg.number + ") failed");
+	return result;
+}
+
+// Return an array of msgs
+function load_msgs(msgbase, which, mode, usernumber)
+{
+	var mail = (msgbase.attributes & SMB_EMAIL);
+	var list = [];
+	if(mail && which !== undefined && which != MAIL_ALL) {
+		var idxlist = msgbase.get_index();
+		var total_msgs = idxlist.length;
+		for(var i = 0; i < total_msgs; i++) {
+			var idx = idxlist[i];
+			if((idx.attr&MSG_DELETE) && !(mode&LM_INCDEL))
+				continue;
+			if((idx.attr&MSG_SPAM)) {
+				if(mode&LM_NOSPAM)
+					continue;
+			} else {
+				if(mode&LM_SPAMONLY)
+					continue;
+			}
+			if((idx.attr&MSG_READ) && (mode&LM_UNREAD))
+				continue;
+			switch(which) {
+				case MAIL_YOUR:
+					if(idx.to != usernumber)
+						continue;
+					break;
+				case MAIL_SENT:
+					if(idx.from != usernumber)
+						continue;
+					break;
+				case MAIL_ANY:
+					if(idx.to != usernumber
+						&& idx.from != usernumber)
+						continue;
+					break;
+			}
+			list.push(msgbase.get_msg_header(/* by_offset: */true, i, /* expand_fields: */false));
+		}
+	} else {
+		list = msgbase.get_all_msg_headers(/* votes: */false, /* expand_fields: */false);
+	}
+	var msgs = [];
+	for(var i in list) {
+		var msg = list[i];
+		msg.attributes = msg_attributes(msg, msgbase);
+		msg.num = msgs.length + 1;
+		msg.score = 0;
+		if(msg.upvotes)
+			msg.score += msg.upvotes;
+		if(msg.downvotes)
+			msg.score -= msg.downvotes;
+		msgs.push(msg);
+	}
+	if(mode&LM_REVERSE)
+		msgs.reverse();
+	return msgs;
+}
+
+var which;
+var usernumber;
+var lm_mode;
+var preview;
+var msgbase_code;
+
+for(var i in argv) {
+	var arg = argv[i].toLowerCase();
+	switch(arg) {
+		case '-p':
+		case '-preview':
+			preview = true;
+			break;
+		case '-nospam':
+			if(lm_mode === undefined)
+				lm_mode = 0;
+			lm_mode |= LM_NOSPAM;
+			break;
+		case '-spam':
+			if(lm_mode === undefined)
+				lm_mode = 0;
+			lm_mode |= LM_SPAMONLY;
+			break;
+		case '-unread':
+			if(lm_mode === undefined)
+				lm_mode = 0;
+			lm_mode |= LM_UNREAD;
+			break;
+		case '-reverse':
+			if(lm_mode === undefined)
+				lm_mode = 0;
+			lm_mode |= LM_REVERSE;
+			break;
+		case '-all':
+			which = MAIL_ALL;
+			break;
+		case '-sent':
+			which = MAIL_SENT;
+			break;
+		default:
+			if(msgbase_code === undefined)
+				msgbase_code = arg;
+			else if(which === undefined)
+				which = parseInt(arg, 10);
+			else if(usernumber === undefined)
+				usernumber = parseInt(arg, 10);
+			else if(lm_mode === undefined)
+				lm_mode = parseInt(arg, 10);
+			break;
+	}
+}
+
+if(!msgbase_code)
+	msgbase_code = bbs.cursub_code;
+
+var msgbase = new MsgBase(msgbase_code);
+if(!msgbase.open()) {
+	alert(msgbase.error);
+	exit();
+}
+
+var options=load({}, "modopts.js", "msglist:" + msgbase_code);
+if(!options)
+	options=load({}, "modopts.js", "msglist");
+if(!options)
+	options = {};
+if(options.large_msg_threshold === undefined)
+	options.large_msg_threshold = 0x10000;
+if(options.preview_properties === undefined)
+	options.preview_properties = "date,attributes,subject";
+if(options.date_fmt === undefined)
+	options.date_fmt = "%Y-%m-%d";
+// options.date_time_fmt = "%a %b %d %Y %H:%M:%S";
+
+if(!msgbase.total_msgs) {
+	alert("No messages");
+	exit();
+}
+
+function remove_list_format_property(name)
+{
+	for(var i in list_formats) {
+		var j = list_formats[i].indexOf(name);
+		if(j >= 0) {
+			list_formats[i].splice(j, 1);
+			if(!list_formats[i].length)
+				list_formats.splice(i, 1);
+		}
+	}
+}
+
+var curmsg = 0;
+if(msgbase.cfg) {
+	list_formats = sub_list_formats;
+	if(msgbase.cfg.settings & (SUB_FIDO | SUB_QNET | SUB_INET)) {
+		list_formats = list_formats.concat(net_sub_list_formats);
+		if(msgbase.cfg.settings & SUB_FIDO)
+			list_formats = list_formats.concat(fido_sub_list_formats);
+	} else
+		list_formats = list_formats.concat(local_sub_list_formats);
+	if(!(msgbase.cfg.settings & SUB_MSGTAGS))
+		remove_list_format_property("tags");
+	if(msgbase.cfg.settings & SUB_NOVOTING)
+		remove_list_format_property("score");
+	if(!(msgbase.cfg.settings & SUB_TOUSER))
+		remove_list_format_property("to");
+	curmsg = msg_area.sub[msgbase.cfg.code].last_read;
+} else {
+	if(which === undefined)
+		which = MAIL_YOUR;
+	if(which != MAIL_YOUR)
+		list_formats = list_formats.concat(mail_sent_list_formats);
+	list_formats = list_formats.concat(mail_list_formats);
+}
+list_formats = list_formats.concat(extra_list_formats);
+
+if(lm_mode === undefined)
+	lm_mode = 0;
+
+if((system.settings&SYS_SYSVDELM) && (user.is_sysop || (system.settings&SYS_USRVDELM)))
+	lm_mode |= LM_INCDEL;
+
+if(msgbase.attributes & SMB_EMAIL) {
+	if(isNaN(which))
+		which = MAIL_YOUR;
+//	lm_mode |= LM_REVERSE;
+	if(lm_mode&(LM_NOSPAM | LM_SPAMONLY))
+		remove_list_format_property("spam");
+}
+
+if(!usernumber)
+	usernumber = user.number;
+
+console.print("Loading messages \x01i...\x01n ");
+var list = load_msgs(msgbase, which, lm_mode, usernumber);
+if(!list || !list.length) {
+	alert("No messages");
+	exit();
+}
+
+js.on_exit("console.status = " + console.status);
+console.status |= CON_CR_CLREOL;
+js.on_exit("console.ctrlkey_passthru = " + console.ctrlkey_passthru);
+console.ctrlkey_passthru |= (1<<16);      // Disable Ctrl-P handling in sbbs
+console.ctrlkey_passthru |= (1<<26);      // Disable Ctrl-Z handling in sbbs
+js.on_exit("bbs.sys_status &= ~SS_MOFF");
+bbs.sys_status |= SS_MOFF; // Disable automatic messages
+
+var grp_name;
+var sub_name = "???";
+if(msgbase.cfg) {
+	grp_name = msgbase.cfg.grp_name;
+	sub_name = msgbase.cfg.name;
+} else {
+	grp_name = "E-mail";
+	switch(which) {
+		case MAIL_YOUR:
+			sub_name = "Inbox";
+			break;
+		case MAIL_SENT:
+			sub_name = "Sent";
+			break;
+		case MAIL_ALL:
+			sub_name = "All";
+			break;
+		case MAIL_ANY:
+			sub_name = "Any";
+			break;
+		default:
+			sub_name = String(which);
+			break;
+	}
+}
+var result = list_msgs(msgbase, list, curmsg, preview, grp_name, sub_name);
+if(result) {
+	if(msgbase.cfg) {
+		msg_area.sub[msgbase.cfg.code].last_read = result;
+		bbs.scan_msgs(msgbase.cfg.code);
+	}
+}