'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 $this->znfp($this->fqfa,'z'); case 'fn': return $this->znfp($this->fqfa,'n'); case 'ff': return $this->znfp($this->fqfa,'f'); case 'fp': return $this->znfp($this->fqfa,'p'); case 'tz': return $this->znfp($this->fqda,'z'); case 'tn': return $this->znfp($this->fqda,'n'); case 'tf': return $this->znfp($this->fqda,'f'); case 'tp': return $this->znfp($this->fqda,'p'); default: return isset($this->{$k}) ? $this->{$k} : NULL; } } public function __set($k,$v) { switch ($k) { case 'fqfa': case 'fqda': $this->{$k} = $v; if ($this->fqfa AND $this->fqda) $this->intl = sprintf('%s %s',$this->fqda,$this->fqfa); default: $this->{$k} = $v; } } /** * Export an FTN message, ready for sending. * * @return string */ public function __toString(): string { $return = ''; $return .= pack(join('',collect($this->struct)->pluck(1)->toArray()), $this->ff, $this->tf, $this->fn, $this->tn, $this->flags, 0 // @todo 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"; $return .= $this->message."\00"; return $return; } public function description() { switch ($this->type()) { case 'echomail': return sprintf('Echomail: '.$this->echoarea); case 'netmail': return sprintf('Netmail: %s->%s',$this->fqfa,$this->fqda); default: return 'UNKNOWN'; } } /** * 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 = array_get($result,'onet'); $this->psf = array_get($result,'onode'); $this->_src = sprintf('%s/%s', $this->psn, $this->psf ); // For Echomail this is the packet dst. $this->pdn = array_get($result,'dnet'); $this->pdf = array_get($result,'dnode'); $this->_dst = sprintf('%s/%s', $this->pdn, $this->pdf ); $this->flags = array_get($result,'flags'); $this->cost = array_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)); $matches = []; preg_match('/^.*\((.*)\)$/',$this->origin,$matches); if (($this->type() == 'Netmail' AND array_get($matches,1) != $this->fqfa) OR ! array_get($matches,1)) throw new InvalidFidoPacketException(sprintf('Source address mismatch? [%s,%s]',$this->fqfa,array_get($matches,1))); $this->fqfa = array_get($matches,1); } $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]); } } } public function type() { if ($this->echoarea) return 'echomail'; if ($this->intl) return 'netmail'; return 'UNKNOWN'; } private function znfp(string $data,string $key) { switch ($key) { case 'z': return substr($data,0,strpos($data,':')); case 'n': $x = strpos($data,':')+1; return substr($data,$x,strpos($data,'/')-$x); case 'f': $x = strpos($data,'/')+1; return substr($data,$x,strpos($data,'.') ?: strlen($data)-$x); case 'p': $x = strpos($data,'.'); return $x ? substr($data,$x+1) : 0; default: abort(500,'Unknown key: '.$key); } } }