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