diff --git a/app/Classes/FTN.php b/app/Classes/FTN.php
new file mode 100644
index 0000000..46b1045
--- /dev/null
+++ b/app/Classes/FTN.php
@@ -0,0 +1,31 @@
+sortBy(function($k,$v) {return $k[0];})
+ ->transform(function($k,$v) {return $k[1].$v;})->toArray()));
+ }
+}
\ No newline at end of file
diff --git a/app/Classes/FTNMessage.php b/app/Classes/FTNMessage.php
new file mode 100644
index 0000000..f5efba5
--- /dev/null
+++ b/app/Classes/FTNMessage.php
@@ -0,0 +1,251 @@
+ 'CHRS: ',
+ 'charset' => 'CHARSET: ',
+ 'codepage' => 'CODEPAGE: ',
+ 'pid' => 'PID: ',
+ 'tid' => 'TID: ',
+ ];
+
+ public function __construct(string $header)
+ {
+ // 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.
+
+ // FTS-0001.016 Message header 12 bytes
+ // node, net, flags, cost
+ $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],
+ ];
+
+ $result = unpack($this->unpackheader($struct),$header);
+
+ $this->src = sprintf('%s/%s',array_get($result,'onet'),array_get($result,'onode'));
+ $this->dst = sprintf('%s/%s',array_get($result,'dnet'),array_get($result,'dnode'));
+ $this->flags = array_get($result,'flags');
+ $this->cost = array_get($result,'cost');
+ }
+
+ public function __get($k)
+ {
+ return $this->{$k};
+ }
+
+ public function __set($k,$v)
+ {
+ switch ($k)
+ {
+ case 'message':
+ // Remove DOS \n\r
+ $v = preg_replace("/\n\r/","\r",$v);
+
+ $this->parsemessage($v);
+ break;
+
+ case 'origin':
+ $this->parseorigin($v);
+ break;
+
+ default:
+ $this->{$k} = $v;
+ }
+ }
+
+ public function parsemessage(string $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->__set('origin',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->fqfd,$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
+ */
+ public 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 description()
+ {
+ switch ($this->type())
+ {
+ case 'Echomail': return sprintf('Echomail: '.$this->echoarea);
+ case 'Netmail': return sprintf('Netmail: %s->%s',$this->fqfa,$this->fqfd);
+ default:
+ return 'UNKNOWN';
+ }
+ }
+
+ public function type()
+ {
+ if ($this->echoarea)
+ return 'Echomail';
+
+ if ($this->intl)
+ return 'Netmail';
+
+ return 'UNKNOWN';
+ }
+}
\ No newline at end of file
diff --git a/app/Classes/FTNPacket.php b/app/Classes/FTNPacket.php
new file mode 100644
index 0000000..09298e2
--- /dev/null
+++ b/app/Classes/FTNPacket.php
@@ -0,0 +1,159 @@
+filename = $file;
+
+ if ($file)
+ return $this->OpenFile($file);
+ }
+
+ /**
+ * Open a packet file
+ *
+ * @param string $file
+ * @throws InvalidFidoPacketException
+ */
+ private function OpenFile(string $file)
+ {
+ $f = fopen($file,'r');
+// $fstat = fstat($f);
+
+ // PKT Header
+ $header = fread($f,0x3a);
+
+ // Could not read header
+ if (strlen($header) != 0x3a)
+ throw new InvalidFidoPacketException('Length of Header too short: '.$file);
+
+ // Not a type 2 packet
+ if (array_get(unpack('vv',substr($header,0x12)),'v') != 2)
+ throw new InvalidFidoPacketException('Not a type 2 packet:'. $file);
+
+ $this->setHeader($header);
+ $this->messages = collect();
+
+ while (! feof($f))
+ {
+ $x = fread($f,2);
+
+ // End of Packet?
+ if (strlen($x) == 2 and $x == "\00\00")
+ {
+ break;
+ }
+
+ // Messages start with 02H 00H
+ if (strlen($x) == 2 AND $x != "\02\00")
+ throw new InvalidFidoPacketException('Not a valid packet: '.$x);
+
+ // No message attached
+ else if (! strlen($x))
+ break;
+
+ $message = new FTNMessage(fread($f,0xc));
+ $message->date = $this->readnullfield($f);
+ $message->to = $this->readnullfield($f);
+ $message->from = $this->readnullfield($f);
+ $message->subject = $this->readnullfield($f);
+ $message->message = $this->readnullfield($f);
+
+ $this->messages->push($message);
+ }
+ }
+
+ private function readnullfield($f)
+ {
+ $result = '';
+
+ while (($x = fgetc($f) OR strlen($x)) AND $x !== "\00")
+ {
+ $result .= $x;
+ }
+
+ return $result;
+ }
+
+ public function setHeader(string $header)
+ {
+ $pack1 = [
+ 'onode'=>[0x00,'v',2],
+ 'dnode'=>[0x02,'v',2],
+ 'y'=>[0x04,'v',2],
+ 'm'=>[0x06,'v',2],
+ 'd'=>[0x08,'v',2],
+ 'H'=>[0x0a,'v',2],
+ 'M'=>[0x0c,'v',2],
+ 'S'=>[0x0e,'v',2],
+ 'baud'=>[0x10,'v',2],
+ 'pktver'=>[0x12,'v',2],
+ 'onet'=>[0x14,'v',2],
+ 'dnet'=>[0x16,'v',2],
+ 'prodcode-lo'=>[0x18,'C',1],
+ 'prodrev-maj'=>[0x19,'C',1],
+ ];
+
+ $pack2 = [
+ 'qozone'=>[0x22,'v',2],
+ 'qdzone'=>[0x24,'v',2],
+ 'filler'=>[0x26,'v',2],
+ 'capvalid'=>[0x28,'v',2],
+ 'prodcode-hi'=>[0x2a,'C',1],
+ 'prodrev-min'=>[0x2b,'C',1],
+ 'capword'=>[0x2c,'v',1],
+ 'ozone'=>[0x2e,'v',2],
+ 'dzone'=>[0x30,'v',2],
+ 'opoint'=>[0x32,'v',2],
+ 'dpoint'=>[0x34,'v',2],
+ ];
+
+ $result1 = unpack($this->unpackheader($pack1),substr($header,0,0x1a));
+ $result2 = unpack($this->unpackheader($pack2),substr($header,0x22,0x14));
+
+ $this->pktsrc = sprintf('%s:%s/%s.%s',
+ array_get($result2,'ozone'),
+ array_get($result1,'onet'),
+ array_get($result1,'onode'),
+ array_get($result2,'dpoint')
+ );
+
+ $this->pktdst = sprintf('%s:%s/%s.%s',
+ array_get($result2,'dzone'),
+ array_get($result1,'dnet'),
+ array_get($result1,'dnode'),
+ array_get($result2,'dpoint')
+ );
+
+ $this->date = sprintf ('%d-%d-%d %d:%d:%d',
+ array_get($result1,'y'),
+ array_get($result1,'m'),
+ array_get($result1,'d'),
+ array_get($result1,'H'),
+ array_get($result1,'M'),
+ array_get($result1,'S')
+ );
+
+ $this->baud = array_get($result1,'baud');
+ $this->pktver = array_get($result1,'pktver');
+
+ $this->password = array_get(unpack('A*p',substr($header,0x1a,8)),'p');
+ $this->proddata = array_get(unpack('A*p',substr($header,0x36,4)),'p');
+ }
+}
\ No newline at end of file
diff --git a/app/Console/Commands/FtnPkt.php b/app/Console/Commands/FtnPkt.php
new file mode 100644
index 0000000..87b7f8b
--- /dev/null
+++ b/app/Console/Commands/FtnPkt.php
@@ -0,0 +1,71 @@
+argument('file'));
+
+ $this->info(sprintf('Packet: %s has %s messages. Addr: %s->%s (Date: %s)',
+ $pkt->filename,
+ $pkt->messages->count(),
+ $pkt->pktsrc,
+ $pkt->pktdst,
+ $pkt->date
+ ));
+
+ foreach ($pkt->messages as $o)
+ {
+ $this->warn(sprintf('-- From: %s(%s)->%s(%s), Type: %s, Size: %d, FQFA: %s',
+ $o->from,
+ $o->src,
+ $o->to,
+ $o->dst,
+ $o->description(),
+ strlen($o->message),
+ $o->fqfa
+ ));
+
+ if ($o->unknown->count())
+ $this->error(sprintf('?? %s Unknown headers',$o->unknown->count()));
+ }
+
+ if ($this->option('dump'))
+ dump($o);
+ }
+}
\ No newline at end of file
diff --git a/app/Exceptions/InvalidFidoPacketException.php b/app/Exceptions/InvalidFidoPacketException.php
new file mode 100644
index 0000000..3e4a327
--- /dev/null
+++ b/app/Exceptions/InvalidFidoPacketException.php
@@ -0,0 +1,10 @@
+
-
-
-
-
-
-
-
-
-## About Laravel
-
-Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel attempts to take the pain out of development by easing common tasks used in the majority of web projects, such as:
-
-- [Simple, fast routing engine](https://laravel.com/docs/routing).
-- [Powerful dependency injection container](https://laravel.com/docs/container).
-- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
-- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
-- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
-- [Robust background job processing](https://laravel.com/docs/queues).
-- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
-
-Laravel is accessible, yet powerful, providing tools needed for large, robust applications.
-
-## Learning Laravel
-
-Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of any modern web application framework, making it a breeze to get started learning the framework.
-
-If you're not in the mood to read, [Laracasts](https://laracasts.com) contains over 1100 video tutorials on a range of topics including Laravel, modern PHP, unit testing, JavaScript, and more. Boost the skill level of yourself and your entire team by digging into our comprehensive video library.
-
-## Laravel Sponsors
-
-We would like to extend our thanks to the following sponsors for helping fund on-going Laravel development. If you are interested in becoming a sponsor, please visit the Laravel [Patreon page](https://patreon.com/taylorotwell):
-
-- **[Vehikl](https://vehikl.com/)**
-- **[Tighten Co.](https://tighten.co)**
-- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
-- **[64 Robots](https://64robots.com)**
-- **[Cubet Techno Labs](https://cubettech.com)**
-- **[British Software Development](https://www.britishsoftware.co)**
-- **[Webdock, Fast VPS Hosting](https://www.webdock.io/en)**
-- [UserInsights](https://userinsights.com)
-- [Fragrantica](https://www.fragrantica.com)
-- [SOFTonSOFA](https://softonsofa.com/)
-- [User10](https://user10.com)
-- [Soumettre.fr](https://soumettre.fr/)
-- [CodeBrisk](https://codebrisk.com)
-- [1Forge](https://1forge.com)
-- [TECPRESSO](https://tecpresso.co.jp/)
-- [Runtime Converter](http://runtimeconverter.com/)
-- [WebL'Agence](https://weblagence.com/)
-- [Invoice Ninja](https://www.invoiceninja.com)
-- [iMi digital](https://www.imi-digital.de/)
-- [Earthlink](https://www.earthlink.ro/)
-- [Steadfast Collective](https://steadfastcollective.com/)
-- [We Are The Robots Inc.](https://watr.mx/)
-- [Understand.io](https://www.understand.io/)
-
-## Contributing
-
-Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
-
-## Security Vulnerabilities
-
-If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
-
-## License
-
-The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).