953 lines
28 KiB
PHP
953 lines
28 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\Models\{Address,Domain,Zone};
|
|
use App\Rules\{TwoByteInteger,TwoByteIntegerWithZero};
|
|
use App\Traits\EncodeUTF8;
|
|
|
|
/**
|
|
* Class Message
|
|
* 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',
|
|
];
|
|
|
|
// Single value kludge items
|
|
private array $_kludge = [
|
|
'chrs' => 'CHRS: ',
|
|
'charset' => 'CHARSET: ',
|
|
'codepage' => 'CODEPAGE: ',
|
|
'dbid' => 'DBID: ',
|
|
'pid' => 'PID: ',
|
|
'tid' => 'TID: ',
|
|
];
|
|
|
|
// Flags for messages
|
|
public const FLAG_PRIVATE = 1<<0;
|
|
public const FLAG_CRASH = 1<<1;
|
|
public const FLAG_RECD = 1<<2;
|
|
public const FLAG_SENT = 1<<3;
|
|
public const FLAG_FILEATTACH = 1<<4;
|
|
public const FLAG_INTRANSIT = 1<<5;
|
|
public const FLAG_ORPHAN = 1<<6;
|
|
public const FLAG_KILLSENT = 1<<7;
|
|
public const FLAG_LOCAL = 1<<8;
|
|
public const FLAG_HOLD = 1<<9;
|
|
public const FLAG_UNUSED_10 = 1<<10;
|
|
public const FLAG_FREQ = 1<<11;
|
|
public const FLAG_RETRECEIPT = 1<<12;
|
|
public const FLAG_ISRETRECEIPT = 1<<13;
|
|
public const FLAG_AUDITREQ = 1<<14;
|
|
public const FLAG_FILEUPDATEREQ = 1<<15;
|
|
public const FLAG_ECHOMAIL = 1<<16;
|
|
|
|
// FTS-0001.016 Message header 32 bytes node, net, flags, cost, date
|
|
private 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
|
|
|
|
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 = '';
|
|
$this->tagline = '';
|
|
$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();
|
|
}
|
|
|
|
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->src,'p');
|
|
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->dst,'p',0); // @todo this wont work for netmails, since dst is not set for in transit messages
|
|
|
|
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($e->getMessage());
|
|
}
|
|
|
|
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};
|
|
|
|
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,
|
|
$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);
|
|
|
|
$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 {
|
|
$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);
|
|
|
|
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
|
|
* @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&0x7fff,$node&0x7fff);
|
|
// @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();
|
|
|
|
foreach ($result as $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);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
|
|
foreach ($result as $kl) {
|
|
// $kl is our line starting with a \x01 kludge delimiter
|
|
|
|
// 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 are is a return in this middle of this line, that means the text message starts after the return
|
|
// If our message has started, then we'll assume the binary is part of the message.
|
|
if (strlen($this->message) || ($retpos !== strlen($kl)-1)) {
|
|
|
|
// If there was no return, its part of the message.
|
|
if ($retpos === FALSE) {
|
|
$this->message .= "\x01".$kl;
|
|
|
|
continue;
|
|
}
|
|
|
|
// Anything after the origin line is also kludge data.
|
|
if ($originpos = strrpos($kl,"\r * Origin: ")) {
|
|
if (! $this->message) {
|
|
$this->message .= substr($kl,$retpos+1,$originpos-$retpos-1);
|
|
|
|
} else {
|
|
$this->message .= "\x01".substr($kl,0,$originpos);
|
|
|
|
$retpos = 0;
|
|
}
|
|
|
|
$this->parseOrigin(substr($kl,$originpos+1));
|
|
$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('FTN [%s] doesnt match message header',$matches[1]),['ftn'=>$this->src,'fn'=>$this->fn,'ff'=>$this->ff]);
|
|
}
|
|
|
|
// The message is the rest?
|
|
} elseif (strlen($kl) > $retpos+1) {
|
|
// Since netmail doesnt have an origin - our source:
|
|
$this->message .= substr($message, 0, $msgpos);
|
|
|
|
$this->message_src = substr($kl,$retpos+1);
|
|
|
|
$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('INTL src address [%s] doesnt match packet',$src));
|
|
}
|
|
|
|
$this->dst = Address::parseFTN($dst);
|
|
if (($this->dst['n'] !== $this->tn) || ($this->dst['f'] !== $this->tf)) {
|
|
Log::error(sprintf('INTL dst address [%s] doesnt match packet',$dst));
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
} |