446 lines
12 KiB
PHP
446 lines
12 KiB
PHP
<?php
|
|
|
|
namespace App\Classes;
|
|
|
|
use Illuminate\Support\Arr;
|
|
|
|
use App\Exceptions\InvalidFidoPacketException;
|
|
|
|
/**
|
|
* Class FTNMessage
|
|
* NOTE: FTN Echomail Messages are ZONE agnostic.
|
|
*
|
|
* @package App\Classes
|
|
*/
|
|
class FTNMessage extends FTN
|
|
{
|
|
private $src = NULL; // SRC N/F from packet
|
|
private $dst = NULL; // DST N/F from packet
|
|
|
|
private $flags = NULL; // Flags from packet
|
|
private $cost = 0; // Cost from packet
|
|
|
|
// @todo need to validate these string lengths when creating packet.
|
|
private $from = NULL; // FTS-0001.016 From Name: upto 36 chars null terminated
|
|
private $to = NULL; // FTS-0001.016 To Name: upto 36 chars null terminated
|
|
private $subject = NULL; // FTS-0001.016 Subject: upto 72 chars null terminated
|
|
private $date = NULL; // FTS-0001.016 Date: upto 20 chars null terminated
|
|
|
|
private $message = NULL; // The actual message content
|
|
private $echoarea = NULL; // FTS-0004.001
|
|
private $intl = NULL;
|
|
private $msgid = NULL;
|
|
private $reply = NULL; // Message thread reply source
|
|
private $origin = NULL; // FTS-0004.001
|
|
|
|
private $kludge = []; // Hold kludge items
|
|
private $path = []; // FTS-0004.001
|
|
private $seenby = []; // FTS-0004.001
|
|
private $via = [];
|
|
private $_other = [];
|
|
private $unknown = [];
|
|
|
|
// We auto create these values - they are used to create packets.
|
|
private $_fqfa = NULL; // Fully qualified fidonet source where packet originated
|
|
private $_fqda = NULL; // Fully qualified fidonet destination address (Netmail)
|
|
|
|
// Single value kludge items
|
|
private $_kludge = [
|
|
'chrs' => 'CHRS: ',
|
|
'charset' => 'CHARSET: ',
|
|
'codepage' => 'CODEPAGE: ',
|
|
'pid' => 'PID: ',
|
|
'tid' => 'TID: ',
|
|
];
|
|
|
|
// Flags for messages
|
|
const FLAG_PRIVATE = 0b1;
|
|
const FLAG_CRASH = 0b10;
|
|
const FLAG_RECD = 0b100;
|
|
const FLAG_SENT = 0b1000;
|
|
const FLAG_FILEATTACH = 0b10000;
|
|
const FLAG_INTRANSIT = 0b100000;
|
|
const FLAG_ORPHAN = 0b1000000;
|
|
const FLAG_KILLSENT = 0b10000000;
|
|
const FLAG_LOCAL = 0b100000000;
|
|
const FLAG_HOLD = 0b1000000000;
|
|
const FLAG_UNUSED_10 = 0b10000000000;
|
|
const FLAG_FREQ = 0b100000000000;
|
|
const FLAG_RETRECEIPT = 0b1000000000000;
|
|
const FLAG_ISRETRECEIPT = 0b10000000000000;
|
|
const FLAG_AUDITREQ = 0b100000000000000;
|
|
const FLAG_FILEUPDATEREQ = 0b1000000000000000;
|
|
|
|
// FTS-0001.016 Message header 12 bytes
|
|
// node, net, flags, cost
|
|
private $struct = [
|
|
'onode'=>[0x00,'v',2],
|
|
'dnode'=>[0x02,'v',2],
|
|
'onet'=>[0x04,'v',2],
|
|
'dnet'=>[0x06,'v',2],
|
|
'flags'=>[0x08,'v',2],
|
|
'cost'=>[0x0a,'v',2],
|
|
];
|
|
|
|
public function __construct(string $header=NULL)
|
|
{
|
|
// Initialise vars
|
|
$this->kludge = collect(); // The message kludge lines
|
|
$this->path = collect(); // The message PATH lines
|
|
$this->seenby = collect(); // The message SEEN-BY lines
|
|
$this->via = collect(); // The path the message has gone using Via lines
|
|
$this->_other = collect(); // Temporarily hold attributes we dont process yet.
|
|
$this->unknown = collect(); // Temporarily hold attributes we have no logic for.
|
|
|
|
if ($header)
|
|
$this->parseheader($header);
|
|
}
|
|
|
|
public function __get($k)
|
|
{
|
|
switch ($k)
|
|
{
|
|
case 'fz': return ftn_address_split($this->_fqfa,'z');
|
|
case 'fn': return ftn_address_split($this->_fqfa,'n');
|
|
case 'ff': return ftn_address_split($this->_fqfa,'f');
|
|
case 'fp': return ftn_address_split($this->_fqfa,'p');
|
|
|
|
case 'fqfa': return $this->_fqfa;
|
|
|
|
// Echomails dont have a fully qualified from address
|
|
case 'tz': return ftn_address_split($this->_fqda,'z');
|
|
case 'tn': return ftn_address_split($this->_fqda,'n');
|
|
case 'tf': return ftn_address_split($this->_fqda,'f');
|
|
case 'tp': return ftn_address_split($this->_fqda,'p');
|
|
|
|
case 'tearline':
|
|
return '--- FTNHub';
|
|
|
|
case 'type':
|
|
if ($this->echoarea)
|
|
return 'echomail';
|
|
|
|
if ($this->intl)
|
|
return 'netmail';
|
|
|
|
return NULL;
|
|
|
|
default:
|
|
return isset($this->{$k}) ? $this->{$k} : NULL;
|
|
}
|
|
}
|
|
|
|
public function __set($k,$v)
|
|
{
|
|
switch ($k)
|
|
{
|
|
case 'fqfa':
|
|
case 'fqda':
|
|
$this->{'_'.$k} = $v;
|
|
|
|
if ($this->_fqfa AND $this->_fqda)
|
|
$this->intl = sprintf('%s %s',$this->_fqda,$this->_fqfa);
|
|
|
|
case 'origin':
|
|
if (! $this->_fqfa)
|
|
throw new \Exception('Must set from address before origin');
|
|
|
|
$this->origin = sprintf(' * Origin: %s (%s)',$v,$this->_fqfa);
|
|
break;
|
|
|
|
default:
|
|
$this->{$k} = $v;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Export an FTN message, ready for sending.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function __toString(): string
|
|
{
|
|
// if (f->net == 65535) { /* Point packet - Get Net from auxNet */
|
|
$return = '';
|
|
|
|
$return .= pack(join('',collect($this->struct)->pluck(1)->toArray()),
|
|
$this->ff,
|
|
$this->tf,
|
|
$this->fn,
|
|
$this->tn,
|
|
$this->flags,
|
|
$this->cost
|
|
);
|
|
|
|
// @todo use pack for this.
|
|
$return .= $this->date->format('d M y H:i:s')."\00";
|
|
$return .= $this->to."\00";
|
|
$return .= $this->from."\00";
|
|
$return .= $this->subject."\00";
|
|
|
|
// Add some kludges
|
|
$return .= "\01MSGID ".$this->_fqfa." 1"."\r";
|
|
|
|
foreach ($this->_kludge as $k=>$v)
|
|
{
|
|
if ($x=$this->kludge->get($k))
|
|
$return .= chr(1).$v.$x."\r";
|
|
}
|
|
|
|
$return .= $this->message."\r";
|
|
$return .= $this->tearline."\r";
|
|
$return .= $this->origin."\r";
|
|
|
|
switch ($this->type)
|
|
{
|
|
case 'echomail':
|
|
break;
|
|
|
|
case 'netmail':
|
|
foreach ($this->via as $k=>$v)
|
|
$return .= "\01Via: ".$v."\r";
|
|
|
|
// @todo Set product name/version as var
|
|
$return .= sprintf('%sVia: %s @%s.UTC %s %i.%i',
|
|
chr(1),
|
|
'10:0/0',
|
|
now('UTC')->format('Ymd.His'),
|
|
'FTNHub',
|
|
1,1)."\r";
|
|
|
|
break;
|
|
}
|
|
|
|
$return .= "\00";
|
|
|
|
return $return;
|
|
}
|
|
|
|
/**
|
|
* Return an array of flag descriptions
|
|
*
|
|
* @return array
|
|
*
|
|
* http://ftsc.org/docs/fsc-0001.000
|
|
* AttributeWord bit meaning
|
|
--- --------------------
|
|
0 + Private
|
|
1 + s Crash
|
|
2 Recd
|
|
3 Sent
|
|
4 + FileAttached
|
|
5 InTransit
|
|
6 Orphan
|
|
7 KillSent
|
|
8 Local
|
|
9 s HoldForPickup
|
|
10 + unused
|
|
11 s FileRequest
|
|
12 + s ReturnReceiptRequest
|
|
13 + s IsReturnReceipt
|
|
14 + s AuditRequest
|
|
15 s FileUpdateReq
|
|
|
|
s - this bit is supported by SEAdog only
|
|
+ - this bit is not zeroed before packeting
|
|
*/
|
|
public function flags(int $flags): array
|
|
{
|
|
return [
|
|
'private'=>$this->isFlagSet($flags,self::FLAG_PRIVATE),
|
|
'crash'=>$this->isFlagSet($flags,self::FLAG_CRASH),
|
|
'recd'=>$this->isFlagSet($flags,self::FLAG_RECD),
|
|
'sent'=>$this->isFlagSet($flags,self::FLAG_SENT),
|
|
'killsent'=>$this->isFlagSet($flags,self::FLAG_KILLSENT),
|
|
'local'=>$this->isFlagSet($flags,self::FLAG_LOCAL),
|
|
];
|
|
}
|
|
|
|
private function isFlagSet($value,$flag)
|
|
{
|
|
return (($value & $flag) == $flag);
|
|
}
|
|
|
|
/**
|
|
* Parse the head of an FTN message
|
|
*
|
|
* @param string $header
|
|
*/
|
|
public function parseheader(string $header)
|
|
{
|
|
$result = unpack($this->unpackheader($this->struct),$header);
|
|
|
|
// For Echomail this is the packet src.
|
|
$this->psn = Arr::get($result,'onet');
|
|
$this->psf = Arr::get($result,'onode');
|
|
|
|
$this->src = sprintf('%s/%s',
|
|
$this->psn,
|
|
$this->psf
|
|
);
|
|
|
|
// For Echomail this is the packet dst.
|
|
$this->pdn = Arr::get($result,'dnet');
|
|
$this->pdf = Arr::get($result,'dnode');
|
|
|
|
$this->dst = sprintf('%s/%s',
|
|
$this->pdn,
|
|
$this->pdf
|
|
);
|
|
|
|
$this->flags = Arr::get($result,'flags');
|
|
$this->cost = Arr::get($result,'cost');
|
|
}
|
|
|
|
public function parsemessage(string $message)
|
|
{
|
|
// Remove DOS \n\r
|
|
$message = preg_replace("/\n\r/","\r",$message);
|
|
|
|
// Split out the <SOH> lines
|
|
$result = collect(explode("\01",$message))->filter();
|
|
|
|
foreach ($result as $k => $v)
|
|
{
|
|
// Search for \r - if that is the end of the line, then its a kludge
|
|
$x = strpos($v,"\r");
|
|
|
|
// If there are more characters, then put the kludge back into the result, so that we process it.
|
|
if ($x != strlen($v)-1)
|
|
{
|
|
/**
|
|
* Anything after the origin line is also kludge data.
|
|
*/
|
|
if ($y = strpos($v,"\r * Origin: "))
|
|
{
|
|
$this->message .= substr($v,$x+1,$y-$x-1);
|
|
$this->parseorigin(substr($v,$y));
|
|
|
|
// If this is netmail, the FQFA will have been set by the INTL line, we can skip the rest of this
|
|
$matches = [];
|
|
|
|
// Capture the fully qualified 4D name from the Origin Line - it tells us the ZONE.
|
|
preg_match('/^.*\((.*)\)$/',$this->origin,$matches);
|
|
|
|
// Double check we have an address in the origin line
|
|
if (! Arr::get($matches,1))
|
|
throw new InvalidFidoPacketException(sprintf('No address in Origin?',$matches));
|
|
|
|
// Double check, our src and origin match
|
|
if (! preg_match('#^[0-9]+:'.$this->src.'#',$matches[1]))
|
|
throw new InvalidFidoPacketException(sprintf('Source address mismatch? [%s,%s]',$this->_fqfa,$matches[1]));
|
|
|
|
// If this is netmail, a double check our FQFA matches
|
|
if ($this->type == 'netmail') {
|
|
if ($this->_fqfa != $matches[1])
|
|
throw new InvalidFidoPacketException(sprintf('Source address mismatch? [%s,%s]',$this->_fqfa,$matches[1]));
|
|
|
|
// For other types, this is our only way of getting a FQFA
|
|
} else {
|
|
$this->_fqfa = $matches[1];
|
|
|
|
// Our FQDA is not available, we'll assume its the same zone as our FQFA
|
|
$this->_fqda = sprintf('%d:%s',ftn_address_split($this->_fqfa,'z'),$this->dst);
|
|
}
|
|
}
|
|
|
|
$v = substr($v,0,$x+1);
|
|
}
|
|
|
|
foreach ($this->_kludge as $a => $b) {
|
|
if ($t = $this->kludge($b,$v)) {
|
|
$this->kludge->put($a,$t);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ($t)
|
|
continue;
|
|
|
|
if ($t = $this->kludge('AREA:',$v))
|
|
$this->echoarea = $t;
|
|
|
|
// From point: <SOH>"FMPT <point number><CR>
|
|
elseif ($t = $this->kludge('FMPT ',$v))
|
|
$this->_other->push($t);
|
|
|
|
/*
|
|
* The INTL control paragraph shall be used to give information about
|
|
* the zone numbers of the original sender and the ultimate addressee
|
|
* of a message.
|
|
*
|
|
* <SOH>"INTL "<destination address>" "<origin address><CR>
|
|
*/
|
|
elseif ($t = $this->kludge('INTL ',$v))
|
|
{
|
|
$this->intl = $t;
|
|
list($this->_fqda,$this->_fqfa) = explode(' ',$t);
|
|
}
|
|
|
|
elseif ($t = $this->kludge('MSGID: ',$v))
|
|
$this->msgid = $t;
|
|
|
|
elseif ($t = $this->kludge('PATH: ',$v))
|
|
$this->path->push($t);
|
|
|
|
elseif ($t = $this->kludge('REPLY: ',$v))
|
|
$this->reply = $t;
|
|
|
|
// To Point: <SOH>TOPT <point number><CR>
|
|
elseif ($t = $this->kludge('TOPT ',$v))
|
|
$this->_other->push($t);
|
|
|
|
// Time Zone of the sender.
|
|
elseif ($t = $this->kludge('TZUTC: ',$v))
|
|
$this->tzutc= $t;
|
|
|
|
// <SOH>Via <FTN Address> @YYYYMMDD.HHMMSS[.Precise][.Time Zone] <Program Name> <Version> [Serial Number]<CR>
|
|
elseif ($t = $this->kludge('Via ',$v))
|
|
$this->via->push($t);
|
|
|
|
// We got a kludge line we dont know about
|
|
else {
|
|
$this->unknown->push(chop($v,"\r"));
|
|
|
|
//dd(['v'=>$v,'t'=>$t]);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Process the data after the ORIGIN
|
|
* There may be kludge lines after the origin - notably SEEN-BY
|
|
*
|
|
* @param string $message
|
|
*/
|
|
private function parseorigin(string $message)
|
|
{
|
|
// Split out each line
|
|
$result = collect(explode("\r",$message))->filter();
|
|
|
|
foreach ($result as $k => $v) {
|
|
foreach ($this->_kludge as $a => $b) {
|
|
if ($t = $this->kludge($b,$v)) {
|
|
$this->kludge->put($a,$t);
|
|
break;
|
|
}
|
|
}
|
|
|
|
if ($t = $this->kludge('SEEN-BY: ', $v))
|
|
$this->seenby->push($t);
|
|
|
|
elseif ($t = $this->kludge('PATH: ', $v))
|
|
$this->path->push($t);
|
|
|
|
elseif ($t = $this->kludge(' \* Origin: ',$v))
|
|
$this->origin = $t;
|
|
|
|
// We got unknown Kludge lines in the origin
|
|
else {
|
|
$this->unknown->push($v);
|
|
|
|
//dd(['v'=>$v,'t'=>$t,'message'=>$message]);
|
|
}
|
|
}
|
|
}
|
|
} |