WIP: Lots of progress with QWK reader/writer
This commit is contained in:
parent
d8f0601914
commit
8a81b34ed0
|
@ -49,7 +49,8 @@ const SYSTEM_META_NAMES = {
|
|||
const ADDRESS_FLAVOR = {
|
||||
Local : 'local', // local / non-remote addressing
|
||||
FTN : 'ftn', // FTN style
|
||||
Email : 'email',
|
||||
Email : 'email', // From email
|
||||
QWK : 'qwk', // QWK packet
|
||||
};
|
||||
|
||||
const STATE_FLAGS0 = {
|
||||
|
@ -94,14 +95,6 @@ const QWKPropertyNames = {
|
|||
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)!
|
||||
const MESSAGE_ROW_MAP = {
|
||||
reply_to_message_id : 'replyToMsgId',
|
||||
|
|
|
@ -29,6 +29,7 @@ exports.getDefaultMessageAreaTagByConfTag = getDefaultMessageAreaTagByConfTag;
|
|||
exports.getSuitableMessageConfAndAreaTags = getSuitableMessageConfAndAreaTags;
|
||||
exports.getMessageConferenceByTag = getMessageConferenceByTag;
|
||||
exports.getMessageAreaByTag = getMessageAreaByTag;
|
||||
exports.getMessageConfTagByAreaTag = getMessageConfTagByAreaTag;
|
||||
exports.changeMessageConference = changeMessageConference;
|
||||
exports.changeMessageArea = changeMessageArea;
|
||||
exports.hasMessageConfAndAreaRead = hasMessageConfAndAreaRead;
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
|
||||
|
||||
const ArchiveUtil = require('./archive_util');
|
||||
const { Errors } = require('./enig_error');
|
||||
const Message = require('./message');
|
||||
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 temptmp = require('temptmp').createTrackedSession('qwk_packet');
|
||||
const temptmp = require('temptmp');
|
||||
const async = require('async');
|
||||
const fs = require('graceful-fs');
|
||||
const paths = require('path');
|
||||
|
@ -16,27 +18,64 @@ const moment = require('moment');
|
|||
const _ = require('lodash');
|
||||
const IniConfigParser = require('ini-config-parser');
|
||||
|
||||
const SMBTZToUTCOffset = (smbTZ) => {
|
||||
// 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
|
||||
const enigmaVersion = require('../package.json').version;
|
||||
|
||||
// US Daylight
|
||||
// :TODO: FINISH ME!
|
||||
// Synchronet smblib TZ to a UTC offset
|
||||
// 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:
|
||||
// - http://fileformats.archiveteam.org/wiki/QWK
|
||||
|
@ -95,14 +134,29 @@ const MessageHeaderParser = new Parser()
|
|||
.uint16('relNum')
|
||||
.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 {
|
||||
constructor(packetPath, mode=QWKPacketReader.Modes.Guess, options = { keepTearAndOrigin : true } ) {
|
||||
constructor(
|
||||
packetPath,
|
||||
{ mode = QWKPacketReader.Modes.Guess, keepTearAndOrigin = true } = { mode : QWKPacketReader.Modes.Guess, keepTearAndOrigin : true })
|
||||
{
|
||||
super();
|
||||
|
||||
this.packetPath = packetPath;
|
||||
this.mode = mode;
|
||||
this.options = options;
|
||||
this.options = { mode, keepTearAndOrigin };
|
||||
this.temptmp = temptmp.createTrackedSession('qwkpacketreader');
|
||||
}
|
||||
|
||||
static get Modes() {
|
||||
|
@ -137,7 +191,7 @@ class QWKPacketReader extends EventEmitter {
|
|||
},
|
||||
// create a temporary location to do processing
|
||||
(archiveType, callback) => {
|
||||
temptmp.mkdir( { prefix : 'enigqwkpacket-'}, (err, tempDir) => {
|
||||
this.temptmp.mkdir( { prefix : 'enigqwkreader-'}, (err, tempDir) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
|
@ -172,20 +226,20 @@ class QWKPacketReader extends EventEmitter {
|
|||
|
||||
switch (key) {
|
||||
case 'MESSAGES.DAT' : // QWK
|
||||
if (this.mode === QWKPacketReader.Modes.Guess) {
|
||||
this.mode = QWKPacketReader.Modes.QWK;
|
||||
if (this.options.mode === QWKPacketReader.Modes.Guess) {
|
||||
this.options.mode = QWKPacketReader.Modes.QWK;
|
||||
}
|
||||
if (this.mode === QWKPacketReader.Modes.QWK) {
|
||||
if (this.options.mode === QWKPacketReader.Modes.QWK) {
|
||||
out.messages = { filename };
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ID.MSG' :
|
||||
if (this.mode === QWKPacketReader.Modes.Guess) {
|
||||
this.mode = Modes.REP;
|
||||
if (this.options.mode === QWKPacketReader.Modes.Guess) {
|
||||
this.options.mode = Modes.REP;
|
||||
}
|
||||
|
||||
if (this.mode === QWKPacketReader.Modes.REP) {
|
||||
if (this.options.mode === QWKPacketReader.Modes.REP) {
|
||||
out.messages = { filename };
|
||||
}
|
||||
break;
|
||||
|
@ -258,12 +312,9 @@ class QWKPacketReader extends EventEmitter {
|
|||
(callback) => {
|
||||
return this.processPacketFiles(callback);
|
||||
},
|
||||
(tempDir, callback) => {
|
||||
return callback(null);
|
||||
}
|
||||
],
|
||||
err => {
|
||||
temptmp.cleanup();
|
||||
this.temptmp.cleanup();
|
||||
|
||||
if (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);
|
||||
fs.readFile(path, { encoding : 'utf8' }, (err, iniData) => {
|
||||
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
|
||||
}
|
||||
|
||||
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) {
|
||||
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);
|
||||
|
@ -401,7 +457,6 @@ class QWKPacketReader extends EventEmitter {
|
|||
}
|
||||
|
||||
readMessages(cb) {
|
||||
// :TODO: update to use proper encoding: if headers.dat specifies UTF-8, use that, else CP437
|
||||
if (!this.packetInfo.messages) {
|
||||
return cb(Errors.DoesNotExist('No messages file found within QWK packet'));
|
||||
}
|
||||
|
@ -420,7 +475,6 @@ class QWKPacketReader extends EventEmitter {
|
|||
const FTNPropertyMapping = {
|
||||
'X-FTN-AREA' : Message.FtnPropertyNames.FtnArea,
|
||||
'X-FTN-SEEN-BY' : Message.FtnPropertyNames.FtnSeenBy,
|
||||
'X-FTN-FLAGS' : Message.FtnPropertyNames
|
||||
};
|
||||
|
||||
const FTNKludgeMapping = {
|
||||
|
@ -452,6 +506,10 @@ class QWKPacketReader extends EventEmitter {
|
|||
Reply : '@REPLY:',
|
||||
TZ : '@TZ:', // https://github.com/kvadevack/synchronet/blob/master/src/smblib/smbdefs.h
|
||||
ReplyTo : '@REPLYTO:',
|
||||
|
||||
// :TODO: Look into other non-standards
|
||||
// https://github.com/wmcbrine/MultiMail/blob/master/mmail/qwk.cc
|
||||
// title, @subject, etc.
|
||||
};
|
||||
|
||||
let blockCount = 0;
|
||||
|
@ -480,7 +538,7 @@ class QWKPacketReader extends EventEmitter {
|
|||
if (0 === blockCount) {
|
||||
// first 128 bytes is a space padded ID
|
||||
const id = buffer.toString('ascii').trim();
|
||||
this.emit('generator', id);
|
||||
this.emit('creator', id);
|
||||
state = 'header';
|
||||
} else {
|
||||
switch (state) {
|
||||
|
@ -493,7 +551,7 @@ class QWKPacketReader extends EventEmitter {
|
|||
header[field] = iconv.decode(header[field], encodingToSpec).trim();
|
||||
});
|
||||
|
||||
header.timestamp = moment(header.timestamp, 'MM-DD-YYHH:mm');
|
||||
header.timestamp = moment(header.timestamp, QWKHeaderTimestampFormat);
|
||||
|
||||
currMessage = {
|
||||
header,
|
||||
|
@ -523,7 +581,7 @@ class QWKPacketReader extends EventEmitter {
|
|||
|
||||
case 'message' :
|
||||
if (!currMessage.body) {
|
||||
currMessage.body = buffer;
|
||||
currMessage.body = Buffer.from(buffer);
|
||||
} else {
|
||||
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.
|
||||
// If the message is UTF-8, we assume it's using standard line feeds.
|
||||
if (encoding !== 'utf8') {
|
||||
let i = 0;
|
||||
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;
|
||||
}
|
||||
replaceCharInBuffer(currMessage.body, QWKLF, 0x0a);
|
||||
}
|
||||
|
||||
//
|
||||
|
@ -583,15 +632,15 @@ class QWKPacketReader extends EventEmitter {
|
|||
} else if (line.startsWith(Kludges.Subject)) {
|
||||
currMessage.subject = line.substring(Kludges.Subject.length).trim();
|
||||
} else if (line.startsWith(Kludges.Via)) {
|
||||
qwkKludge.via = line;
|
||||
qwkKludge['@VIA'] = line;
|
||||
} 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)) {
|
||||
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)) {
|
||||
qwkKludge.synchronet_timezone = line.substring(Kludges.TZ.length).trim();
|
||||
qwkKludge['@TZ'] = line.substring(Kludges.TZ.length).trim();
|
||||
} else if (line.startsWith(Kludges.ReplyTo)) {
|
||||
qwkKludge.reply_to = line.substring(Kludges.ReplyTo.length).trim();
|
||||
qwkKludge['@REPLYTO'] = line.substring(Kludges.ReplyTo.length).trim();
|
||||
} else {
|
||||
bodyState = 'body'; // past this point and up to any tear/origin/etc., is the real message body
|
||||
bodyLines.push(line);
|
||||
|
@ -620,12 +669,12 @@ class QWKPacketReader extends EventEmitter {
|
|||
const ext = currMessage.headersExtension;
|
||||
|
||||
// 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.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
|
||||
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:
|
||||
// 20180101174837-0600 4168
|
||||
|
@ -640,7 +689,7 @@ class QWKPacketReader extends EventEmitter {
|
|||
}
|
||||
|
||||
if (ext.Tags) {
|
||||
currMessage.hashTags = ext.Tags.split(' ');
|
||||
currMessage.hashTags = (ext.Tags).toString().split(' ');
|
||||
}
|
||||
|
||||
// FTN style properties/kludges represented as X-FTN-XXXX
|
||||
|
@ -668,6 +717,9 @@ class QWKPacketReader extends EventEmitter {
|
|||
hashTags : currMessage.hashTags,
|
||||
});
|
||||
|
||||
// Indicate this message was imported from a QWK packet
|
||||
message.meta.System[Message.SystemMetaNames.ExternalFlavor] = Message.AddressFlavor.QWK;
|
||||
|
||||
if (!_.isEmpty(qwkKludge)) {
|
||||
message.meta.QwkKludge = qwkKludge;
|
||||
}
|
||||
|
@ -692,8 +744,8 @@ class QWKPacketReader extends EventEmitter {
|
|||
}
|
||||
|
||||
// Update the timestamp if we have a valid TZ
|
||||
if (useTZKludge && _.has(message, 'meta.QwkKludge.synchronet_timezone')) {
|
||||
const tzOffset = SMBTZToUTCOffset(message.meta.QwkKludge.synchronet_timezone);
|
||||
if (useTZKludge && qwkKludge['@TZ']) {
|
||||
const tzOffset = SMBTZToUTCOffset[qwkKludge['@TZ']];
|
||||
if (tzOffset) {
|
||||
message.modTimestamp.utcOffset(tzOffset);
|
||||
}
|
||||
|
@ -704,7 +756,7 @@ class QWKPacketReader extends EventEmitter {
|
|||
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_conf_num = currMessage.header.confNum;
|
||||
} 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 = {
|
||||
QWKPacketReader,
|
||||
// QWKPacketWriter,
|
||||
QWKPacketWriter,
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue