diff --git a/webv4/lib/forum.js b/webv4/lib/forum.js
index 630e51543c99c1088e68edcbd56ab67c91ca7a8a..5d50da09693fc7bb685ae298d4603f83258cac73 100644
--- a/webv4/lib/forum.js
+++ b/webv4/lib/forum.js
@@ -1,6 +1,9 @@
 require('sbbsdefs.js', 'MSG_DELETE');
 require('xjs.js', 'xjs_compile');
 load(settings.web_lib + 'mime-decode.js');
+load(settings.web_lib + 'avatars.js');
+
+var avatars = new Avatars();
 
 function listGroups() {
     const response = [];
@@ -40,36 +43,11 @@ function listSubs(group) {
             moderated_ars: sub.moderated_ars,
             is_moderated: sub.is_moderated,
             scan_ptr: sub.scan_ptr,
-            scan_cfg: sub.scan_cfg
+            scan_cfg: sub.scan_cfg,
         };
     });
 }
 
-function listThreads(sub, offset, count) {
-
-    offset = parseInt(offset);
-    if (isNaN(offset) || offset < 0) return false;
-    count = parseInt(count);
-    if (isNaN(count) || count < 1) return false;
-
-    var threads = getMessageThreads(sub, settings.max_messages);
-    if (offset >= threads.order.length) return false;
-
-    var stop = Math.min(threads.order.length, offset + count);
-    var ret = { total: threads.order.length, threads : [] };
-    for (var n = offset; n < stop; n++) {
-        var thread = threads.thread[threads.order[n]];
-        var msgs = Object.keys(thread.messages);
-        thread.first = thread.messages[msgs[0]];
-        thread.last = thread.messages[msgs[msgs.length - 1]];
-        thread.messages = msgs.length;
-        ret.threads.push(thread);
-    }
-
-    return ret;
-
-}
-
 function getNewestMessageInSub(sub) {
     const mb = new MsgBase(sub.code);
     if (!mb.open()) return;
@@ -81,7 +59,7 @@ function getNewestMessageInSub(sub) {
         ret = {
             from: h.from,
             subject: h.subject,
-            when_written_time: h.when_written_time,
+            date: h.when_written_time, // should just be a timestamp; all date formatting should be client-side in forum.xjs
         };
         break;
     }
@@ -95,7 +73,6 @@ function getNewestMessagePerSub(grp) {
     return msg_area.grp_list[grp].sub_list.reduce(function (a, c) {
         const s = getNewestMessageInSub(c);
         if (s !== undefined) a[c.code] = s;
-        log(LOG_DEBUG, JSON.stringify(s));
         return a;
     }, {});
 }
@@ -103,22 +80,24 @@ function getNewestMessagePerSub(grp) {
 function getSubUnreadCount(sub) {
     var ret = {
         scanned: 0,
-        total: 0
+        total: 0,
     };
-    if (typeof msg_area.sub[sub] === 'undefined') return ret;
+    if (msg_area.sub[sub] === undefined) return ret;
     try {
-        var ua = crc16_calc(user.alias.toLowerCase());
-        var un = crc16_calc(user.name.toLowerCase());
-        var ud = crc16_calc(user.number);
         var sy = msg_area.sub[sub].scan_cfg&SCAN_CFG_YONLY;
         var sn = msg_area.sub[sub].scan_cfg&SCAN_CFG_NEW;
         var msgBase = new MsgBase(sub);
         msgBase.open();
         for (var m = msg_area.sub[sub].scan_ptr + 1; m <= msgBase.last_msg; m++) {
-            var i = msgBase.get_msg_index(m);
-            if (i === null || i.attr&MSG_DELETE || i.attr&MSG_NODISP) continue;
-            if ((sy && (i.to === ua || i.to === un || (sub === 'mail' && i.to == ud))) || sn) ret.scanned++;
+            var h = msgBase.get_msg_header(m);
+            if (h === null || h.attr&MSG_DELETE || h.attr&MSG_NODISP) continue;
+            if ((sy && (h.to_ext === user.number || h.to === user.alias || h.to === user.name)) || sn) ret.scanned++;
             ret.total++;
+            ret.newest = {
+                from: h.from,
+                subject: h.subject,
+                date: h.when_written_time
+            };
         }
         msgBase.close();
     } catch (err) {
@@ -139,7 +118,7 @@ function getGroupUnreadCount(group) {
         scanned : 0,
         total : 0
     };
-    if (typeof msg_area.grp_list[group] === 'undefined') return ret;
+    if (msg_area.grp_list[group] === undefined) return ret;
     msg_area.grp_list[group].sub_list.forEach(function (sub) {
         var count = getSubUnreadCount(sub.code);
         ret.scanned += count.scanned;
@@ -158,7 +137,7 @@ function getGroupUnreadCounts() {
 function getUnreadInThread(sub, thread, mkeys) {
     if (typeof thread == 'number') {
         var threads = getMessageThreads(sub, settings.max_messages);
-        if (typeof threads.thread[thread] == 'undefined') return 0;
+        if (threads.thread[thread] === undefined) return 0;
         thread = threads.thread[thread];
     }
     var count = 0;
@@ -170,6 +149,7 @@ function getUnreadInThread(sub, thread, mkeys) {
 }
 
 function getThreadVoteTotals(thread, mkeys) {
+    if (!mkeys) mkeys = Object.keys(thread.messages); // Not sure why it doesn't just do this already - does anything else call getThreadVoteTotals?
     return mkeys.reduce(function (a, c, i) {
         if (thread.messages[c].upvotes > 0) {
             if (i == 0) a.up.p++;
@@ -184,58 +164,88 @@ function getThreadVoteTotals(thread, mkeys) {
     }, { up: { p: 0, t: 0 }, down: { p: 0, t: 0 }, total: 0 });
 }
 
-// { [thread_id]: { total, unread, votes: { up: { parent, total }, down: { parent, total }, total }, newest } }
-function getThreadStats(sub, offset, page_size, guest) {
+// Called from lib/events/forum.js to scan a sub for updates
+// Very similar to listThreads, but the reply is smaller and there is no paging/offset
+function getThreadStats(sub, guest) {
     const threads = getMessageThreads(sub, settings.max_messages);
-    const ret = {};
-    if (!offset) offset = 0;
-    if (!page_size) page_size = threads.order.length - offset;
-    offset = offset * page_size;
-    var stop = Math.min(threads.order.length, offset + page_size);
-    for (var n = offset; n < stop; n++) {
-        var thread = threads.thread[threads.order[n]];
-        var mkeys = Object.keys(thread.messages);
-        ret[threads.order[n]] = {
-            total: mkeys.length,
+    const ret = {
+        sub: sub.code,
+        scan_cfg: sub.scan_cfg,
+    };
+    threads.order.forEach(function (e) {
+        const thread = threads.thread[e];
+        const mkeys = Object.keys(thread.messages);
+        ret[e] = {
+            id: e,
+            last: {
+                from: thread.messages[mkeys[mkeys.length - 1]].from,
+                when_written_time: thread.messages[mkeys[mkeys.length - 1]].when_written_time,
+            },
+            messages: mkeys.length,
             unread: guest ? 0 : getUnreadInThread(sub, thread, mkeys),
             votes: getThreadVoteTotals(thread, mkeys),
-            newest: {
-                from: thread.messages[mkeys[mkeys.length - 1]].from,
-                date: system.timestr(thread.messages[mkeys[mkeys.length - 1]].when_written_time)
-            }
         };
+    });
+    return ret;
+}
+
+function listThreads(sub, count, after) {
+
+    count = parseInt(count, 10);
+    if (isNaN(count) || count < 1) return false;
+
+    var threads = getMessageThreads(sub, settings.max_messages);
+    var offset = 0;
+    if (after) offset = threads.order.indexOf(after) + 1;
+
+    var msgs;
+    var thread;
+    var stop = Math.min(threads.order.length, offset + count);
+    var ret = { total: threads.order.length, threads : [] };
+    if (sub.scan_cfg&SCAN_CFG_NEW) {
+        ret.scan_cfg = 'new';
+    } else if (sub.scan_cfg&SCAN_CFG_YONLY) {
+        ret.scan_cfg = 'you_only';
+    }
+    for (var n = offset; n < stop; n++) {
+        thread = threads.thread[threads.order[n]];
+        msgs = Object.keys(thread.messages);
+        ret.threads.push({
+            id: thread.id,
+            subject: thread.subject,
+            first: thread.messages[msgs[0]],
+            last: thread.messages[msgs[msgs.length - 1]],
+            messages: msgs.length,
+            unread: is_user() ? getUnreadInThread(sub, thread) : 0,
+            votes: getThreadVoteTotals(thread),
+        });
     }
+
     return ret;
+
 }
 
 function getVotesInThread(sub, thread) {
     var ret = { t : { u : 0, d : 0 }, m : {} };
-    if (typeof msg_area.sub[sub] === 'undefined') return ret;
+    if (msg_area.sub[sub] === undefined) return ret;
     if (typeof thread === 'number') {
         var threads = getMessageThreads(sub, settings.max_messages);
-        if (typeof threads.thread[thread] === 'undefined') return ret;
+        if (threads.thread[thread] === undefined) return ret;
         thread = threads.thread[thread];
     }
     var msgBase = new MsgBase(sub);
     if (!msgBase.open()) return ret;
-    Object.keys(thread.messages).forEach(
-        function (m) {
-            if (thread.messages[m].upvotes > 0 ||
-                thread.messages[m].downvotes > 0
-            ) {
-                ret.t.up += thread.messages[m].upvotes;
-                ret.t.down += thread.messages[m].downvotes;
-                ret.m[thread.messages[m].number] = {
-                    u : thread.messages[m].upvotes,
-                    d : thread.messages[m].downvotes,
-                    v : msgBase.how_user_voted(
-                        thread.messages[m].number,
-                        msgBase.cfg.settings&SUB_NAME ? user.name : user.alias
-                    )
-                };
-            }
+    Object.keys(thread.messages).forEach(function (m) {
+        if (thread.messages[m].upvotes > 0 || thread.messages[m].downvotes > 0) {
+            ret.t.up += thread.messages[m].upvotes;
+            ret.t.down += thread.messages[m].downvotes;
+            ret.m[thread.messages[m].number] = {
+                u: thread.messages[m].upvotes,
+                d: thread.messages[m].downvotes,
+                v: msgBase.how_user_voted(thread.messages[m].number, msgBase.cfg.settings&SUB_NAME ? user.name : user.alias),
+            };
         }
-    );
+    });
     msgBase.close();
     return ret;
 }
@@ -243,40 +253,36 @@ function getVotesInThread(sub, thread) {
 function getVotesInThreads(sub) {
     var threads = getMessageThreads(sub, settings.max_messages);
     var ret = {};
-    Object.keys(threads.thread).forEach(
-        function(t) {
-            Object.keys(threads.thread[t].messages).forEach(
-                function (m, i) {
-                    if (threads.thread[t].messages[m].upvotes < 1 &&
-                        threads.thread[t].messages[m].downvotes < 1
-                    ) {
-                        return;
-                    }
-                    if (typeof ret[t] === 'undefined') {
-                        ret[t] = { p : { u : 0, d : 0 }, t : { u : 0, d : 0 } };
-                        if (i < 1) {
-                            ret[t].p.u = threads.thread[t].messages[m].upvotes;
-                            ret[t].p.d = threads.thread[t].messages[m].downvotes;
-                        }
-                    }
-                    ret[t].t.u += threads.thread[t].messages[m].upvotes;
-                    ret[t].t.d += threads.thread[t].messages[m].downvotes;
+    Object.keys(threads.thread).forEach(function (t) {
+        Object.keys(threads.thread[t].messages).forEach(function (m, i) {
+            if (threads.thread[t].messages[m].upvotes < 1 && threads.thread[t].messages[m].downvotes < 1) return;
+            if (ret[t] === undefined) {
+                ret[t] = { p: { u: 0, d: 0 }, t: { u: 0, d: 0 } };
+                if (i < 1) {
+                    ret[t].p.u = threads.thread[t].messages[m].upvotes;
+                    ret[t].p.d = threads.thread[t].messages[m].downvotes;
                 }
-            );
-        }
-    );
+            }
+            ret[t].t.u += threads.thread[t].messages[m].upvotes;
+            ret[t].t.d += threads.thread[t].messages[m].downvotes;
+        });
+    });
     return ret;
 }
 
 function getUserPollData(sub, id) {
-    var ret = { answers : 0, tally : [], show_results : false };
-    if (typeof msg_area.sub[sub] === 'undefined') return ret;
+    var ret = {
+        answers: 0,
+        tally: [],
+        show_results: false,
+    };
+    if (msg_area.sub[sub] === undefined) return ret;
     id = parseInt(id);
     if (isNaN(id)) return ret;
     var msgBase = new MsgBase(sub);
     if (!msgBase.open()) return ret;
     // var header = msgBase.get_msg_header(id);
-    // Temporary use of get_all_msg_headers() to get header.tally for polls
+    // Temporary use of get_all_msg_headers() to get header.tally for polls -- lol, "temporary"
     var headers = msgBase.get_all_msg_headers();
     var header = null;
     for (var h in headers) {
@@ -290,10 +296,7 @@ function getUserPollData(sub, id) {
         return ret;
     }
     if (header.tally && Array.isArray(header.tally)) ret.tally = header.tally;
-    ret.answers = msgBase.how_user_voted(
-        header.number,
-        msgBase.cfg.settings&SUB_NAME ? user.name : user.alias
-    );
+    ret.answers = msgBase.how_user_voted(header.number, msgBase.cfg.settings&SUB_NAME ? user.name : user.alias);
     msgBase.close();
     var pollAttr = header.auxattr&POLL_RESULTS_MASK;
     if (header.from === user.alias || header.from === user.name) {
@@ -309,32 +312,19 @@ function getUserPollData(sub, id) {
 }
 
 function getMailHeaders(sent, ascending) {
-    if (typeof sent !== 'undefined' &&
-        sent &&
-        user.security.restrictions&UFLAG_K
-    ) {
-        return []; // They'll just see nothing.  Provide actual feedback?  Does anyone use REST K?
-    }
+    if (sent !== undefined && sent && user.security.restrictions&UFLAG_K) return []; // They'll just see nothing.  Provide actual feedback?  Does anyone use REST K?
     var headers = [];
     var msgBase = new MsgBase('mail');
     if (!msgBase.open()) return headers;
     for (var m = msgBase.first_msg; m <= msgBase.last_msg; m++) {
         var h = msgBase.get_msg_header(m);
         if (h === null || h.attr&MSG_DELETE) continue;
-        if (    (typeof sent != 'undefined' && sent) &&
-                h.from_ext != user.number
-        ) {
-            continue;
-        } else if (
-            (typeof sent == 'undefined' || !sent) &&
-            h.to_ext != user.number
-        ) {
-            continue;
-        }
+        if ((sent !== undefined && sent) && h.from_ext != user.number) continue;
+        if ((sent === undefined || !sent) && h.to_ext != user.number) continue;
         headers.push(h);
     }
     msgBase.close();
-    if (typeof ascending === 'undefined' || !ascending) headers.reverse();
+    if (ascending === undefined || !ascending) headers.reverse(); // not sure why the double !checks re: ascending and sent
     return headers;
 }
 
@@ -347,7 +337,7 @@ function get_mail_headers(filter, ascending) {
         headers: [],
         sent: { read: 0, unread: 0 },
         spam: { read: 0, unread: 0 },
-        inbox: { read: 0, unread: 0 }
+        inbox: { read: 0, unread: 0 },
     };
     if (filter == 'sent' && user.security.restrictions&UFLAG_K) return ret; // I don't remember what this is for.
     const msg_base = new MsgBase('mail');
@@ -376,8 +366,8 @@ function get_mail_headers(filter, ascending) {
 
 function mimeDecode(header, body, code) {
     const ret = {
-        type : '',
-        body : []
+        type: '',
+        body: [],
     };
     const msg = mime_decode(header, body, code);
     if (msg.inlines) {
@@ -404,8 +394,8 @@ function mimeDecode(header, body, code) {
 function getMailBody(number) {
 
     var ret = {
-        type : '',
-        body : ''
+        type: '',
+        body: ''
     };
 
     number = Number(number);
@@ -455,10 +445,11 @@ function getSignature() {
     if (!file_exists(fn)) return '';
     var f = new File(fn);
     f.open('r');
-    if(js.global.utf8_encode)
-	var signature = utf8_encode(f.read());
-    else
+    if (js.global.utf8_encode) {
+    	var signature = utf8_encode(f.read());
+    } else {
         var signature = ascii_str(f.read());
+    }
     f.close();
     return signature;
 }
@@ -576,9 +567,7 @@ function postReply(sub, body, pid) {
             thread_back: pHeader.number,
         };
         if (sub === 'mail') {
-            if (typeof pHeader.from_net_addr !== 'undefined') {
-                header.to_net_addr = pHeader.from_net_addr;
-            }
+            if (typeof pHeader.from_net_addr !== 'undefined') header.to_net_addr = pHeader.from_net_addr;
             ret = postMail(header, body);
         } else {
             ret = postMessage(sub, header, body);
@@ -591,16 +580,9 @@ function postReply(sub, body, pid) {
 
 function postPoll(sub, subject, votes, results, answers, comments) {
 
-    if (user.alias == settings.guest || user.security.restrictions&UFLAG_V) {
-        return false;
-    }
-
-    if (typeof msg_area.sub[sub] === 'undefined' || !msg_area.sub[sub].can_post) {
-        return false;
-    }
-
+    if (user.alias == settings.guest || user.security.restrictions&UFLAG_V) return false;
+    if (typeof msg_area.sub[sub] === 'undefined' || !msg_area.sub[sub].can_post) return false;
     if (typeof subject !== 'string' || subject.length < 1) return false;
-
     if (!Array.isArray(answers) || answers.length < 2) return false;
 
     votes = parseInt(votes);
@@ -608,38 +590,34 @@ function postPoll(sub, subject, votes, results, answers, comments) {
     if (votes > answers) votes = answers;
 
     results = parseInt(results);
-    if (isNaN(results) || results < 0 || results > 3) {
-        return false;
-    }
+    if (isNaN(results) || results < 0 || results > 3) return false;
 
     var header = {
-        attr : MSG_POLL,
-        subject : subject.substr(0, LEN_TITLE),
-        from : msg_area.sub[sub].settings&SUB_AONLY ? "Anonymous" : (msg_area.sub[sub].settings&SUB_NAME ? user.name : user.alias),
-        from_ext : user.number,
-        to : 'All',
-        field_list : [],
-        auxattr : (results<<POLL_RESULTS_SHIFT) | MSG_HFIELDS_UTF8,
-        votes : votes
+        attr: MSG_POLL,
+        subject: subject.substr(0, LEN_TITLE),
+        from: msg_area.sub[sub].settings&SUB_AONLY ? 'Anonymous' : (msg_area.sub[sub].settings&SUB_NAME ? user.name : user.alias),
+        from_ext: user.number,
+        to: 'All',
+        field_list: [],
+        auxattr: (results<<POLL_RESULTS_SHIFT) | MSG_HFIELDS_UTF8,
+        votes: votes
     };
 
     if (Array.isArray(comments)) {
-        comments.forEach(
-            function (e) {
-                header.field_list.push(
-                    { type : SMB_COMMENT, data : e.substr(0, LEN_TITLE) }
-                );
-            }
-        );
+        comments.forEach(function (e) {
+            header.field_list.push({
+                type: SMB_COMMENT,
+                data: e.substr(0, LEN_TITLE),
+            });
+        });
     }
 
-    answers.forEach(
-        function (e) {
-            header.field_list.push(
-                { type : SMB_POLL_ANSWER, data : e.substr(0, LEN_TITLE) }
-            );
-        }
-    );
+    answers.forEach(function (e) {
+        header.field_list.push({
+            type: SMB_POLL_ANSWER,
+            data: e.substr(0, LEN_TITLE),
+        });
+    });
 
     var msgBase = new MsgBase(sub);
     if (!msgBase.open()) return false;
@@ -656,16 +634,12 @@ function postPoll(sub, subject, votes, results, answers, comments) {
 // - This is another sub on which the user is an operator
 function deleteMessage(sub, number) {
     number = parseInt(number);
-    if (typeof msg_area.sub[sub] === 'undefined' && sub !== 'mail') {
-        return false;
-    }
+    if (msg_area.sub[sub] === undefined && sub !== 'mail') return false;
     var msgBase = new MsgBase(sub);
     if (!msgBase.open()) return false;
     var header = msgBase.get_msg_header(number);
     if (header === null) return false;
-    if (sub === 'mail' &&
-        (header.to_ext == user.number || header.from_ext == user.number)
-    ) {
+    if (sub === 'mail' && (header.to_ext == user.number || header.from_ext == user.number)) {
         var ret = msgBase.remove_msg(number);
     } else if (sub !== 'mail' && msg_area.sub[sub].is_operator) {
         var ret = msgBase.remove_msg(number);
@@ -677,31 +651,25 @@ function deleteMessage(sub, number) {
 }
 
 function deleteMail(numbers) {
-    if (typeof numbers === 'undefined' || !Array.isArray(numbers)) return false;
+    if (numbers === undefined || !Array.isArray(numbers)) return false;
     var msgBase = new MsgBase('mail');
     if (!msgBase.open()) return false;
-    numbers.forEach(
-        function (e) {
-            e = parseInt(e);
-            if (isNaN(e) || e < msgBase.first_msg || e > msgBase.last_msg) return;
-            var header = msgBase.get_msg_header(e);
-            if (header === null) return;
-            if (header.to_ext == user.number || header.from_ext == user.number) {
-                msgBase.remove_msg(e);
-            }
+    numbers.forEach(function (e) {
+        e = parseInt(e);
+        if (isNaN(e) || e < msgBase.first_msg || e > msgBase.last_msg) return;
+        var header = msgBase.get_msg_header(e);
+        if (header === null) return;
+        if (header.to_ext == user.number || header.from_ext == user.number) {
+            msgBase.remove_msg(e);
         }
-    );
+    });
     msgBase.close();
     return true;
 }
 
 function voteMessage(sub, number, up) {
-    if (typeof msg_area.sub[sub] === 'undefined' && sub !== 'mail') {
-        return false;
-    }
-    if (user.alias == settings.guest || user.security.restrictions&UFLAG_V) {
-        return false;
-    }
+    if (typeof msg_area.sub[sub] === 'undefined' && sub !== 'mail') return false;
+    if (user.alias == settings.guest || user.security.restrictions&UFLAG_V) return false;
     if (msg_area.sub[sub].settings&SUB_NOVOTING) return false;
     number = parseInt(number);
     if (isNaN(number)) return false;
@@ -714,16 +682,14 @@ function voteMessage(sub, number, up) {
         msgBase.close();
         return false;
     }
-    var uv = msgBase.how_user_voted(
-        header.number, msgBase.cfg.settings&SUB_NAME ? user.name : user.alias
-    );
+    var uv = msgBase.how_user_voted(header.number, msgBase.cfg.settings&SUB_NAME ? user.name : user.alias);
     if (uv === 0) {
         var vh = {
-            'from' : msgBase.cfg.settings&SUB_NAME ? user.name : user.alias,
-            'from_ext' : user.number,
-            'from_net_type' : NET_NONE,
-            'thread_back' : header.number,
-            'attr' : up ? MSG_UPVOTE : MSG_DOWNVOTE
+            from: msgBase.cfg.settings&SUB_NAME ? user.name : user.alias,
+            from_ext: user.number,
+            from_net_type: NET_NONE,
+            thread_back: header.number,
+            attr: up ? MSG_UPVOTE : MSG_DOWNVOTE,
         };
         var ret = msgBase.vote_msg(vh);
     }
@@ -734,44 +700,30 @@ function voteMessage(sub, number, up) {
 function submitPollAnswers(sub, number, answers) {
     if (typeof msg_area.sub[sub] === 'undefined') return false;
     if (msg_area.sub[sub].settings&SUB_NOVOTING) return false;
-    if (user.alias == settings.guest || user.security.restrictions&UFLAG_V) {
-        return false;
-    }
+    if (user.alias == settings.guest || user.security.restrictions&UFLAG_V) return false;
     number = parseInt(number);
     if (isNaN(number)) return false;
     var msgBase = new MsgBase(sub);
     if (!msgBase.open()) return false;
     var ret = false;
     var header = msgBase.get_msg_header(number);
-    if (header !== null &&
-        header.attr&MSG_POLL &&
-        !(header.auxattr&POLL_CLOSED) &&
-        answers.length > 0 &&
-        (   answers.length <= header.votes ||
-            (answers.length == 1 && header.votes == 0)
-        )
-    ) {
-        var uv = msgBase.how_user_voted(
-            number, msgBase.cfg.settings&SUB_NAME ? user.name : user.alias
-        );
+    if (header !== null && header.attr&MSG_POLL && !(header.auxattr&POLL_CLOSED) && answers.length > 0 && (answers.length <= header.votes || (answers.length == 1 && header.votes == 0))) {
+        var uv = msgBase.how_user_voted(number, msgBase.cfg.settings&SUB_NAME ? user.name : user.alias);
         if (uv === 0) {
             var a = 0;
-            answers.forEach(
-                function (e) {
-                    e = parseInt(e);
-                    if (isNaN(e) || e < 0 || e > 15) return;
-                    a|=(1<<e);
-                }
-            );
-            ret = msgBase.vote_msg(
-                {   'from' : msgBase.cfg.settings&SUB_NAME ? user.name : user.alias,
-                    'from_ext' : user.number,
-                    'from_net_type' : NET_NONE,
-                    'thread_back' : number,
-                    'attr' : MSG_VOTE,
-                    'votes' : a
-                }
-            );
+            answers.forEach(function (e) {
+                e = parseInt(e);
+                if (isNaN(e) || e < 0 || e > 15) return;
+                a|=(1<<e);
+            });
+            ret = msgBase.vote_msg({
+                from: msgBase.cfg.settings&SUB_NAME ? user.name : user.alias,
+                from_ext: user.number,
+                from_net_type: NET_NONE,
+                thread_back: number,
+                attr: MSG_VOTE,
+                votes: a,
+            });
         }
     }
     msgBase.close();
@@ -781,18 +733,15 @@ function submitPollAnswers(sub, number, answers) {
 // Deuce's URL-ifier
 function linkify(body) {
     urlRE = /(?:https?|ftp|telnet|ssh|gopher|rlogin|news):\/\/[^\s'"'<>()]*|[-\w.+]+@(?:[-\w]+\.)+[\w]{2,6}/gi;
-    body = body.replace(
-        urlRE,
-        function (str) {
-            var ret=''
-            var p=0;
-            var link=str.replace(/\.*$/, '');
-            var linktext=link;
-            if (link.indexOf('://') === -1) link = 'mailto:' + link;
-            return ('<a class="ulLink" href="' + link + '">' + linktext + '</a>' + str.substr(linktext.length));
-        }
-    );
-    return (body);
+    body = body.replace(urlRE, function (str) {
+        var ret = '';
+        var p = 0;
+        var link = str.replace(/\.*$/, '');
+        var linktext = link;
+        if (link.indexOf('://') === -1) link = 'mailto:' + link;
+        return ('<a class="ulLink" href="' + link + '">' + linktext + '</a>' + str.substr(linktext.length));
+    });
+    return body;
 }
 
 // Somewhat modified version of Deuce's "magical quoting stuff" from v3
@@ -801,47 +750,37 @@ function quotify(body) {
     var blockquote_start = '<blockquote>';
     var blockquote_end = '</blockquote>';
 
-    var lines = body.split(/\r?\n/);
-    body = '';
-
     var quote_depth=0;
     var prefixes = [];
 
-    for (l in lines) {
-
+    const ret = body.split(/\r?\n/).reduce(function (a, c) {
+        var line = '';
         var line_prefix = '';
-        var m = lines[l].match(/^((?:\s?[^\s]{0,3}&gt;\s?)+)/);
-
+        var m = c.match(/^((?:\s?[^\s]{0,3}&gt;\s?)+)/);
         if (m !== null) {
-
-            var new_prefixes = m[1].match(/\s?[^\s]{0,3}&gt;\s?/g);
             var p;
-            var broken = false;
-
-            line = lines[l];
-
+            var broken = false;            
+            var new_prefixes = m[1].match(/\s?[^\s]{0,3}&gt;\s?/g);
+            line = c;
             // If the new length is smaller than the old one, close the extras
             for (p = new_prefixes.length; p < prefixes.length; p++) {
                 if (quote_depth < 1) continue;
                 line_prefix = line_prefix + blockquote_end;
                 quote_depth--;
             }
-
             for (p in new_prefixes) {
                 // Remove prefix from start of line
                 line = line.substr(new_prefixes[p].length);
-
-                if (typeof prefixes[p] === "undefined") {
+                if (prefixes[p] === undefined) {
                     /* New depth */
                     line_prefix = line_prefix + blockquote_start;
                     quote_depth++;
                 } else if (broken) {
                     line_prefix = line_prefix + blockquote_start;
                     quote_depth++;
-                } else if (prefixes[p].replace(/^\s*(.*?)\s*$/,"$1") != new_prefixes[p].replace(/^\s*(.*?)\s*$/,"$1")) {
+                } else if (prefixes[p].replace(/^\s*(.*?)\s*$/, '$1') != new_prefixes[p].replace(/^\s*(.*?)\s*$/, '$1')) {
                     // Close all remaining old prefixes and start one new one
-                    var o;
-                    for (o = p; o < prefixes.length && o < new_prefixes.length; o++) {
+                    for (var o = p; o < prefixes.length && o < new_prefixes.length; o++) {
                         if (quote_depth > 0) {
                             line_prefix = blockquote_end + line_prefix;
                             quote_depth--;
@@ -852,33 +791,27 @@ function quotify(body) {
                     broken = true;
                 }
             }
-
             prefixes = new_prefixes.slice();
             line = line_prefix + line;
-
         } else {
-
             for (p = 0; p < prefixes.length; p++) {
                 if (quote_depth < 1) continue;
                 line_prefix = line_prefix + blockquote_end;
                 quote_depth--;
             }
             prefixes = [];
-            line = line_prefix + lines[l];
-
+            line = line_prefix + c;
         }
-
-        body = body + line + "\r\n";
-
-    }
+        return a + line + '\r\n';
+    }, '');
 
     if (quote_depth !== 0) {
         for (;quote_depth > 0; quote_depth--) {
-            body += blockquote_end;
+            ret += blockquote_end;
         }
     }
 
-    return body.replace(/\<\/blockquote\>\r\n<blockquote\>/g, "\r\n");
+    return ret.replace(/\<\/blockquote\>\r\n<blockquote\>/g, '\r\n');
 
 }
 
@@ -948,7 +881,7 @@ function setScanCfg(sub, cfg) {
         SCAN_CFG_YONLY
     ];
 
-    if (typeof msg_area.sub[sub] === 'undefined') return false;
+    if (msg_area.sub[sub] === undefined) return false;
 
     cfg = parseInt(cfg);
     if (isNaN(cfg) || cfg < 0 || cfg > 2) return false;
@@ -962,14 +895,17 @@ function setScanCfg(sub, cfg) {
 
 function getMessageThreads(sub, max) {
 
-    var threads = { thread : {}, order : [] };
+    var threads = {
+        thread: {},
+        order: [],
+    };
     var subjects = {};
 
-    if (typeof msg_area.sub[sub] === 'undefined') return threads;
+    if (msg_area.sub[sub] === undefined) return threads;
     if (!msg_area.sub[sub].can_read) return threads;
 
     function addToThread(thread_id, header, subject) {
-        if (typeof subject !== 'undefined') subjects[subject] = thread_id;
+        if (subject !== undefined) subjects[subject] = thread_id;
         if (header.when_written_time > threads.thread[thread_id].newest) {
             threads.thread[thread_id].newest = header.when_written_time;
         }
@@ -977,42 +913,33 @@ function getMessageThreads(sub, max) {
             threads.thread[thread_id].unread++;
         }
         threads.thread[thread_id].messages[header.number] = {
-            attr : header.attr,
-            auxattr : header.auxattr,
-            number : header.number,
-            from : (header.attr&MSG_ANONYMOUS) ? "Anonymous" : (header.is_utf8 ? header.from : utf8_encode(header.from)),
-            from_ext : header.from_ext,
-            from_net_addr : header.from_net_addr,
-            to : header.is_utf8 ? header.to : utf8_encode(header.to),
-            when_written_time : header.when_written_time,
-            upvotes : (header.attr&MSG_POLL ? 0 : (header.upvotes || 0)),
-            downvotes : (header.attr&MSG_POLL ? 0 : (header.downvotes || 0)),
+            attr: header.attr,
+            auxattr: header.auxattr,
+            number: header.number,
+            from: (header.attr&MSG_ANONYMOUS) ? "Anonymous" : (header.is_utf8 ? header.from : utf8_encode(header.from)),
+            from_ext: header.from_ext,
+            from_net_addr: header.from_net_addr,
+            to: header.is_utf8 ? header.to : utf8_encode(header.to),
+            when_written_time: header.when_written_time,
+            upvotes: (header.attr&MSG_POLL ? 0 : (header.upvotes || 0)),
+            downvotes: (header.attr&MSG_POLL ? 0 : (header.downvotes || 0)),
             is_utf8: header.is_utf8
         };
         if (header.attr&MSG_POLL) {
-            header.field_list.sort(
-                function (a, b) {
-                    if (a.type === 0x62) return -1;
-                    if (b.type === 0x62) return 1;
-                    return 0;
-                }
-            );
+            header.field_list.sort(function (a, b) {
+                if (a.type === 0x62) return -1;
+                if (b.type === 0x62) return 1;
+                return 0;
+            });
             threads.thread[thread_id].messages[header.number].poll_comments = [];
             threads.thread[thread_id].messages[header.number].poll_answers = [];
-            header.field_list.forEach(
-                function (e) {
-                    switch (e.type) {
-                        case SMB_COMMENT:
-                            threads.thread[thread_id].messages[header.number].poll_comments.push(e);
-                            break;
-                        case SMB_POLL_ANSWER:
-                            threads.thread[thread_id].messages[header.number].poll_answers.push(e);
-                            break;
-                        default:
-                            break;
-                    }
+            header.field_list.forEach(function (e) {
+                if (e.type === SMB_COMMENT) {
+                    threads.thread[thread_id].messages[header.number].poll_comments.push(e);
+                } else if (e.type === SMB_POLL_ANSWER) {
+                    threads.thread[thread_id].messages[header.number].poll_answers.push(e);
                 }
-            );
+            });
             threads.thread[thread_id].messages[header.number].votes = header.votes;
             threads.thread[thread_id].messages[header.number].tally = header.tally || [];
             threads.thread[thread_id].messages[header.number].subject = header.subject;
@@ -1048,48 +975,65 @@ function getMessageThreads(sub, max) {
     msgBase.close();
     if (!headers) return threads;
 
-    Object.keys(headers).forEach(
-
-        function(h) {
-
-            if (headers[h] === null || headers[h].attr&MSG_DELETE) {
-                delete headers[h];
-                return;
-            }
-
-            if (settings.forum_no_spam && is_spam(header)) {
-                delete headers[h];
-                return;
-            }
-
-            if (sub === 'mail' &&
-                headers[h].to !== user.alias &&
-                headers[h].to !== user.name &&
-                headers[h].to_ext !== user.number &&
-                headers[h].from !== user.alias &&
-                headers[h].from !== user.name &&
-                headers[h].from_ext !== user.number
-            ) {
-                delete headers[h];
-                return;
-            }
+    Object.keys(headers).forEach(function (h) {
 
-            var subject = headers[h].subject.replace(/^(re:\s*)*/ig, '');
+        if (headers[h] === null || headers[h].attr&MSG_DELETE) {
+            delete headers[h];
+            return;
+        }
 
-            if (typeof subjects[subject] !== 'undefined') {
+        if (settings.forum_no_spam && is_spam(header)) {
+            delete headers[h];
+            return;
+        }
 
-                addToThread(subjects[subject], headers[h]);
+        if (sub === 'mail' &&
+            headers[h].to !== user.alias &&
+            headers[h].to !== user.name &&
+            headers[h].to_ext !== user.number &&
+            headers[h].from !== user.alias &&
+            headers[h].from !== user.name &&
+            headers[h].from_ext !== user.number
+        ) {
+            delete headers[h];
+            return;
+        }
 
-            } else if (headers[h].thread_id !== 0) {
+        var subject = headers[h].subject.replace(/^(re:\s*)*/ig, '');
 
-                if (typeof threads.thread[headers[h].thread_id]
-                    !== 'undefined'
-                ) {
-                    addToThread(headers[h].thread_id, headers[h], subject);
-                } else {
-                    threads.thread[headers[h].thread_id] = {
-                        id: headers[h].thread_id,
-                        newest : 0,
+        if (subjects[subject] !== undefined) {
+            addToThread(subjects[subject], headers[h]);
+        } else if (headers[h].thread_id !== 0) {
+            if (threads.thread[headers[h].thread_id] === undefined) {
+                threads.thread[headers[h].thread_id] = {
+                    id: headers[h].thread_id,
+                    newest: 0,
+                    subject: headers[h].subject,
+                    messages: {},
+                    votes: {
+                        up: 0,
+                        down: 0
+                    },
+                    unread: 0
+                };
+            }
+            addToThread(headers[h].thread_id, headers[h], subject);
+        } else if (headers[h].thread_back !== 0) {
+            if (threads.thread[headers[h].thread_back] !== undefined) {
+                addToThread(headers[h].thread_back, headers[h], subject);
+            } else {
+                var threaded = false;
+                for (var t in threads.thread) {
+                    if (threads.thread[t].messages[headers[h].thread_back] !== undefined) {
+                        addToThread(t, headers[h], subject);
+                        threaded = true;
+                        break;
+                    }
+                }
+                if (!threaded) {
+                    threads.thread[headers[h].thread_back] = {
+                        id: headers[h].thread_back,
+                        newest: 0,
                         subject: headers[h].subject,
                         messages: {},
                         votes: {
@@ -1098,69 +1042,27 @@ function getMessageThreads(sub, max) {
                         },
                         unread: 0
                     };
-                    addToThread(headers[h].thread_id, headers[h], subject);
-                }
-
-            } else if (headers[h].thread_back !== 0) {
-
-                if (typeof threads.thread[headers[h].thread_back]
-                    !== 'undefined'
-                ) {
                     addToThread(headers[h].thread_back, headers[h], subject);
-                } else {
-                    var threaded = false;
-                    for (var t in threads.thread) {
-                        if (typeof
-                            threads.thread[t].messages[headers[h].thread_back]
-                            !== 'undefined'
-                        ) {
-                            addToThread(t, headers[h], subject);
-                            threaded = true;
-                            break;
-                        }
-                    }
-                    if (!threaded) {
-                        threads.thread[headers[h].thread_back] = {
-                            id: headers[h].thread_back,
-                            newest: 0,
-                            subject: headers[h].subject,
-                            messages: {},
-                            votes: {
-                                up: 0,
-                                down: 0
-                            },
-                            unread: 0
-                        };
-                        addToThread(
-                            headers[h].thread_back,
-                            headers[h],
-                            subject
-                        );
-                    }
                 }
-
-            } else {
-
-                threads.thread[headers[h].number] = {
-                    id: headers[h].number,
-                    newest: 0,
-                    subject: headers[h].subject,
-                    messages: {},
-                    votes: {
-                        up: 0,
-                        down: 0
-                    },
-                    unread: 0
-                };
-                addToThread(headers[h].number, headers[h], subject);
-
             }
-
-            delete headers[h];
-
+        } else {
+            threads.thread[headers[h].number] = {
+                id: headers[h].number,
+                newest: 0,
+                subject: headers[h].subject,
+                messages: {},
+                votes: {
+                    up: 0,
+                    down: 0
+                },
+                unread: 0
+            };
+            addToThread(headers[h].number, headers[h], subject);
         }
 
-    );
+        delete headers[h];
+
+    });
 
     threads.order = Object.keys(threads.thread).sort(function (a, b) {
         return threads.thread[b].newest - threads.thread[a].newest;
@@ -1170,15 +1072,54 @@ function getMessageThreads(sub, max) {
 
 }
 
+function getMessageThread(sub, thread, count, after) {
+
+    thread = parseInt(thread, 10);
+    if (isNaN(thread)) return [];
+    count = parseInt(count, 10);
+    if (isNaN(count)) return [];
+
+    const t = getMessageThreads(sub, settings.max_messages).thread[thread];
+    const mkeys = Object.keys(t.messages);
+    var m; // Current message
+    var r = 0; // Messages returned
+    var n = 0; // Index into t.messages
+    if (after) n = mkeys.indexOf(after) + 1;
+
+    const msgBase = new MsgBase(sub);
+
+    return function threadIterator() {
+        if (r >= count || n >= mkeys.length) {
+            if (msgBase.is_open) msgBase.close();
+            return null; // Done
+        }
+        if (!msgBase.is_open && !msgBase.open()) {
+            throw new Error('Failed to open ' + sub);
+        }
+        m = t.messages[mkeys[n]];
+        const body = msgBase.get_msg_body(m.number);
+        if (body === null) {
+            n++;
+            return threadIterator();
+        }
+        if (r == 0) m.subject = t.subject;
+        m.body = formatMessage(body);
+        n++;
+        r++;
+        return m;
+    }
+
+}
+
 function isValidRequest() {
     if (Request.has_param('group')) {
         const grp = Request.get_param('group');
-        if (typeof msg_area.grp_list[grp] == 'undefined') return false;
+        if (msg_area.grp_list[grp] === undefined) return false;
         if (!user.compare_ars(msg_area.grp_list[grp].ars)) return false;
     }
     if (Request.has_param('sub')) {
         const sub = Request.get_param('sub');
-        if (typeof msg_area.sub[sub] == 'undefined') return false;
+        if (msg_area.sub[sub] === undefined) return false;
     }
     return true;
 }