'tzutc', ]; // Flags for messages /** @var int Private message */ public const FLAG_PRIVATE = 1<<0; /** @var int Crash priority message (Crash + Hold = Direct) */ public const FLAG_CRASH = 1<<1; /** @var int Read by addressee */ public const FLAG_RECD = 1<<2; /** @var int Message has been sent */ public const FLAG_SENT = 1<<3; /** @var int File attached (filename in subject) */ public const FLAG_FILEATTACH = 1<<4; /** @var int Message in transit to another destination */ public const FLAG_INTRANSIT = 1<<5; /** @var int Unknown destination - node not in nodelist */ public const FLAG_ORPHAN = 1<<6; /** @var int Kill after mailing */ public const FLAG_KILLSENT = 1<<7; /** @var int Message originated here */ public const FLAG_LOCAL = 1<<8; /** @var int Hold message here to be collected (Crash + Hold = Direct) */ public const FLAG_HOLD = 1<<9; /** @var int Reserved for future use by FTS-0001 */ public const FLAG_UNUSED_10 = 1<<10; /** @var int Requesting a file (filename in subject) */ public const FLAG_FREQ = 1<<11; /** @var int Return Receipt requested */ public const FLAG_RETRECEIPT = 1<<12; // (RRQ) /** @var int Return Receipt message in response to an RRQ */ public const FLAG_ISRETRECEIPT = 1<<13; /** @var int Request audit trail */ public const FLAG_AUDITREQ = 1<<14; // (ARQ) /** @var int Requesting a file update (filename in subject) */ public const FLAG_FILEUPDATEREQ = 1<<15; // (URQ) /** @var int Echomail has been scanned out */ public const FLAG_ECHOMAIL = 1<<16; /** @var int Use packet password on the subject line for this message */ public const FLAG_PKTPASSWD = 1<<17; // FTS-0001.016 Message header 32 bytes node, net, flags, cost, date 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 ]; 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 array $header; // Message Header private Collection $kludges; // TZUTC that needs to be converted to be used by Carbon @see self::kludges private Echomail|Netmail $mo; // The object storing this packet message private Address $us; // Our address for this message // Convert characters into printable chars // https://int10h.org/oldschool-pc-fonts/readme/#437_charset private const CP437 = [ 0x01 => 0x263a, 0x02 => 0x263b, 0x03 => 0x2665, 0x04 => 0x2666, 0x05 => 0x2663, 0x06 => 0x2660, 0x07 => 0x2022, 0x08 => 0x25d8, 0x09 => 0x25cb, 0x0a => 0x2509, 0x0b => 0x2642, 0x0c => 0x2640, 0x0d => 0x266a, 0x0e => 0x266b, 0x0f => 0x263c, 0x10 => 0x25ba, 0x11 => 0x25ca, 0x12 => 0x2195, 0x13 => 0x203c, 0x14 => 0x00b6, 0x15 => 0x00a7, 0x16 => 0x25ac, 0x17 => 0x21a8, 0x18 => 0x2191, 0x19 => 0x2193, 0x1a => 0x2192, 0x1b => 0x2190, 0x1c => 0x221f, 0x1d => 0x2194, 0x1e => 0x25bc, 0x1f => 0x25bc, 0x7f => 0x2302, 0x80 => 0x00c7, 0x81 => 0x00fc, 0x82 => 0x00e9, 0x83 => 0x00e2, 0x84 => 0x00e4, 0x85 => 0x00e0, 0x86 => 0x00e5, 0x87 => 0x00e7, 0x88 => 0x00ea, 0x89 => 0x00eb, 0x8a => 0x00e8, 0x8b => 0x00ef, 0x8c => 0x00ee, 0x8d => 0x00ec, 0x8e => 0x00c4, 0x8f => 0x00c5, 0x90 => 0x00c9, 0x91 => 0x00e6, 0x92 => 0x00c6, 0x93 => 0x00f4, 0x94 => 0x00f6, 0x95 => 0x00f2, 0x96 => 0x00fb, 0x97 => 0x00f9, 0x98 => 0x00ff, 0x99 => 0x00d6, 0x9a => 0x00dc, 0x9b => 0x00a2, 0x9c => 0x00a3, 0x9d => 0x00a5, 0x9e => 0x20a7, 0x9f => 0x0192, 0xa0 => 0x00e1, 0xa1 => 0x00ed, 0xa2 => 0x00f3, 0xa3 => 0x00fa, 0xa4 => 0x00f1, 0xa5 => 0x00d1, 0xa6 => 0x00aa, 0xa7 => 0x00ba, 0xa8 => 0x00bf, 0xa9 => 0x2310, 0xaa => 0x00ac, 0xab => 0x00bd, 0xac => 0x00bc, 0xad => 0x00a1, 0xae => 0x00ab, 0xaf => 0x00bb, 0xb0 => 0x2591, 0xb1 => 0x2592, 0xb2 => 0x2593, 0xb3 => 0x2502, 0xb4 => 0x2524, 0xb5 => 0x2561, 0xb6 => 0x2562, 0xb7 => 0x2556, 0xb8 => 0x2555, 0xb9 => 0x2563, 0xba => 0x2551, 0xbb => 0x2557, 0xbc => 0x255d, 0xbd => 0x255c, 0xbe => 0x255b, 0xbf => 0x2510, 0xc0 => 0x2514, 0xc1 => 0x2534, 0xc2 => 0x252c, 0xc3 => 0x251c, 0xc4 => 0x2500, 0xc5 => 0x253c, 0xc6 => 0x255e, 0xc7 => 0x255f, 0xc8 => 0x255a, 0xc9 => 0x2554, 0xca => 0x2569, 0xcb => 0x2566, 0xcc => 0x2560, 0xcd => 0x2550, 0xce => 0x256c, 0xcf => 0x2567, 0xd0 => 0x2568, 0xd1 => 0x2564, 0xd2 => 0x2565, 0xd3 => 0x2559, 0xd4 => 0x2558, 0xd5 => 0x2552, 0xd6 => 0x2553, 0xd7 => 0x256b, 0xd8 => 0x256a, 0xd9 => 0x2518, 0xda => 0x250c, 0xdb => 0x2588, 0xdc => 0x2584, 0xdd => 0x258c, 0xde => 0x2590, 0xdf => 0x2580, 0xe0 => 0x03b1, 0xe1 => 0x00df, 0xe2 => 0x0393, 0xe3 => 0x03c0, 0xe4 => 0x03a3, 0xe5 => 0x03c3, 0xe6 => 0x00b5, 0xe7 => 0x03c4, 0xe8 => 0x03a6, 0xe9 => 0x0398, 0xea => 0x03a9, 0xeb => 0x03b4, 0xec => 0x221e, 0xed => 0x03c6, 0xee => 0x03b5, 0xef => 0x2229, 0xf0 => 0x2261, 0xf1 => 0x00b1, 0xf2 => 0x2265, 0xf3 => 0x2264, 0xf4 => 0x2320, 0xf5 => 0x2321, 0xf6 => 0x00f7, 0xf7 => 0x2248, 0xf8 => 0x00b0, 0xf9 => 0x2219, 0xfa => 0x00b7, 0xfb => 0x221a, 0xfc => 0x207f, 0xfd => 0x00b2, 0xfe => 0x25a0, 0xff => 0x00a0, ]; public 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 * @throws \Exception */ public static function packMessage(Echomail|Netmail $o): self { $oo = new self($o->fftn->zone); $oo->mo = $o; $oo->us = our_address($o->tftn); 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 $zone * @return Echomail|Netmail * @throws \Exception */ 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)); $header_len = self::header_len(); $o = new self($zone); try { $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,$header_len)]); throw new InvalidPacketException($e->getMessage()); } $ptr = 0; // To User $o->header['user_to'] = strstr(substr($msg,$header_len+$ptr),"\x00",TRUE); $ptr += strlen($o->header['user_to'])+1; // From User $o->header['user_from'] = strstr(substr($msg,$header_len+$ptr),"\x00",TRUE); $ptr += strlen($o->header['user_from'])+1; // Subject $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,$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->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->clone()->utc(); $o->mo->tzoffset = $o->datetime->utcOffset(); $o->mo->flags = $o->header['flags']; $o->mo->cost = $o->header['cost']; if ($o->fftn) $o->mo->fftn_id = $o->fftn->id; else $o->mo->set_fftn = $o->fftn_t; switch (get_class($o->mo)) { case Echomail::class: // Echomails dont have a to address break; case Netmail::class: if ($o->tftn) $o->mo->tftn_id = $o->tftn->id; else $o->mo->set_tftn = $o->tftn_t; break; default: throw new InvalidPacketException('Unknown message class: '.get_class($o->mo)); } $o->validate(); return $o->mo; } /** * Translate the string into something printable via the web * * @param string $string * @param array $skip * @return string */ public static function tr(string $string,array $skip=[0x0a,0x0d]): string { $tr = []; foreach (self::CP437 as $k=>$v) { if (in_array($k,$skip)) continue; $tr[chr($k)] = '&#'.$v; } return strtr($string,$tr); } /** * 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 $zone */ public function __construct(Zone $zone) { $this->zone = $zone; $this->kludges = 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 (int)Arr::get($this->src,'z'); case 'fn': return (int)($x=$this->src) ? Arr::get($x,'n') : Arr::get($this->header,'onet'); case 'ff': return (int)($x=$this->src) ? Arr::get($x,'f') : Arr::get($this->header,'onode'); case 'fp': return (int)$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 'fzone': // Use the zone if this class was called with it. if ($this->zone && ($this->fz === $this->zone->zone_id)) return $this->zone; if ($this->fdomain) { if (($x=$this->fdomain->zones->search(function($item) { return $item->zone_id === $this->fz; })) !== FALSE) return $this->fdomain->zones->get($x); } // No domain, so we'll use the default zone return Zone::where('zone_id',$this->fz) ->where('default',TRUE) ->single(); case '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 (int)Arr::get($this->isEchomail() ? $this->src : $this->dst,'z'); case 'tn': return (int)Arr::get($this->header,'dnet'); case 'tf': return (int)Arr::get($this->header,'dnode'); case 'tp': return (int)$this->mo->kludges->get('TOPT') ?: Arr::get($this->header,'dpoint',0); case 'tzone': // Use the zone if this class was called with it. if ($this->zone && ($this->tz === $this->zone->zone_id)) return $this->zone; if ($this->tdomain) { if (($x=$this->tdomain->zones->search(function($item) { return $item->zone_id === $this->tz; })) !== FALSE) return $this->tdomain->zones->get($x); } // No domain, so we'll use the default zone return Zone::where('zone_id',$this->tz) ->where('default',TRUE) ->single(); 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 'fftn_t': case 'fftn': case 'tftn_t': case 'tftn': return parent::__get($key); // Convert our message (header[datetime]) with our TZUTC into a Carbon date case 'datetime': try { 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', sprintf('%s %s%04d',$x,($this->tzutc < 0) ? '-' : '+',abs($this->tzutc))); } catch (InvalidFormatException|\Exception $e) { Log::error(sprintf('%s:! Date doesnt parse [%s] (%s)',self::LOGKEY,$e->getMessage(),Arr::get($this->header,$key))); throw new \Exception(sprintf('%s (%s)',$e->getMessage(),hex_dump(Arr::get($this->header,$key)))); } // 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')); $src = Address::parseFTN($src); // @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')]); } return $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] : '')); } 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())); } // 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)); } } else { throw new InvalidPacketException('Dont know what type of packet this is'); } 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; case 'tzutc': return $this->kludges->get($key); default: throw new \Exception('Unknown key: '.$key); } } public function __set(string $key,mixed $value): void { switch ($key) { case 'tzutc': if (! is_numeric($value)) throw new InvalidPacketException('TZUTC is not numeric '.$value); $this->kludges->put($key,$value); } } /** * Export an FTN message, ready for sending. * * @return string * @throws \Exception */ public function __toString(): string { $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->mo->to."\00"; $return .= $this->mo->from."\00"; $return .= $this->mo->subject."\00"; if (($this->mo instanceof Netmail) && $this->mo->isFlagSet(self::FLAG_LOCAL)) { if ((! $this->mo->kludges->has('FMPT')) && $this->mo->fftn->point_id) $this->mo->kludges->put('FMPT',$this->mo->fftn->point_id); if ((! $this->mo->kludges->has('TOPT')) && $this->mo->tftn->point_id) $this->mo->kludges->put('TOPT',$this->mo->tftn->point_id); } $this->mo->kludges->put($this->mo->isFlagSet(self::FLAG_LOCAL) ? 'PID:' : 'TID:',sprintf('%s %s',Setup::PRODUCT_NAME_SHORT,Setup::version())); $this->mo->kludges->put('DBID:',$this->mo->id); if ($this->mo instanceof Echomail) $return .= sprintf("AREA:%s\r",strtoupper($this->mo->echoarea->name)); // Rebuild the INTL kludge line elseif ($this->mo instanceof Netmail) $this->mo->kludges->put('INTL',sprintf('%s %s',$this->mo->tftn->ftn3d,$this->mo->fftn->ftn3d)); // Add some kludges $return .= sprintf("\01TZUTC: %s\r",str_replace('+','',$this->mo->date->getOffsetString(''))); if ($this->mo->msgid) $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."\r"; if ($this->mo instanceof Netmail) { foreach ($this->mo->path as $ao) $return .= sprintf("\x01Via %s\r",$this->mo->via($ao)); // Add our address $return .= sprintf("\x01Via %s @%s.UTC %s %s\r", $this->us->ftn3d, Carbon::now()->format('Ymd.His'), Setup::PRODUCT_NAME_SHORT,Setup::version()); } else { // FTS-0004.001/FSC-0068.001 The message SEEN-BY lines // FTS-0004.001/FSC-0068.001 The message PATH lines // @todo This unique() function here shouldnt be required, but is while system generated messages are storing path/seenby $path = $this ->mo ->path ->push($this->us) ->unique('ftn') ->filter(fn($item)=>is_null($item->point_id) || ($item->point_id === 0)); // Create our rogue seenby objects $seenby = $this->mo->seenby; if ($this->mo->rogue_seenby->count()) { $do = $this->mo->echoarea->domain; foreach ($this->mo->rogue_seenby as $item) $seenby->push(Address::newFTN(sprintf('%s@%s',$item,$do->name))); } $seenby = $seenby ->push($this->us) ->filter(fn($item)=>is_null($item->point_id) || ($item->point_id === 0)) ->unique('ftn') ->sortBy(function($item) { return sprintf('%05d%05d',$item->host_id,$item->node_id);}); $return .= $this->aka_trim($seenby,'SEEN-BY:')."\r"; $return .= "\x01".$this->aka_trim($path,'PATH:')."\r"; } $return .= "\00"; return $return; } /** * Reduce our PATH/SEEN-BY for messages as per FSC-0068 * * @param Collection $path * @param string $prefix * @param int $len * @param string $delim * @return 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 $ao) { [$host,$node] = explode('/',$ao->ftn2d); if (($c+strlen(' '.$host)) > $len) { $result .= ($x=$delim.$prefix); $c = strlen($x); $cur = NULL; } if ($host !== $cur) { $cur = $host; $result .= ($x=' '.$ao->ftn2d); } else { $result .= ($x=' '.$node); } $c += strlen($x); } return $result; } private function isEchomail(): bool { return $this->mo instanceof Echomail; } /** * If this message doesnt have an AREATAG, then its a netmail. * * @return bool */ public function isNetmail(): bool { return $this->mo instanceof Netmail; } /** * Extract information out of the message text. * * @param string $message * @param Echomail|Netmail $o * @return Echomail|Netmail * @throws InvalidPacketException */ public 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); // First find our kludge lines $ptr_start = 0; $ptr_end = 0; try { while (substr($message,$ptr_start,1) === "\x01") { $ptr_end = strpos($message,"\r",$ptr_start); $m = []; $kludge = ($x=substr($message,$ptr_start+1,$ptr_end-$ptr_start-1)); preg_match('/^([^\s]+:?)+\s+(.*)$/',$kludge,$m); $ptr_start = $ptr_end+1; if (! $m) { Log::alert(sprintf('%s:! Invalid Kluge Line [%s]',self::LOGKEY,$x)); continue; } // Catch any kludges we need to process here if (array_key_exists($m[1],self::kludges)) { // Some earlier mystic message had a blank value for TZUTC if ((($m[1]) === 'TZUTC:') && (! $m[2])) $m[2] = '0000'; $this->{self::kludges[$m[1]]} = $m[2]; } else $o->kludges = [$m[1],$m[2]]; } // Next our message content ends with '\r * Origin: ... \r' or ... // FTS-0004.001 if ($ptr_end=strrpos($message,"\r * Origin: ",$ptr_start)) { // Find the $ptr_end = strpos($message,"\r",$ptr_end+1); // If there is no ptr_end, then this is not an origin if (! $ptr_end) throw new InvalidPacketException('Couldnt find the end of the origin'); } elseif (! $ptr_end=strpos($message,"\r\x01",$ptr_start)) { $ptr_end = strlen($message); } $remaining = substr($message,$ptr_end+1); // At this point, the remaining part of the message should start with \x01, PATH or SEEN-BY if ((substr($remaining,0,9) !== 'SEEN-BY: ') && (substr($remaining,0,5) !== 'PATH:') && ($x=strpos($remaining,"\x01")) !== 0) { if ($x) $ptr_end += $x; else $ptr_end += strlen($remaining); } // Process the message content if ($content=substr($message,$ptr_start,$ptr_end-$ptr_start)) { $o->msg_src = $content; $o->msg_crc = md5($content); $ptr_content_start = 0; // See if we have a tagline if ($ptr_content_end=strrpos($content,"\r... ",$ptr_content_start)) { $o->msg = substr($content,$ptr_content_start,$ptr_content_end+1); $ptr_content_start = $ptr_content_end+5; $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); $ptr_content_start = strlen($content); } else { $o->set_tagline = substr($content,$ptr_content_start,$ptr_content_end-$ptr_content_start); $ptr_content_start = $ptr_content_end; } } // See if we have a tearline if ($ptr_content_end=strrpos($content,"\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+5; $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); $ptr_content_start = strlen($content); } 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=strrpos($content,"\r * Origin: ",$ptr_content_start)) { if (! $ptr_content_start) $o->msg = substr($content,$ptr_content_start,$ptr_content_end); $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); $ptr_content_start = strlen($content); } else { $o->set_origin = substr($content,$ptr_content_start,$ptr_content_end-$ptr_content_start); $ptr_content_start = $ptr_content_end+1; } } // If there wasnt any tagline/tearline/origin, then the whole content is the message if (! $ptr_content_start) { $o->msg = $content; $ptr_content_start = $ptr_end-$ptr_start; } // Trim any right \r from the message $o->msg = rtrim($o->msg,"\r"); // Quick validation that we are done if ($ptr_content_start !== strlen($content)) { Log::alert(sprintf('%s:! We failed parsing the message start [%d] content [%d]',self::LOGKEY,$ptr_content_start,strlen($content))); $o->msg = substr($message,0,$ptr_end); } } $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]]; } } catch (\Exception $e) { Log::error(sprintf('%s:! Error parsing message, now at offset [0x%02x] (%s)', self::LOGKEY, $ptr_start, $e->getMessage()),['dump'=>hex_dump($message)]); throw new InvalidPacketException('Error parsing message'); } return $o; } /** * Validate details about this message * * @return \Illuminate\Contracts\Validation\Validator */ public function validate(): ValidatorResult { // Check lengths $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', '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', // @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->mo instanceof Netmail) && (! $this->mo->kludges->has('INTL'))) $validator->errors()->add('no-intl','Netmail message is missing INTL KLUDGE.'); if ($this->zone->domain->flatten) { if (! $this->zone->domain->zones->pluck('zone_id')->contains($this->fz)) $validator->errors()->add('invalid-zone',sprintf('Message from zone [%d] doesnt match any zone in domain for packet zone [%d].',$this->fz,$this->zone->zone_id)); if (! $this->zone->domain->zones->pluck('zone_id')->contains($this->tz)) $validator->errors()->add('invalid-zone',sprintf('Message to zone [%d] doesnt match any zone in domain for packet zone [%d].',$this->fz,$this->zone->zone_id)); } else { if ($this->zone->zone_id !== $this->fz) $validator->errors()->add('invalid-zone',sprintf('Message from zone [%d] doesnt match packet zone [%d].',$this->fz,$this->zone->zone_id)); if ($this->zone->zone_id !== $this->tz) $validator->errors()->add('invalid-zone',sprintf('Message to zone [%d] doesnt match packet zone [%d].',$this->tz,$this->zone->zone_id)); } if (! $this->fftn) $validator->errors()->add('from',sprintf('Undefined Node [%s] sent message.',$this->fftn_t)); if ($this->isNetmail() && (! $this->tftn)) $validator->errors()->add('to',sprintf('Undefined Node [%s] for destination.',$this->tftn_t)); }); $this->mo->errors = $validator->errors(); if ($validator->fails()) Log::debug(sprintf('%s:! Message fails validation (%s@%s->%s@%s)',self::LOGKEY,$this->mo->from,$this->fftn_t,$this->mo->to,$this->tftn_t),['result'=>$validator->errors()]); return $validator; } }