'CHRS: ', 'charset' => 'CHARSET: ', 'codepage' => 'CODEPAGE: ', 'dbid' => 'DBID: ', 'pid' => 'PID: ', 'tid' => 'TID: ', ]; // 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 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 USER_FROM_LEN = 36; // FTS-0001.016 From Name: upto 36 chars null terminated private const USER_TO_LEN = 36; // FTS-0001.016 To Name: upto 36 chars null terminated private const SUBJECT_LEN = 71; // FTS-0001.016 Subject: upto 72 chars null terminated private const AREATAG_LEN = 35; // private ?ValidatorResult $errors = NULL; // Packet validation private array $header; // Message Header private Collection $kludge; // Hold kludge items 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. public bool $packed = FALSE; // Has the message been packed successfully // Convert characters into printable chars // https://int10h.org/oldschool-pc-fonts/readme/#437_charset private const CP437 = [ 0x01 => 0x263a, 0x02 => 0x263b, 0x03 => 0x2665, 0x04 => 0x2666, 0x05 => 0x2663, 0x06 => 0x2660, 0x07 => 0x2022, 0x08 => 0x25d8, 0x09 => 0x25cb, 0x0a => 0x2509, 0x0b => 0x2642, 0x0c => 0x2640, 0x0d => 0x266a, 0x0e => 0x266b, 0x0f => 0x263c, 0x10 => 0x25ba, 0x11 => 0x25ca, 0x12 => 0x2195, 0x13 => 0x203c, 0x14 => 0x00b6, 0x15 => 0x00a7, 0x16 => 0x25ac, 0x17 => 0x21a8, 0x18 => 0x2191, 0x19 => 0x2193, 0x1a => 0x2192, 0x1b => 0x2190, 0x1c => 0x221f, 0x1d => 0x2194, 0x1e => 0x25bc, 0x1f => 0x25bc, 0x7f => 0x2302, 0x80 => 0x00c7, 0x81 => 0x00fc, 0x82 => 0x00e9, 0x83 => 0x00e2, 0x84 => 0x00e4, 0x85 => 0x00e0, 0x86 => 0x00e5, 0x87 => 0x00e7, 0x88 => 0x00ea, 0x89 => 0x00eb, 0x8a => 0x00e8, 0x8b => 0x00ef, 0x8c => 0x00ee, 0x8d => 0x00ec, 0x8e => 0x00c4, 0x8f => 0x00c5, 0x90 => 0x00c9, 0x91 => 0x00e6, 0x92 => 0x00c6, 0x93 => 0x00f4, 0x94 => 0x00f6, 0x95 => 0x00f2, 0x96 => 0x00fb, 0x97 => 0x00f9, 0x98 => 0x00ff, 0x99 => 0x00d6, 0x9a => 0x00dc, 0x9b => 0x00a2, 0x9c => 0x00a3, 0x9d => 0x00a5, 0x9e => 0x20a7, 0x9f => 0x0192, 0xa0 => 0x00e1, 0xa1 => 0x00ed, 0xa2 => 0x00f3, 0xa3 => 0x00fa, 0xa4 => 0x00f1, 0xa5 => 0x00d1, 0xa6 => 0x00aa, 0xa7 => 0x00ba, 0xa8 => 0x00bf, 0xa9 => 0x2310, 0xaa => 0x00ac, 0xab => 0x00bd, 0xac => 0x00bc, 0xad => 0x00a1, 0xae => 0x00ab, 0xaf => 0x00bb, 0xb0 => 0x2591, 0xb1 => 0x2592, 0xb2 => 0x2593, 0xb3 => 0x2502, 0xb4 => 0x2524, 0xb5 => 0x2561, 0xb6 => 0x2562, 0xb7 => 0x2556, 0xb8 => 0x2555, 0xb9 => 0x2563, 0xba => 0x2551, 0xbb => 0x2557, 0xbc => 0x255d, 0xbd => 0x255c, 0xbe => 0x255b, 0xbf => 0x2510, 0xc0 => 0x2514, 0xc1 => 0x2534, 0xc2 => 0x252c, 0xc3 => 0x251c, 0xc4 => 0x2500, 0xc5 => 0x253c, 0xc6 => 0x255e, 0xc7 => 0x255f, 0xc8 => 0x255a, 0xc9 => 0x2554, 0xca => 0x2569, 0xcb => 0x2566, 0xcc => 0x2560, 0xcd => 0x2550, 0xce => 0x256c, 0xcf => 0x2567, 0xd0 => 0x2568, 0xd1 => 0x2564, 0xd2 => 0x2565, 0xd3 => 0x2559, 0xd4 => 0x2558, 0xd5 => 0x2552, 0xd6 => 0x2553, 0xd7 => 0x256b, 0xd8 => 0x256a, 0xd9 => 0x2518, 0xda => 0x250c, 0xdb => 0x2588, 0xdc => 0x2584, 0xdd => 0x258c, 0xde => 0x2590, 0xdf => 0x2580, 0xe0 => 0x03b1, 0xe1 => 0x00df, 0xe2 => 0x0393, 0xe3 => 0x03c0, 0xe4 => 0x03a3, 0xe5 => 0x03c3, 0xe6 => 0x00b5, 0xe7 => 0x03c4, 0xe8 => 0x03a6, 0xe9 => 0x0398, 0xea => 0x03a9, 0xeb => 0x03b4, 0xec => 0x221e, 0xed => 0x03c6, 0xee => 0x03b5, 0xef => 0x2229, 0xf0 => 0x2261, 0xf1 => 0x00b1, 0xf2 => 0x2265, 0xf3 => 0x2264, 0xf4 => 0x2320, 0xf5 => 0x2321, 0xf6 => 0x00f7, 0xf7 => 0x2248, 0xf8 => 0x00b0, 0xf9 => 0x2219, 0xfa => 0x00b7, 0xfb => 0x221a, 0xfc => 0x207f, 0xfd => 0x00b2, 0xfe => 0x25a0, 0xff => 0x00a0, ]; public function __construct(Zone $zone=NULL) { $this->zone = $zone; $this->header = []; $this->user_from = ''; $this->user_to = ''; $this->subject = ''; $this->message = ''; $this->msgid = ''; $this->gateid = ''; $this->replyid = ''; $this->echoarea = ''; $this->intl = ''; $this->tearline = 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) { 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 'fd': return Arr::get($this->src,'d'); case 'fdomain': // We'll use the zone's domain if this method class was called with a zone if ($this->zone && (($this->zone->domain->name === Arr::get($this->src,'d')) || ! Arr::get($this->src,'d'))) return $this->zone->domain; // If we get the domain from the packet, we'll find it if ($x=Arr::get($this->src,'d')) { return Domain::where('name',$x)->single(); } return NULL; case 'tdomain': // We'll use the zone's domain if this method class was called with a zone if ($this->zone && (($this->zone->domain->name === Arr::get($this->dst,'d')) || ! Arr::get($this->dst,'d'))) return $this->zone->domain; // If we get the domain from the packet, we'll find it if ($x=Arr::get($this->dst,'d')) { return Domain::where('name',$x)->single(); } // Otherwise we'll assume the same as the source domain return $this->fdomain ?: NULL; case 'fzone': // Use the zone if this class was called with it. if ($this->zone && ($this->fz === $this->zone->zone_id)) return $this->zone; if ($this->fdomain) { if (($x=$this->fdomain->zones->search(function($item) { return $item->zone_id === $this->fz; })) !== FALSE) return $this->fdomain->zones->get($x); } // No domain, so we'll use the default zone return Zone::where('zone_id',$this->fz) ->where('default',TRUE) ->single(); case 'tzone': // Use the zone if this class was called with it. if ($this->zone && ($this->tz === $this->zone->zone_id)) return $this->zone; if ($this->tdomain) { if (($x=$this->tdomain->zones->search(function($item) { return $item->zone_id === $this->tz; })) !== FALSE) return $this->tdomain->zones->get($x); } // No domain, so we'll use the default zone return Zone::where('zone_id',$this->tz) ->where('default',TRUE) ->single(); // To Addresses // Echomail doesnt have a zone, so we'll use the source zone case 'tz': return Arr::get($this->echoarea ? $this->src : $this->dst,'z'); case 'tn': return Arr::get($this->header,'dnet'); case 'tf': return Arr::get($this->header,'dnode'); case 'tp': return Arr::get($this->point,'dst',Arr::get($this->header,'dpoint',0)); case 'fftn': case 'fftn_o': case 'tftn': case 'tftn_o': return parent::__get($key); // For 5D we need to include the domain case 'fboss': return sprintf('%d:%d/%d',$this->fz,$this->fn,$this->ff).(($x=$this->fdomain) ? '@'.$x->name : ''); case 'tboss': return sprintf('%d:%d/%d',$this->tz,$this->tn,$this->tf).(($x=$this->tdomain) ? '@'.$x->name : ''); case 'fboss_o': return Address::findFTN($this->fboss); case 'tboss_o': return Address::findFTN($this->tboss); case 'date': try { if (str_contains($x=chop(Arr::get($this->header,$key)),"\x00")) throw new \Exception('Date contains null values.'); return Carbon::createFromFormat('d M y H:i:s O', sprintf('%s %s%04d',$x,($this->tzutc < 0) ? '-' : '+',abs($this->tzutc))); } catch (InvalidFormatException|\Exception $e) { Log::error(sprintf('%s:! Date doesnt parse [%s] (%s)',self::LOGKEY,$e->getMessage(),Arr::get($this->header,$key))); throw new \Exception(sprintf('%s (%s)',$e->getMessage(),hex_dump(Arr::get($this->header,$key)))); } case 'flags': case 'cost': return Arr::get($this->header,$key); case 'tzutc': case 'user_to': case 'user_from': case 'subject': case 'echoarea': case 'msgid': case 'replyid': case 'gateid': case 'message': case 'message_src': case 'tearline': case 'tagline': case 'origin': case 'kludge': case 'rescanned': case 'path': case 'seenby': case 'unknown': case 'via': case 'errors': return $this->{$key}; case 'dbid': return $this->kludge->get($key); default: throw new \Exception('Unknown key: '.$key); } } /** * When we serialise this object, we'll need to utf8_encode some values * * @return array */ public function __serialize(): array { return $this->encode(); } public function __set($key,$value) { switch ($key) { case 'flags': case 'header': case 'tzutc': case 'user_from': case 'user_to': case 'subject': case 'gateid': case 'msgid': case 'replyid': case 'echoarea': case 'intl': case 'message': case 'kludge': case 'tagline': case 'tearline': case 'origin': case 'seenby': case 'path': case 'via': $this->{$key} = $value; break; default: throw new \Exception('Unknown key: '.$key); } } /** * Export an FTN message, ready for sending. * * @return string */ public function __toString(): string { $return = pack(collect(self::header)->pluck(1)->join(''), $this->ff, // 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 .= $this->user_to."\00"; $return .= $this->user_from."\00"; $return .= $this->subject."\00"; if (! $this->isNetmail()) $return .= sprintf("AREA:%s\r",strtoupper($this->echoarea)); // If the message is local, then our kludges are not in the msg itself, we'll add them if ($this->isFlagSet(self::FLAG_LOCAL)) { if ($this->isNetmail()) { $return .= sprintf("\01INTL %s\r",$this->intl); if ($this->fp) $return .= sprintf("\01FMPT %d\r",$this->fp); if ($this->tp) $return .= sprintf("\01TOPT %d\r",$this->tp); } $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->isNetmail()) { foreach ($this->via as $via) $return .= sprintf("\01Via %s\r",$via); } else { // Seenby & PATH - FSC-0068 $return .= $this->optimise_path($this->seenby,'SEEN-BY:')."\r"; $return .= "\x01".$this->optimise_path($this->path,'PATH:')."\r"; } $return .= "\00"; return $return; } /** * When we unserialize, we'll restore (utf8_decode) some values * * @param array $values */ public function __unserialize(array $values): void { $this->decode($values); } /** * 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 */ function optimise_path(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); if (($c+strlen(' '.$host)) > $len) { $result .= ($x=$delim.$prefix); $c = strlen($x); $cur = NULL; } if ($host !== $cur) { $cur = $host; $result .= ($x=' '.$address); } else { $result .= ($x=' '.$node); } $c += strlen($x); } return $result; } /** * Parse a message from a packet * * @param string $msg * @param Zone|null $zone * @return Message * @throws \Exception */ public static function parseMessage(string $msg,Zone $zone=NULL): self { Log::info(sprintf('%s:= Processing message [%d] bytes from zone [%d]',self::LOGKEY,strlen($msg),$zone?->zone_id)); $o = new self($zone); $o->dump = $msg; try { $o->header = unpack(self::unpackheader(self::header),substr($msg,0,self::HEADER_LEN)); } catch (\Exception $e) { Log::error(sprintf('%s:! Error bad packet header',self::LOGKEY),['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()); }] ]); if ($validator->fails()) $o->errors = $validator; return $o; } $ptr = 0; // To User $o->user_to = strstr(substr($msg,self::HEADER_LEN+$ptr),"\x00",TRUE); $ptr += strlen($o->user_to)+1; // From User $o->user_from = strstr(substr($msg,self::HEADER_LEN+$ptr),"\x00",TRUE); $ptr += strlen($o->user_from)+1; // Subject $o->subject = strstr(substr($msg,self::HEADER_LEN+$ptr),"\x00",TRUE); $ptr += strlen($o->subject)+1; // Check if this is an Echomail if (! strncmp(substr($msg,self::HEADER_LEN+$ptr),'AREA:',5)) { $o->echoarea = 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; } $o->unpackMessage(substr($msg,self::HEADER_LEN+$ptr)); if (($x=$o->validate())->fails()) { Log::debug(sprintf('%s:! Message fails validation (%s@%s->%s@%s)',self::LOGKEY,$o->user_from,$o->fftn,$o->user_to,$o->tftn),['result'=>$x->errors()]); //throw new \Exception('Message validation fails:'.join(' ',$x->errors()->all())); } return $o; } /** * Translate the string into something printable via the web * * @param string $string * @param array $skip * @return string */ public static function tr(string $string,array $skip=[0x0a,0x0d]): string { $tr = []; foreach (self::CP437 as $k=>$v) { if (in_array($k,$skip)) continue; $tr[chr($k)] = '&#'.$v; } return strtr($string,$tr); } /** * If this message doesnt have an AREATAG, then its a netmail. * * @return bool */ public function isNetmail(): bool { return ! $this->echoarea; } /** * Return an array of flag descriptions * * @return Collection * * http://ftsc.org/docs/fsc-0001.000 * AttributeWord bit meaning * --- -------------------- * 0 + Private * 1 + s Crash * 2 Recd * 3 Sent * 4 + FileAttached * 5 InTransit * 6 Orphan * 7 KillSent * 8 Local * 9 s HoldForPickup * 10 + unused * 11 s FileRequest * 12 + s ReturnReceiptRequest * 13 + s IsReturnReceipt * 14 + s AuditRequest * 15 s FileUpdateReq * * s - this bit is supported by SEAdog only * + - this bit is not zeroed before packeting */ public function flags(): Collection { return collect([ 'private' => $this->isFlagSet(self::FLAG_PRIVATE), 'crash' => $this->isFlagSet(self::FLAG_CRASH), 'recd' => $this->isFlagSet(self::FLAG_RECD), 'sent' => $this->isFlagSet(self::FLAG_SENT), 'fileattach' => $this->isFlagSet(self::FLAG_FILEATTACH), 'intransit' => $this->isFlagSet(self::FLAG_INTRANSIT), 'orphan' => $this->isFlagSet(self::FLAG_ORPHAN), 'killsent' => $this->isFlagSet(self::FLAG_KILLSENT), 'local' => $this->isFlagSet(self::FLAG_LOCAL), 'hold' => $this->isFlagSet(self::FLAG_HOLD), 'unused-10' => $this->isFlagSet(self::FLAG_UNUSED_10), 'filereq' => $this->isFlagSet(self::FLAG_FREQ), 'receipt-req' => $this->isFlagSet(self::FLAG_RETRECEIPT), 'receipt' => $this->isFlagSet(self::FLAG_ISRETRECEIPT), 'audit' => $this->isFlagSet(self::FLAG_AUDITREQ), 'fileupdate' => $this->isFlagSet(self::FLAG_FILEUPDATEREQ), ]); } private function isFlagSet($flag): bool { return ($this->flags & $flag); } /** * 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); } } /** * Extract information out of the message text. * * @param string $message * @throws \Exception */ public function unpackMessage(string $message): void { // Remove DOS \n\r $message = preg_replace("/\n\r/","\r",$message); $message = preg_replace("/\r\n/","\r",$message); // Split out the lines $result = collect(explode("\x01",$message))->filter(); $this->message = ''; $this->message_src = ''; $msgpos = 0; $haveOrigin = FALSE; while ($result->count()) { // $kl is our line starting with a \x01 kludge delimiter $kl = $result->shift(); // 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 else $this->unknown->push(chop($kl,"\r")); } // Work out our zone/point // http://ftsc.org/docs/fsc-0068.001 // MSGID should be the basis of the source, 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] : '')); } 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())); } // 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)); } } /** * Validate details about this message * * @return \Illuminate\Contracts\Validation\Validator */ public function validate(): ValidatorResult { // Check lengths $validator = Validator::make([ 'user_from' => $this->user_from, 'user_to' => $this->user_to, 'subject' => $this->subject, 'onode' => $this->ff, 'dnode' => $this->tf, 'onet' => $this->fn, 'dnet' => $this->tn, 'flags' => $this->flags, 'cost' => $this->cost, 'echoarea' => $this->echoarea, 'ozone' => $this->fz, 'dzone' => $this->tz, ],[ 'user_from' => 'required|min:1|max:'.self::USER_FROM_LEN, 'user_to' => 'required|min:1|max:'.self::USER_TO_LEN, 'subject' => 'present|max:'.self::SUBJECT_LEN, 'onode' => ['required',new TwoByteIntegerWithZero], 'dnode' => ['required',new TwoByteIntegerWithZero], 'onet' => ['required',new TwoByteInteger], 'dnet' => ['required',new TwoByteInteger], 'flags' => 'required|numeric', 'cost' => 'required|numeric', 'echoarea' => 'nullable|max:'.self::AREATAG_LEN, 'ozone' => ['required'], 'dzone' => ['required'] ]); $validator->after(function($validator) { if ($this->zone->domain->flatten) { if (! $this->zone->domain->zones->pluck('zone_id')->contains($this->fz)) $validator->errors()->add('invalid-zone',sprintf('Message 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 zone [%d] doesnt match packet zone [%d].',$this->fz,$this->zone->zone_id)); } if (! $this->fboss_o) $validator->errors()->add('from',sprintf('Undefined Node [%s] sent message.',$this->fboss)); if (! $this->tboss_o) $validator->errors()->add('to',sprintf('Undefined Node [%s] for destination.',$this->tboss)); }); if ($validator->fails()) $this->errors = $validator; return $validator; } }