clrghouz/app/Classes/FTN/Message.php

1082 lines
33 KiB
PHP

<?php
namespace App\Classes\FTN;
use Carbon\Carbon;
use Carbon\Exceptions\InvalidFormatException;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Validator as ValidatorResult;
use App\Classes\FTN as FTNBase;
use App\Http\Controllers\DomainController;
use App\Models\{Address,Domain,Zone};
use App\Rules\{TwoByteInteger,TwoByteIntegerWithZero};
use App\Traits\EncodeUTF8;
/**
* Class Message
* Represents the structure of a message in a packet
* NOTE: FTN Echomail Messages are ZONE agnostic.
*
* @package App\Classes
*/
class Message extends FTNBase
{
use EncodeUTF8;
private const LOGKEY = 'FM-';
private const cast_utf8 = [
'user_to',
'user_from',
'subject',
'message',
'message_src',
'origin',
'tearline',
'tagline',
'dump',
];
// Single value kludge items
private array $_kludge = [
'chrs' => 'CHRS: ',
'charset' => 'CHARSET: ',
'codepage' => 'CODEPAGE: ',
'dbid' => 'DBID: ',
'pid' => 'PID: ',
'tid' => 'TID: ',
];
// Flags for messages
/** @var int Private message */
public const FLAG_PRIVATE = 1<<0;
/** @var int Crash priority message (Crash + Hold = Direct) */
public const FLAG_CRASH = 1<<1;
/** @var int Read by addressee */
public const FLAG_RECD = 1<<2;
/** @var int Message has been sent */
public const FLAG_SENT = 1<<3;
/** @var int File attached (filename in subject) */
public const FLAG_FILEATTACH = 1<<4;
/** @var int Message in transit to another destination */
public const FLAG_INTRANSIT = 1<<5;
/** @var int Unknown destination - node not in nodelist */
public const FLAG_ORPHAN = 1<<6;
/** @var int Kill after mailing */
public const FLAG_KILLSENT = 1<<7;
/** @var int Message originated here */
public const FLAG_LOCAL = 1<<8;
/** @var int Hold message here to be collected (Crash + Hold = Direct) */
public const FLAG_HOLD = 1<<9;
/** @var int Reserved for future use by FTS-0001 */
public const FLAG_UNUSED_10 = 1<<10;
/** @var int Requesting a file (filename in subject) */
public const FLAG_FREQ = 1<<11;
/** @var int Return Receipt requested */
public const FLAG_RETRECEIPT = 1<<12; // (RRQ)
/** @var int Return Receipt message in response to an RRQ */
public const FLAG_ISRETRECEIPT = 1<<13;
/** @var int Request audit trail */
public const FLAG_AUDITREQ = 1<<14; // (ARQ)
/** @var int Requesting a file update (filename in subject) */
public const FLAG_FILEUPDATEREQ = 1<<15; // (URQ)
/** @var int Echomail has been scanned out */
public const FLAG_ECHOMAIL = 1<<16;
/** @var int Use packet password on the subject line for this message */
public const FLAG_PKTPASSWD = 1<<17;
// FTS-0001.016 Message header 32 bytes node, net, flags, cost, date
public const HEADER_LEN = 0x20; // Length of message header
private const header = [ // Struct of message header
'onode' => [0x00,'v',2], // Originating Node
'dnode' => [0x02,'v',2], // Destination Node
'onet' => [0x04,'v',2], // Originating Net
'dnet' => [0x06,'v',2], // Destination Net
'flags' => [0x08,'v',2], // Message Flags
'cost' => [0x0a,'v',2], // Send Cost
'date' => [0x0c,'a20',20] // Message Date FTS-0001.016 Date: upto 20 chars null terminated
];
private const USER_FROM_LEN = 36; // FTS-0001.016 From Name: upto 36 chars null terminated
private const USER_TO_LEN = 36; // FTS-0001.016 To Name: upto 36 chars null terminated
private const SUBJECT_LEN = 71; // FTS-0001.016 Subject: upto 72 chars null terminated
private const AREATAG_LEN = 35; //
private ?ValidatorResult $errors = NULL; // Packet validation
private array $header; // Message Header
private Collection $kludge; // Hold kludge items
public string $dump; // Raw message
private string $user_from; // User message is From
private string $user_to; // User message is To
private string $subject; // Message subject
private string $msgid; // MSG ID
private string $replyid; // Reply ID
private string $gateid; // MSG ID if the message came via gate
private string $echoarea; // FTS-0004.001
private string $intl; // Netmail details
private string $message; // The parsed message content
private string $message_src; // The actual message part
private ?string $tagline;
private ?string $tearline;
private string $origin; // FTS-0004.001
private int $tzutc;
private array $point; // Point the message belongs to (Netmail)
private array $src; // Address the message is from
private array $dst; // Address the message is to
private Collection $rescanned; // Message was created as a result of a rescan
private Collection $path; // FTS-0004.001 The message PATH lines
private Collection $pathaddress; // Collection of Addresses after parsing seenby
private Collection $rogue_path; // Collection of FTNs in the Seen-by that are not defined
private Collection $seenby; // FTS-0004.001 The message SEEN-BY lines
private Collection $seenaddress; // Collection of Addresses after parsing seenby
private Collection $rogue_seenby; // Collection of FTNs in the Seen-by that are not defined
private Collection $via; // The path the message has gone using Via lines (Netmail)
private Collection $unknown; // Temporarily hold attributes we have no logic for.
public bool $packed = FALSE; // Has the message been packed successfully
// Convert characters into printable chars
// https://int10h.org/oldschool-pc-fonts/readme/#437_charset
private const CP437 = [
0x01 => 0x263a, 0x02 => 0x263b, 0x03 => 0x2665, 0x04 => 0x2666,
0x05 => 0x2663, 0x06 => 0x2660, 0x07 => 0x2022, 0x08 => 0x25d8,
0x09 => 0x25cb, 0x0a => 0x2509, 0x0b => 0x2642, 0x0c => 0x2640,
0x0d => 0x266a, 0x0e => 0x266b, 0x0f => 0x263c,
0x10 => 0x25ba, 0x11 => 0x25ca, 0x12 => 0x2195, 0x13 => 0x203c,
0x14 => 0x00b6, 0x15 => 0x00a7, 0x16 => 0x25ac, 0x17 => 0x21a8,
0x18 => 0x2191, 0x19 => 0x2193, 0x1a => 0x2192, 0x1b => 0x2190,
0x1c => 0x221f, 0x1d => 0x2194, 0x1e => 0x25bc, 0x1f => 0x25bc,
0x7f => 0x2302,
0x80 => 0x00c7, 0x81 => 0x00fc, 0x82 => 0x00e9, 0x83 => 0x00e2,
0x84 => 0x00e4, 0x85 => 0x00e0, 0x86 => 0x00e5, 0x87 => 0x00e7,
0x88 => 0x00ea, 0x89 => 0x00eb, 0x8a => 0x00e8, 0x8b => 0x00ef,
0x8c => 0x00ee, 0x8d => 0x00ec, 0x8e => 0x00c4, 0x8f => 0x00c5,
0x90 => 0x00c9, 0x91 => 0x00e6, 0x92 => 0x00c6, 0x93 => 0x00f4,
0x94 => 0x00f6, 0x95 => 0x00f2, 0x96 => 0x00fb, 0x97 => 0x00f9,
0x98 => 0x00ff, 0x99 => 0x00d6, 0x9a => 0x00dc, 0x9b => 0x00a2,
0x9c => 0x00a3, 0x9d => 0x00a5, 0x9e => 0x20a7, 0x9f => 0x0192,
0xa0 => 0x00e1, 0xa1 => 0x00ed, 0xa2 => 0x00f3, 0xa3 => 0x00fa,
0xa4 => 0x00f1, 0xa5 => 0x00d1, 0xa6 => 0x00aa, 0xa7 => 0x00ba,
0xa8 => 0x00bf, 0xa9 => 0x2310, 0xaa => 0x00ac, 0xab => 0x00bd,
0xac => 0x00bc, 0xad => 0x00a1, 0xae => 0x00ab, 0xaf => 0x00bb,
0xb0 => 0x2591, 0xb1 => 0x2592, 0xb2 => 0x2593, 0xb3 => 0x2502,
0xb4 => 0x2524, 0xb5 => 0x2561, 0xb6 => 0x2562, 0xb7 => 0x2556,
0xb8 => 0x2555, 0xb9 => 0x2563, 0xba => 0x2551, 0xbb => 0x2557,
0xbc => 0x255d, 0xbd => 0x255c, 0xbe => 0x255b, 0xbf => 0x2510,
0xc0 => 0x2514, 0xc1 => 0x2534, 0xc2 => 0x252c, 0xc3 => 0x251c,
0xc4 => 0x2500, 0xc5 => 0x253c, 0xc6 => 0x255e, 0xc7 => 0x255f,
0xc8 => 0x255a, 0xc9 => 0x2554, 0xca => 0x2569, 0xcb => 0x2566,
0xcc => 0x2560, 0xcd => 0x2550, 0xce => 0x256c, 0xcf => 0x2567,
0xd0 => 0x2568, 0xd1 => 0x2564, 0xd2 => 0x2565, 0xd3 => 0x2559,
0xd4 => 0x2558, 0xd5 => 0x2552, 0xd6 => 0x2553, 0xd7 => 0x256b,
0xd8 => 0x256a, 0xd9 => 0x2518, 0xda => 0x250c, 0xdb => 0x2588,
0xdc => 0x2584, 0xdd => 0x258c, 0xde => 0x2590, 0xdf => 0x2580,
0xe0 => 0x03b1, 0xe1 => 0x00df, 0xe2 => 0x0393, 0xe3 => 0x03c0,
0xe4 => 0x03a3, 0xe5 => 0x03c3, 0xe6 => 0x00b5, 0xe7 => 0x03c4,
0xe8 => 0x03a6, 0xe9 => 0x0398, 0xea => 0x03a9, 0xeb => 0x03b4,
0xec => 0x221e, 0xed => 0x03c6, 0xee => 0x03b5, 0xef => 0x2229,
0xf0 => 0x2261, 0xf1 => 0x00b1, 0xf2 => 0x2265, 0xf3 => 0x2264,
0xf4 => 0x2320, 0xf5 => 0x2321, 0xf6 => 0x00f7, 0xf7 => 0x2248,
0xf8 => 0x00b0, 0xf9 => 0x2219, 0xfa => 0x00b7, 0xfb => 0x221a,
0xfc => 0x207f, 0xfd => 0x00b2, 0xfe => 0x25a0, 0xff => 0x00a0,
];
public function __construct(Zone $zone=NULL)
{
$this->zone = $zone;
$this->header = [];
$this->user_from = '';
$this->user_to = '';
$this->subject = '';
$this->message = '';
$this->msgid = '';
$this->gateid = '';
$this->replyid = '';
$this->echoarea = '';
$this->intl = '';
$this->tearline = NULL;
$this->tagline = NULL;
$this->origin = '';
$this->tzutc = 0;
$this->src = [];
$this->dst = [];
$this->point = [];
$this->kludge = collect();
$this->rescanned = collect();
$this->path = collect();
$this->seenby = collect();
$this->seenaddress = collect();
$this->pathaddress = collect();
$this->rogue_seenby = collect();
$this->rogue_path = collect();
$this->via = collect();
$this->unknown = collect();
}
// Fix for a call to pluck('date') (which is resolved via __get()), but it returns false.
public function __isset($key) {
return (bool)$this->{$key};
}
public function __get($key)
{
switch ($key) {
// From Addresses
case 'fz': return Arr::get($this->src,'z');
case 'fn': return $this->src ? Arr::get($this->src,'n') : Arr::get($this->header,'onet');
case 'ff': return $this->src ? Arr::get($this->src,'f') : Arr::get($this->header,'onode');
case 'fp': return Arr::get($this->point,'src',Arr::get($this->src,'p',Arr::get($this->header,'opoint',0)));
case 'fd': return Arr::get($this->src,'d');
case 'fdomain':
// We'll use the zone's domain if this method class was called with a zone
if ($this->zone && (($this->zone->domain->name === Arr::get($this->src,'d')) || ! Arr::get($this->src,'d')))
return $this->zone->domain;
// If we get the domain from the packet, we'll find it
if ($x=Arr::get($this->src,'d')) {
return Domain::where('name',$x)->single();
}
return NULL;
case 'tdomain':
// We'll use the zone's domain if this method class was called with a zone
if ($this->zone && (($this->zone->domain->name === Arr::get($this->dst,'d')) || ! Arr::get($this->dst,'d')))
return $this->zone->domain;
// If we get the domain from the packet, we'll find it
if ($x=Arr::get($this->dst,'d')) {
return Domain::where('name',$x)->single();
}
// Otherwise we'll assume the same as the source domain
return $this->fdomain ?: NULL;
case 'fzone':
// Use the zone if this class was called with it.
if ($this->zone && ($this->fz === $this->zone->zone_id))
return $this->zone;
if ($this->fdomain) {
if (($x=$this->fdomain->zones->search(function($item) { return $item->zone_id === $this->fz; })) !== FALSE)
return $this->fdomain->zones->get($x);
}
// No domain, so we'll use the default zone
return Zone::where('zone_id',$this->fz)
->where('default',TRUE)
->single();
case 'tzone':
// Use the zone if this class was called with it.
if ($this->zone && ($this->tz === $this->zone->zone_id))
return $this->zone;
if ($this->tdomain) {
if (($x=$this->tdomain->zones->search(function($item) { return $item->zone_id === $this->tz; })) !== FALSE)
return $this->tdomain->zones->get($x);
}
// No domain, so we'll use the default zone
return Zone::where('zone_id',$this->tz)
->where('default',TRUE)
->single();
// To Addresses
// Echomail doesnt have a zone, so we'll use the source zone
case 'tz': return Arr::get($this->echoarea ? $this->src : $this->dst,'z');
case 'tn': return Arr::get($this->header,'dnet');
case 'tf': return Arr::get($this->header,'dnode');
case 'tp': return Arr::get($this->point,'dst',Arr::get($this->header,'dpoint',0));
case 'fftn':
case 'fftn_o':
case 'tftn':
case 'tftn_o':
return parent::__get($key);
// For 5D we need to include the domain
case 'fboss':
return sprintf('%d:%d/%d',$this->fz,$this->fn,$this->ff).(($x=$this->fdomain) ? '@'.$x->name : '');
case 'tboss':
return sprintf('%d:%d/%d',$this->tz,$this->tn,$this->tf).(($x=$this->tdomain) ? '@'.$x->name : '');
case 'fboss_o':
return Address::findFTN($this->fboss);
case 'tboss_o':
return Address::findFTN($this->tboss);
case 'date':
try {
if (str_contains($x=chop(Arr::get($this->header,$key)),"\x00"))
throw new \Exception('Date contains null values.');
return Carbon::createFromFormat('d M y H:i:s O',
sprintf('%s %s%04d',$x,($this->tzutc < 0) ? '-' : '+',abs($this->tzutc)));
} catch (InvalidFormatException|\Exception $e) {
Log::error(sprintf('%s:! Date doesnt parse [%s] (%s)',self::LOGKEY,$e->getMessage(),Arr::get($this->header,$key)));
throw new \Exception(sprintf('%s (%s)',$e->getMessage(),hex_dump(Arr::get($this->header,$key))));
}
case 'flags':
case 'cost':
return Arr::get($this->header,$key);
case 'tzutc':
case 'user_to':
case 'user_from':
case 'subject':
case 'echoarea':
case 'msgid':
case 'replyid':
case 'gateid':
case 'message':
case 'message_src':
case 'tearline':
case 'tagline':
case 'origin':
case 'kludge':
case 'rescanned':
case 'path':
case 'seenby':
case 'pathaddress':
case 'seenaddress':
case 'rogue_path':
case 'rogue_seenby':
case 'unknown':
case 'via':
case 'errors':
return $this->{$key};
case 'dbid':
return $this->kludge->get($key);
default:
throw new \Exception('Unknown key: '.$key);
}
}
/**
* When we serialise this object, we'll need to utf8_encode some values
*
* @return array
*/
public function __serialize(): array
{
return $this->encode();
}
public function __set($key,$value)
{
switch ($key) {
case 'flags':
case 'header':
case 'tzutc':
case 'user_from':
case 'user_to':
case 'subject':
case 'gateid':
case 'msgid':
case 'replyid':
case 'echoarea':
case 'intl':
case 'message':
case 'kludge':
case 'tagline':
case 'tearline':
case 'origin':
case 'seenby':
case 'path':
case 'via':
$this->{$key} = $value;
break;
default:
throw new \Exception('Unknown key: '.$key);
}
}
/**
* Export an FTN message, ready for sending.
*
* @return string
*/
public function __toString(): string
{
$return = pack(collect(self::header)->pluck(1)->join(''),
$this->ff,
$this->tf,
$this->fn,
$this->tn,
$this->flags&~(self::FLAG_INTRANSIT|self::FLAG_LOCAL), // Turn off our local/intransit bits
$this->cost,
$this->date->format('d M y H:i:s'),
);
$return .= $this->user_to."\00";
$return .= $this->user_from."\00";
$return .= $this->subject."\00";
if (! $this->isNetmail())
$return .= sprintf("AREA:%s\r",$this->echoarea);
// If the message is local, then our kludges are not in the msg itself, we'll add them
if ($this->isFlagSet(self::FLAG_LOCAL)) {
if ($this->isNetmail()) {
$return .= sprintf("\01INTL %s\r",$this->intl);
if ($this->fp)
$return .= sprintf("\01FMPT %d\r",$this->fp);
if ($this->tp)
$return .= sprintf("\01TOPT %d\r",$this->tp);
}
$return .= sprintf("\01TZUTC: %s\r",str_replace('+','',$this->date->getOffsetString('')));
// Add some kludges
$return .= sprintf("\01MSGID: %s\r",$this->msgid);
if ($this->replyid)
$return .= sprintf("\01REPLY: %s\r",$this->replyid);
if ($this->gateid)
$return .= sprintf("\01GATE: %s\r",$this->gateid);
foreach ($this->_kludge as $k=>$v) {
if ($x=$this->kludge->get($k))
$return .= sprintf("\01%s%s\r",$v,$x);
}
$return .= $this->message;
if ($this->tagline)
$return .= sprintf("... %s\r",$this->tagline);
if ($this->tearline)
$return .= sprintf("--- %s\r",$this->tearline);
if ($this->origin)
$return .= sprintf(" * Origin: %s\r",$this->origin);
} else {
if ($this->isFlagSet(self::FLAG_INTRANSIT) && $this->isNetmail()) {
$return .= sprintf("\01INTL %s\r",$this->intl);
if ($this->fp)
$return .= sprintf("\01FMPT %d\r",$this->fp);
if ($this->tp)
$return .= sprintf("\01TOPT %d\r",$this->tp);
}
$return .= $this->message;
}
if ($this->isNetmail()) {
foreach ($this->via as $via)
$return .= sprintf("\01Via %s\r",$via);
} else {
// Seenby & PATH - FSC-0068
$return .= sprintf("SEEN-BY: %s\r",wordwrap(optimize_path($this->seenby)->join(' '),70,"\rSEEN-BY: "));
$return .= sprintf("\01PATH: %s\r",wordwrap(optimize_path($this->path)->join(' '),73,"\rPATH: "));
}
$return .= "\00";
return $return;
}
/**
* When we unserialize, we'll restore (utf8_decode) some values
*
* @param array $values
*/
public function __unserialize(array $values): void
{
$this->decode($values);
}
/**
* Parse a message from a packet
*
* @param string $msg
* @param Zone|null $zone
* @return Message
* @throws \Exception
*/
public static function parseMessage(string $msg,Zone $zone=NULL): self
{
Log::info(sprintf('%s:= Processing message [%d] bytes from zone [%d]',self::LOGKEY,strlen($msg),$zone?->zone_id));
$o = new self($zone);
$o->dump = $msg;
try {
$o->header = unpack(self::unpackheader(self::header),substr($msg,0,self::HEADER_LEN));
} catch (\Exception $e) {
Log::error(sprintf('%s:! Error bad packet header',self::LOGKEY));
$validator = Validator::make([
'header' => substr($msg,0,self::HEADER_LEN),
],[
'header' => [function ($attribute,$value,$fail) use ($e) { return $fail($e->getMessage()); }]
]);
if ($validator->fails())
$o->errors = $validator;
return $o;
}
$ptr = 0;
// To User
$o->user_to = strstr(substr($msg,self::HEADER_LEN+$ptr),"\x00",TRUE);
$ptr += strlen($o->user_to)+1;
// From User
$o->user_from = strstr(substr($msg,self::HEADER_LEN+$ptr),"\x00",TRUE);
$ptr += strlen($o->user_from)+1;
// Subject
$o->subject = strstr(substr($msg,self::HEADER_LEN+$ptr),"\x00",TRUE);
$ptr += strlen($o->subject)+1;
// Check if this is an Echomail
if (! strncmp(substr($msg,self::HEADER_LEN+$ptr),'AREA:',5)) {
$o->echoarea = substr($msg,self::HEADER_LEN+$ptr+5,strpos($msg,"\r",self::HEADER_LEN+$ptr+5)-(self::HEADER_LEN+$ptr+5));
$ptr += strlen($o->echoarea)+5+1;
}
$o->unpackMessage(substr($msg,self::HEADER_LEN+$ptr));
if (($x=$o->validate())->fails()) {
Log::debug(sprintf('%s:! Message fails validation (%s@%s->%s@%s)',self::LOGKEY,$o->user_from,$o->fftn,$o->user_to,$o->tftn),['result'=>$x->errors()]);
//throw new \Exception('Message validation fails:'.join(' ',$x->errors()->all()));
}
return $o;
}
/**
* Translate the string into something printable via the web
*
* @param string $string
* @param array $skip
* @return string
*/
public static function tr(string $string,array $skip=[0x0a,0x0d]): string
{
$tr = [];
foreach (self::CP437 as $k=>$v) {
if (in_array($k,$skip))
continue;
$tr[chr($k)] = '&#'.$v;
}
return strtr($string,$tr);
}
/**
* If this message doesnt have an AREATAG, then its a netmail.
*
* @return bool
*/
public function isNetmail(): bool
{
return ! $this->echoarea;
}
/**
* Return an array of flag descriptions
*
* @return Collection
*
* 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(): Collection
{
return collect([
'private' => $this->isFlagSet(self::FLAG_PRIVATE),
'crash' => $this->isFlagSet(self::FLAG_CRASH),
'recd' => $this->isFlagSet(self::FLAG_RECD),
'sent' => $this->isFlagSet(self::FLAG_SENT),
'fileattach' => $this->isFlagSet(self::FLAG_FILEATTACH),
'intransit' => $this->isFlagSet(self::FLAG_INTRANSIT),
'orphan' => $this->isFlagSet(self::FLAG_ORPHAN),
'killsent' => $this->isFlagSet(self::FLAG_KILLSENT),
'local' => $this->isFlagSet(self::FLAG_LOCAL),
'hold' => $this->isFlagSet(self::FLAG_HOLD),
'unused-10' => $this->isFlagSet(self::FLAG_UNUSED_10),
'filereq' => $this->isFlagSet(self::FLAG_FREQ),
'receipt-req' => $this->isFlagSet(self::FLAG_RETRECEIPT),
'receipt' => $this->isFlagSet(self::FLAG_ISRETRECEIPT),
'audit' => $this->isFlagSet(self::FLAG_AUDITREQ),
'fileupdate' => $this->isFlagSet(self::FLAG_FILEUPDATEREQ),
]);
}
private function isFlagSet($flag): bool
{
return ($this->flags & $flag);
}
/**
* Parse the Seenby/path lines and return a collection of addresses
*
* @param string $type Type of address, ie: seenby/path
* @param Collection $addresses
* @param Collection $rogue
* @return Collection
* @throws \Exception
*/
private function parseAddresses(string $type,Collection $addresses,Collection &$rogue): Collection
{
static $aos = NULL;
if (! $aos)
$aos = collect();
$nodes = collect();
$net = NULL;
foreach ($addresses as $line) {
foreach (explode(' ',$line) as $item) {
if (($x=strpos($item,'/')) !== FALSE) {
$net = (int)substr($item,0,$x);
$node = (int)substr($item,$x+1);
} else {
$node = (int)$item;
}
$ftn = sprintf('%d:%d/%d',$this->fz,$net&DomainController::NUMBER_MAX,$node&DomainController::NUMBER_MAX);
// @todo This should be enhanced to include the address at the time of the message.
if ($aos->has($ftn))
$ao = $aos->get($ftn);
else
$aos->put($ftn,($ao=(Address::findFTN($ftn))?->id));
if (! $ao) {
Log::alert(sprintf('%s:! Undefined Node [%s] in %s.',self::LOGKEY,$ftn,$type));
$rogue->push($ftn);
} else {
$nodes->push($ao);
}
}
}
return $nodes;
}
/**
* 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();
while ($result->count()) {
$v = $result->shift();
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)) {
// Check in case there is a kludge that starts with SOH
if ($soh=strpos($t,"\x01")) {
$this->origin = substr($t,0,$soh);
$result->push(substr($t,$soh+1));
} else {
$this->origin = $t;
}
}
// We got unknown Kludge lines in the origin
else
$this->unknown->push($v);
}
}
/**
* Parse the via address and return a collection of addresses
*
* <FTN Address> @YYYYMMDD.HHMMSS[.Precise][.Time Zone] <Program Name> <Version> [Serial Number]
*
* @param Collection $via
* @param Collection $rogue
* @return Collection
*/
private function parseVia(Collection $via,Collection &$rogue): Collection
{
$nodes = collect();
foreach ($via as $line) {
$m = [];
if (preg_match('/^([0-9]+:[0-9]+\/[0-9]+(\..*)?)\s+@([0-9.a-zA-Z]+)\s+(.*)$/',$line,$m)) {
// Address
$ao = Address::findFTN($m[1]);
// Time
$t = [];
$datetime = '';
if (! preg_match('/^([0-9]+\.[0-9]+)(\.?(.*))?$/',$m[3],$t))
Log::alert(sprintf('%s:! Unable to determine time from [%s]',self::LOGKEY,$m[3]));
else
$datetime = Carbon::createFromFormat('Ymd.His',$t[1],$t[3] ?? '');
if (! $ao) {
Log::alert(sprintf('%s:! Undefined Node [%s] for Netmail.',self::LOGKEY,$m[1]));
//$rogue->push(['node'=>$m[1],'datetime'=>$datetime,'program'=>$m[4]]);
} else {
$nodes->push(['node'=>$ao,'datetime'=>$datetime,'program'=>$m[4]]);
}
}
}
return $nodes;
}
/**
* Extract information out of the message text.
*
* @param string $message
* @throws \Exception
*/
public function unpackMessage(string $message): void
{
// Remove DOS \n\r
$message = preg_replace("/\n\r/","\r",$message);
$message = preg_replace("/\r\n/","\r",$message);
// Split out the <SOH> lines
$result = collect(explode("\x01",$message))->filter();
$this->message = '';
$this->message_src = '';
$msgpos = 0;
$haveOrigin = FALSE;
while ($result->count()) {
// $kl is our line starting with a \x01 kludge delimiter
$kl = $result->shift();
// Search for \r - if that is the end of the line, then its a kludge
$retpos = strpos($kl,"\r");
$msgpos += 1+strlen($kl); // SOH+text
$t = '';
// If there is a return in this middle of this line, that means the text message starts after the return,
// ie: SOH+text\rmessage
// Just in case we already parsed this as part of our message, we'll continue, since its probably a
// binary message with a SOH inside it.
if (strlen($this->message) || ($retpos !== strlen($kl)-1)) {
// If there was no return, its part of the message, so we need to add back the SOH.
if ($retpos === FALSE) {
$this->message .= "\x01".$kl;
continue;
}
// Check if this has the origin line. Anything before is the message, anything after the origin
// line is also kludge data.
if ($originpos=strrpos($kl,"\r * Origin: ")) {
// Anything after the return (from the kludge) is a message.
if (! $this->message) {
$this->message .= substr($kl,$retpos+1,$originpos-$retpos-1);
// But if we are already sourcing a message, then its part of it message.
} else {
$this->message .= "\x01".substr($kl,0,$originpos);
$retpos = 0;
}
// See if we have a tagline
if ($tl=strrpos($kl,"\r... ")) {
$tlr = strpos(substr($kl,$tl+6),"\r");
$this->tagline = substr($kl,$tl+5,$tlr);
}
// Message is finished, now we are parsing origin data (and more kludges)
$this->parseOrigin(substr($kl,$originpos+1));
$haveOrigin = TRUE;
// Our message source (for resending, is everything up to the end of the origin line.
$this->message_src = substr($message, 0, $msgpos - (1+strlen($kl)) + $originpos + 12 + strlen($this->origin) + 1);
$kl = substr($kl,0,$retpos);
// 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)) {
Log::error(sprintf('%s:! Origin line doesnt have an address',self::LOGKEY));
} else {
// Double check, our src and origin match
try {
$this->src = Address::parseFTN($matches[1]);
} catch (\Exception $e) {
Log::error(sprintf('%s:! Origin line address [%s] is invalid [%s]',self::LOGKEY,$this->origin,$e->getMessage()));
}
}
// We'll double check our FTN
if ($this->isNetmail() && (($this->src['n'] !== $this->fn) || ($this->src['f'] !== $this->ff))) {
Log::error(sprintf('%s:! FTN [%s] doesnt match message header',self::LOGKEY,$matches[1]),['ftn'=>$this->src,'fn'=>$this->fn,'ff'=>$this->ff]);
}
// The message is the rest?
// Netmails dont generally have an origin line
} elseif (strlen($kl) > $retpos+1) {
// We still have some text to process, add it to the list
if ($haveOrigin && ($retpos+1 < strlen($kl))) {
$result->push(substr($kl,$retpos+1));
// If this was the overflow from echomail, then our message_src would be defined, and thus its not part of the message
} else {
$this->message .= substr($kl,$retpos+1);
$this->message_src = substr($message, 0, $msgpos);
}
$kl = substr($kl,0,$retpos);
}
if (! $kl)
continue;
}
foreach ($this->_kludge as $a => $b) {
if ($t = $this->kludge($b,$kl)) {
$this->kludge->put($a,$t);
break;
}
}
// There is more text.
if ($t)
continue;
// From point: <SOH>"FMPT <point number><CR>
if ($t = $this->kludge('FMPT ',$kl))
$this->point['src'] = $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 ',$kl)) {
$this->intl = $t;
// INTL kludge is in Netmail, so we'll do some validation:
list($dst,$src) = explode(' ',$t);
$this->src = Address::parseFTN($src);
if (($this->src['n'] !== $this->fn) || ($this->src['f'] !== $this->ff)) {
Log::error(sprintf('%s:! INTL src address [%s] doesnt match packet',self::LOGKEY,$src),['src'=>$this->src,'fn'=>$this->fn,'ff'=>$this->ff]);
}
$this->dst = Address::parseFTN($dst);
if (($this->dst['n'] !== $this->tn) || ($this->dst['f'] !== $this->tf)) {
Log::error(sprintf('%s:! INTL dst address [%s] doesnt match packet',self::LOGKEY,$dst),['dst'=>$this->dst,'tn'=>$this->tn,'tf'=>$this->tf]);
}
}
elseif (($t = $this->kludge('TZUTC: ',$kl)) && is_numeric($t))
$this->tzutc = $t;
elseif ($t = $this->kludge('MSGID: ',$kl))
$this->msgid = $t;
elseif ($t = $this->kludge('REPLY: ',$kl))
$this->replyid = $t;
elseif ($t = $this->kludge('GATE: ',$kl))
$this->gateid = $t;
elseif ($t = $this->kludge('PATH: ',$kl))
$this->path->push($t);
elseif ($t = $this->kludge('RESCANNED ',$kl))
$this->rescanned->push($t);
elseif ($t = $this->kludge('SEEN-BY: ',$kl))
$this->seenby->push($t);
// To Point: <SOH>TOPT <point number><CR>
elseif ($t = $this->kludge('TOPT ',$kl))
$this->point['dst'] = $t;
// <SOH>Via <FTN Address> @YYYYMMDD.HHMMSS[.Precise][.Time Zone] <Program Name> <Version> [Serial Number]<CR>
elseif ($t = $this->kludge('Via ',$kl))
$this->via->push($t);
// We got a kludge line we dont know about
else
$this->unknown->push(chop($kl,"\r"));
}
// Work out our zone/point
// http://ftsc.org/docs/fsc-0068.001
// MSGID should be the basis of the source, we'll overwrite our src from origin if we have it
// If the message was gated, we'll use the gateid
$m = [];
if (($this->msgid || $this->gateid) && preg_match('#([0-9]+:[0-9]+/[0-9]+)?\.?([0-9]+)?(\.[0-9]+)?@?([A-Za-z-_~]+)?\ +#',$this->gateid ?: $this->msgid,$m)) {
try {
$this->src = Address::parseFTN($m[1].((isset($m[2]) && $m[2] != '') ? '.'.$m[2] : '').(isset($m[4]) ? '@'.$m[4] : ''));
} catch (\Exception $e) {
Log::error(sprintf('%s:! MSGID [%s] address is invalid [%s]',self::LOGKEY,$this->msgid,$e->getMessage()));
}
// Without a MSGID, get our domain from the origin
} elseif ($this->origin && preg_match('#\(([0-9]+:[0-9]+/[0-9]+)?\.?([0-9]+)?@?([A-Za-z-_~]+)?\)$#',$this->origin,$m)) {
$this->src = Address::parseFTN($m[1].((isset($m[2]) && $m[2] != '') ? '.'.$m[2] : '').(isset($m[3]) ? '@'.$m[3] : ''));
// Otherwise get it from our zone object and packet header
} elseif ($this->zone) {
$this->src = Address::parseFTN(sprintf('%d:%d/%d.%d@%s',$this->zone->zone_id,$this->fn,$this->ff,$this->fp,$this->zone->domain->name));
}
// Parse SEEN-BY
if ($this->seenby->count())
$this->seenaddress = $this->parseAddresses('seenby',$this->seenby,$this->rogue_seenby);
// Parse PATH
if ($this->path->count())
$this->pathaddress = $this->parseAddresses('path',$this->path,$this->rogue_path);
// Parse VIA
if ($this->via->count())
$this->pathaddress = $this->parseVia($this->via,$this->rogue_path);
}
/**
* Validate details about this message
*
* @return \Illuminate\Contracts\Validation\Validator
*/
public function validate(): ValidatorResult
{
// Check lengths
$validator = Validator::make([
'user_from' => $this->user_from,
'user_to' => $this->user_to,
'subject' => $this->subject,
'onode' => $this->ff,
'dnode' => $this->tf,
'onet' => $this->fn,
'dnet' => $this->tn,
'flags' => $this->flags,
'cost' => $this->cost,
'echoarea' => $this->echoarea,
'ozone' => $this->fz,
'dzone' => $this->tz,
],[
'user_from' => 'required|min:1|max:'.self::USER_FROM_LEN,
'user_to' => 'required|min:1|max:'.self::USER_TO_LEN,
'subject' => 'present|max:'.self::SUBJECT_LEN,
'onode' => ['required',new TwoByteIntegerWithZero],
'dnode' => ['required',new TwoByteIntegerWithZero],
'onet' => ['required',new TwoByteInteger],
'dnet' => ['required',new TwoByteInteger],
'flags' => 'required|numeric',
'cost' => 'required|numeric',
'echoarea' => 'nullable|max:'.self::AREATAG_LEN,
'ozone' => ['required'],
'dzone' => ['required']
]);
$validator->after(function($validator) {
if (! $this->fboss_o)
$validator->errors()->add('from',sprintf('Undefined Node [%s] sent message.',$this->fboss));
if (! $this->tboss_o)
$validator->errors()->add('to',sprintf('Undefined Node [%s] for destination.',$this->tboss));
});
if ($validator->fails())
$this->errors = $validator;
return $validator;
}
}