WIP: Lots of progress with QWK reader/writer

This commit is contained in:
Bryan Ashby 2020-04-25 11:25:47 -06:00
parent d8f0601914
commit 8a81b34ed0
No known key found for this signature in database
GPG Key ID: B49EB437951D2542
3 changed files with 492 additions and 74 deletions

View File

@ -49,7 +49,8 @@ const SYSTEM_META_NAMES = {
const ADDRESS_FLAVOR = { const ADDRESS_FLAVOR = {
Local : 'local', // local / non-remote addressing Local : 'local', // local / non-remote addressing
FTN : 'ftn', // FTN style FTN : 'ftn', // FTN style
Email : 'email', Email : 'email', // From email
QWK : 'qwk', // QWK packet
}; };
const STATE_FLAGS0 = { const STATE_FLAGS0 = {
@ -94,14 +95,6 @@ const QWKPropertyNames = {
InReplyToNum : 'qwk_in_reply_to_num', // note that we prefer the 'InReplyToMsgId' kludge if available InReplyToNum : 'qwk_in_reply_to_num', // note that we prefer the 'InReplyToMsgId' kludge if available
}; };
const QWKKludgeNames = {
Via : 'via',
MessageId : 'msg_id',
InReplyToMsgId : 'in_reply_to_msg_id',
SyncTZ : 'synchronet_timezone',
ReplyTo : 'reply_to',
};
// :TODO: this is a ugly hack due to bad variable names - clean it up & just _.camelCase(k)! // :TODO: this is a ugly hack due to bad variable names - clean it up & just _.camelCase(k)!
const MESSAGE_ROW_MAP = { const MESSAGE_ROW_MAP = {
reply_to_message_id : 'replyToMsgId', reply_to_message_id : 'replyToMsgId',

View File

@ -29,6 +29,7 @@ exports.getDefaultMessageAreaTagByConfTag = getDefaultMessageAreaTagByConfTag;
exports.getSuitableMessageConfAndAreaTags = getSuitableMessageConfAndAreaTags; exports.getSuitableMessageConfAndAreaTags = getSuitableMessageConfAndAreaTags;
exports.getMessageConferenceByTag = getMessageConferenceByTag; exports.getMessageConferenceByTag = getMessageConferenceByTag;
exports.getMessageAreaByTag = getMessageAreaByTag; exports.getMessageAreaByTag = getMessageAreaByTag;
exports.getMessageConfTagByAreaTag = getMessageConfTagByAreaTag;
exports.changeMessageConference = changeMessageConference; exports.changeMessageConference = changeMessageConference;
exports.changeMessageArea = changeMessageArea; exports.changeMessageArea = changeMessageArea;
exports.hasMessageConfAndAreaRead = hasMessageConfAndAreaRead; exports.hasMessageConfAndAreaRead = hasMessageConfAndAreaRead;

View File

@ -1,12 +1,14 @@
const ArchiveUtil = require('./archive_util'); const ArchiveUtil = require('./archive_util');
const { Errors } = require('./enig_error'); const { Errors } = require('./enig_error');
const Message = require('./message'); const Message = require('./message');
const { splitTextAtTerms } = require('./string_util'); const { splitTextAtTerms } = require('./string_util');
const { getMessageConfTagByAreaTag } = require('./message_area');
const StatLog = require('./stat_log');
const Config = require('./config').get;
const SysProps = require('./system_property');
const { EventEmitter } = require('events'); const { EventEmitter } = require('events');
const temptmp = require('temptmp').createTrackedSession('qwk_packet'); const temptmp = require('temptmp');
const async = require('async'); const async = require('async');
const fs = require('graceful-fs'); const fs = require('graceful-fs');
const paths = require('path'); const paths = require('path');
@ -16,27 +18,64 @@ const moment = require('moment');
const _ = require('lodash'); const _ = require('lodash');
const IniConfigParser = require('ini-config-parser'); const IniConfigParser = require('ini-config-parser');
const SMBTZToUTCOffset = (smbTZ) => { const enigmaVersion = require('../package.json').version;
// convert a Synchronet smblib TZ to a UTC offset
// see https://github.com/kvadevack/synchronet/blob/master/src/smblib/smbdefs.h
return {
// US Standard
'40F0' : '-04:00', // Atlantic
'412C' : '-05:00', // Eastern
'4168' : '-06:00', // Central
'41A4' : '-07:00', // Mountain
'41E0' : '-08:00', // Pacific
'421C' : '-09:00', // Yukon
'4258' : '-10:00', // Hawaii/Alaska
'4294' : '-11:00', // Bering
// US Daylight // Synchronet smblib TZ to a UTC offset
// :TODO: FINISH ME! // see https://github.com/kvadevack/synchronet/blob/master/src/smblib/smbdefs.h
const SMBTZToUTCOffset = {
// US Standard
'40F0' : '-04:00', // Atlantic
'412C' : '-05:00', // Eastern
'4168' : '-06:00', // Central
'41A4' : '-07:00', // Mountain
'41E0' : '-08:00', // Pacific
'421C' : '-09:00', // Yukon
'4258' : '-10:00', // Hawaii/Alaska
'4294' : '-11:00', // Bering
}[smbTZ]; // US Daylight
'C0F0' : '-03:00', // Atlantic
'C12C' : '-04:00', // Eastern
'C168' : '-05:00', // Central
'C1A4' : '-06:00', // Mountain
'C1E0' : '-07:00', // Pacific
'C21C' : '-08:00', // Yukon
'C258' : '-09:00', // Hawaii/Alaska
'C294' : '-10:00', // Bering
// "Non-Standard"
'2294' : '-11:00', // Midway
'21E0' : '-08:00', // Vancouver
'21A4' : '-07:00', // Edmonton
'2168' : '-06:00', // Winnipeg
'212C' : '-05:00', // Bogota
'20F0' : '-04:00', // Caracas
'20B4' : '-03:00', // Rio de Janeiro
'2078' : '-02:00', // Fernando de Noronha
'203C' : '-01:00', // Azores
'1000' : '+00:00', // London
'103C' : '+01:00', // Berlin
'1078' : '+02:00', // Athens
'10B4' : '+03:00', // Moscow
'10F0' : '+04:00', // Dubai
'110E' : '+04:30', // Kabul
'112C' : '+05:00', // Karachi
'114A' : '+05:30', // Bombay
'1159' : '+05:45', // Kathmandu
'1168' : '+06:00', // Dhaka
'11A4' : '+07:00', // Bangkok
'11E0' : '+08:00', // Hong Kong
'121C' : '+09:00', // Tokyo
'1258' : '+10:00', // Sydney
'1294' : '+11:00', // Noumea
'12D0' : '+12:00', // Wellington
}; };
const QWKMessageBlockSize = 128; const UTCOffsetToSMBTZ = _.invert(SMBTZToUTCOffset);
const QWKMessageBlockSize = 128;
const QWKHeaderTimestampFormat = 'MM-DD-YYHH:mm';
const QWKLF = 0xe3;
// See the following: // See the following:
// - http://fileformats.archiveteam.org/wiki/QWK // - http://fileformats.archiveteam.org/wiki/QWK
@ -95,14 +134,29 @@ const MessageHeaderParser = new Parser()
.uint16('relNum') .uint16('relNum')
.uint8('netTag'); .uint8('netTag');
const replaceCharInBuffer = (buffer, search, replace) => {
let i = 0;
search = Buffer.from([search]);
while (i < buffer.length) {
i = buffer.indexOf(search, i);
if (-1 === i) {
break;
}
buffer[i] = replace;
++i;
}
}
class QWKPacketReader extends EventEmitter { class QWKPacketReader extends EventEmitter {
constructor(packetPath, mode=QWKPacketReader.Modes.Guess, options = { keepTearAndOrigin : true } ) { constructor(
packetPath,
{ mode = QWKPacketReader.Modes.Guess, keepTearAndOrigin = true } = { mode : QWKPacketReader.Modes.Guess, keepTearAndOrigin : true })
{
super(); super();
this.packetPath = packetPath; this.packetPath = packetPath;
this.mode = mode; this.options = { mode, keepTearAndOrigin };
this.options = options; this.temptmp = temptmp.createTrackedSession('qwkpacketreader');
} }
static get Modes() { static get Modes() {
@ -137,7 +191,7 @@ class QWKPacketReader extends EventEmitter {
}, },
// create a temporary location to do processing // create a temporary location to do processing
(archiveType, callback) => { (archiveType, callback) => {
temptmp.mkdir( { prefix : 'enigqwkpacket-'}, (err, tempDir) => { this.temptmp.mkdir( { prefix : 'enigqwkreader-'}, (err, tempDir) => {
if (err) { if (err) {
return callback(err); return callback(err);
} }
@ -172,20 +226,20 @@ class QWKPacketReader extends EventEmitter {
switch (key) { switch (key) {
case 'MESSAGES.DAT' : // QWK case 'MESSAGES.DAT' : // QWK
if (this.mode === QWKPacketReader.Modes.Guess) { if (this.options.mode === QWKPacketReader.Modes.Guess) {
this.mode = QWKPacketReader.Modes.QWK; this.options.mode = QWKPacketReader.Modes.QWK;
} }
if (this.mode === QWKPacketReader.Modes.QWK) { if (this.options.mode === QWKPacketReader.Modes.QWK) {
out.messages = { filename }; out.messages = { filename };
} }
break; break;
case 'ID.MSG' : case 'ID.MSG' :
if (this.mode === QWKPacketReader.Modes.Guess) { if (this.options.mode === QWKPacketReader.Modes.Guess) {
this.mode = Modes.REP; this.options.mode = Modes.REP;
} }
if (this.mode === QWKPacketReader.Modes.REP) { if (this.options.mode === QWKPacketReader.Modes.REP) {
out.messages = { filename }; out.messages = { filename };
} }
break; break;
@ -258,12 +312,9 @@ class QWKPacketReader extends EventEmitter {
(callback) => { (callback) => {
return this.processPacketFiles(callback); return this.processPacketFiles(callback);
}, },
(tempDir, callback) => {
return callback(null);
}
], ],
err => { err => {
temptmp.cleanup(); this.temptmp.cleanup();
if (err) { if (err) {
return this.emit('error', err); return this.emit('error', err);
@ -386,14 +437,19 @@ class QWKPacketReader extends EventEmitter {
const path = paths.join(this.packetInfo.tempDir, this.packetInfo.headers.filename); const path = paths.join(this.packetInfo.tempDir, this.packetInfo.headers.filename);
fs.readFile(path, { encoding : 'utf8' }, (err, iniData) => { fs.readFile(path, { encoding : 'utf8' }, (err, iniData) => {
if (err) { if (err) {
this.emit('warning', Errors.Invalid(`HEADERS.DAT file appears to be invalid (${err.message})`)); this.emit('warning', Errors.Invalid(`Problem reading HEADERS.DAT: ${err.message}`));
return cb(null); // non-fatal return cb(null); // non-fatal
} }
try { try {
this.packetInfo.headers.ini = IniConfigParser.parse(iniData); const parserOptions = {
lineComment : false, // no line comments; consume full lines
nativeType : false, // just keep everything as strings
dotKey : false, // 'a.b.c = value' stays 'a.b.c = value'
};
this.packetInfo.headers.ini = IniConfigParser.parse(iniData, parserOptions);
} catch (e) { } catch (e) {
this.emit('warning', Errors.Invalid(`HEADERS.DAT file appears to be invalid (${e.message})`)); this.emit('warning', Errors.Invalid(`HEADERS.DAT file appears to be invalid: ${e.message}`));
} }
return cb(null); return cb(null);
@ -401,7 +457,6 @@ class QWKPacketReader extends EventEmitter {
} }
readMessages(cb) { readMessages(cb) {
// :TODO: update to use proper encoding: if headers.dat specifies UTF-8, use that, else CP437
if (!this.packetInfo.messages) { if (!this.packetInfo.messages) {
return cb(Errors.DoesNotExist('No messages file found within QWK packet')); return cb(Errors.DoesNotExist('No messages file found within QWK packet'));
} }
@ -420,7 +475,6 @@ class QWKPacketReader extends EventEmitter {
const FTNPropertyMapping = { const FTNPropertyMapping = {
'X-FTN-AREA' : Message.FtnPropertyNames.FtnArea, 'X-FTN-AREA' : Message.FtnPropertyNames.FtnArea,
'X-FTN-SEEN-BY' : Message.FtnPropertyNames.FtnSeenBy, 'X-FTN-SEEN-BY' : Message.FtnPropertyNames.FtnSeenBy,
'X-FTN-FLAGS' : Message.FtnPropertyNames
}; };
const FTNKludgeMapping = { const FTNKludgeMapping = {
@ -452,6 +506,10 @@ class QWKPacketReader extends EventEmitter {
Reply : '@REPLY:', Reply : '@REPLY:',
TZ : '@TZ:', // https://github.com/kvadevack/synchronet/blob/master/src/smblib/smbdefs.h TZ : '@TZ:', // https://github.com/kvadevack/synchronet/blob/master/src/smblib/smbdefs.h
ReplyTo : '@REPLYTO:', ReplyTo : '@REPLYTO:',
// :TODO: Look into other non-standards
// https://github.com/wmcbrine/MultiMail/blob/master/mmail/qwk.cc
// title, @subject, etc.
}; };
let blockCount = 0; let blockCount = 0;
@ -480,7 +538,7 @@ class QWKPacketReader extends EventEmitter {
if (0 === blockCount) { if (0 === blockCount) {
// first 128 bytes is a space padded ID // first 128 bytes is a space padded ID
const id = buffer.toString('ascii').trim(); const id = buffer.toString('ascii').trim();
this.emit('generator', id); this.emit('creator', id);
state = 'header'; state = 'header';
} else { } else {
switch (state) { switch (state) {
@ -493,7 +551,7 @@ class QWKPacketReader extends EventEmitter {
header[field] = iconv.decode(header[field], encodingToSpec).trim(); header[field] = iconv.decode(header[field], encodingToSpec).trim();
}); });
header.timestamp = moment(header.timestamp, 'MM-DD-YYHH:mm'); header.timestamp = moment(header.timestamp, QWKHeaderTimestampFormat);
currMessage = { currMessage = {
header, header,
@ -523,7 +581,7 @@ class QWKPacketReader extends EventEmitter {
case 'message' : case 'message' :
if (!currMessage.body) { if (!currMessage.body) {
currMessage.body = buffer; currMessage.body = Buffer.from(buffer);
} else { } else {
currMessage.body = Buffer.concat([currMessage.body, buffer]); currMessage.body = Buffer.concat([currMessage.body, buffer]);
} }
@ -534,16 +592,7 @@ class QWKPacketReader extends EventEmitter {
// First, replace QWK style line feeds (0xe3) unless the message is UTF-8. // First, replace QWK style line feeds (0xe3) unless the message is UTF-8.
// If the message is UTF-8, we assume it's using standard line feeds. // If the message is UTF-8, we assume it's using standard line feeds.
if (encoding !== 'utf8') { if (encoding !== 'utf8') {
let i = 0; replaceCharInBuffer(currMessage.body, QWKLF, 0x0a);
const QWKLF = Buffer.from([0xe3]);
while (i < currMessage.body.length) {
i = currMessage.body.indexOf(QWKLF, i);
if (-1 === i) {
break;
}
currMessage.body[i] = 0x0a;
++i;
}
} }
// //
@ -583,15 +632,15 @@ class QWKPacketReader extends EventEmitter {
} else if (line.startsWith(Kludges.Subject)) { } else if (line.startsWith(Kludges.Subject)) {
currMessage.subject = line.substring(Kludges.Subject.length).trim(); currMessage.subject = line.substring(Kludges.Subject.length).trim();
} else if (line.startsWith(Kludges.Via)) { } else if (line.startsWith(Kludges.Via)) {
qwkKludge.via = line; qwkKludge['@VIA'] = line;
} else if (line.startsWith(Kludges.MsgID)) { } else if (line.startsWith(Kludges.MsgID)) {
qwkKludge.msg_id = line.substring(Kludges.MsgID.length).trim(); qwkKludge['@MSGID'] = line.substring(Kludges.MsgID.length).trim();
} else if (line.startsWith(Kludges.Reply)) { } else if (line.startsWith(Kludges.Reply)) {
qwkKludge.in_reply_to_msg_id = line.substring(Kludges.Reply.length).trim(); qwkKludge['@REPLY'] = line.substring(Kludges.Reply.length).trim();
} else if (line.startsWith(Kludges.TZ)) { } else if (line.startsWith(Kludges.TZ)) {
qwkKludge.synchronet_timezone = line.substring(Kludges.TZ.length).trim(); qwkKludge['@TZ'] = line.substring(Kludges.TZ.length).trim();
} else if (line.startsWith(Kludges.ReplyTo)) { } else if (line.startsWith(Kludges.ReplyTo)) {
qwkKludge.reply_to = line.substring(Kludges.ReplyTo.length).trim(); qwkKludge['@REPLYTO'] = line.substring(Kludges.ReplyTo.length).trim();
} else { } else {
bodyState = 'body'; // past this point and up to any tear/origin/etc., is the real message body bodyState = 'body'; // past this point and up to any tear/origin/etc., is the real message body
bodyLines.push(line); bodyLines.push(line);
@ -620,12 +669,12 @@ class QWKPacketReader extends EventEmitter {
const ext = currMessage.headersExtension; const ext = currMessage.headersExtension;
// to and subject can be overridden yet again if entries are present // to and subject can be overridden yet again if entries are present
currMessage.toName = ext.To || currMessage.toName currMessage.toName = ext.To || currMessage.toName;
currMessage.subject = ext.Subject || currMessage.subject; currMessage.subject = ext.Subject || currMessage.subject;
currMessage.from = ext.Sender || currMessage.from; // why not From? Who the fuck knows. currMessage.from = ext.Sender || currMessage.fromName; // why not From? Who the fuck knows.
// possibly override message ID kludge // possibly override message ID kludge
qwkKludge.msg_id = ext['Message-ID'] || qwkKludge.msg_id; qwkKludge['@MSGID'] = ext['Message-ID'] || qwkKludge['@MSGID'];
// WhenWritten contains a ISO-8601-ish timestamp and a Synchronet/SMB style TZ offset: // WhenWritten contains a ISO-8601-ish timestamp and a Synchronet/SMB style TZ offset:
// 20180101174837-0600 4168 // 20180101174837-0600 4168
@ -640,7 +689,7 @@ class QWKPacketReader extends EventEmitter {
} }
if (ext.Tags) { if (ext.Tags) {
currMessage.hashTags = ext.Tags.split(' '); currMessage.hashTags = (ext.Tags).toString().split(' ');
} }
// FTN style properties/kludges represented as X-FTN-XXXX // FTN style properties/kludges represented as X-FTN-XXXX
@ -668,6 +717,9 @@ class QWKPacketReader extends EventEmitter {
hashTags : currMessage.hashTags, hashTags : currMessage.hashTags,
}); });
// Indicate this message was imported from a QWK packet
message.meta.System[Message.SystemMetaNames.ExternalFlavor] = Message.AddressFlavor.QWK;
if (!_.isEmpty(qwkKludge)) { if (!_.isEmpty(qwkKludge)) {
message.meta.QwkKludge = qwkKludge; message.meta.QwkKludge = qwkKludge;
} }
@ -692,8 +744,8 @@ class QWKPacketReader extends EventEmitter {
} }
// Update the timestamp if we have a valid TZ // Update the timestamp if we have a valid TZ
if (useTZKludge && _.has(message, 'meta.QwkKludge.synchronet_timezone')) { if (useTZKludge && qwkKludge['@TZ']) {
const tzOffset = SMBTZToUTCOffset(message.meta.QwkKludge.synchronet_timezone); const tzOffset = SMBTZToUTCOffset[qwkKludge['@TZ']];
if (tzOffset) { if (tzOffset) {
message.modTimestamp.utcOffset(tzOffset); message.modTimestamp.utcOffset(tzOffset);
} }
@ -704,7 +756,7 @@ class QWKPacketReader extends EventEmitter {
qwk_in_reply_to_num : currMessage.header.replyToNum, qwk_in_reply_to_num : currMessage.header.replyToNum,
}; };
if (this.mode === QWKPacketReader.Modes.QWK) { if (this.options.mode === QWKPacketReader.Modes.QWK) {
message.meta.QwkProperty.qwk_msg_num = currMessage.header.num; message.meta.QwkProperty.qwk_msg_num = currMessage.header.num;
message.meta.QwkProperty.qwk_conf_num = currMessage.header.confNum; message.meta.QwkProperty.qwk_conf_num = currMessage.header.confNum;
} else { } else {
@ -735,7 +787,379 @@ class QWKPacketReader extends EventEmitter {
} }
}; };
class QWKPacketWriter extends EventEmitter {
constructor(
{
enableQWKE = true,
enableHeadersExtension = true,
enableAtKludges = true,
encoding = 'cp437',
systemDomain = 'enigma-bbs',
bbsID = '',
toUser = '',
} = QWKPacketWriter.DefaultOptions)
{
super();
this.options = {
enableQWKE,
enableHeadersExtension,
enableAtKludges,
systemDomain,
bbsID,
toUser,
encoding : encoding.toLowerCase(),
};
this.temptmp = temptmp.createTrackedSession('qwkpacketwriter');
}
static get DefaultOptions() {
return {
enableQWKE : true,
enableHeadersExtension : true,
enableAtKludges : true,
encoding : 'cp437',
systemDomain : 'enigma-bbs',
bbsID : '',
toUser : '',
};
}
init() {
async.series(
[
(callback) => {
return StatLog.init(callback);
},
(callback) => {
this.temptmp.mkdir( { prefix : 'enigqwkwriter-'}, (err, workDir) => {
this.workDir = workDir;
return callback(err);
});
},
(callback) => {
this.messagesStream = fs.createWriteStream(paths.join(this.workDir, 'messages.dat'));
if (this.options.enableHeadersExtension) {
this.headersDatStream = fs.createWriteStream(paths.join(this.workDir, 'headers.dat'));
}
// First block is a space padded ID
const id = `Created with ENiGMA 1/2 BBS v${enigmaVersion} Copyright (c) 2015-2020 Bryan Ashby`;
this.messagesStream.write(id.padEnd(QWKMessageBlockSize, ' '), 'ascii');
this.currentMessageOffset = QWKMessageBlockSize;
this.totalMessages = 0;
this.areaTagsSeen = new Set();
return callback(null);
},
],
err => {
if (err) {
return this.emit('error', err);
}
this.emit('ready');
}
)
}
makeMessageIdentifier(message) {
return `<${message.messageId}.${message.messageUuid}@${this.options.systemDomain}>`;
}
appendMessage(message) {
//
// Each message has to:
// - Append to MESSAGES.DAT
// - Append to HEADERS.DAT if enabled
//
// If this is a personal (ie: non-network) packet:
// - Produce PERSONAL.NDX
// - Produce 000.NDX with pointers to the users personal "inbox" mail
// - Produce ####.NDX with pointers to the public/conference mail
// - Produce TOREADER.EXT if QWKE support is enabled
//
let fullMessageBody = '';
// Start of body is kludges if enabled
if (this.options.enableQWKE) {
if (message.toUserName.length > 25) {
fullMessageBody += `To: ${message.toUserName}\n`;
}
if (message.fromUserName.length > 25) {
fullMessageBody += `From: ${message.fromUserName}\n`;
}
if (message.subject.length > 25) {
fullMessageBody += `Subject: ${message.subject}\n`;
}
}
if (this.options.enableAtKludges) {
// Add in original kludges (perhaps in a different order) if
// they were originally imported
if (Message.AddressFlavor.QWK == message.meta.System[Message.SystemMetaNames.ExternalFlavor]) {
if (message.meta.QwkKludge) {
for (let [kludge, value] of Object.entries(message.meta.QwkKludge)) {
fullMessageBody += `${kludge}: ${value}\n`;
};
}
} else {
fullMessageBody += `@MSGID: ${this.makeMessageIdentifier(message)}\n`;
}
}
// The actual message contents
fullMessageBody += message.message;
// :TODO: sanitize line feeds -> \n ????
// splitTextAtTerms(message.message).forEach(line => {
// appendBodyLine(line);
// });
const encodedMessage = iconv.encode(fullMessageBody, this.options.encoding);
//
// QWK spec wants line feeds as 0xe3 for some reason, so we'll have
// to replace the \n's. If we're going against the spec and using UTF-8
// we can just leave them be.
//
if ('utf8' !== this.options.encoding) {
replaceCharInBuffer(encodedMessage, 0x0a, QWKLF);
}
// Messages must comprise of multiples of 128 bit blocks with the last
// block padded by spaces or nulls (we use nulls)
const fullBlocks = Math.trunc(encodedMessage.length / QWKMessageBlockSize);
const remainBytes = QWKMessageBlockSize - (encodedMessage.length % QWKMessageBlockSize);
// The first block is always a header
this._writeMessageHeader(
message,
fullBlocks + 1 + (remainBytes ? 1 : 0),
);
this.messagesStream.write(encodedMessage);
if (remainBytes) {
this.messagesStream.write(Buffer.alloc(remainBytes, 0x00));
}
if (this.options.enableHeadersExtension) {
this._appendHeadersExtensionData(message);
}
this.currentMessageOffset += fullBlocks * QWKMessageBlockSize;
if (remainBytes)
{
this.currentMessageOffset += QWKMessageBlockSize;
}
this.totalMessages += 1;
this.areaTagsSeen.add(message.areaTag);
}
appendNewFile() {
}
finish(packetPath) {
async.series(
[
(callback) => {
this.messagesStream.on('close', () => {
return callback(null);
});
this.messagesStream.end();
},
(callback) => {
if (!this.headersDatStream) {
return callback(null);
}
this.headersDatStream.on('close', () => {
return callback(null);
});
this.headersDatStream.end();
},
(callback) => {
return this._createControlData(callback);
}
],
err => {
this.temptmp.cleanup();
if (err) {
return this.emit('error', err);
}
this.emit('finished');
}
)
}
_writeMessageHeader(message, totalBlocks) {
const asciiNum = (n, l) => {
if (isNaN(n)) {
return '';
}
return n.toString().substr(0, l);
};
const status = 'FIXME';
const totalBlocksStr = asciiNum(totalBlocks, 6);//totalBlocks.toString().padEnd(6, ' ');
const messageStatus = 255; // :TODO: ever anything different?
const confNumber = 1004; // :TODO: areaTag -> conf mapping
const netTag = ' '; // :TODO:
if (totalBlocksStr.length > 6) {
return this.emit('warning', Errors.General('Message too large for packet'), message);
}
const header = Buffer.alloc(QWKMessageBlockSize, ' ');
header.write(status[0], 0, 1, 'ascii');
header.write(asciiNum(message.messageId), 1, 'ascii'); // :TODO: It seems Sync puts the relative, as in # of messages we've called appendMessage()?!
header.write(message.modTimestamp.format(QWKHeaderTimestampFormat), 8, 13, 'ascii');
header.write(message.toUserName.substr(0, 25), 21, 'ascii');
header.write(message.fromUserName.substr(0, 25), 46, 'ascii');
header.write(message.subject.substr(0, 25), 71, 'ascii');
header.write(' '.repeat(12), 96, 'ascii'); // we don't use the password field
header.write(asciiNum(message.replyToMsgId), 108, 'ascii');
header.write(asciiNum(totalBlocks, 6), 116, 'ascii');
header.writeUInt8(messageStatus, 122);
header.writeUInt16LE(confNumber, 123);
header.writeUInt16LE(0, 125); // :TODO: Check if others actually populate this
header.write(netTag[0], 127, 1, 'ascii');
this.messagesStream.write(header);
}
_createControlData(cb) {
const controlStream = fs.createWriteStream(paths.join(this.workDir, 'control.dat'));
controlStream.setDefaultEncoding('ascii');
controlStream.on('close', () => {
return cb(null);
});
controlStream.on('error', err => {
return cb(err);
});
const controlData = [
Config().general.boardName,
'Earth',
'XXX-XXX-XXX',
`${StatLog.getSystemStat(SysProps.SysOpUsername)}, Sysop`,
`0000,${this.options.bbsID}`,
moment().format('MM-DD-YYYY,HH:mm:ss'),
this.options.toUser,
'', // name of Qmail menu
'0', // uh, OK
this.totalMessages.toString(),
// this next line is total conferences - 1:
// We have areaTag <> conference mapping, so the number should work out
(this.areaTagsSeen.size - 1).toString(),
// :TODO: append all areaTag->conf number/IDs and names (13 chars max)
'0', 'First Conf',
'HELLO',
'BBSNEWS',
'GOODBYE',
];
controlData.forEach(line => {
controlStream.write(`${line}\r\n`);
});
controlStream.end();
}
_makeSynchronetTimestamp(ts) {
const syncTimestamp = ts.format('YYYYMMDDHHmmssZZ');
const syncTZ = UTCOffsetToSMBTZ[ts.format('Z')] || '0000'; // :TODO: what if we don't have a map?
return `${syncTimestamp} ${syncTZ}`;
}
_appendHeadersExtensionData(message) {
const messageData = {
// Synchronet style
Utf8 : ('utf8' === this.options.encoding ? 'true' : 'false'),
'Message-ID' : this.makeMessageIdentifier(message),
WhenWritten : this._makeSynchronetTimestamp(message.modTimestamp),
// WhenImported : '', // :TODO: only if we have a imported time from another external system?
ExportedFrom : `${this.options.systemID} ${message.areaTag} ${message.messageId}`,
Sender : message.fromUserName,
// :TODO: if exporting for QWK-Net style/etc.
//SenderNetAddr
SenderIpAddr : '127.0.0.1', // no sir, that's private.
SenderHostName : this.options.systemDomain,
// :TODO: if exported:
//SenderProtocol
Organization : 'BBS',
//'Reply-To' : :TODO: "address to direct replies".... ?!
Subject : message.subject,
To : message.toUserName,
//ToNetAddr : :TODO: net addr to?!
// :TODO: Only set if not imported:
Tags : message.hashTags.join(' '),
// :TODO: Needs tested with Sync/etc.; Sync wants Conference *numbers*
Conference : getMessageConfTagByAreaTag(message.areaTag),
// ENiGMA Headers
MessageUUID : message.uuid,
ModTimestamp : message.modTimestamp.format('YYYY-MM-DDTHH:mm:ss.SSSZ'),
AreaTag : message.areaTag,
};
const externalFlavor = message.meta.System[Message.SystemMetaNames.ExternalFlavor];
if (externalFlavor === Message.AddressFlavor.FTN) {
// Add FTN properties if it came from such an origin
if (message.meta.FtnProperty) {
const ftnProp = message.meta.FtnProperty;
messageData['X-FTN-AREA'] = ftnProp[Message.FtnPropertyNames.FtnArea];
messageData['X-FTN-SEEN-BY'] = fntProp[Message.FtnPropertyNames.FtnSeenBy];
}
if (message.meta.FtnKludge) {
const ftnKludge = message.meta.FtnKludge;
messageData['X-FTN-PATH'] = ftnKludge.PATH;
messageData['X-FTN-MSGID'] = ftnKludge.MSGID;
messageData['X-FTN-REPLY'] = fntKludge.REPLY;
messageData['X-FTN-PID'] = fntKludge.PID;
messageData['X-FTN-FLAGS'] = ftnKludge.FLAGS;
messageData['X-FTN-TID'] = fntKludge.TID;
messageData['X-FTN-CHRS'] = fntKludge.CHRS;
}
} else {
messageData.WhenExported = this._makeSynchronetTimestamp(moment());
messageData.Editor = `ENiGMA 1/2 BBS FSE v${enigmaVersion}`;
}
this.headersDatStream.write(iconv.encode(`[${this.currentMessageOffset.toString(16)}]\r\n`, this.options.encoding));
for (let [name, value] of Object.entries(messageData)) {
if (value) {
this.headersDatStream.write(iconv.encode(`${name}: ${value}\r\n`, this.options.encoding));
}
}
this.headersDatStream.write('\r\n');
}
}
module.exports = { module.exports = {
QWKPacketReader, QWKPacketReader,
// QWKPacketWriter, QWKPacketWriter,
} }