View Single Post
  #4 (permalink)  
Old 01-16-2008, 03:35 PM
troy troy is offline
Junior Member
 
Posts: 8
Default

I ran into a situation where a service recipient had deleted their Zimbra sent folder. Zimbra NE allows me to restore the user’s account to another account (restored_username) just fine but then what do I do to get the mail folder over to the other (production) account?

As suggested, imapsync is one option. It won't do tags though.

To fix my problem, I wrote something in perl to copy entire folders to another account. It's limited by a zmmailbox limitation of 1000 messages but you can use the move option and run it more than once. It's also a little slow so I implemented forking to do some of the grunt work in parallel.

For what it's worth, here is how I am copying folders to other mailboxes:
Code:
#!/usr/bin/perl

# Name:      zimbracopyfolder
# Purpose:   The original intent was to help users out when they cry 
#            "Help! I deleted all of my Sent items!" and the like.
# Version:   0.88 (alpha)
# Author:    Troy Adams (troy { at } troyadams { dot } ca)
# Date:      Fri Dec 21 12:50:33 MST 2007
# License:   GPL-3
# Usage:     zimbracopyfolder --help
# Resources: http://www.zimbra.com/forums/administrators/10393-solved-how-use-zmmailbox-show-original.html
#            http://www.zimbra.com/forums/administrators/5065-restore-mail-another-users-mailbox.html
#            http://blog.troyadams.ca/?p=3
# Warning:   No warranty!  This is program is for academic analysis only. 
#            Read the code yourself!
# 
# Example:   
#            Step 1) restore from zimbra backup
#               # zmrestore --createAccount --prefix restored_ \
#                           --restoreFullBackupOnly --account test@mydomain.ca
#            Step 2) copy the required folder from the restored_ account to the production account:
#               # zimbracopyfolder --source restored_test@mydomain.ca:/Sent \
#                              --destination test@mydomain.ca:/restore_Sent \
#                              --noverify --verbose --forks 16
#  Limitations:
#            It seems that, due to a limitation of zmmailbox, this program can
#            only handle folders with a maximum of 1000 messages.
#
# Log: zimbracopyfolder,v
#
# Revision 1.1  2007/10/29 15:39:21  troy
# Initial revision
# 

# benchmarks (tested in the Athabasca University data center):
#   (1) 19 minutes for 129 messages with NUM_FORKS=8;  6.78 msgs/minute
#   (2) 43 minutes for 185 messages with NUM_FORKS=16; 4.30 msgs/minute
#   (3)  6 minutes for  36 messages with NUM_FORKS=1;  6    msgs/minute
#   bypass extra shell call to $zmMAILBOX by calling java direct
#   (4) 5m08s      for  29 messages with NUM_FORKS=1;  5.64 msgs/minute
#   (5) 1m59s      for  16 messages with NUM_FORKS=1;  8.07 msgs/minute
#   (6) 1m50s      for  16 messages with NUM_FORKS=4;  8.73 msgs/minute
#   (7) 1m45s      for  16 messages with NUM_FORKS=8;  9.14 msgs/minute
#   (8) 1m20s      for  16 messages with NUM_FORKS=16; 12.0 msgs/minute
#   (9) 3m08s      for  32 messages with NUM_FORKS=16; 10.2 msgs/minute
#   back to calling /opt/zimbra/bin/zmmailbox (no more bypass)
#  (10) 3m08s      for  32 messages with NUM_FORKS=16; 10.2 msgs/minute
#   best we have so far is 12.0 msgs/minute; that isn't too good!
#   I modified the thread queue handler to shift off the top instead of
#   pop off the bottom - silly me.  Now we get:
#  (11) 3m36s      for  49 messages with NUM_FORKS=16; 13.6 msgs/minute
#


# Plan:
#   (1) aquire and verify cmd-line args such as source and destination
#   (2) split out and verify src/dst components
#   (3) enumerate source target contents
#   (4) construct destination target
#   (5) test destination target for content
#   (6) export->import full messages
#   (7) test destination target again for content

# Errors/features that we don't yet code for:
#   (1)   ERROR: zclient.CLIENT_ERROR (unknown tag: 71)
#         We need to build support for on-the-fly tag creation?
#   (2)   we can only do 1000 msgs at a time; to do a folder with
#         more than 1000 msgs then just use the --move option 
#         and keep executing until all the msgs are moved.
#   (3)   the hostname portion of the source and destination are
#         for future implementation of cross-host copying
#

use Env;
our $id=0;          # don't think we need this globally
my $NUM_FORKS=1;
my $preservetags=0;

my $i = 0;
my $TEMP_FILE = "/tmp/zimbracopyfoldertmp.$$";
my $TEMP_FILE2 = "/tmp/zimbracopyfoldertmp-metadata.$$";
my $EXEC_FILE = "/tmp/zimbracopyfolder.execute.$$";
my $MAX_SEARCH = 1000;                           # seems like $zmMAILBOX will only return 1000 hits at most

#my $zmMAILBOX="/opt/zimbra/bin/zmjava com.zimbra.cs.zclient.ZMailboxUtil";
my $zmMAILBOX="/opt/zimbra/bin/zmmailbox";

sub quit
{
  if (-f $TEMP_FILE) { unlink $TEMP_FILE or die "ERROR: cannot remove '$TEMP_FILE'.\n" }
  if (-f $EXEC_FILE) { unlink $EXEC_FILE or die "ERROR: cannot remove '$EXEC_FILE'.\n" }
  exit shift;
}

sub giveup
{
  print STDERR "@_\n";
  quit 1
}


sub execute 
{
  my @exec_cmd = @_;
  push @exec_cmd, ">$EXEC_FILE";
  my $x = "@exec_cmd";
  my @system = ($x);
  my @output = ();

  print "executing '$x'\n" unless $DEBUG<2;
  if ($commandlog)
  {
    open  CMDLOG, ">>$commandlog";
    print CMDLOG  "$x\n";
    close CMDLOG;
  }

  system($x) == 0 or return 1;
  open EXEC_TMP, "$EXEC_FILE" or giveup "ERROR: failed to retrieve execution output\n";
  while (<EXEC_TMP>)
  {
    push @output, $_;
  }
  close EXEC_TMP;

  unlink "$EXEC_FILE" or giveup "ERROR: failed to cleanup execution output\n"; 

  return (0, @output);
}

sub usage
{
  print "$0 --source username\@host:folderpath --destination username\@host:folderpath\n";
  print "            --move         Move the data after the copy (optional).\n";
  print "            --noverify     Shut off some verification (optional).\n";
  print "            --forks num    The number of parallel copy processes.\n";
  print "            --preservetags Preserve the message tags.\n";
  print "            --verbose      Be more noisy.\n";
  print "\n";
  print "$0 does not currently support copying across hosts but that is fine.\n";
  print "\n";
  print "example usage:\n";
  print "  $0 --source restored_username\@mydomain.com:/Sent --destination username\@mydomain.com:/Sent --noverify --verbose --forks 16\n";
  print "\n";
  print "Warning:   No warranty!  Read the code yourself!\n";
  print "\n";
  print "\n";
}

# (1) aquire and verify command-line options
use Getopt::Long;
GetOptions("verbose!"=>\$verbose,            # optional flag verbose
           "source=s"=>\$source,             # source is mandatory
           "destination=s"=>\$destination,   # destination is mandatory
           "commandlog:s"=>\$commandlog,     # optional command log file
           "help!"=>\$help,                  # option help
           "forks:i"=>\$forks,               # optional number of forks
           "preservetags!"=>\$preservetags,  # optional flag preservetags
           "noverify!"=>\$noverify,          # optional flag noverify
           "move!"=>\$move);                 # optional flag move

if ($move) { print "WARNING: move enabled! source contents will be deleted upon successful copy!\n"; sleep 3 }
if ($ARGV[0])
{
  print "I didn't understand the following command-line options:\n";
  foreach (@ARGV) 
  {
    print "  $_\n";
  }
}

if ($forks > 1) { $NUM_FORKS = $forks }

if ($help) { usage ; quit 0 }
print "source:      $source\n" unless !$verbose;
print "destination: $destination\n" unless !$verbose;


# (2) split out and verify src/dst components
(my $source_user, my $source_host, my $source_path) = split /[\@\:]/, $source;
(my $destination_user, my $destination_host, my $destination_path) = split /[\@\:]/, $destination;
if ($source_user eq $destination_user) { $preservetags=1; }

print STDERR "DEBUG: $source_user, $source_host, $source_path\n" unless !$DEBUG;
print STDERR "DEBUG: $destination_user, $destination_host, $destination_path\n" unless !$DEBUG;


# (3) enumerate source target contents
($code, @output) = execute "$zmMAILBOX -z -m $source_user search -l $MAX_SEARCH -t message 'in:\"$source_path\"' 2>&1";
$code == 0 or giveup "ERROR: enumerate source target contents failure";
$i = 0; # source target message counter
foreach (@output)
{
  next unless (/  mess   /);
  $i++;
  #//s/^.*\. //;
  s/^.{1,4}\.\s+([\d ]{1,6}) /$1 /;
  push @source_id_list, (split / +/)[0];
}
print "zimbracopyfolder: source target contains $i ";
print "or more " unless $i <1000;
print "messages\n";
if ($i == $MAX_SEARCH)
{
  print "WARNING: It seems that, due to a limitation of zmmailbox, this program can\n";
  print "WARNING: only handle folders with a maximum of 1000 messages.\n"
}

# (4) construct destination target
($code, @output) = execute "$zmMAILBOX -z -m $destination_user getFolder $destination_path >/dev/null 2>&1";
if ($code != 0) 
{ 
  ($code, @output) = execute "$zmMAILBOX -z -m $destination_user createFolder $destination_path";
}
$code == 0 or giveup "ERROR: construct destination target failure";

# (5) test destination target for content
($code, @output) = execute "$zmMAILBOX -z -m $destination_user search -l 10 -t message 'in:\"$destination_path\"' 2>&1";
$code == 0 or giveup "ERROR: test destination target for content failure";
$i = 0; # destination target message counter
foreach (@output)
{
  next unless /  mess   /;    #/
  $i++;
  push @destination_list, $_;
}
if ($i>0)
{
  print STDERR "ERROR: destination target contains messages!\n";
  foreach (@destination_list)
  {
    print STDERR "$_" unless $DEBUG <2;
  }
  quit 1 unless $noverify;
}

# (6) export->import full messages
print "zimbracopyfolder: copying...\n";
my $num_queued=0;
my $num_dequeued=0;
foreach $id (@source_id_list)
{
  print " FORK QUEUE: \n@children\n" unless !$DEBUG;

  # (6.0) fork off up to $NUM_FORKS forks
   if ($#children+1 >= $NUM_FORKS)
      {
        # hold up, we got too many forks!
        $child = shift @children;
          { 
            print "   BLOCKING  -- $pid\n" unless !$DEBUG; 
            waitpid($child, 0);       # Hold off on further execution until this child exits
            $num_dequeued++;
            print "   DEQUEUED  <- $pid\n" unless !$DEBUG; 
            print "   DEQUEUED  $num_dequeued of ",$#source_id_list+1,"\n" unless !$verbose; 
            print " FORK QUEUE: \n@children\n" unless !$DEBUG;
          }
        #print "PUSH -> $pid\n";
      }
  print "   FORKED:   $pid\n" unless !$DEBUG;
  my $pid = fork();
  if ($pid)
    {
      # begin parent code
      # just make record and keep on moving
      push(@children, $pid);
      $num_queued++;
      print "   QUEUED   -> $pid to handle ($id)\n" unless !$DEBUG;
      print "   QUEUED   $num_queued of ",$#source_id_list+1,"\n" unless !$verbose;
      # end is parent code
    }
  else
    {
      # begin child code (the forked proc)
      $EXEC_FILE .= ".$$";
      print "   copy message ID='$id'; exec file = '$EXEC_FILE'\n" unless !$DEBUG;

      # (6.1) export content
      ($code, @output) = execute "$zmMAILBOX -z -m $source_user getMessage $id";
      if ($code != 0)
      {
        print STDERR "ERROR: export message failure:\n";
        foreach (@output) { print; }
        quit 1;
      }
      open TMPMSG, ">$TEMP_FILE.$$";
      foreach (@output) { print TMPMSG }
      close TMPMSG;
    
      # (6.2) export metadata
      ($code, @output) = execute "$zmMAILBOX -z -m $source_user getMessage --verbose $id";
      if ($code != 0)
      {
        print STDERR "ERROR: export metadata failure:\n";
        foreach (@output) { print; }
        quit 1;
      }

      my $metadata_date="";
      my $metadata_tags="";
      foreach (@output) 
        { 
          next unless /tags\":|receivedDate:/;
          chomp;       # trailing newline
          s/^\s+//;    # leading whitespace
          s/,$//;      # trailing comma
          if (/"tags":/) { ($nothing,$metadata_tags) = split / / }
          if (/receivedDate:/) { ($nothing,$metadata_date) = split / / }
          #print
        }
      if ($metadata_tags =~ /null/ || !$preservetags ) { $metadata_tags="" }
    
      # (6.3) import content and metadata
      if ($metadata_tags && $metadata_date) 
        {
          ($code, @output) = execute "$zmMAILBOX -z -m $destination_user addMessage --date $metadata_date --tags $metadata_tags $destination_path $TEMP_FILE.$$";
        }
      else
        {
          ($code, @output) = execute "$zmMAILBOX -z -m $destination_user addMessage --date $metadata_date $destination_path $TEMP_FILE.$$";
        }
              unlink "$TEMP_FILE.$$" or print "ERROR: failed to cleanup temporary message file\n";
              if ($code != 0)
              {
                print STDERR "ERROR: import messages failure:\n";
                foreach (@output) { print; }
        quit 1;
      }
      if ($move)
      {
        ($code, @output) = execute "$zmMAILBOX -z -m $source_user deleteMessage $id";
        if ($code != 0)
        {
          print STDERR "ERROR: delete messages failure:\n";
          foreach (@output) { print; }
          quit 1;
        }
      }
      exit 0
    }
}

foreach $child (@children) 
          { 
            print "   BLOCKING  -- $pid\n" unless !$DEBUG; 
            waitpid($child, 0);       # Hold off on further execution until this child exits
            print "   DEQUEUED  <- $pid\n" unless !$DEBUG; 
            print " FORK QUEUE: \n@children\n" unless !$DEBUG;
          }
print "zimbracopyfolder: done copying.\n";

# (7) test destination target again for content
($code, @output) = execute "$zmMAILBOX -z -m $destination_user search -l $MAX_SEARCH -t message 'in:\"$destination_path\"' 2>&1";
$code == 0 or giveup "ERROR: test destination target again for content failure";
$i = 0; # destination target message counter
foreach (@output)
{
  next unless /  mess   /;    #/
  $i++;
  push @destination_list, $_;
}
if ($i>0)
{
  print "zimbracopyfolder: destination target contains $i messages:\n";
  foreach (@destination_list) { print unless $DEBUG <2 } 
}
else
{
  print STDERR "ERROR: destination target contains $i messages!\n";
  quit 1;
}

quit 0;

Last edited by troy : 01-16-2008 at 04:07 PM.
Reply With Quote