- 1 pass version. Faster, uses less memory, works as a filter

pp [2006-05-09 18:11:30]
- 1 pass version. Faster, uses less memory, works as a filter


git-svn-id: https://siedziba.pl:790/svn/repos/dbxrecover@235 455248ca-bdda-0310-9134-f4ebb693071a
Filename
dbxrecover-1p
dbxrecover
diff --git a/dbxrecover-1p b/dbxrecover-1p
new file mode 100755
index 0000000..db37ab6
--- /dev/null
+++ b/dbxrecover-1p
@@ -0,0 +1,310 @@
+#!/usr/bin/perl
+
+# dbxrecover, a program for recovering mail from damaged Outlook dbx files.
+# Copyright (C) 2005  Piotr Pawlow <pp@siedziba.pl>
+
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+
+use strict;
+use warnings;
+
+{
+  package DBX::ChunkList;
+
+  sub new
+  {
+    my $class = shift;
+    my $self = {};
+    bless ($self, $class);
+    $self->{list} = [ @_ ];
+    return $self;
+  }
+
+  sub join
+  {
+    my $self = shift;
+    my $data = '';
+    $self->join_int(\$data);
+    return $data;
+  }
+
+  sub join_int
+  {
+    no warnings qw(recursion);
+
+    my $self = shift;
+    my $dataref = shift;
+    my $list = $self->{list};
+    for my $elem (@{$list})
+    {
+      if (ref($elem))
+      {
+        $elem->join_int($dataref)
+      }
+      else
+      {
+        $$dataref .= $elem;
+      }
+    }
+  }
+
+  sub first
+  {
+    my $self = shift;
+    my $list = $self->{list};
+    my $elem = ${$list}[0];
+    $elem = $elem->first() if ref($elem);
+    return $elem;
+  }
+
+  sub push
+  {
+    my $self = shift;
+    my $a1 = $self->{list};
+    my $a2 = shift;
+    my $a2_list = $a2->{list};
+    if (scalar @{$a2_list} < 32) # FIXME: Arbitrary value choosen without much care. Probably not optimal.
+    {
+      push @{$a1}, @{$a2_list};
+    }
+    else
+    {
+      push @{$a1}, $a2;
+    }
+  }
+}
+
+{
+  package DBX::ChainFinder;
+  use BerkeleyDB;
+  use File::Temp;
+
+  sub new
+  {
+    my $class = shift;
+    my $self = {};
+    bless ($self, $class);
+    $self->reset;
+    return $self;
+  }
+
+  sub reset
+  {
+    my $self = shift;
+
+    $self->{id} = {};
+    $self->{next} = {};
+    $self->{stats} =
+    { 'chunkstot'    => 0,
+      'chainfrag'    => 0,
+      'chainscompl'   => 0,
+    };
+  }
+
+  # FIXME: this function is too convoluted
+  sub combine
+  {
+    my $self = shift;
+    my $hash = shift;
+    my $otherhash = shift;
+    my $key = shift;
+    my $data = shift;
+    my $idx = shift;
+
+    while (defined($hash->{$key}))
+    {
+      my @chunkkeys = sort keys %{$hash->{$key}};
+      my $chunkkey = pop @chunkkeys;
+      my $chunkref = $hash->{$key}->{$chunkkey};
+      my $chunkid = $chunkref->{head}->[$idx];
+      delete $hash->{$key}->{$chunkkey};
+      delete $hash->{$key} if (!scalar %{$hash->{$key}});
+      delete $otherhash->{$chunkid}->{$chunkkey};
+      delete $otherhash->{$chunkid} if (!scalar %{$otherhash->{$chunkid}});
+      if ($idx == 1)
+      {
+        $data->push($chunkref);
+      }
+      else
+      {
+        $chunkref->push($data);
+        $data = $chunkref;
+      }
+      $key = $chunkid;
+      $self->{stats}->{chainfrag}--;
+    }
+    return ($key, $data);
+  }
+
+  sub add
+  {
+    my $self = shift;
+    my $id = shift;
+    my $next = shift;
+    my $data = new DBX::ChunkList(shift);
+    my $chunkid = $self->{stats}->{chunkstot}++;
+    my $idhash = $self->{id};
+    my $nexthash = $self->{next};
+
+    ($next, $data) = $self->combine($idhash, $nexthash, $next, $data, 1);
+    ($id, $data) = $self->combine($nexthash, $idhash, $id, $data, 0);
+
+    # the test for first chunk is very simple and naive, but it works well enough
+    $self->{stats}->{chainscompl}++,return($data->join()) if (($next == 0) && ($data->first() =~ /^(From|From:|Return-Path:|Received:) /));
+    $idhash->{$id} = {} if (!defined($idhash->{$id}));
+    $nexthash->{$next} = {} if (!defined($nexthash->{$next}));
+    $data->{head} = [$id, $next];
+    $idhash->{$id}->{$chunkid} = $data;
+    $nexthash->{$next}->{$chunkid} = $data;
+    $self->{stats}->{chainfrag}++;
+    return undef;
+  }
+
+  sub remaining
+  {
+    my $self = shift;
+    return undef if (!scalar %{$self->{id}});
+    while (1)
+    {
+      (my $id, my $list) = each %{$self->{id}} or next;
+      (my $cid, my $chain) = each %{$list} or next;
+      my $data = $chain->join();
+      delete $self->{id}->{$id}->{$cid};
+      delete $self->{id}->{$id} if (!scalar %{$self->{id}->{$id}});
+      return $data;
+    }
+  }
+
+  sub stats
+  {
+    my $self = shift;
+    return $self->{stats};
+  }
+}
+
+{
+  package DBX::Scan;
+  sub new
+  {
+    my $class = shift;
+    my $self = {};
+    $self->{file} = shift;
+    $self->{cf} = DBX::ChainFinder->new;
+    bless ($self, $class);
+    return $self;
+  }
+
+  sub scan
+  {
+    my $self = shift;
+    my $file = $self->{file};
+    $file->binmode();
+    $self->{cf}->reset;
+    my $time = 0;
+    my $header;
+
+    $file->read($header, 16);
+
+    while ((length $header) == 16)
+    {
+      my ($id, $chunksize, $datasize, $next) = unpack("VVVV", $header);
+
+      # test for a valid header
+      # chunk size is always 512 bytes in all dbx files I have seen
+      if (
+           ($chunksize == 512) &&
+           ($datasize <= $chunksize) &&
+           ($datasize > 0) &&
+           ($id != 0) &&
+           (($next == 0) || ($datasize == $chunksize))
+         )
+      {
+        my $data;
+        $file->read($data, $datasize);
+
+        # if the header seems valid, we skip the whole chunk
+
+        # the chance we miss a start of another chunk is low, because
+        # the test above is pretty strict and false positives are quite rare
+
+        # it also helps in cases when there are dbx files contained inside dbx
+
+        my $tmp;
+        $file->read($tmp, $chunksize-$datasize);
+
+        my $message = $self->{cf}->add($id, $next, $data);
+        $self->printmsg($message);
+
+        if (time - $time > 1)
+        {
+          $self->printstats;
+          $time = time;
+        }
+
+        $file->read($header, 16);
+      }
+      else
+      {
+        # skip 4 bytes and try again
+        # headers were always at 4 byte boundary in every dbx file I have seen
+
+        my $tmp;
+        $file->read($tmp, 4);
+        $header = substr($header, 4).$tmp;
+      }
+    }
+    $self->printstats;
+    print STDERR "\nPrinting remaining chains...";
+    my $message;
+    do {
+      $message = $self->{cf}->remaining;
+      $self->printmsg($message);
+    } while defined($message);
+    print STDERR "\n";
+  }
+
+  sub printmsg
+  {
+    my $self = shift;
+    my $message = shift;
+    if (defined($message))
+    {
+      $message =~ s/^>*From />$&/m;		# mbox "From " escaping
+      # we don't have the original envelope address, so just output a fake one
+      print "From unknown\@unknown.invalid Mon Jan 1 00:00:00 1970\r\n";
+      print $message;
+      print "\r\n\r\n";
+    }
+  }
+
+  sub printstats
+  {
+    my $self = shift;
+    my $stats = $self->{cf}->stats;
+    print STDERR "Chunks found: ".$stats->{chunkstot}."; ";
+    print STDERR "Chain fragments: ".$stats->{chainfrag}."; ";
+    print STDERR "Chains completed: ".$stats->{chainscompl}."\r";
+  }
+}
+
+use IO::File;
+
+my $dbxname=($ARGV[0] || "-");
+print STDERR "Opening $dbxname for reading...\n";
+my $dbxfile = IO::File->new("<$dbxname") or die("Error opening file!\n");
+my $dbxscanner = DBX::Scan->new($dbxfile);
+print STDERR "Searching for messages...\n";
+$dbxscanner->scan;
+print STDERR "\n";
diff --git a/dbxrecover b/dbxrecover-2p
similarity index 100%
rename from dbxrecover
rename to dbxrecover-2p
ViewGit