diff --git a/app/Classes/FTN.php b/app/Classes/FTN.php index c38d051..9dc8e25 100644 --- a/app/Classes/FTN.php +++ b/app/Classes/FTN.php @@ -11,7 +11,7 @@ abstract class FTN public function __get(string $key) { switch ($key) { - case 'fftn': + case 'fftn_t': return sprintf('%d:%d/%d.%d', $this->fz, $this->fn, @@ -19,7 +19,7 @@ abstract class FTN $this->fp, ).($this->zone ? sprintf('@%s',$this->zone->domain->name) : ''); - case 'tftn': + case 'tftn_t': return sprintf('%d:%d/%d.%d', $this->tz, $this->tn, @@ -27,10 +27,10 @@ abstract class FTN $this->tp, ).($this->zone ? sprintf('@%s',$this->zone->domain->name) : ''); - case 'fftn_o': - return Address::findFTN($this->fftn); - case 'tftn_o': - return Address::findFTN($this->tftn); + case 'fftn': + return Address::findFTN($this->fftn_t); + case 'tftn': + return Address::findFTN($this->tftn_t); default: throw new \Exception('Unknown key: '.$key); diff --git a/app/Classes/FTN/Message.php b/app/Classes/FTN/Message.php index 92e39c3..f78ae44 100644 --- a/app/Classes/FTN/Message.php +++ b/app/Classes/FTN/Message.php @@ -11,43 +11,27 @@ 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\Exceptions\InvalidPacketException; +use App\Models\{Address,Domain,Echomail,Netmail,Zone}; use App\Rules\{TwoByteInteger,TwoByteIntegerWithZero}; use App\Traits\{EncodeUTF8,ObjectIssetFix}; /** * Class Message * Represents the structure of a message in a packet - * NOTE: FTN Echomail Messages are ZONE agnostic. * + * @note FTN packed echomail messages are ZONE agnostic. * @package App\Classes */ class Message extends FTNBase { - use EncodeUTF8,ObjectIssetFix; + use ObjectIssetFix; 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: ', + // Kludges handled here + private const kludges = [ + 'TZUTC:'=>'tzutc', ]; // Flags for messages @@ -89,57 +73,24 @@ class Message extends FTNBase 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 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 + 'datetime' => [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; // + public const USER_FROM_LEN = 36; // FTS-0001.016 From Name: upto 36 chars null terminated + public const USER_TO_LEN = 36; // FTS-0001.016 To Name: upto 36 chars null terminated + public const SUBJECT_LEN = 71; // FTS-0001.016 Subject: upto 72 chars null terminated + public 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 $seenby; // FTS-0004.001 The message SEEN-BY lines - private Collection $via; // The path the message has gone using Via lines (Netmail) - private Collection $unknown; // Temporarily hold attributes we have no logic for. + private int $tzutc = 0; // TZUTC that needs to be converted to be used by Carbon @see self::kludges + private Echomail|Netmail $mo; // The object storing this packet message /** @deprecated Not sure why this is needed? */ public bool $packed = FALSE; // Has the message been packed successfully @@ -190,66 +141,126 @@ class Message extends FTNBase 0xfc => 0x207f, 0xfd => 0x00b2, 0xfe => 0x25a0, 0xff => 0x00a0, ]; + public static function header_len(): int + { + return collect(static::HEADER)->sum(function($item) { return Arr::get($item,2); }); + } + + /** + * Pack a message for rendering in a packet + * + * @param Echomail|Netmail $o + * @return self + */ + public static function packMessage(Echomail|Netmail $o): self + { + $oo = new self($o->fftn->zone); + $oo->mo = $o; + + return $oo; + } + /** * Parse a message from a packet * + * Each message has 3 discreet structures + * + Pre kludge lines, each line starting with and completing with + * + Message content, ending with + * - tagline starting with ... (optional) + * - tearline starting with --- (optional) + * - origin line starting with ' * Origin ' (optional for netmail) + * + Post kludge lines, which may or may not start with . There should be an if there wasnt an origin line. + * - []Via ... (netmail) + * - SEEN-BY: ... (echomail) + * - PATH: ... (echomail) + * * @param string $msg - * @param Zone|null $zone - * @return Message + * @param Zone $zone + * @return Echomail|Netmail * @throws \Exception */ - public static function parseMessage(string $msg,Zone $zone=NULL): self + public static function parseMessage(string $msg,Zone $zone): Echomail|Netmail { - Log::info(sprintf('%s:= Processing message [%d] bytes from zone [%d]',self::LOGKEY,strlen($msg),$zone?->zone_id)); + Log::info(sprintf('%s:= Processing message [%d] bytes from zone [%d]',self::LOGKEY,strlen($msg),$zone->zone_id)); + $header_len = self::header_len(); $o = new self($zone); - $o->dump = $msg; try { - $o->header = unpack(self::unpackheader(self::header),substr($msg,0,self::HEADER_LEN)); + $o->header = unpack(self::unpackheader(self::HEADER),substr($msg,0,$header_len)); } catch (\Exception $e) { - Log::error(sprintf('%s:! Error bad packet header',self::LOGKEY),['e'=>$e->getMessage(),'header'=>substr($msg,0,self::HEADER_LEN)]); - $validator = Validator::make([ - 'header' => substr($msg,0,self::HEADER_LEN), - ],[ - 'header' => [function ($attribute,$value,$fail) use ($e) { return $fail($e->getMessage()); }] - ]); + Log::error(sprintf('%s:! Error bad packet header',self::LOGKEY),['e'=>$e->getMessage(),'header'=>substr($msg,0,$header_len)]); - if ($validator->fails()) - $o->errors = $validator; - - return $o; + throw new InvalidPacketException($e->getMessage()); } $ptr = 0; // To User - $o->user_to = strstr(substr($msg,self::HEADER_LEN+$ptr),"\x00",TRUE); - $ptr += strlen($o->user_to)+1; + $o->header['user_to'] = strstr(substr($msg,$header_len+$ptr),"\x00",TRUE); + $ptr += strlen($o->header['user_to'])+1; // From User - $o->user_from = strstr(substr($msg,self::HEADER_LEN+$ptr),"\x00",TRUE); - $ptr += strlen($o->user_from)+1; + $o->header['user_from'] = strstr(substr($msg,$header_len+$ptr),"\x00",TRUE); + $ptr += strlen($o->header['user_from'])+1; // Subject - $o->subject = strstr(substr($msg,self::HEADER_LEN+$ptr),"\x00",TRUE); - $ptr += strlen($o->subject)+1; + $o->header['subject'] = strstr(substr($msg,$header_len+$ptr),"\x00",TRUE); + $ptr += strlen($o->header['subject'])+1; // Check if this is an Echomail - if (! strncmp(substr($msg,self::HEADER_LEN+$ptr),'AREA:',5)) { - $o->echoarea = strtoupper(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; + if (! strncmp(substr($msg,$header_len+$ptr),'AREA:',5)) { + $o->header['echoarea'] = strtoupper(substr($msg,$header_len+$ptr+5,strpos($msg,"\r",$header_len+$ptr+5)-($header_len+$ptr+5))); + $ptr += strlen($o->header['echoarea'])+5+1; + + $eao = $zone->domain->echoareas->where('name',$o->header['echoarea'])->pop(); + + $o->mo = new Echomail; + + if ($eao) + $o->mo->echoarea_id = $eao->id; + else + $o->mo->set_echoarea = $o->header['echoarea']; + + } else { + $o->mo = new Netmail; } - $o->unpackMessage(substr($msg,self::HEADER_LEN+$ptr)); + $o->mo = $o->unpackMessage(substr($msg,$header_len+$ptr),$o->mo); + + $o->mo->to = $o->header['user_to']; + $o->mo->from = $o->header['user_from']; + $o->mo->subject = $o->header['subject']; + + $o->mo->datetime = $o->datetime; + $o->mo->tzoffset = $o->datetime->utcOffset(); + $o->mo->flags = $o->header['flags']; + $o->mo->cost = $o->header['cost']; + + $o->mo->msg_crc = md5($o->mo->msg_src); + + $o->mo->fftn_id = $o->fftn?->id; + + switch (get_class($o->mo)) { + case Echomail::class: + break; + + case Netmail::class: + $o->mo->tftn_id = $o->tftn?->id; + break; + + default: + throw new InvalidPacketException('Unknown message class: '.get_class($o->mo)); + } 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())); + $o->mo->errors = $x; + + Log::debug(sprintf('%s:! Message fails validation (%s@%s->%s@%s)',self::LOGKEY,$o->mo->from,$o->fftn_t,$o->mo->to,$o->tftn_t),['result'=>$o->mo->errors->errors()]); } - return $o; + return $o->mo; } /** @@ -275,80 +286,28 @@ class Message extends FTNBase /** * Packets have no concept of a zone, so we add it for any calculations that could use it + * This is taken from the sender * - * @param Zone|NULL $zone + * @param Zone $zone */ - public function __construct(Zone $zone=NULL) + public function __construct(Zone $zone) { $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->via = collect(); - $this->unknown = collect(); } public function __get($key) { + // @todo Do we need all these key values? + Log::debug(sprintf('%s:/ Requesting key for Message::class [%s]',self::LOGKEY,$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 'fn': return ($x=$this->src) ? Arr::get($x,'n') : Arr::get($this->header,'onet'); + case 'ff': return ($x=$this->src) ? Arr::get($x,'f') : Arr::get($this->header,'onode'); + case 'fp': return $this->mo->kludges->get('FMPT:') ?: 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)) @@ -363,6 +322,24 @@ class Message extends FTNBase return Zone::where('zone_id',$this->fz) ->where('default',TRUE) ->single(); + 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; + + // To Addresses + // Echomail doesnt have a zone, so we'll use the source zone + case 'tz': return Arr::get($this->isEchomail() ? $this->src : $this->dst,'z'); + case 'tn': return Arr::get($this->header,'dnet'); + case 'tf': return Arr::get($this->header,'dnode'); + case 'tp': return $this->mo->kludges->get('TOPT:') ?: Arr::get($this->header,'dpoint',0); case 'tzone': // Use the zone if this class was called with it. @@ -378,21 +355,27 @@ class Message extends FTNBase return Zone::where('zone_id',$this->tz) ->where('default',TRUE) ->single(); + 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; - // 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)); + // 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 'fftn_t': case 'fftn': - case 'fftn_o': + case 'tftn_t': case 'tftn': - case 'tftn_o': return parent::__get($key); // For 5D we need to include the domain + /* @deprecated - is this required? */ case 'fboss': return sprintf('%d:%d/%d',$this->fz,$this->fn,$this->ff).(($x=$this->fdomain) ? '@'.$x->name : ''); case 'tboss': @@ -402,9 +385,10 @@ class Message extends FTNBase case 'tboss_o': return Address::findFTN($this->tboss); - case 'date': + // Convert our message (header[datetime]) with our TZUTC into a Carbon date + case 'datetime': try { - if (str_contains($x=chop(Arr::get($this->header,$key)),"\x00")) + if (str_contains($x=rtrim(Arr::get($this->header,$key),"\x00"),"\x00")) throw new \Exception('Date contains null values.'); return Carbon::createFromFormat('d M y H:i:s O', @@ -415,40 +399,90 @@ class Message extends FTNBase 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); + // The source of the message + case 'src': + // If this is a netmail, then our 4D details are in the netmail + if ($this->mo instanceof Netmail) { + /* + * 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. + * + * "INTL "" " + */ + if ($this->mo->kludges->has('INTL')) { + // INTL kludge is in Netmail, so we'll do some validation: + list($dst,$src) = explode(' ',$this->mo->kludges->get('INTL')); - case 'tzutc': + $src = Address::parseFTN($src); - case 'user_to': - case 'user_from': - case 'subject': - case 'echoarea': + // @todo move to validation + if (($src['n'] !== Arr::get($this->header,'onet')) || ($src['f'] !== Arr::get($this->header,'onode'))) { + Log::error(sprintf('%s:! INTL src address [%s] doesnt match packet',self::LOGKEY,$src),['src'=>$src,'fn'=>Arr::get($this->header,'onet'),'ff'=>Arr::get($this->header,'onode')]); + } - case 'msgid': - case 'replyid': - case 'gateid': + return $src; + } - case 'message': - case 'message_src': + } elseif ($this->mo instanceof Echomail) { + // Work out our zone/point + // http://ftsc.org/docs/fsc-0068.001 + // MSGID should be the basis of the source, if it cannot be obtained from the origin + // If the message was gated, we'll use the gateid + $m = []; + if ($this->mo->origin && preg_match('#\(([0-9]+:[0-9]+/[0-9]+)?\.?([0-9]+)?@?([A-Za-z-_~]+)?\)$#',$this->mo->origin,$m)) { + return Address::parseFTN($m[1].((isset($m[2]) && $m[2] != '') ? '.'.$m[2] : '').(isset($m[3]) ? '@'.$m[3] : '')); - case 'tearline': - case 'tagline': - case 'origin': + } elseif (($this->mo->msgid || $this->mo->gateid) && preg_match('#([0-9]+:[0-9]+/[0-9]+)?\.?([0-9]+)?(\.[0-9]+)?@?([A-Za-z-_~]+)?\ +#',$this->mo->gateid ?: $this->mo->msgid,$m)) { + try { + return 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->mo->msgid,$e->getMessage())); + } - case 'kludge': - case 'rescanned': - case 'path': - case 'seenby': - case 'unknown': - case 'via': + // Otherwise get it from our zone object and packet header + } elseif ($this->zone) { + return Address::parseFTN(sprintf('%d:%d/%d.%d@%s',$this->zone->zone_id,$this->fn,$this->ff,$this->fp,$this->zone->domain->name)); + } - case 'errors': - return $this->{$key}; + } else { + throw new InvalidPacketException('Dont know what type of packet this is'); + } - case 'dbid': - return $this->kludge->get($key); + break; + + case 'dst': + // If this is a netmail, then our 4D details are in the netmail + if ($this->mo instanceof Netmail) { + /* + * 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. + * + * "INTL "" " + */ + if ($this->mo->kludges->has('INTL')) { + // INTL kludge is in Netmail, so we'll do some validation: + list($dst,$src) = explode(' ',$this->mo->kludges->get('INTL')); + + $dst = Address::parseFTN($dst); + + // @todo move to validation + if (($dst['n'] !== $this->tn) || ($dst['f'] !== $this->tf)) { + Log::error(sprintf('%s:! INTL dst address [%s] doesnt match packet',self::LOGKEY,$dst),['dst'=>$dst,'tn'=>$this->tn,'tf'=>$this->tf]); + } + + return $dst; + } + + } elseif ($this->mo instanceof Echomail) { + // Echomail doesnt have DST addresses + + } else { + throw new InvalidPacketException('Dont know what type of packet this is'); + } + + break; default: throw new \Exception('Unknown key: '.$key); @@ -465,121 +499,71 @@ class Message extends FTNBase 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 + * @throws \Exception */ public function __toString(): string { - $return = pack(collect(self::header)->pluck(1)->join(''), - $this->ff, // Originating Node - $this->tf, // Destination Node - $this->fn, // Originating Net - $this->tn, // Destination Net - $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 = pack(collect(self::HEADER)->pluck(1)->join(''), + $this->mo->fftn->node_id, // Originating Node + $this->mo->tftn->node_id, // Destination Node + $this->mo->fftn->host_id, // Originating Net + $this->mo->tftn->host_id, // Destination Net + $this->mo->flags&~(self::FLAG_INTRANSIT|self::FLAG_LOCAL), // Turn off our local/intransit bits + $this->mo->cost, + $this->mo->date->format('d M y H:i:s'), ); - $return .= $this->user_to."\00"; - $return .= $this->user_from."\00"; - $return .= $this->subject."\00"; + $return .= $this->mo->to."\00"; + $return .= $this->mo->from."\00"; + $return .= $this->mo->subject."\00"; - if (! $this->isNetmail()) - $return .= sprintf("AREA:%s\r",strtoupper($this->echoarea)); + if ($this->mo instanceof Echomail) + $return .= sprintf("AREA:%s\r",strtoupper($this->mo->echoarea->name)); - // 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->mo instanceof Netmail) { + // @todo If the message is local, then our kludges are not in the msg itself, we'll add them + if ($this->isFlagSet(self::FLAG_LOCAL)) + Log::debug(sprintf('%s:^ Local message, we may need to set our INTL/FMPT/TOPT',self::LOGKEY)); - if ($this->fp) - $return .= sprintf("\01FMPT %d\r",$this->fp); - if ($this->tp) - $return .= sprintf("\01TOPT %d\r",$this->tp); - } + $return .= sprintf("\01INTL %s\r",$this->mo->kludges->get('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 { - 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->mo->kludges->has('FMPT')) + $return .= sprintf("\01FMPT %d\r",$this->mo->kludges->get('FMPT')); + if ($this->mo->kludges->has('TOPT')) + $return .= sprintf("\01TOPT %d\r",$this->mo->kludges->get('TOPT')); } - if ($this->isNetmail()) { - foreach ($this->via as $via) - $return .= sprintf("\01Via %s\r",$via); + // Add some kludges + $return .= sprintf("\01TZUTC: %s\r",str_replace('+','',$this->mo->date->getOffsetString(''))); + + $return .= sprintf("\01MSGID: %s\r",$this->mo->msgid); + + if ($this->mo->replyid) + $return .= sprintf("\01REPLY: %s\r",$this->mo->replyid); + + foreach ($this->mo->kludges as $k=>$v) + $return .= sprintf("\01%s %s\r",$k,$v); + + $return .= $this->mo->content; + + if ($this->mo instanceof Netmail) { + // @todo automatically include our address in the path + + foreach ($this->mo->path as $ao) + $return .= sprintf("\x01Via %s\r",$this->mo->via($ao)); } else { - // Seenby & PATH - FSC-0068 - $return .= $this->optimise_path($this->seenby,'SEEN-BY:')."\r"; - $return .= "\x01".$this->optimise_path($this->path,'PATH:')."\r"; + // FTS-0004.001/FSC-0068.001 The message SEEN-BY lines + // FTS-0004.001/FSC-0068.001 The message PATH lines + // @todo we need to include our rogue_seenbys + // @todo make sure a point is NOT in the seenby/path when exporting + // @todo automatically include our address in the seenby/path + $return .= $this->aka_trim($this->mo->seenby,'SEEN-BY:')."\r"; + $return .= "\x01".$this->aka_trim($this->mo->path,'PATH:')."\r"; } $return .= "\00"; @@ -597,20 +581,6 @@ class Message extends FTNBase $this->decode($values); } - /** - * Determine if a line is a kludge line. - * - * @param string $kludge - * @param string $string - * @return string - */ - private function kludge(string $kludge,string $string) - { - return (preg_match("/^{$kludge}/",$string)) - ? chop(preg_replace("/^{$kludge}/",'',$string),"\r") - : FALSE; - } - /** * Reduce our PATH/SEEN-BY for messages as per FSC-0068 * @@ -620,14 +590,14 @@ class Message extends FTNBase * @param string $delim * @return string */ - private function optimise_path(Collection $path,string $prefix,int $len=79,string $delim="\r"): string + private function aka_trim(Collection $path,string $prefix,int $len=79,string $delim="\r"): string { $cur = NULL; $result = $prefix; $c = strlen($prefix); - foreach ($path as $address) { - [$host,$node] = explode('/',$address); + foreach ($path as $ao) { + [$host,$node] = explode('/',$ao->ftn2d); if (($c+strlen(' '.$host)) > $len) { $result .= ($x=$delim.$prefix); @@ -637,7 +607,7 @@ class Message extends FTNBase if ($host !== $cur) { $cur = $host; - $result .= ($x=' '.$address); + $result .= ($x=' '.$ao->ftn2d); } else { $result .= ($x=' '.$node); @@ -649,6 +619,11 @@ class Message extends FTNBase return $result; } + private function isEchomail(): bool + { + return $this->mo instanceof Echomail; + } + /** * If this message doesnt have an AREATAG, then its a netmail. * @@ -656,292 +631,129 @@ class Message extends FTNBase */ 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); - } - - /** - * 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); - } + return $this->mo instanceof Netmail; } /** * Extract information out of the message text. * * @param string $message - * @throws \Exception + * @param Echomail|Netmail $o + * @return Echomail|Netmail + * @throws InvalidPacketException */ - public function unpackMessage(string $message): void + private function unpackMessage(string $message,Echomail|Netmail $o): Echomail|Netmail { // Remove DOS \n\r $message = preg_replace("/\n\r/","\r",$message); $message = preg_replace("/\r\n/","\r",$message); - // Split out the lines - $result = collect(explode("\x01",$message))->filter(); + // First find our kludge lines + $ptr_start = 0; - $this->message = ''; - $this->message_src = ''; - $msgpos = 0; - $haveOrigin = FALSE; + while (substr($message,$ptr_start,1) === "\x01") { + $ptr_end = strpos($message,"\r",$ptr_start); - while ($result->count()) { - // $kl is our line starting with a \x01 kludge delimiter - $kl = $result->shift(); + $m = []; + $kludge = substr($message,$ptr_start+1,$ptr_end-$ptr_start-1); + preg_match('/^([^\s]+:?)+\s+(.*)$/',$kludge,$m); - // 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); - - // 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: "FMPT - 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. - * - * "INTL "" " - */ - 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: TOPT - elseif ($t = $this->kludge('TOPT ',$kl)) - $this->point['dst'] = $t; - - // Via @YYYYMMDD.HHMMSS[.Precise][.Time Zone] [Serial Number] - // @todo The via line is still showing in the main message? https://clrghouz.bbs.dege.au/netmail/view/707 - // @todo Need to make sure that the CRC doesnt include this - elseif ($t = $this->kludge('Via ',$kl)) - $this->via->push($t); - - // We got a kludge line we dont know about + // Catch any kludges we need to process here + if (array_key_exists($m[1],self::kludges)) + $this->{self::kludges[$m[1]]} = $m[2]; else - $this->unknown->push(chop($kl,"\r")); + $o->kludges = [$m[1],$m[2]]; + + $ptr_start = $ptr_end+1; } - // Work out our zone/point - // http://ftsc.org/docs/fsc-0068.001 - // MSGID should be the basis of the source, if it cannot be obtained from the origin - // If the message was gated, we'll use the gateid - $m = []; - if ($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] : '')); + // Next our message content ends with '\r * Origin: ... \r' or ... + // FTS-0004.001 + if ($ptr_end=strpos($message,"\r * Origin: ",$ptr_start)) { + // Find the + $ptr_end = strpos($message,"\r",$ptr_end+1); - } elseif (($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())); + if (! $ptr_end) + throw new InvalidPacketException('Couldnt find the end of the origin'); + + } elseif (! $ptr_end=strpos($message,"\r\x01",$ptr_start)) { + throw new InvalidPacketException('Couldnt parse the end of the content'); + } + + // Process the message content + if ($content=substr($message,$ptr_start,$ptr_end-$ptr_start)) { + $o->msg_src = $content; + $ptr_content_start = 0; + + // See if we have a tagline + if ($ptr_content_end=strpos($content,"\r\r... ",$ptr_content_start)) { + $o->msg = substr($content,$ptr_content_start,$ptr_content_end); + $ptr_content_start = $ptr_content_end+6; + + $ptr_content_end = strpos($content,"\r",$ptr_content_start); + + // If there is no terminating "\r", then that's it + if (! $ptr_content_end) + $o->set_tagline = substr($content,$ptr_content_start); + else + $o->set_tagline = substr($content,$ptr_content_start,$ptr_content_end-$ptr_content_start); + + $ptr_content_start = $ptr_content_end; } - // 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)); + // See if we have a tearline + if ($ptr_content_end=strpos($content,"\r\r--- ",$ptr_content_start)) { + if (! $ptr_content_start) + $o->msg = substr($content,$ptr_content_start,$ptr_content_end-1); + $ptr_content_start = $ptr_content_end+6; + + $ptr_content_end = strpos($content,"\r",$ptr_content_start); + + // If there is no terminating "\r", then that's it + if (! $ptr_content_end) + $o->set_tearline = substr($content,$ptr_content_start); + else + $o->set_tearline = substr($content,$ptr_content_start,$ptr_content_end-$ptr_content_start); + + $ptr_content_start = $ptr_content_end; + } + + // See if we have an origin + if ($ptr_content_end=strpos($content,"\r * Origin: ",$ptr_content_start)) { + if (! $ptr_content_start) + $o->msg = substr($content,$ptr_content_start,$ptr_content_end-1); + $ptr_content_start = $ptr_content_end+12; + + $ptr_content_end = strpos($content,"\r",$ptr_content_start); + + // If there is no terminating "\r", then that's it + if (! $ptr_content_end) + $o->set_origin = substr($content,$ptr_content_start); + else + $o->set_origin = substr($content,$ptr_content_start,$ptr_content_end-$ptr_content_start); + + $ptr_content_start = $ptr_content_end; + } + + // Quick validation that we are done + if ($ptr_content_start) + throw new InvalidPacketException('There is more data in the message content?'); } + + $ptr_start = $ptr_end+1; + + // Finally work out control kludges + foreach (collect(explode("\r",substr($message,$ptr_start)))->filter() as $line) { + // If the line starts with ignore it + if (substr($line,0,1) === "\x01") + $line = ltrim($line,"\x01"); + + $m = []; + preg_match('/^([^\s]+:?)+\s+(.*)$/',$line,$m); + $o->kludges = [$m[1],$m[2]]; + } + + return $o; } /** @@ -952,33 +764,49 @@ class Message extends FTNBase 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 = Validator::make( + array_merge($this->mo->toArray(),[ + 'echoarea' => $this->isEchomail() ? ($this->mo->set->get('set_echoarea') ?: $this->mo->echoarea->name ) : NULL, + 'onode' => $this->ff, + 'dnode' => $this->tf, + 'onet' => $this->fn, + 'dnet' => $this->tn, + 'ozone' => $this->fz, + 'dzone' => $this->tz, + ]) + ,[ + 'from' => 'required|min:1|max:'.self::USER_FROM_LEN, + 'to' => 'required|min:1|max:'.self::USER_TO_LEN, + 'subject' => 'required|max:'.self::SUBJECT_LEN, + 'datetime' => 'required|date', // |after:30 days ago', // @todo within "x" days + 'tzoffset' => 'present|numeric', + 'flags' => 'required|numeric', + 'cost' => 'required|numeric', + 'msgid' => 'sometimes|min:1', + 'replyid' => 'sometimes|min:1', + 'msg' => 'required|min:1', // @todo max message length? + 'msg_crc' => 'required|size:32', + 'tagline' => 'sometimes|min:1|max:255', + 'tearline' => 'sometimes|min:1|max:255', + 'origin' => 'sometimes|min:1|max:255', + 'local' => 'sometimes|boolean', + 'fftn_id' => 'required|exists:App\Models\Address,id', + 'tftn_id' => $this->isNetmail() ? 'required|exists:App\Models\Address,id' : 'prohibited', + 'rogue_path' => $this->isEchomail() ? 'sometimes|array' : 'prohibited', + 'rogue_seenby' => $this->isEchomail() ? 'sometimes|array' : 'prohibited', + 'kludges' => 'present|array|min:1', // @todo add in required KEYS like INTL for netmails + + 'onode' => ['required',new TwoByteIntegerWithZero], + 'dnode' => ['required',new TwoByteIntegerWithZero], + 'onet' => ['required',new TwoByteInteger], + 'dnet' => ['required',new TwoByteInteger], + 'ozone' => ['required',new TwoByteInteger], + 'dzone' => ['required',new TwoByteInteger], + + 'echoarea_id' => $this->isEchomail() ? 'sometimes|exists:App\Models\Echoarea,id' : 'prohibited', + 'echoarea' => $this->isEchomail() ? 'required|max:'.self::AREATAG_LEN : 'prohibited', + ] + ); $validator->after(function($validator) { if ($this->zone->domain->flatten) { @@ -997,7 +825,7 @@ class Message extends FTNBase }); if ($validator->fails()) - $this->errors = $validator; + $this->mo->errors = $validator; return $validator; } diff --git a/app/Classes/FTN/Packet.php b/app/Classes/FTN/Packet.php index a5a6531..033f13d 100644 --- a/app/Classes/FTN/Packet.php +++ b/app/Classes/FTN/Packet.php @@ -10,23 +10,30 @@ use Illuminate\Support\Facades\Notification; use Symfony\Component\HttpFoundation\File\File; use App\Classes\FTN as FTNBase; -use App\Models\{Address,Domain,Software,System,Zone}; +use App\Exceptions\InvalidPacketException; +use App\Models\{Address,Domain,Echomail,Netmail,Software,Zone}; use App\Notifications\Netmails\EchomailBadAddress; /** * Represents a Fidonet Packet, that contains an array of messages. * - * Thus this object is iterable as an array of Message::class. + * Thus this object is iterable as an array of Echomail::class or Netmail::class. */ -class Packet extends FTNBase implements \Iterator, \Countable +abstract class Packet extends FTNBase implements \Iterator, \Countable { private const LOGKEY = 'PKT'; protected const PACKED_MSG_LEAD = "\02\00"; protected const PACKED_END = "\00\00"; + // @todo Rename this regex to something more descriptive, ie: FILENAME_REGEX public const regex = '([[:xdigit:]]{4})(?:-(\d{4,10}))?-(.+)'; + /** + * Packet types we support, in specific order for auto-detection to work + * + * @var string[] + */ public const PACKET_TYPES = [ '2.2' => FTNBase\Packet\FSC45::class, '2+' => FTNBase\Packet\FSC48::class, @@ -35,115 +42,15 @@ class Packet extends FTNBase implements \Iterator, \Countable ]; protected array $header; // Packet Header - protected ?string $name; // Packet name + protected ?string $name = NULL; // Packet name public File $file; // Packet filename - public Collection $messages; // Messages in the Packet + protected Address $fftn_p; // Address the packet is from (when packing messages) + protected Address $tftn_p; // Address the packet is to (when packing messages) + protected Collection $messages; // Messages in the Packet public Collection $errors; // Messages that fail validation protected int $index; // Our array index - /** - * @param string|null $header - * @throws \Exception - */ - public function __construct(string $header=NULL) - { - $this->messages = collect(); - $this->errors = collect(); - $this->domain = NULL; - $this->name = NULL; - - if ($header) - $this->header = unpack(self::unpackheader(static::HEADER),$header); - } - - /** - * @throws \Exception - */ - public function __get($key) - { - switch ($key) { - // From Addresses - case 'fz': return Arr::get($this->header,'ozone'); - case 'fn': return Arr::get($this->header,'onet'); - case 'ff': return Arr::get($this->header,'onode'); - case 'fp': return Arr::get($this->header,'opoint'); - case 'fd': return rtrim(Arr::get($this->header,'odomain',"\x00")); - - // To Addresses - case 'tz': return Arr::get($this->header,'dzone'); - case 'tn': return Arr::get($this->header,'dnet'); - case 'tf': return Arr::get($this->header,'dnode'); - case 'tp': return Arr::get($this->header,'dpoint'); - case 'td': return rtrim(Arr::get($this->header,'ddomain',"\x00")); - - case 'date': - return Carbon::create( - Arr::get($this->header,'y'), - Arr::get($this->header,'m')+1, - Arr::get($this->header,'d'), - Arr::get($this->header,'H'), - Arr::get($this->header,'M'), - Arr::get($this->header,'S') - ); - - case 'password': - return rtrim(Arr::get($this->header,$key),"\x00"); - - case 'fftn': - case 'fftn_o': - case 'tftn': - case 'tftn_o': - return parent::__get($key); - - case 'software': - $code = Arr::get($this->header,'prodcode-hi')<<8|Arr::get($this->header,'prodcode-lo'); - Software::unguard(); - $o = Software::singleOrNew(['code'=>$code,'type'=>Software::SOFTWARE_TOSSER]); - Software::reguard(); - - return $o; - - case 'software_ver': - return sprintf('%d.%d',Arr::get($this->header,'prodrev-maj'),Arr::get($this->header,'prodrev-min')); - - case 'capability': - // This needs to be defined in child classes, since not all children have it - return NULL; - - // Packet Type - case 'type': - return static::TYPE; - - // Packet name: - case 'name': - return $this->{$key} ?: sprintf('%08x',timew()); - - default: - throw new \Exception('Unknown key: '.$key); - } - } - - /** - * Return the packet - * - * @return string - * @throws \Exception - */ - public function __toString(): string - { - $return = $this->header(); - - foreach ($this->messages as $o) { - if ($o->packed) - $return .= self::PACKED_MSG_LEAD.$o; - } - - $return .= "\00\00"; - - return $return; - } - /* STATIC */ /** @@ -164,10 +71,7 @@ class Packet extends FTNBase implements \Iterator, \Countable * @param string $header * @return bool */ - public static function is_type(string $header): bool - { - return FALSE; - } + abstract public static function is_type(string $header): bool; /** * Process a packet file @@ -224,7 +128,9 @@ class Packet extends FTNBase implements \Iterator, \Countable // No message attached } else - throw new InvalidPacketException('Not a valid packet, not EOP or SOM'.bin2hex($x)); + throw new InvalidPacketException('Not a valid packet, not EOP or SOM:'.bin2hex($x)); + + Log::info(sprintf('%s:- Packet [%s] is a [%s] packet, dated [%s]',self::LOGKEY,$o->name,get_class($o),$o->date)); // Work out the packet zone if ($o->fz && ($o->fd || $domain)) { @@ -233,16 +139,10 @@ class Packet extends FTNBase implements \Iterator, \Countable ->where('zone_id',$o->fz) ->where('name',$o->fd ?: $domain->name) ->single(); - - // We need not knowing the domain, we use the default zone - } else { - $o->zone = Zone::where('zone_id',$o->fz) - ->where('default',TRUE) - ->single(); } // If zone is not set, then we need to use a default zone - the messages may not be from this zone. - if (! $o->zone) { + if (empty($o->zone)) { Log::alert(sprintf('%s:! We couldnt work out the packet zone, so we have fallen back to the default for [%d]',self::LOGKEY,$o->fz)); $o->zone = Zone::where('zone_id',$o->fz) @@ -252,7 +152,7 @@ class Packet extends FTNBase implements \Iterator, \Countable $message = ''; // Current message we are building $msgbuf = ''; - $leader = Message::HEADER_LEN+strlen(self::PACKED_MSG_LEAD); + $leader = Message::header_len()+strlen(self::PACKED_MSG_LEAD); // We loop through reading from the buffer, to find our end of message tag while ((! feof($f) && ($readbuf=fread($f,$leader)))) { @@ -269,7 +169,7 @@ class Packet extends FTNBase implements \Iterator, \Countable $msgbuf = substr($msgbuf,$end+3); continue; - // If we have more to read + // If we have more to read } elseif ($read_ptr < $size) { continue; } @@ -285,18 +185,103 @@ class Packet extends FTNBase implements \Iterator, \Countable } /** - * Location of the version - * - * @return int + * @param string|null $header + * @throws \Exception */ - public static function version_offset(): int + public function __construct(string $header=NULL) { - return Arr::get(collect(static::HEADER)->get('type'),0); + $this->messages = collect(); + $this->errors = collect(); + + if ($header) + $this->header = unpack(self::unpackheader(static::HEADER),$header); } - public static function version_offset_len(): int + /** + * @throws \Exception + */ + public function __get($key) { - return Arr::get(collect(static::HEADER)->get('type'),2); + Log::debug(sprintf('%s:/ Requesting key for Packet::class [%s]',self::LOGKEY,$key)); + + switch ($key) { + // From Addresses + case 'fz': return Arr::get($this->header,'ozone'); + case 'fn': return Arr::get($this->header,'onet'); + case 'ff': return Arr::get($this->header,'onode'); + case 'fp': return Arr::get($this->header,'opoint'); + case 'fd': return rtrim(Arr::get($this->header,'odomain',"\x00")); + + // To Addresses + case 'tz': return Arr::get($this->header,'dzone'); + case 'tn': return Arr::get($this->header,'dnet'); + case 'tf': return Arr::get($this->header,'dnode'); + case 'tp': return Arr::get($this->header,'dpoint'); + case 'td': return rtrim(Arr::get($this->header,'ddomain',"\x00")); + + case 'date': + return Carbon::create( + Arr::get($this->header,'y'), + Arr::get($this->header,'m')+1, + Arr::get($this->header,'d'), + Arr::get($this->header,'H'), + Arr::get($this->header,'M'), + Arr::get($this->header,'S') + ); + + case 'password': + return rtrim(Arr::get($this->header,$key),"\x00"); + + case 'fftn_t': + case 'fftn': + case 'tftn_t': + case 'tftn': + return parent::__get($key); + + case 'software': + $code = Arr::get($this->header,'prodcode-hi')<<8|Arr::get($this->header,'prodcode-lo'); + Software::unguard(); + $o = Software::singleOrNew(['code'=>$code,'type'=>Software::SOFTWARE_TOSSER]); + Software::reguard(); + + return $o; + + case 'software_ver': + return sprintf('%d.%d',Arr::get($this->header,'prodrev-maj'),Arr::get($this->header,'prodrev-min')); + + case 'capability': + // This needs to be defined in child classes, since not all children have it + return NULL; + + // Packet Type + case 'type': + return static::TYPE; + + // Packet name: + case 'name': + return $this->{$key} ?: sprintf('%08x',timew()); + + default: + throw new \Exception('Unknown key: '.$key); + } + } + + /** + * Return the packet + * + * @return string + * @throws \Exception + */ + public function __toString(): string + { + $return = $this->header(); + + foreach ($this->messages as $o) + $return .= self::PACKED_MSG_LEAD.$o->packet($this->tftn_p); + + $return .= "\00\00"; + + return $return; } /* INTERFACE */ @@ -309,7 +294,7 @@ class Packet extends FTNBase implements \Iterator, \Countable return $this->messages->count(); } - public function current(): Message + public function current(): Echomail|Netmail { return $this->messages->get($this->index); } @@ -342,6 +327,7 @@ class Packet extends FTNBase implements \Iterator, \Countable * @param Address $oo * @param Address $o * @param string|null $passwd Override the password used in the packet + * @deprecated Use Packet::generate(), which should generate a packet of the right type */ public function addressHeader(Address $oo,Address $o,string $passwd=NULL): void { @@ -375,12 +361,38 @@ class Packet extends FTNBase implements \Iterator, \Countable * Add a message to this packet * * @param Message $o + * @deprecated No longer used when Address::class is updated */ public function addMail(Message $o): void { $this->messages->push($o); } + public function for(Address $ao): self + { + $this->tftn_p = $ao; + $this->fftn_p = our_address($ao); + + return $this; + } + + /** + * Generate a packet + * + * @param Collection $msgs + * @return string + * @throws InvalidPacketException + */ + public function generate(Collection $msgs): string + { + $this->messages = $msgs; + + if (empty($this->tftn_p) || empty($this->fftn_p)) + throw new InvalidPacketException('Cannot generate a packet without a destination address'); + + return (string)$this; + } + /** * Parse a message in a mail packet * @@ -393,7 +405,8 @@ class Packet extends FTNBase implements \Iterator, \Countable $msg = Message::parseMessage($message,$this->zone); - // If the message from domain is different to the packet address domain, we'll skip this message + // @todo If the message from domain (eg: $msg->fftn->zone->domain) is different to the packet address domain ($pkt->fftn->zone->domain), we'll skip this message + Log::debug(sprintf('%s:^ Message [%s] - Packet from domain [%d], Message domain [%d]',self::LOGKEY,$msg->msgid,$this->fftn->zone->domain_id,$msg->fftn->zone->domain_id)); // If the message is invalid, we'll ignore it if ($msg->errors) { @@ -401,14 +414,17 @@ class Packet extends FTNBase implements \Iterator, \Countable // If the messages is not for the right zone, we'll ignore it if ($msg->errors->messages()->has('invalid-zone')) { - Log::alert(sprintf('%s:! Message is from an invalid zone [%s], packet is from [%s] - ignoring it',self::LOGKEY,$msg->fftn,$msg->zone->domain->name)); + Log::alert(sprintf('%s:! Message [%s] is from an invalid zone [%s], packet is from [%s] - ignoring it',self::LOGKEY,$msg->msgid,$msg->fftn->zone->zone_id,$this->fftn->zone->zone_id)); if (! $msg->rescanned->count()) - Notification::route('netmail',$this->fftn_o)->notify(new EchomailBadAddress($msg)); + Notification::route('netmail',$this->fftn)->notify(new EchomailBadAddress($msg)); return; } + // @todo If the $msg->fftn doesnt exist, we'll need to create it + // @todo If the $msg->tftn doesnt exist (netmail), we'll need to create it (ergo intransit) +/* // If the to address doenst exist, we'll create a new entry if ($msg->errors->messages()->has('to') && $msg->tzone) { try { @@ -470,10 +486,13 @@ class Packet extends FTNBase implements \Iterator, \Countable Log::alert(sprintf('%s:- From FTN is not defined, creating new entry for [%s] (%d)',self::LOGKEY,$msg->fboss,$ao->id)); } +*/ - if ($msg->errors->messages()->has('user_from') || $msg->errors->messages()->has('user_to')) { + // If the from/to user is missing + if ($msg->errors->messages()->has('from') || $msg->errors->messages()->has('to')) { Log::error(sprintf('%s:! Skipping message [%s] due to errors (%s)...',self::LOGKEY,$msg->msgid,join(',',$msg->errors->messages()->keys()))); $this->errors->push($msg); + return; } } @@ -481,8 +500,10 @@ class Packet extends FTNBase implements \Iterator, \Countable $this->messages->push($msg); } + /** @deprecated Is this used? */ public function pluck(string $key): Collection { + throw new \Exception(sprintf('%s:! This function is deprecated - [%s]',self::LOGKEY,$key)); return $this->messages->pluck($key); } } \ No newline at end of file diff --git a/app/Classes/FTN/Packet/FSC48.php b/app/Classes/FTN/Packet/FSC48.php index f986184..84574ed 100644 --- a/app/Classes/FTN/Packet/FSC48.php +++ b/app/Classes/FTN/Packet/FSC48.php @@ -59,37 +59,40 @@ final class FSC48 extends Packet /** * Create our message packet header + * // @todo add the ability to overwrite the password */ protected function header(): string { + $oldest = $this->messages->sortBy(function($item) { return $item->datetime; })->last(); + try { return pack(collect(self::HEADER)->pluck(1)->join(''), - $this->ff, // Orig Node - $this->tf, // Dest Node - Arr::get($this->header,'y'), // Year - Arr::get($this->header,'m'), // Month - Arr::get($this->header,'d'), // Day - Arr::get($this->header,'H'), // Hour - Arr::get($this->header,'M'), // Minute - Arr::get($this->header,'S'), // Second - 0, // Baud - 2, // Packet Version (should be 2) - $this->fp ? 0xffff : $this->fn, // Orig Net (0xFFFF when OrigPoint != 0) - $this->tn, // Dest Net - (Setup::PRODUCT_ID & 0xff), // Product Code Lo - Setup::PRODUCT_VERSION_MAJ, // Product Version Major - $this->password, // Packet Password - $this->fz, // Orig Zone - $this->tz, // Dest Zone - $this->fp ? $this->fn : 0x00, // Aux Net - Arr::get($this->header,'capvalid',1<<0), // fsc-0039.004 (copy of 0x2c) - ((Setup::PRODUCT_ID >> 8) & 0xff), // Product Code Hi - Setup::PRODUCT_VERSION_MIN, // Product Version Minor - Arr::get($this->header,'capword',1<<0), // Capability Word - $this->fz, // Orig Zone - $this->tz, // Dest Zone - $this->fp, // Orig Point - $this->tp, // Dest Point + $this->fftn_p->node_id, // Orig Node + $this->tftn_p->node_id, // Dest Node + $oldest->datetime->format('Y'), // Year + $oldest->datetime->format('m')-1, // Month + $oldest->datetime->format('d'), // Day + $oldest->datetime->format('H'), // Hour + $oldest->datetime->format('i'), // Minute + $oldest->datetime->format('s'), // Second + 0, // Baud + 2, // Packet Version (should be 2) + $this->fftn_p->point_id ? 0xffff : $this->fftn_p->node_id, // Orig Net (0xFFFF when OrigPoint != 0) + $this->tftn_p->host_id, // Dest Net + (Setup::PRODUCT_ID & 0xff), // Product Code Lo + Setup::PRODUCT_VERSION_MAJ, // Product Version Major + $this->tftn_p->session('pktpass'), // Packet Password + $this->fftn_p->zone->zone_id, // Orig Zone + $this->tftn_p->zone->zone_id, // Dest Zone + $this->fftn_p->point_id ? $this->fftn_p->node_id : 0x00, // Aux Net + 1<<0, // fsc-0039.004 (copy of 0x2c) + ((Setup::PRODUCT_ID >> 8) & 0xff), // Product Code Hi + Setup::PRODUCT_VERSION_MIN, // Product Version Minor + 1<<0, // Capability Word + $this->fftn_p->zone->zone_id, // Orig Zone + $this->tftn_p->zone->zone_id, // Dest Zone + $this->fftn_p->point_id, // Orig Point + $this->tftn_p->point_id, // Dest Point strtoupper(hexstr(Setup::PRODUCT_ID)), // ProdData ); diff --git a/app/Classes/FTN/Process.php b/app/Classes/FTN/Process.php index 43a71cf..0542054 100644 --- a/app/Classes/FTN/Process.php +++ b/app/Classes/FTN/Process.php @@ -2,7 +2,7 @@ namespace App\Classes\FTN; -use App\Models\Echoarea; +use App\Models\{Echoarea,Echomail,Netmail}; /** * Abstract class to hold the common functions for automatic responding to echomail/netmail messages @@ -19,8 +19,8 @@ abstract class Process /** * Return TRUE if the process class handled the message. * - * @param Message $msg + * @param Echomail|Netmail $mo * @return bool */ - abstract public static function handle(Message $msg): bool; + abstract public static function handle(Echomail|Netmail $mo): bool; } \ No newline at end of file diff --git a/app/Classes/FTN/Process/Echomail/Test.php b/app/Classes/FTN/Process/Echomail/Test.php index d47d28f..8208f7d 100644 --- a/app/Classes/FTN/Process/Echomail/Test.php +++ b/app/Classes/FTN/Process/Echomail/Test.php @@ -5,7 +5,8 @@ namespace App\Classes\FTN\Process\Echomail; use Illuminate\Support\Facades\Notification; use Illuminate\Support\Facades\Log; -use App\Classes\FTN\{Message,Process}; +use App\Classes\FTN\Process; +use App\Models\{Echomail,Netmail}; use App\Notifications\Echomails\Test as TestNotification; /** @@ -19,16 +20,16 @@ final class Test extends Process private const testing = ['test','testing']; - public static function handle(Message $msg): bool + public static function handle(Echomail|Netmail $mo): bool { - if (! self::canProcess($msg->echoarea) - || (strtolower($msg->user_to) !== 'all') - || (! in_array(strtolower($msg->subject),self::testing))) + if (! self::canProcess($mo->echoarea) + || (strtolower($mo->to) !== 'all') + || (! in_array(strtolower($mo->subject),self::testing))) return FALSE; Log::info(sprintf('%s:- Processing TEST message from (%s) [%s] in [%s]',self::LOGKEY,$msg->user_from,$msg->fftn,$msg->echoarea)); - Notification::route('echomail',$msg->echoarea)->notify(new TestNotification($msg)); + Notification::route('echomail',$mo->echoarea)->notify(new TestNotification($msg)); return TRUE; } diff --git a/app/Classes/Protocol.php b/app/Classes/Protocol.php index 506a184..27aa251 100644 --- a/app/Classes/Protocol.php +++ b/app/Classes/Protocol.php @@ -11,6 +11,8 @@ use App\Classes\Sock\SocketClient; use App\Classes\Sock\SocketException; use App\Models\{Address,Mailer,Setup,System,SystemLog}; +// @todo after receiving a mail packet/file, dont acknowledge it until we can validate that we can read it properly. + abstract class Protocol { // Enable extra debugging diff --git a/app/Console/Commands/Debug/PacketAddress.php b/app/Console/Commands/Debug/PacketAddress.php index 80a933b..27ce0bf 100644 --- a/app/Console/Commands/Debug/PacketAddress.php +++ b/app/Console/Commands/Debug/PacketAddress.php @@ -50,8 +50,8 @@ class PacketAddress extends Command exit(1); } - $o = $o->where('id',$this->argument('dbid'))->get(); + echo hex_dump($ao->system->packet($ao)->generate($o->where('id',$this->argument('dbid'))->get())); - dd(hex_dump($ao->getPacket($o))); + return Command::SUCCESS; } } \ No newline at end of file diff --git a/app/Console/Commands/PacketInfo.php b/app/Console/Commands/PacketInfo.php index af657f1..2e03ed4 100644 --- a/app/Console/Commands/PacketInfo.php +++ b/app/Console/Commands/PacketInfo.php @@ -7,7 +7,7 @@ use Illuminate\Support\Facades\Storage; use App\Classes\File; use App\Classes\FTN\Packet; -use App\Models\Address; +use App\Models\{Address,Echomail}; class PacketInfo extends Command { @@ -31,7 +31,7 @@ class PacketInfo extends Command * Execute the console command. * * @return mixed - * @throws \App\Classes\FTN\InvalidPacketException + * @throws \App\Exceptions\InvalidPacketException */ public function handle() { @@ -55,27 +55,38 @@ class PacketInfo extends Command $this->alert(sprintf('File Name: %s',$x)); $this->info(sprintf('Packet Type : %s (%s)',$pkt->type,get_class($pkt))); - $this->info(sprintf('From : %s to %s',$pkt->fftn,$pkt->tftn)); - $this->info(sprintf('Dated : %s (%s)',$pkt->date,$pkt->date->timestamp)); - $this->info(sprintf('Password : %s (%s)',$pkt->password,$pkt->password ? 'SET' : 'NOT set')); - $this->info(sprintf('Messages : %d',$pkt->messages->count())); + $this->info(sprintf('From : %s to %s',$pkt->fftn->ftn,$pkt->tftn->ftn)); + $this->info(sprintf('Dated : %s (%s) [%s]',$pkt->date,$pkt->date->timestamp,$pkt->date->tz->toOffsetName())); + $this->info(sprintf('Password : %s (%s)',$pkt->password ?: '-',$pkt->password ? 'SET' : 'NOT set')); + $this->info(sprintf('Messages : %d',$pkt->count())); $this->info(sprintf('Tosser : %d (%s) version %s',$pkt->software->code,$pkt->software->name,$pkt->software_ver)); $this->info(sprintf('Capabilities: %x',$pkt->capability)); $this->info(sprintf('Has Errors : %s',$pkt->errors->count() ? 'YES' : 'No')); $this->info(sprintf('Messages : %d',$pkt->count())); foreach ($pkt as $msg) { - try { - $this->warn(sprintf('- Date : %s',$msg->date)); - $this->warn(sprintf(' - Flags : %s',$msg->flags()->filter()->keys()->join(', '))); - $this->warn(sprintf(' - From : %s (%s)',$msg->user_from,$msg->fftn)); - $this->warn(sprintf(' - To : %s (%s)',$msg->user_to,$msg->tftn)); - $this->warn(sprintf(' - Subject: %s',$msg->subject)); - $this->warn(sprintf(' - Area : %s',$msg->echoarea)); + echo "\n"; - if ($msg->errors) + try { + $this->warn(sprintf('- Date : %s (%s)',$msg->datetime,$msg->datetime->tz->toOffsetName())); + $this->warn(sprintf(' - Errors : %s',$msg->errors?->errors()->count() ? 'YES' : 'No')); + $this->warn(sprintf(' - Flags : %s',$msg->flags()->keys()->join(', '))); + $this->warn(sprintf(' - Cost : %d',$msg->cost)); + $this->warn(sprintf(' - From : %s (%s)',$msg->from,$msg->fftn->ftn)); + if ($msg instanceof Echomail) + $this->warn(sprintf(' - To : %s',$msg->to)); + else + $this->warn(sprintf(' - To : %s (%s)',$msg->to,$msg->tftn->ftn)); + $this->warn(sprintf(' - Subject: %s',$msg->subject)); + if ($msg instanceof Echomail) + $this->warn(sprintf(' - Area : %s',$msg->echoarea->name)); + + if ($msg->errors) { + echo "\n"; + $this->error("Errors:"); foreach ($msg->errors->errors()->all() as $error) - $this->line(' - '.$error); + $this->error(' - '.$error); + } } catch (\Exception $e) { $this->error('! ERROR: '.$e->getMessage()); diff --git a/app/Console/Commands/PacketProcess.php b/app/Console/Commands/PacketProcess.php index 22a8c16..5dee3db 100644 --- a/app/Console/Commands/PacketProcess.php +++ b/app/Console/Commands/PacketProcess.php @@ -2,13 +2,12 @@ namespace App\Console\Commands; -use Carbon\Carbon; use Illuminate\Console\Command; use Illuminate\Support\Facades\Storage; use App\Classes\File; use App\Classes\FTN\Packet; -use App\Jobs\MessageProcess as Job; +use App\Jobs\PacketProcess as Job; use App\Models\Address; class PacketProcess extends Command @@ -35,7 +34,7 @@ class PacketProcess extends Command * Execute the console command. * * @return void - * @throws \App\Classes\FTN\InvalidPacketException + * @throws \App\Exceptions\InvalidPacketException */ public function handle() { @@ -56,17 +55,8 @@ class PacketProcess extends Command exit(1); } - foreach ($f as $packet) { - foreach ($pkt = Packet::process($packet,$f->itemName(),$f->itemSize(),$a?->zone->domain) as $msg) { - // @todo Quick check that the packet should be processed by us. - $this->info(sprintf('Processing message from [%s] with msgid [%s] in (%s)',$msg->fboss,$msg->msgid,$f->pktName())); + $x = Job::dispatchSync($rel_name,$a->zone->domain,$this->option('dontqueue')); - // Dispatch job. - if ($this->option('dontqueue')) - Job::dispatchSync($msg,$f->pktName(),$a,$pkt->fftn_o,Carbon::now(),$this->option('nobot')); - else - Job::dispatch($msg,$f->pktName(),$a,$pkt->fftn_o,Carbon::now(),$this->option('nobot')); - } - } + dd(['job completed'=>$x]); } } \ No newline at end of file diff --git a/app/Console/Commands/PacketSystem.php b/app/Console/Commands/PacketSystem.php index 1878164..272379b 100644 --- a/app/Console/Commands/PacketSystem.php +++ b/app/Console/Commands/PacketSystem.php @@ -27,7 +27,7 @@ class PacketSystem extends Command * Execute the console command. * * @return mixed - * @throws \App\Classes\FTN\InvalidPacketException + * @throws \App\Exceptions\InvalidPacketException */ public function handle() { diff --git a/app/Classes/FTN/InvalidPacketException.php b/app/Exceptions/InvalidPacketException.php similarity index 75% rename from app/Classes/FTN/InvalidPacketException.php rename to app/Exceptions/InvalidPacketException.php index a27f600..4b3b4b1 100644 --- a/app/Classes/FTN/InvalidPacketException.php +++ b/app/Exceptions/InvalidPacketException.php @@ -1,6 +1,6 @@ msg = $msg; - $this->packet = $packet; - $this->sender = $sender; - $this->pktsrc = $pktsrc; - $this->recvtime = $recvtime; + // @todo We need to serialize this model here, because laravel has an error unserializing it (Model Not Found) + $this->mo = serialize($mo); $this->skipbot = $skipbot; } @@ -44,7 +43,7 @@ class MessageProcess implements ShouldQueue { switch ($key) { case 'subject': - return sprintf('%s-%s-%s',$this->packet,$this->sender->ftn,$this->msg->msgid); + return sprintf('%s-%s-%s',$this->pktname,$this->mo->set->get('set_sender')->ftn,$this->mo->msgid); default: return NULL; @@ -52,21 +51,25 @@ class MessageProcess implements ShouldQueue } /** - * When calling MessageProcess - we assume that the packet is from a valid source, and - * the destination (netmail/echomail) is also valid + * At this point, we know that the packet is from a system we know about, and the packet is to us: + * + From a system that is configured with us, and the password has been validated + * + From a system that is not configured with us, and it may have netmails for us */ public function handle() { + $this->mo = unserialize($this->mo); + // Load our details $ftns = our_address(); // If we are a netmail - if ($this->msg->isNetmail()) { + if ($this->mo instanceof Netmail) { + // @todo generate exception when netmail to system that doesnt exist (node/point) and its this host's responsibility Log::info(sprintf('%s:- Processing Netmail [%s] to (%s) [%s] from (%s) [%s].', self::LOGKEY, - $this->msg->msgid, - $this->msg->user_to,$this->msg->tftn, - $this->msg->user_from,$this->msg->fftn, + $this->mo->msgid, + $this->mo->to,$this->mo->tftn->ftn, + $this->mo->from,$this->mo->fftn->ftn, )); // @todo Enable checks to reject old messages @@ -74,89 +77,51 @@ class MessageProcess implements ShouldQueue // Check for duplicate messages // FTS-0009.001 - if ($this->msg->msgid) { - $o = Netmail::where('msgid',$this->msg->msgid) - ->where('fftn_id',($x=$this->msg->fboss_o) ? $x->id : NULL) + if ($this->mo->msgid) { + Log::debug(sprintf('%s:- Checking for duplicate from host [%s].',self::LOGKEY,$this->mo->fftn->ftn)); + + $o = Netmail::where('msgid',$this->mo->msgid) + ->where('fftn_id',$this->mo->fftn->id) ->where('datetime','>',Carbon::now()->subYears(3)) ->single(); - Log::debug(sprintf('%s:- Checking for duplicate from host id [%d].',self::LOGKEY,($x=$this->msg->fboss_o) ? $x->id : NULL)); - if ($o) { - Log::alert(sprintf('%s:! Duplicate netmail [%s] in [%s] from (%s) [%s] to (%s) - ignorning.', + Log::alert(sprintf('%s:! Duplicate netmail #%d [%s] from (%s) [%s] to (%s) - ignoring.', self::LOGKEY, - $this->msg->msgid, - $this->msg->echoarea, - $this->msg->user_from,$this->msg->fftn, - $this->msg->user_to, + $o->id, + $this->mo->msgid, + $this->mo->from,$this->mo->fftn->ftn, + $this->mo->to, )); - if (! $o->msg_crc) - $o->msg_crc = md5($this->msg->message); - - $o->save(); - return; } } // @todo Enable checks to see if this is a file request or file send - $o = new Netmail; - $o->to = $this->msg->user_to; - $o->from = $this->msg->user_from; - - $o->fftn_id = ($x=$this->msg->fftn_o) ? $x->id : NULL; - $o->tftn_id = ($x=$this->msg->tftn_o) ? $x->id : NULL; - - $o->datetime = $this->msg->date; - $o->tzoffset = $this->msg->date->utcOffset(); - - $o->flags = $this->msg->flags; - $o->cost = $this->msg->cost; - $o->msgid = $this->msg->msgid; - - $o->tagline = $this->msg->tagline; - $o->tearline = $this->msg->tearline; - $o->origin = $this->msg->origin; - - $o->subject = $this->msg->subject; - $o->msg = $this->msg->message; - - foreach ($this->msg->via as $v) - $o->msg .= sprintf("\01Via %s\r",$v); - - $o->msg_src = $this->msg->message_src; - $o->msg_crc = md5($this->msg->message); - - $o->set_pkt = $this->packet; - $o->set_sender = $this->sender; - $o->set_path = $this->msg->via; - $o->set_recvtime = $this->recvtime; // Strip any local/transit flags - $o->flags &= ~(Message::FLAG_LOCAL|Message::FLAG_INTRANSIT); + $this->mo->flags &= ~(Message::FLAG_LOCAL|Message::FLAG_INTRANSIT); // Determine if the message is to this system, or in transit - if ($ftns->search(function($item) { return $this->msg->tftn === $item->ftn; }) !== FALSE) { - // @todo Check if it is a duplicate message - // @todo Check if the message is from a system we know about - + if ($ftns->contains($this->mo->tftn)) { $processed = FALSE; // If the message is to a bot, we'll process it if (! $this->skipbot) foreach (config('process.robots') as $class) { - if ($processed=$class::handle($this->msg)) { - $o->flags |= Message::FLAG_RECD; - $o->save(); + if ($processed=$class::handle($this->mo)) { + $this->mo->flags |= Message::FLAG_RECD; + $this->mo->save(); Log::info(sprintf('%s:= Netmail [%s] from (%s:%s) - was processed by us internally [%d]', self::LOGKEY, - $this->msg->msgid, - $this->msg->user_from, - $this->msg->fftn, - $o->id, + $this->mo->msgid, + $this->mo->from, + $this->mo->fftn->ftn, + $this->mo->id, )); + break; } } @@ -165,38 +130,40 @@ class MessageProcess implements ShouldQueue // Check if the netmail is to a user, with netmail forwarding enabled $uo = User::active() ->where(function($query) { - return $query->whereRaw(sprintf("LOWER(name)='%s'",strtolower($this->msg->user_to))) - ->orWhereRaw(sprintf("LOWER(alias)='%s'",strtolower($this->msg->user_to))); + return $query->whereRaw(sprintf("LOWER(name)='%s'",strtolower($this->mo->to))) + ->orWhereRaw(sprintf("LOWER(alias)='%s'",strtolower($this->mo->to))); }) ->whereNotNull('system_id') ->single(); - if ($uo && ($ao=$uo->system->match($this->msg->tftn_o->zone)?->pop())) { + if ($uo && ($ao=$uo->system->match($this->mo->tftn->zone)?->pop())) { $note = "+--[ FORWARDED MESSAGE ]----------------------------------+\r"; $note .= "+ This message has been forwarded to you, it was originally sent to you\r"; - $note .= sprintf("+ at [%s]\r",$this->msg->tftn_o->ftn); + $note .= sprintf("+ at [%s]\r",$this->mo->tftn->ftn); $note .= "+---------------------------------------------------------+\r\r"; - $o->msg = $note.$this->msg->message; - $o->tftn_id = $ao->id; - $o->flags |= Message::FLAG_INTRANSIT; - $o->save(); + + $this->mo->msg = $note.$this->mo->content; + $this->mo->tftn_id = $ao->id; + $this->mo->flags |= Message::FLAG_INTRANSIT; + $this->mo->save(); + $processed = TRUE; // Dont send an advisement to an areabot - if (! in_array(strtolower($this->msg->user_from),config('fido.areabots'))) - Notification::route('netmail',$this->msg->fftn_o)->notify(new NetmailForward($this->msg,$ao)); + if (! in_array(strtolower($this->mo->from),config('fido.areabots'))) + Notification::route('netmail',$this->mo->fftn)->notify(new NetmailForward($this->mo,$ao)); // We'll ignore messages from *fix users - } elseif (in_array(strtolower($this->msg->user_from),config('fido.areabots'))) { - $o->flags |= Message::FLAG_RECD; - $o->save(); + } elseif (in_array(strtolower($this->mo->from),config('fido.areabots'))) { + $this->mo->flags |= Message::FLAG_RECD; + $this->mo->save(); Log::alert(sprintf('%s:! Ignoring Netmail [%s] to the Hub from (%s:%s) - its from a bot [%d]', self::LOGKEY, - $this->msg->msgid, - $this->msg->user_from, - $this->msg->fftn, - $o->id, + $this->mo->msgid, + $this->mo->from, + $this->mo->fftn->ftn, + $this->mo->id, )); $processed = TRUE; @@ -205,99 +172,87 @@ class MessageProcess implements ShouldQueue // If not processed, no users here! if (! $processed) { - Log::alert(sprintf('%s:! Netmail to the Hub from (%s) [%s] but no users here.',self::LOGKEY,$this->msg->user_from,$this->msg->fftn)); + Log::alert(sprintf('%s:! Netmail to the Hub from (%s) [%s] but no users here.',self::LOGKEY,$this->mo->from,$this->mo->fftn->ftn)); - Notification::route('netmail',$this->msg->fftn_o)->notify(new Reject($this->msg)); + Notification::route('netmail',$this->mo->fftn)->notify(new Reject($this->mo)); } // If in transit, store for collection } else { - // @todo Check if the message is to a system we know about // @todo In transit loop checking // @todo In transit TRACE response - $o->flags |= Message::FLAG_INTRANSIT; - $o->save(); + $this->mo->flags |= Message::FLAG_INTRANSIT; + $this->mo->save(); Log::info(sprintf('%s:= Netmail [%s] in transit to (%s:%s) from (%s:%s) [%d].', self::LOGKEY, - $this->msg->msgid, - $this->msg->user_to,$this->msg->tftn, - $this->msg->user_from,$this->msg->fftn, - $o->id, + $this->mo->msgid, + $this->mo->to,$this->mo->tftn->ftn, + $this->mo->from,$this->mo->fftn->ftn, + $this->mo->id, )); } // Else we are echomail } else { - Log::debug(sprintf('%s:- Looking for echomail area [%s] for mail from [%s]',self::LOGKEY,$this->msg->echoarea,$this->msg->fboss)); + // The packet sender + $sender = $this->mo->set->get('set_sender'); - if (! $this->msg->fboss_o) { - Log::error(sprintf('%s:! Cannot process message for echomail area [%s] for mail from [%s] with msgid [%s] - no boss object?',self::LOGKEY,$this->msg->echoarea,$this->msg->fboss,$this->msg->msgid)); + // @todo Check that this does evaulate to true if a message has been rescanned + $rescanned = $this->mo->kludges->get('RESCANNED',FALSE); + + // Echoarea doesnt exist, cant import the message + if (! $this->mo->echoarea) { + Log::alert(sprintf('%s:! Echoarea [%s] doesnt exist for zone [%d@%s]',self::LOGKEY,$this->mo->set->get('set_echoarea'),$sender->zone->zone_id,$sender->zone->domain->name)); + + Notification::route('netmail',$sender)->notify(new EchoareaNotExist($this->mo)); return; } - $ea = Echoarea::where('name',strtoupper($this->msg->echoarea)) - ->where('domain_id',$this->msg->fboss_o->zone->domain_id) - ->single(); + Log::debug(sprintf('%s:- Processing echomail [%s] in [%s] from [%s].',self::LOGKEY,$this->mo->msgid,$this->mo->echoarea->name,$sender->ftn)); - if (! $ea) { - Log::alert(sprintf('%s:! Echoarea [%s] doesnt exist for zone [%d-%d]',self::LOGKEY,$this->msg->echoarea,$this->msg->fboss_o->zone->domain_id,$this->msg->fboss_o->zone->zone_id)); - - Notification::route('netmail',$this->pktsrc)->notify(new EchoareaNotExist($this->msg)); - return; - } - - Log::debug(sprintf('%s:- Processing echomail [%s] in [%s].',self::LOGKEY,$this->msg->msgid,$this->msg->echoarea)); - - if (! $this->pktsrc->zone->domain->zones->pluck('zone_id')->contains($this->msg->fboss_o->zone->zone_id)) { + // Message from zone is incorrect for echoarea + if (! $this->mo->echoarea->domain->zones->contains($this->mo->fftn->zone)) { Log::alert(sprintf('%s:! The message [%s] is from a different zone [%d] than the packet sender [%d] - not importing', self::LOGKEY, - $this->msg->msgid, - $this->msg->fboss_o->zone->zone_id, - $this->pktsrc->zone->zone_id)); + $this->mo->msgid, + $this->mo->fftn->zone->zone_id, + $this->mo->fftn->zone->zone_id)); return; } // Check for duplicate messages // FTS-0009.001 - if ($this->msg->msgid) { - $o = Echomail::where('msgid',$this->msg->msgid) - ->where('fftn_id',($x=$this->msg->fboss_o) ? $x->id : NULL) - ->where('datetime','>=',$this->msg->date->subYears(3)) - ->where('datetime','<=',$this->msg->date) + if ($this->mo->msgid) { + $o = Echomail::where('msgid',$this->mo->msgid) + ->where('fftn_id',$this->mo->fftn->id) + ->where('datetime','>=',$this->mo->date->subYears(3)) + ->where('datetime','<=',$this->mo->date) ->single(); - Log::debug(sprintf('%s:- Checking for duplicate from host id [%d].',self::LOGKEY,($x=$this->msg->fboss_o) ? $x->id : NULL)); + Log::debug(sprintf('%s:- Checking for duplicate from host id [%d].',self::LOGKEY,$this->mo->fftn->id)); if ($o) { + // @todo Actually update seenby Log::alert(sprintf('%s:! Duplicate echomail [%s] in [%s] from (%s) [%s] to (%s) - updating seenby.', self::LOGKEY, - $this->msg->msgid, - $this->msg->echoarea, - $this->msg->user_from,$this->msg->fftn, - $this->msg->user_to, + $this->mo->msgid, + $this->mo->echoarea->name, + $this->mo->from,$this->mo->fftn->ftn, + $this->mo->to, )); - if (! $o->msg_crc) - $o->msg_crc = md5($this->msg->message); - - $o->save(); + //$o->save(); // @todo This duplicate message may have gone via a different path, be nice to record it. + + /* // If we didnt get the path on the original message, we'll override it if (! $o->path->count()) { $dummy = collect(); - $path = $this->parseAddresses('path',$this->msg->path,$this->pktsrc->zone,$dummy); - - /* - // If our sender is not in the path, add it - if (! $path->contains($this->sender->id)) { - Log::alert(sprintf('%s:? Echomail adding sender to PATH [%s] for [%d].',self::LOGKEY,$x->ftn,$o->id)); - $path->push($this->sender->id); - } - */ + $path = $this->parseAddresses('path',$this->mo->path,$sender->zone,$dummy); $ppoid = NULL; foreach ($path as $aoid) { @@ -310,23 +265,24 @@ class MessageProcess implements ShouldQueue $ppoid = $po[0]->id; } } + */ // @todo if we have an export for any of the seenby addresses, remove it - $seenby = $this->parseAddresses('seenby',$this->msg->seenby,$this->pktsrc->zone,$o->rogue_seenby); - $x = $o->seenby()->syncWithoutDetaching($seenby); + + $seenby = $this->parseAddresses('seenby',$this->mo->seenby,$sender->zone,$o->rogue_seenby); + $this->mo->seenby()->syncWithoutDetaching($seenby); // In case our rogue_seenby changed - if ($o->getDirty()) - $o->save(); + $this->mo->save(); return; } } // Find another message with the same msg_crc - if ($this->msg->message) { - $o = Echomail::where('msg_crc',$xx=md5($this->msg->message)) - ->where('fftn_id',($x=$this->msg->fboss_o) ? $x->id : NULL) + if ($this->mo->msg_crc) { + $o = Echomail::where('msg_crc',$xx=md5($this->mo->msg_crc)) + ->where('fftn_id',$this->mo->fftn->id) ->where('datetime','>',Carbon::now()->subWeek()) ->get(); @@ -339,73 +295,38 @@ class MessageProcess implements ShouldQueue } // If the node is not subscribed - if ($this->pktsrc->echoareas->search(function($item) use ($ea) { return $item->id === $ea->id; }) === FALSE) { - Log::alert(sprintf('%s:! FTN [%s] is not subscribed to [%s] for [%s].',self::LOGKEY,$this->pktsrc->ftn,$ea->name,$this->msg->msgid)); + if ($sender->echoareas->search(function($item) { return $item->id === $this->mo->echoarea->id; }) === FALSE) { + Log::alert(sprintf('%s:! FTN [%s] is not subscribed to [%s] for [%s].',self::LOGKEY,$sender->ftn,$this->mo->echoarea->name,$this->mo->msgid)); - if (! $this->msg->rescanned->count()) - Notification::route('netmail',$this->pktsrc)->notify(new EchoareaNotSubscribed($this->msg)); + if (! $rescanned) + Notification::route('netmail',$sender)->notify(new EchoareaNotSubscribed($this->mo)); } // Can the system send messages to this area? - if (! $ea->can_write($this->pktsrc->security)) { - Log::alert(sprintf('%s:! FTN [%s] is not allowed to post [%s] to [%s].',self::LOGKEY,$this->pktsrc->ftn,$this->msg->msgid,$ea->name)); - if (! $this->msg->rescanned->count()) - Notification::route('netmail',$this->pktsrc)->notify(new EchoareaNoWrite($this->msg)); + if (! $this->mo->echoarea->can_write($sender->security)) { + Log::alert(sprintf('%s:! FTN [%s] is not allowed to post [%s] to [%s].',self::LOGKEY,$sender->ftn,$this->mo->msgid,$this->mo->echoarea->name)); + if (! $rescanned) + Notification::route('netmail',$sender)->notify(new EchoareaNoWrite($this->mo)); return; } // We know about this area, store it - $o = new Echomail; - $o->init(); - - $o->to = $this->msg->user_to; - $o->from = $this->msg->user_from; - $o->subject = $this->msg->subject; - $o->datetime = $this->msg->date; - $o->tzoffset = $this->msg->date->utcOffset(); - - if ($x=$this->msg->fboss_o) { - $o->fftn_id = $x->id; - - } else { - $o->fftn_id = NULL; // @todo This should be the node that originated the message - but since that node is not in the DB it would be null - } - - $o->echoarea_id = $ea->id; - $o->msgid = $this->msg->msgid; - $o->replyid = $this->msg->replyid; - - $o->tagline = $this->msg->tagline; - $o->tearline = $this->msg->tearline; - $o->origin = $this->msg->origin; - - $o->msg = $this->msg->message; - $o->msg_src = $this->msg->message_src; - $o->msg_crc = md5($this->msg->message); - - $o->set_path = $this->msg->path; - $o->set_seenby = $this->msg->seenby; - $o->set_recvtime = $this->recvtime; - $o->set_sender = $this->pktsrc->id; - // Record receiving packet and sender - $o->set_pkt = $this->packet; - - $o->save(); + $this->mo->save(); Log::info(sprintf('%s:= Echomail [%s] in [%s] from (%s) [%s] to (%s) - [%s].', self::LOGKEY, - $this->msg->msgid, - $this->msg->echoarea, - $this->msg->user_from,$this->msg->fftn, - $this->msg->user_to, - $o->id, + $this->mo->msgid, + $this->mo->echoarea->name, + $this->mo->from,$this->mo->fftn->ftn, + $this->mo->to, + $this->mo->id, )); // If the message is to a bot, but not rescanned, or purposely skipbot set, we'll process it - if ((! $this->skipbot) && (! $this->msg->rescanned->count())) + if ((! $this->skipbot) && (! $rescanned)) foreach (config('process.echomail') as $class) { - if ($class::handle($this->msg)) { + if ($class::handle($this->mo)) { break; } } diff --git a/app/Jobs/PacketProcess.php b/app/Jobs/PacketProcess.php index 581e48f..92c84f3 100644 --- a/app/Jobs/PacketProcess.php +++ b/app/Jobs/PacketProcess.php @@ -15,9 +15,9 @@ use Illuminate\Support\Facades\Storage; use League\Flysystem\UnableToMoveFile; use App\Classes\File; -use App\Classes\File\Item; -use App\Classes\FTN\{InvalidPacketException,Packet}; -use App\Models\Address; +use App\Classes\FTN\Packet; +use App\Exceptions\InvalidPacketException; +use App\Models\{Domain,Echomail,Netmail}; use App\Notifications\Netmails\PacketPasswordInvalid; class PacketProcess implements ShouldQueue @@ -26,22 +26,26 @@ class PacketProcess implements ShouldQueue use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - private Item $file; - private Address $ao; + private string $filename; + private Domain $do; private Carbon $rcvd_time; + private bool $interactive; + private bool $nobot; - public function __construct(Item $file,Address $ao,Carbon $rcvd_time) + public function __construct(string $filename,Domain $do,bool $interactive=FALSE,Carbon $rcvd_time=NULL,bool $nobot=FALSE) { - $this->file = $file; - $this->ao = $ao; - $this->rcvd_time = $rcvd_time; + $this->filename = $filename; + $this->do = $do; + $this->interactive = $interactive; + $this->rcvd_time = $rcvd_time ?: Carbon::now(); + $this->nobot = $nobot; } public function __get($key): mixed { switch ($key) { case 'subject': - return $this->file->name; + return $this->filename; default: return NULL; @@ -54,30 +58,47 @@ class PacketProcess implements ShouldQueue */ public function handle() { - Log::info(sprintf('%s:- Processing mail %s [%s]',self::LOGKEY,$this->file->whatType() === Item::IS_PKT ? 'PACKET' : 'ARCHIVE',$this->file->nameas)); + Log::info(sprintf('%s:- Processing mail [%s]',self::LOGKEY,$this->filename)); $fs = Storage::disk(config('fido.local_disk')); // @todo Catch files that we cannot process, eg: ARJ bundles. try { - $f = new File($this->file->full_name); + $f = new File($fs->path($this->filename)); + $processed = FALSE; foreach ($f as $packet) { - $pkt = Packet::process($packet,Arr::get(stream_get_meta_data($packet),'uri'),$f->itemSize(),$this->ao->zone->domain); + $pkt = Packet::process($packet,Arr::get(stream_get_meta_data($packet),'uri'),$f->itemSize(),$this->do); - // Check the messages are from the uplink - if ($this->ao->system->addresses->search(function($item) use ($pkt) { return $item->id === $pkt->fftn_o->id; }) === FALSE) { - Log::error(sprintf('%s:! Packet [%s] is not from this link? [%d]',self::LOGKEY,$pkt->fftn_o->ftn,$this->ao->system_id)); + // Check that the packet is from a system that is defined in the DB + if (! $pkt->fftn) { + Log::error(sprintf('%s:! Packet [%s] is not from a system we know about? [%s]',self::LOGKEY,$this->filename,$pkt->fftn_t)); break; } - // Check the packet password - if (strtoupper($this->ao->session('pktpass')) !== strtoupper($pkt->password)) { - Log::error(sprintf('%s:! Packet from [%s] with password [%s] is invalid.',self::LOGKEY,$this->ao->ftn,$pkt->password)); + if (! our_nodes($this->do)->contains($pkt->fftn)) { + Log::error(sprintf('%s:! Packet [%s] is from a system that is not configured with us? [%s] for [%s]',self::LOGKEY,$this->filename,$pkt->fftn_t,$this->do->name)); - Notification::route('netmail',$this->ao)->notify(new PacketPasswordInvalid($pkt->password,$this->file->nameas)); + // @todo Notification::route('netmail',$pkt->fftn)->notify(new UnexpectedPacketFromYou($this->filename)); + // @todo Parse the packet for netmails and process them. We'll only accept netmails to us, and ignore all others + break; + } + + // Check the packet is to our address, if not we'll reject it. + if (! our_address($this->do)->contains($pkt->tftn)) { + Log::error(sprintf('%s:! Packet [%s] is not to our address? [%s]',self::LOGKEY,$this->filename,$pkt->tftn)); + + // @todo Notification::route('netmail',$pkt->fftn)->notify(new UnexpectedPacketToUs($this->filename)); + break; + } + + // Check the packet password + if (strtoupper($pkt->fftn->session('pktpass')) !== strtoupper($pkt->password)) { + Log::error(sprintf('%s:! Packet from [%s] with password [%s] is invalid.',self::LOGKEY,$pkt->fftn->ftn,$pkt->password)); + + Notification::route('netmail',$pkt->fftn)->notify(new PacketPasswordInvalid($pkt->password,$this->filename)); break; } @@ -89,37 +110,34 @@ class PacketProcess implements ShouldQueue $count = 0; foreach ($pkt as $msg) { - Log::info(sprintf('%s:- Mail from [%s] to [%s]',self::LOGKEY,$msg->fftn,$msg->tftn)); + if ($msg instanceof Netmail) + Log::info(sprintf('%s:- Netmail from [%s] to [%s]',self::LOGKEY,$msg->fftn->ftn,$msg->tftn->ftn)); + elseif ($msg instanceof Echomail) + Log::info(sprintf('%s:- Echomail from [%s]',self::LOGKEY,$msg->fftn->ftn)); - // @todo Quick check that the packet should be processed by us. - // @todo validate that the packet's zone is in the domain. + if ($msg->errors) { + Log::error(sprintf('%s:! Message [%s] has [%d] errors, unable to process',self::LOGKEY,$msg->msgid,$msg->errors->errors()->count())); - /* - * // @todo generate exception when echomail for an area that doesnt exist - * // @todo generate exception when echomail for an area sender cannot post to - * // @todo generate exception when echomail for an area sender not subscribed to - * // @todo generate exception when echomail comes from a system not defined here - * // @todo generate exception when echomail comes from a system doesnt exist - * - * // @todo generate exception when netmail to system that doesnt exist (node/point) - * // @todo generate exception when netmail from system that doesnt exist (node/point) - * // @todo generate warning when netmail comes from a system not defined here - * - * // @todo generate exception when packet has wrong password - */ + continue; + } + + $msg->set_sender = $pkt->fftn->withoutRelations(); + // Record receiving packet and sender + $msg->set_pkt = $f->pktName(); + $msg->set_recvtime = $this->rcvd_time; try { // Dispatch job. - if ($queue) - MessageProcess::dispatch($msg,$f->pktName(),$this->ao->withoutRelations(),$pkt->fftn_o->withoutRelations(),$this->rcvd_time); + if ($queue || (! $this->interactive)) + MessageProcess::dispatch($msg->withoutRelations(),$this->nobot); else - MessageProcess::dispatchSync($msg,$f->pktName(),$this->ao->withoutRelations(),$pkt->fftn_o->withoutRelations(),$this->rcvd_time); + MessageProcess::dispatchSync($msg->withoutRelations(),$this->nobot); + + $count++; } catch (\Exception $e) { Log::error(sprintf('%s:! Got error dispatching message [%s] (%d:%s-%s).',self::LOGKEY,$msg->msgid,$e->getLine(),$e->getFile(),$e->getMessage())); } - - $count++; } if ($count === $pkt->count()) @@ -127,42 +145,41 @@ class PacketProcess implements ShouldQueue } if (! $processed) { - Log::alert(sprintf('%s:- Not deleting packet [%s], it doesnt seem to be processed?',self::LOGKEY,$this->file->nameas)); + Log::alert(sprintf('%s:- Not deleting packet [%s], it doesnt seem to be processed?',self::LOGKEY,$this->filename)); } else { // If we want to keep the packet, we could do that logic here if (config('fido.packet_keep')) { $dir = sprintf('%s/%s/%s/%s',config('fido.dir'),($x=Carbon::now())->format('Y'),$x->format('m'),$x->format('d')); - Log::debug(sprintf('%s:- Moving processed packet [%s] to [%s]',self::LOGKEY,$this->file->rel_name,$dir)); + Log::debug(sprintf('%s:- Moving processed packet [%s] to [%s]',self::LOGKEY,$this->filename,$dir)); try { if ($fs->makeDirectory($dir)) { - $fs->move($this->file->rel_name,$x=sprintf('%s/%s',$dir,$this->file->pref_name)); - Log::info(sprintf('%s:- Moved processed packet [%s] to [%s]',self::LOGKEY,$this->file->rel_name,$x)); + $fs->move($this->filename,$x=sprintf('%s/%s',$dir,$f->itemName())); + Log::info(sprintf('%s:- Moved processed packet [%s] to [%s]',self::LOGKEY,$this->filename,$x)); } else Log::error(sprintf('%s:! Unable to create dir [%s]',self::LOGKEY,$dir)); } catch (UnableToMoveFile $e) { - Log::error(sprintf('%s:! Unable to move packet [%s] to [%s] (%s)',self::LOGKEY,$this->file->full_name,$dir,$e->getMessage())); + Log::error(sprintf('%s:! Unable to move packet [%s] to [%s] (%s)',self::LOGKEY,$this->filename,$dir,$e->getMessage())); } catch (\Exception $e) { - Log::error(sprintf('%s:! Failed moving packet [%s] to [%s] (%s)',self::LOGKEY,$this->file->full_name,$dir,$e->getMessage())); + Log::error(sprintf('%s:! Failed moving packet [%s] to [%s] (%s)',self::LOGKEY,$this->filename,$dir,$e->getMessage())); } } else { - Log::debug(sprintf('%s:- Deleting processed packet [%s]',self::LOGKEY,$this->file->full_name)); + Log::debug(sprintf('%s:- Deleting processed packet [%s]',self::LOGKEY,$this->filename)); - // @todo Change this to use Storage::disk() - unlink($this->file->full_name); + $fs->delete($this->filename); } } } catch (InvalidPacketException $e) { - Log::error(sprintf('%s:- Not deleting packet [%s], as it generated an InvalidPacketException',self::LOGKEY,$this->file->nameas),['e'=>$e->getMessage()]); + Log::error(sprintf('%s:- Not deleting packet [%s], as it generated an InvalidPacketException',self::LOGKEY,$this->filename),['e'=>$e->getMessage()]); } catch (\Exception $e) { - Log::error(sprintf('%s:- Not deleting packet [%s], as it generated an uncaught exception',self::LOGKEY,$this->file->nameas),['e'=>$e->getMessage()]); + Log::error(sprintf('%s:- Not deleting packet [%s], as it generated an uncaught exception',self::LOGKEY,$this->filename),['e'=>$e->getMessage(),'l'=>$e->getLine(),'f'=>$e->getFile()]); } } } \ No newline at end of file diff --git a/app/Models/Address.php b/app/Models/Address.php index 5e4f415..0fe3c3e 100644 --- a/app/Models/Address.php +++ b/app/Models/Address.php @@ -1053,6 +1053,8 @@ class Address extends Model if ($passpos > 8) Log::alert(sprintf('%s:! Password would be greater than 8 chars? [%d]',self::LOGKEY,$passpos)); + // @todo Do the strip pass where, if we dont want the password in the netmail + $pkt = $this->getPacket($x,substr($x->last()->subject,0,$passpos)); if ($pkt && $pkt->count() && $update) @@ -1091,8 +1093,9 @@ class Address extends Model * @param string|null $passwd Override password used in packet * @return Packet|null * @throws \Exception + * @deprecated */ - public function getPacket(Collection $msgs,string $passwd=NULL): ?Packet + private function getPacket(Collection $msgs,string $passwd=NULL): ?Packet { $s = Setup::findOrFail(config('app.id')); $ao = our_address($this); @@ -1104,7 +1107,7 @@ class Address extends Model } // Get packet type - $o = $ao->system->packet(); + $o = $ao->system->packet($this); $o->addressHeader($ao,$this,$passwd); // $oo = Netmail/Echomail Model diff --git a/app/Models/Echomail.php b/app/Models/Echomail.php index b1124a5..ec59abd 100644 --- a/app/Models/Echomail.php +++ b/app/Models/Echomail.php @@ -10,54 +10,77 @@ use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; use App\Casts\{CollectionOrNull,CompressedString}; -use App\Classes\FTN\Message; use App\Interfaces\Packet; -use App\Traits\{EncodeUTF8,MsgID,ParseAddresses,QueryCacheableConfig}; +use App\Traits\{MessageAttributes,MsgID,ParseAddresses,QueryCacheableConfig}; final class Echomail extends Model implements Packet { - use SoftDeletes,EncodeUTF8,MsgID,ParseAddresses,QueryCacheableConfig; + use SoftDeletes,MessageAttributes,MsgID,ParseAddresses,QueryCacheableConfig; private const LOGKEY = 'ME-'; - private Collection $set_seenby; - private Collection $set_path; - private Carbon $set_recvtime; - private string $set_pkt; - private string $set_sender; private bool $no_export = FALSE; + private const kludges = [ + 'MSGID:'=>'msgid', + 'PATH:'=>'set_path', + 'REPLY:'=>'replyid', + 'SEEN-BY:'=>'set_seenby', + ]; + + // When generating a packet for this echomail, the packet recipient is our tftn + public Address $tftn; + protected $casts = [ 'datetime' => 'datetime:Y-m-d H:i:s', 'kludges' => CollectionOrNull::class, 'msg' => CompressedString::class, 'msg_src' => CompressedString::class, 'rogue_seenby' => CollectionOrNull::class, - 'rogue_path' => CollectionOrNull::class, - ]; - - private const cast_utf8 = [ - 'to', - 'from', - 'subject', - 'msg', - 'msg_src', - 'origin', - 'tearline', - 'tagline', + 'rogue_path' => CollectionOrNull::class, // @deprecated? ]; public function __set($key,$value) { switch ($key) { + case 'kludges': + if (! count($value)) + return; + + if (array_key_exists($value[0],self::kludges)) { + $this->{self::kludges[$value[0]]} = $value[1]; + + } else { + $this->kludges->put($value[0],$value[1]); + } + + break; + case 'no_export': - case 'set_path': - case 'set_pkt': - case 'set_sender': - case 'set_recvtime': - case 'set_seenby': $this->{$key} = $value; break; + // Values that we pass to boot() to record how we got this echomail + case 'set_pkt': + case 'set_recvtime': + case 'set_sender': + // @todo We'll normalise these values when saving the netmail + case 'set_tagline': + case 'set_tearline': + case 'set_origin': + // For us to record the echoarea the message is for, if the area isnt defined (eg: packet dump) + case 'set_echoarea': + $this->set->put($key,$value); + break; + + // The path and seenby the echomail went through to get here + case 'set_path': + case 'set_seenby': + if (! $this->set->has($key)) + $this->set->put($key,collect()); + + $this->set->get($key)->push($value); + break; + default: parent::__set($key,$value); } @@ -67,6 +90,12 @@ final class Echomail extends Model implements Packet { parent::boot(); + static::creating(function($model) { + if (! is_null($model->errors)) + throw new \Exception('Cannot save, validation errors exist'); + }); + + // @todo dont save us in the seenby/path, we'll add it dynamically when we send out. // @todo if the message is updated with new SEEN-BY's from another route, we'll delete the pending export for systems (if there is one) static::created(function($model) { $rogue = collect(); @@ -74,8 +103,10 @@ final class Echomail extends Model implements Packet $path = collect(); // Parse PATH - if ($model->set_path->count()) - $path = self::parseAddresses('path',$model->set_path,$model->fftn->zone,$rogue); + if ($model->set->has('set_path')) + $path = self::parseAddresses('path',$model->set->get('set_path'),$model->fftn->zone,$rogue); + + Log::debug(sprintf('%s:^ Message [%d] from point address is [%d]',self::LOGKEY,$model->id,$model->fftn->point_id)); // Make sure our sender is first in the path if (! $path->contains($model->fftn_id)) { @@ -84,9 +115,9 @@ final class Echomail extends Model implements Packet } // Make sure our pktsrc is last in the path - if (isset($model->set_sender) && (! $path->contains($model->set_sender))) { - Log::alert(sprintf('%s:? Echomail adding pktsrc to end of PATH [%s].',self::LOGKEY,$model->set_sender)); - $path->push($model->set_sender); + if ($model->set->has('set_sender') && (! $path->contains($model->set->get('set_sender')->id))) { + Log::alert(sprintf('%s:? Echomail adding pktsrc to end of PATH [%s].',self::LOGKEY,$model->set->get('set_sender')->ftn)); + $path->push($model->set->get('set_sender')->id); } // Save the Path @@ -105,8 +136,8 @@ final class Echomail extends Model implements Packet // @todo move the parseAddress processing into Message::class, and our address to the seenby (and thus no need to add it when we export) // Parse SEEN-BY - if ($model->set_seenby->count()) - $seenby = self::parseAddresses('seenby',$model->set_seenby,$model->fftn->zone,$rogue); + if ($model->set->has('set_seenby')) + $seenby = self::parseAddresses('seenby',$model->set->get('set_seenby'),$model->fftn->zone,$rogue); // Make sure our sender is in the seenby if (! $seenby->contains($model->fftn_id)) { @@ -115,9 +146,9 @@ final class Echomail extends Model implements Packet } // Make sure our pktsrc is in the seenby - if (isset($model->set_sender) && (! $seenby->contains($model->set_sender))) { - Log::alert(sprintf('%s:? Echomail adding pktsrc to SEENBY [%s].',self::LOGKEY,$model->set_sender)); - $seenby->push($model->set_sender); + if ($model->set->has('set_sender') && (! $seenby->contains($model->set->get('set_sender')->id))) { + Log::alert(sprintf('%s:? Echomail adding pktsrc to SEENBY [%s].',self::LOGKEY,$model->set->get('set_sender')->ftn)); + $seenby->push($model->set->get('set_sender')->id); } if (count($rogue)) { @@ -129,26 +160,26 @@ final class Echomail extends Model implements Packet $model->seenby()->sync($seenby); // Our last node in the path is our sender - if (isset($model->set_pkt) && isset($model->set_recvtime)) { + if ($model->set->has('set_pkt') && $model->set->has('set_recvtime')) { if ($path->count()) { DB::update('UPDATE echomail_path set recv_pkt=?,recv_at=? where address_id=? and echomail_id=?',[ - $model->set_pkt, - $model->set_recvtime, + $model->set->get('set_pkt'), + $model->set->get('set_recvtime'), $path->last(), $model->id, ]); } else { - Log::critical(sprintf('%s:! Wasnt able to set packet details for [%d] to [%s] to [%s], no path information',self::LOGKEY,$model->id,$model->set_pkt,$model->set_recvtime)); + Log::critical(sprintf('%s:! Wasnt able to set packet details for [%d] to [%s] to [%s], no path information',self::LOGKEY,$model->id,$model->set->get('set_pkt'),$model->set->get('set_recvtime'))); } } // See if we need to export this message. if ($model->echoarea->sec_read) { - $exportto = ($x=$model + $exportto = $model ->echoarea ->addresses - ->filter(function($item) use ($model) { return $model->echoarea->can_read($item->security); })) + ->filter(function($item) use ($model) { return $model->echoarea->can_read($item->security); }) ->pluck('id') ->diff($seenby); @@ -193,90 +224,19 @@ final class Echomail extends Model implements Packet ->withPivot(['id','parent_id','recv_pkt','recv_at']); } - /* METHODS */ + /* ATTRIBUTES */ - public function init(): void + public function getSeenByAttribute(): Collection { - $this->set_path = collect(); - $this->set_seenby = collect(); + return ((! $this->exists) && $this->set->has('set_seenby')) + ? $this->set->get('set_seenby') + : $this->getRelationValue('seenby'); } - public function jsonSerialize(): array + public function getPathAttribute(): Collection { - return $this->encode(); - } - - /** - * Return this model as a packet - */ - public function packet(Address $ao): Message - { - Log::info(sprintf('%s:+ Bundling [%s]',self::LOGKEY,$this->id)); - - $sysaddress = our_address($this->fftn); - - if (! $sysaddress) - throw new \Exception(sprintf('%s:! We dont have an address in this network? (%s)',self::LOGKEY,$this->fftn->zone->domain->name)); - - // @todo Dont bundle mail to nodes that have been disabled, or addresses that have been deleted - $o = new Message; - - $o->header = [ - 'onode' => $sysaddress->node_id, - 'dnode' => $ao->node_id, - 'onet' => $sysaddress->host_id, - 'dnet' => $ao->host_id, - 'flags' => 0, - 'cost' => 0, - 'date'=>$this->datetime->format('d M y H:i:s'), - ]; - - $o->tzutc = $this->datetime->utcOffset($this->tzoffset)->getOffsetString(''); - $o->user_to = $this->to; - $o->user_from = $this->from; - $o->subject = $this->subject; - $o->echoarea = $this->echoarea->name; - $o->flags = $this->flags; - - if ($this->kludges) - $o->kludge = collect($this->kludges); - - $o->kludge->put('dbid',$this->id); - - $o->msgid = $this->msgid; - if ($this->replyid) - $o->replyid = $this->replyid; - - $o->message = $this->msg; - - if ($this->tagline) - $o->tagline = $this->tagline; - - if ($this->tearline) - $o->tearline = $this->tearline; - - if ($this->origin) - $o->origin = $this->origin; - - $o->seenby = $this->seenby->push($sysaddress)->unique()->pluck('ftn2d'); - - // Add our address to the path and seenby - $o->path = $this->pathorder()->merge($sysaddress->ftn2d); - - $o->packed = TRUE; - - return $o; - } - - public function pathorder(string $display='ftn2d',int $start=NULL): Collection - { - $result = collect(); - - if ($x=$this->path->firstWhere('pivot.parent_id',$start)) { - $result->push($x->$display); - $result->push($this->pathorder($display,$x->pivot->id)); - } - - return $result->flatten()->filter(); + return ((! $this->exists) && $this->set->has('set_path')) + ? $this->set->get('set_path') + : $this->getRelationValue('path'); } } \ No newline at end of file diff --git a/app/Models/Netmail.php b/app/Models/Netmail.php index ff17fa3..46cacf7 100644 --- a/app/Models/Netmail.php +++ b/app/Models/Netmail.php @@ -10,35 +10,30 @@ use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Log; -use App\Casts\CompressedString; -use App\Classes\FTN\Message; +use App\Casts\{CollectionOrNull,CompressedString}; use App\Interfaces\Packet; -use App\Traits\{EncodeUTF8,MsgID}; +use App\Pivots\ViaPivot; +use App\Traits\{MessageAttributes,MsgID}; final class Netmail extends Model implements Packet { + use SoftDeletes,MsgID,MessageAttributes; + private const LOGKEY = 'MN-'; + private const PATH_REGEX = '/^([0-9]+:[0-9]+\/[0-9]+(\..*)?)\s+@([0-9.a-zA-Z]+)\s+(.*)$/'; - use SoftDeletes,EncodeUTF8,MsgID; - - private Collection $set_path; - private Address $set_sender; - private Carbon $set_recvtime; - private string $set_pkt; - - private const cast_utf8 = [ - 'to', - 'from', - 'subject', - 'msg', - 'msg_src', - 'origin', - 'tearline', - 'tagline', + /** + * Kludges that we absorb in this model + */ + private const kludges = [ + 'MSGID:'=>'msgid', + 'REPLY:'=>'replyid', + 'Via' => 'set_path', ]; protected $casts = [ 'datetime' => 'datetime:Y-m-d H:i:s', + 'kludges' => CollectionOrNull::class, 'msg' => CompressedString::class, 'msg_src' => CompressedString::class, 'sent_at' => 'datetime:Y-m-d H:i:s', @@ -47,11 +42,36 @@ final class Netmail extends Model implements Packet public function __set($key,$value) { switch ($key) { - case 'set_path': + case 'kludges': + if (! count($value)) + return; + + if (array_key_exists($value[0],self::kludges)) { + $this->{self::kludges[$value[0]]} = $value[1]; + + } else { + $this->kludges->put($value[0],$value[1]); + } + + break; + + // Values that we pass to boot() to record how we got this netmail case 'set_pkt': case 'set_recvtime': case 'set_sender': - $this->{$key} = $value; + // @todo We'll normalise these values when saving the netmail + case 'set_tagline': + case 'set_tearline': + case 'set_origin': + $this->set->put($key,$value); + break; + + // The path the netmail went through to get here + case 'set_path': + if (! $this->set->has($key)) + $this->set->put($key,collect()); + + $this->set->get($key)->push($value); break; default: @@ -63,43 +83,48 @@ final class Netmail extends Model implements Packet { parent::boot(); + static::creating(function($model) { + if (! is_null($model->errors)) + throw new \Exception('Cannot save, validation errors exist'); + }); + static::created(function($model) { $nodes = collect(); // Parse PATH + // @todo dont save us in the path, we'll add it dynamically when we send out. // @YYYYMMDD.HHMMSS[.Precise][.Time Zone] [Serial Number] - if (isset($model->set_path)) { - if ($model->set_path->count()) { - foreach ($model->set_path as $line) { - $m = []; + if ($model->set->has('set_path')) { + foreach ($model->set->get('set_path') 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]); + if (preg_match(self::PATH_REGEX,$line,$m)) { + // Address + // @todo Do we need to add a domain here, since the path line may not include one + $ao = Address::findFTN($m[1]); - // Time - $t = []; - $datetime = ''; + // 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 (! 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]]); + if (! $ao) { + Log::alert(sprintf('%s:! Undefined Node [%s] in netmail path.',self::LOGKEY,$m[1])); + //$rogue->push(['node'=>$m[1],'datetime'=>$datetime,'program'=>$m[4]]); - } else { - $nodes->push(['node'=>$ao,'datetime'=>$datetime,'program'=>$m[4]]); - } + } else { + $nodes->push(['node'=>$ao,'datetime'=>$datetime,'program'=>$m[4]]); } } - - // If there are no details (Mystic), we'll create a blank - } else { - $nodes->push(['node'=>$model->set_sender,'datetime'=>Carbon::now(),'program'=>'Unknown']); } + + // If there are no details (Mystic), we'll create a blank + } else { + $nodes->push(['node'=>$model->set->get('set_sender'),'datetime'=>Carbon::now(),'program'=>sprintf('%s (%04X)',Setup::PRODUCT_NAME,Setup::PRODUCT_ID)]); } // Save the Path @@ -118,15 +143,25 @@ final class Netmail extends Model implements Packet } // Our last node in the path is our sender - if ($nodes->count() && isset($model->set_pkt) && isset($model->set_sender) && isset($model->set_recvtime)) { + if ($nodes->count() && $model->set->has('set_pkt') && $model->set->has('set_sender') && $model->set->has('set_recvtime')) { DB::update('UPDATE netmail_path set recv_pkt=?,recv_at=?,recv_id=? where address_id=? and netmail_id=?',[ - $model->set_pkt, - $model->set_recvtime, - $model->set_sender->id, + $model->set->get('set_pkt'), + $model->set->get('set_recvtime'), + $model->set->get('set_sender')->id, Arr::get($nodes->last(),'node')->id, $model->id, ]); } + + // Save our origin, tearline & tagline + if ($model->set->has('set_tagline')) + $model->tagline = $model->set->get('set_tagline'); + if ($model->set->has('set_tearline')) + $model->tearline = $model->set->get('set_tearline'); + if ($model->set->has('set_origin')) + $model->origin = $model->set->get('set_origin'); + + $model->save(); }); } @@ -142,13 +177,8 @@ final class Netmail extends Model implements Packet public function path() { return $this->belongsToMany(Address::class,'netmail_path') - ->withPivot(['id','parent_id','datetime','program','recv_pkt','recv_id']); - } - - public function received() - { - return $this->belongsToMany(Address::class,'netmail_path','netmail_id','recv_id') - ->withPivot(['id','parent_id','datetime','program','recv_pkt','recv_id']); + ->withPivot(['id','parent_id','datetime','program','recv_pkt','recv_id']) + ->using(ViaPivot::class); } public function tftn() @@ -158,89 +188,41 @@ final class Netmail extends Model implements Packet ->withTrashed(); } + /* ATTRIBUTES */ + + /** + * Enable rendering the path even if the model hasnt been saved + * + * @return Collection + */ + public function getPathAttribute(): Collection + { + return ((! $this->exists) && $this->set->has('set_path')) + ? $this->set->get('set_path')->map(function($item) { + $m = []; + preg_match(self::PATH_REGEX,$item,$m); + return $m[1]; + }) + : $this->getRelationValue('path'); + } + /* METHODS */ /** - * Return this model as a packet + * Render the via line + * + * @param Address $ao + * @return string + * @throws \Exception */ - public function packet(Address $ao,string $strippass=NULL): Message + public function via(Address $ao): string { - Log::debug(sprintf('%s:+ Bundling [%s]',self::LOGKEY,$this->id)); + if (! $ao->pivot) + throw new \Exception('Cannot render the via line without an address record without a path pivot'); - // @todo Dont bundle mail to nodes that have been disabled, or addresses that have been deleted - $o = new Message; - - try { - $o->header = [ - 'onode' => $this->fftn->node_id, - 'dnode' => $this->tftn->node_id, - 'onet' => $this->fftn->host_id, - 'dnet' => $this->tftn->host_id, - 'opoint' => $this->fftn->point_id, - 'dpoint' => $this->tftn->point_id, - 'flags' => 0, - 'cost' => 0, - 'date'=>$this->datetime->format('d M y H:i:s'), - ]; - - $o->tzutc = $this->datetime->utcOffset($this->tzoffset)->getOffsetString(''); - $o->user_to = $this->to; - $o->user_from = $this->from; - $o->subject = (! is_null($strippass)) ? preg_replace('/^'.$strippass.':/','',$this->subject) : $this->subject; - - // INTL kludge - $o->intl = sprintf('%s %s',$this->tftn->ftn3d,$this->fftn->ftn3d); - $o->flags = $this->flags; - - $o->msgid = $this->msgid - ? $this->msgid - : sprintf('%s %08x',$this->fftn->ftn4d,timew($this->datetime)); - - if ($this->replyid) - $o->replyid = $this->replyid; - - $o->kludge->put('dbid',$this->id); - - $o->message = $this->msg; - $o->tagline = $this->tagline; - $o->tearline = $this->tearline; - $o->origin = $this->origin; - - // VIA kludge - $via = $this->via ?: collect(); - // Add our address to the VIA line - $via->push( - sprintf('%s @%s.UTC %s %d.%d/%s %s', - our_address($this->fftn)->ftn3d, - Carbon::now()->utc()->format('Ymd.His'), - str_replace(' ','_',Setup::PRODUCT_NAME), - Setup::PRODUCT_VERSION_MAJ, - Setup::PRODUCT_VERSION_MIN, - (new Setup)->version, - Carbon::now()->format('Y-m-d'), - )); - - $o->via = $via; - - $o->packed = TRUE; - - } catch (\Exception $e) { - Log::error(sprintf('%s:! Error converting netmail [%s] to a message (%d:%s)',self::LOGKEY,$this->id,$e->getLine(),$e->getMessage())); - dump($this); - } - - return $o; - } - - public function pathorder(string $display='ftn2d',int $start=NULL): Collection - { - $result = collect(); - - if ($x=$this->path->firstWhere('pivot.parent_id',$start)) { - $result->push($x->$display); - $result->push($this->pathorder($display,$x->pivot->id)); - } - - return $result->flatten()->filter(); + return sprintf('%s @%s.UTC %s', + $ao->ftn3d, + $ao->pivot->datetime->format('Ymd.His'), + $ao->pivot->program); } } \ No newline at end of file diff --git a/app/Models/System.php b/app/Models/System.php index 2338099..b930903 100644 --- a/app/Models/System.php +++ b/app/Models/System.php @@ -251,12 +251,15 @@ class System extends Model /** * Return the packet that this system uses * + * @param Address $ao * @return Packet */ - public function packet(): Packet + public function packet(Address $ao): Packet { - return new (collect(Packet::PACKET_TYPES) - ->get($this->pkt_type ?: config('fido.packet_default'))); + // @todo Check that the address is one of the system's addresses + + return (new (collect(Packet::PACKET_TYPES) + ->get($this->pkt_type ?: config('fido.packet_default'))))->for($ao); } public function poll(): ?Job diff --git a/app/Notifications/Channels/EchomailChannel.php b/app/Notifications/Channels/EchomailChannel.php index 4578118..c56357e 100644 --- a/app/Notifications/Channels/EchomailChannel.php +++ b/app/Notifications/Channels/EchomailChannel.php @@ -41,6 +41,6 @@ class EchomailChannel $o = $notification->toEchomail($notifiable); - Log::info(sprintf('%s:= Posted echomail [%s] to [%s]',self::LOGKEY,$o->msgid,$echoarea)); + Log::info(sprintf('%s:= Posted echomail (%d) [%s] to [%s]',self::LOGKEY,$o->id,$o->msgid,$echoarea)); } } \ No newline at end of file diff --git a/app/Notifications/Channels/NetmailChannel.php b/app/Notifications/Channels/NetmailChannel.php index f8adf02..ec7bf97 100644 --- a/app/Notifications/Channels/NetmailChannel.php +++ b/app/Notifications/Channels/NetmailChannel.php @@ -41,6 +41,6 @@ class NetmailChannel $o = $notification->toNetmail($notifiable); - Log::info(sprintf('%s:= Sent netmail [%s] to [%s]',self::LOGKEY,$o->msgid,$ao->ftn)); + Log::info(sprintf('%s:= Sent netmail (%d) [%s] to [%s]',self::LOGKEY,$o->id,$o->msgid,$ao->ftn)); } } \ No newline at end of file diff --git a/app/Notifications/Netmails.php b/app/Notifications/Netmails.php index 3276680..eecc844 100644 --- a/app/Notifications/Netmails.php +++ b/app/Notifications/Netmails.php @@ -52,13 +52,14 @@ abstract class Netmails extends Notification //implements ShouldQueue $ao = $notifiable->routeNotificationFor(static::via); $o = new Netmail; + $o->set_sender = our_address($ao); $o->to = $ao->system->sysop; $o->from = Setup::PRODUCT_NAME; $o->datetime = Carbon::now(); $o->tzoffset = $o->datetime->utcOffset(); - $o->fftn_id = our_address($ao)->id; + $o->fftn_id = $o->set->get('set_sender')->id; $o->tftn_id = $ao->id; $o->flags = (Message::FLAG_LOCAL|Message::FLAG_PRIVATE); $o->cost = 0; diff --git a/app/Notifications/Netmails/EchoareaNotExist.php b/app/Notifications/Netmails/EchoareaNotExist.php index eff2e37..cd66bee 100644 --- a/app/Notifications/Netmails/EchoareaNotExist.php +++ b/app/Notifications/Netmails/EchoareaNotExist.php @@ -7,7 +7,7 @@ use Illuminate\Support\Facades\Log; use App\Classes\FTN\Message; use App\Notifications\Netmails; -use App\Models\{Netmail,System}; +use App\Models\{Echomail,Netmail}; use App\Traits\{MessagePath,PageTemplate}; class EchoareaNotExist extends Netmails @@ -16,14 +16,14 @@ class EchoareaNotExist extends Netmails private const LOGKEY = 'NNW'; - private Message $mo; + private Echomail $mo; /** * Send a sysop a message if they attempt to write to an area that doesnt exist. * * @param Message $mo */ - public function __construct(Message $mo) + public function __construct(Echomail $mo) { parent::__construct(); @@ -44,7 +44,7 @@ class EchoareaNotExist extends Netmails Log::info(sprintf('%s:+ Creating ECHOMAIL NOT EXIST netmail to [%s]',self::LOGKEY,$ao->ftn)); - $o->subject = 'Echoarea doesnt exist - '.$this->mo->echoarea; + $o->subject = 'Echoarea doesnt exist - '.$this->mo->set->get('set_echoarea'); // Message $msg = $this->page(FALSE,'nothere'); @@ -52,7 +52,7 @@ class EchoareaNotExist extends Netmails $msg->addText( sprintf("Your echomail with ID [%s] to [%s] here was received here on [%s] and it looks like you sent it on [%s].\r\r", $this->mo->msgid, - $this->mo->user_to, + $this->mo->to, Carbon::now()->utc()->toDateTimeString(), $this->mo->date->utc()->toDateTimeString(), ) diff --git a/app/Pivots/ViaPivot.php b/app/Pivots/ViaPivot.php new file mode 100644 index 0000000..01f0082 --- /dev/null +++ b/app/Pivots/ViaPivot.php @@ -0,0 +1,12 @@ + 'datetime', + ]; +} diff --git a/app/Traits/MessageAttributes.php b/app/Traits/MessageAttributes.php new file mode 100644 index 0000000..6d858e7 --- /dev/null +++ b/app/Traits/MessageAttributes.php @@ -0,0 +1,184 @@ +set = collect(); + } + + /* ATTRIBUTES */ + + public function getContentAttribute(): string + { + if ($this->msg_src) + return $this->msg_src."\r"; + + // If we have a msg_src attribute, we'll use that + $result = $this->msg."\r\r"; + + if ($this->tagline) + $result .= sprintf("%s\r",$this->tagline); + + if ($this->tearline) + $result .= sprintf("%s\r",$this->tearline); + + if ($this->origin) + $result .= sprintf("%s",$this->origin); + + return rtrim($result,"\r")."\r"; + } + + public function getDateAttribute(): Carbon + { + return $this->datetime->utcOffset($this->tzoffset); + } + + public function getOriginAttribute(string $val=NULL): ?string + { + // If $val is not set, then it may be an unsaved object + return ((! $this->exists) && $this->set->has('set_origin')) + ? sprintf(' * Origin: %s',$this->set->get('set_origin')) + : $val; + } + + public function getTaglineAttribute(string $val=NULL): ?string + { + // If $val is not set, then it may be an unsaved object + return ((! $this->exists) && $this->set->has('set_tagline')) + ? sprintf('... %s',$this->set->get('set_tagline')) + : $val; + } + + public function getTearlineAttribute(string $val=NULL): ?string + { + // If $val is not set, then it may be an unsaved object + return ((! $this->exists) && $this->set->has('set_tearline')) + ? sprintf('--- %s',$this->set->get('set_tearline')) + : $val; + } + + /* METHODS */ + + /** + * 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(Message::FLAG_PRIVATE), + 'crash' => $this->isFlagSet(Message::FLAG_CRASH), + 'recd' => $this->isFlagSet(Message::FLAG_RECD), + 'sent' => $this->isFlagSet(Message::FLAG_SENT), + 'fileattach' => $this->isFlagSet(Message::FLAG_FILEATTACH), + 'intransit' => $this->isFlagSet(Message::FLAG_INTRANSIT), + 'orphan' => $this->isFlagSet(Message::FLAG_ORPHAN), + 'killsent' => $this->isFlagSet(Message::FLAG_KILLSENT), + 'local' => $this->isFlagSet(Message::FLAG_LOCAL), + 'hold' => $this->isFlagSet(Message::FLAG_HOLD), + 'unused-10' => $this->isFlagSet(Message::FLAG_UNUSED_10), + 'filereq' => $this->isFlagSet(Message::FLAG_FREQ), + 'receipt-req' => $this->isFlagSet(Message::FLAG_RETRECEIPT), + 'receipt' => $this->isFlagSet(Message::FLAG_ISRETRECEIPT), + 'audit' => $this->isFlagSet(Message::FLAG_AUDITREQ), + 'fileupdate' => $this->isFlagSet(Message::FLAG_FILEUPDATEREQ), + ])->filter(); + } + + private function isFlagSet($flag): bool + { + return ($this->flags & $flag); + } + + /** + * Return this model as a packet + */ + public function packet(Address $ao): Message + { + Log::debug(sprintf('%s:+ Bundling [%s]',self::LOGKEY,$this->id),['type'=>get_class($this)]); + + // For netmails, our tftn is the next hop + $this->tftn = $ao; + + // @todo Dont bundle mail to nodes that have been disabled, or addresses that have been deleted + return Message::packMessage($this); + } + + /** + * Return our path in order + * + * @param string $display + * @param int|NULL $start + * @return Collection + */ + public function pathorder(string $display='ftn2d',int $start=NULL): Collection + { + $result = collect(); + + if ($x=$this->path->firstWhere('pivot.parent_id',$start)) { + $result->push($x->$display); + $result->push($this->pathorder($display,$x->pivot->id)); + } + + return $result->flatten()->filter(); + } +} \ No newline at end of file diff --git a/app/Traits/MessagePath.php b/app/Traits/MessagePath.php index c56625c..98f9c3d 100644 --- a/app/Traits/MessagePath.php +++ b/app/Traits/MessagePath.php @@ -5,32 +5,32 @@ */ namespace App\Traits; -use App\Classes\FTN\Message; +use App\Models\{Echomail,Netmail}; trait MessagePath { - protected function message_path(Message $mo): string + protected function message_path(Echomail|Netmail $mo): string { $reply = "This is your original message:\r\r"; $reply .= "+--[ BEGIN MESSAGE ]----------------------------------+\r"; - $reply .= sprintf("TO: %s\r",$mo->user_to); + $reply .= sprintf("TO: %s\r",$mo->to); $reply .= sprintf("SUBJECT: %s\r",$mo->subject); - $reply .= str_replace("\r---","\r#--",$mo->message)."\r"; + $reply .= str_replace("\r---","\r#--",$mo->msg)."\r"; $reply .= "+--[ CONTROL LINES ]----------------------------------+\r"; $reply .= sprintf("DATE: %s\r",$mo->date->format('Y-m-d H:i:s')); $reply .= sprintf("MSGID: %s\r",$mo->msgid); - foreach ($mo->kludge as $k=>$v) - $reply .= sprintf("@%s: %s\r",strtoupper($k),$v); + foreach ($mo->kludges as $k=>$v) + $reply .= sprintf("%s %s\r",$k,$v); $reply .= "+--[ PATH ]-------------------------------------------+\r"; - if ($mo->isNetmail()) { + if ($mo instanceof Netmail) { if ($mo->via->count()) - foreach ($mo->via as $via) - $reply .= sprintf("VIA: %s\r",$via); + foreach ($mo->via as $ao) + $reply .= sprintf("VIA: %s\r",$mo->via($ao)); else $reply .= "No path information? This would be normal if this message came directly to the hub\r"; diff --git a/composer.json b/composer.json index 1018dee..b69c651 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,7 @@ "keywords": ["framework","laravel"], "license": "MIT", "require": { - "php": "^8.1|8.2|8.3", + "php": "^8.2|8.3", "ext-bz2": "*", "ext-pcntl": "*", "ext-sockets": "*", diff --git a/database/migrations/2024_05_14_111729_add_kludges_to_netmails.php b/database/migrations/2024_05_14_111729_add_kludges_to_netmails.php new file mode 100644 index 0000000..0fff391 --- /dev/null +++ b/database/migrations/2024_05_14_111729_add_kludges_to_netmails.php @@ -0,0 +1,34 @@ +json('kludges')->nullable(); + }); + Schema::table('echomails', function (Blueprint $table) { + $table->tinyInteger('cost')->nullable(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('netmails', function (Blueprint $table) { + $table->dropColumn('kludges'); + }); + Schema::table('echomails', function (Blueprint $table) { + $table->dropColumn('cost'); + }); + } +}; diff --git a/resources/views/pkt.blade.php b/resources/views/pkt.blade.php index 9a349ca..32c2669 100644 --- a/resources/views/pkt.blade.php +++ b/resources/views/pkt.blade.php @@ -1,3 +1,8 @@ +@php + use App\Models\Netmail; + use App\Classes\FTN\Message; +@endphp + @extends('layouts.app') @section('htmlheader_title') Verify Packet @@ -82,7 +87,7 @@

@@ -98,48 +103,40 @@
- DATE: {{ $msg->date }} + DATE: {{ $msg->datetime }}
- FLAGS: {{ $msg->flags()->filter()->keys()->join(', ') }} + FLAGS: {{ $msg->flags()->keys()->join(', ') }}
- FROM: {!! \App\Classes\FTN\Message::tr($msg->user_from) !!} ({{ $msg->fftn }}) + FROM: {!! Message::tr($msg->from) !!} ({{ $msg->fftn->ftn }})
- TO: {!! \App\Classes\FTN\Message::tr($msg->user_to) !!}@if($msg->isNetmail()) ({{ $msg->tftn }}) @endif + TO: {!! Message::tr($msg->to) !!}@if($msg instanceof Netmail) ({{ $msg->tftn->ftn }}) @endif
- SUBJECT: {!! \App\Classes\FTN\Message::tr($msg->subject) !!} + SUBJECT: {!! Message::tr($msg->subject) !!}
-
{!! \App\Classes\FTN\Message::tr($msg->message).sprintf("\r * Origin: %s",$msg->origin) !!}
+
{!! Message::tr($msg->msg_src) !!}
- @if($msg->tagline) + @if($msg instanceof Netmail)
- TAGLINE:
{{ $msg->tagline }} -
-
- @endif - - @if($msg->isNetmail()) -
-
- VIA:
{!! $msg->via->join('
') !!} + VIA:
{!! $msg->path->join(' -> ') !!}
@else @@ -159,7 +156,7 @@
KLUDGES:
- @foreach ($msg->kludge->sort(function($v,$k) { return $k; })->reverse() as $k => $v) + @foreach ($msg->kludges->sort(function($v,$k) { return $k; })->reverse() as $k => $v) {{ $k }} {{ $v }}
@endforeach
diff --git a/resources/views/widgets/message.blade.php b/resources/views/widgets/message.blade.php index 4160c63..9cab574 100644 --- a/resources/views/widgets/message.blade.php +++ b/resources/views/widgets/message.blade.php @@ -1,10 +1,11 @@ @php use App\Classes\FTN\Message; +use App\Models\{Echomail,Netmail}; @endphp
- TO: {!! Message::tr($msg->to) !!} @if ($msg instanceof \App\Models\Netmail)({{ $msg->tftn->ftn }})@endif + TO: {!! Message::tr($msg->to) !!} @if ($msg instanceof Netmail)({{ $msg->tftn->ftn }})@endif
DATE: {{ $msg->datetime->format('Y-m-d H:i:s') }} @@ -16,19 +17,30 @@ use App\Classes\FTN\Message; FROM: {!! Message::tr($msg->from) !!} ({{ $msg->fftn->ftn }})
- MSGID: {{ $msg->msgid }}@if($x=\App\Models\Echomail::where('replyid',$msg->msgid)->count()) ({{$x}} replies)@endif @if($msg->replyid)
REPLY: {{ $msg->replyid }}@endif + MSGID: {{ $msg->msgid }}@if($x=Echomail::where('replyid',$msg->msgid)->count()) ({{$x}} replies)@endif @if($msg->replyid)
REPLY: {{ $msg->replyid }}@endif
-
-
- SUBJECT: {!! Message::tr($msg->subject) !!} +@if($msg->flags()->count()) +
+
+ FLAGS: {!! $msg->flags()->keys()->map(fn($item)=>strtoupper($item))->join(', ') !!} +
- @if ($msg instanceof \App\Models\Echomail) -
+@endif + +@if ($msg instanceof Echomail) +
+
ECHOAREA: {{ $msg->echoarea->name }} ({{ $msg->echoarea->domain->name }})
- @endif +
+@endif + +
+
+ SUBJECT: {!! Message::tr($msg->subject) !!} +
@@ -39,63 +51,63 @@ use App\Classes\FTN\Message;
-@if ($msg instanceof \App\Models\Echomail) +
+
+ KLUDGES:
+ @foreach($msg->kludges as $k=>$v) + {{ $k }} {{ $v }}
+ @endforeach +
+
+ +@if ($msg instanceof Echomail)
SEENBY:
{!! $msg->seenby->pluck('ftn2d')->join(', ') !!}
- @if ($msg->rogue_seenby->count()) -
[NOTE: Some seen-by values couldnt be identified - ({{ $msg->rogue_seenby->join(',') }})] + @if($msg->rogue_seenby->count()) +
[NOTE: Some seen-by values couldnt be identified - ({{ $msg->rogue_seenby->transform(fn($item)=>str_replace('0:','',$item))->join(',') }})] @endif
@endif -@if ($msg->flags & Message::FLAG_LOCAL) -
-
- Local message -
+ +
+
+ PATH:
{!! $msg->pathorder()->join(' -> ') !!} + + @if(($msg instanceof Echomail) && $msg->rogue_path->count()) +
[NOTE: Some path values couldnt be identified - ({{ $msg->rogue_path->join(',') }})] + @endif
+
-@elseif ((! $msg->flags) || ($msg->flags & (Message::FLAG_INTRANSIT|Message::FLAG_RECD))) - -
-
- PATH:
{!! $msg->pathorder()->join(' -> ') !!} - - @if (($msg instanceof \App\Models\Echomail) && $msg->rogue_path->count()) -
[NOTE: Some path values couldnt be identified - ({{ $msg->rogue_path->join(',') }})] - @endif -
-
- -
-
+
+
+ @if($msg instanceof Netmail) RECEIVED:
- @if ($msg instanceof \App\Models\Netmail) - @foreach ($msg->received as $path) - {{ $path->pivot->recv_pkt }} from {{ $path->ftn }} {{ $msg->created_at }} - @endforeach - @elseif ($msg instanceof \App\Models\Echomail) - {{ ($x=$msg->path->sortBy('pivot.parent_id')->last())->pivot->recv_pkt }} from {{ $x->ftn }} {{ $x->pivot->recv_at }} - @endif -
+ @foreach ($msg->path as $path) + {{ $path->pivot->recv_pkt }} from {{ $path->ftn }} {{ $msg->created_at }} + @endforeach + @elseif ($msg instanceof Echomail) + RECEIVED:
+ {{ ($x=$msg->path->sortBy('pivot.parent_id')->last())->pivot->recv_pkt }} from {{ $x->ftn }} {{ $x->pivot->recv_at }} + @endif
-@endif +
@section('page-scripts')