The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#!/usr/local/bin/perl
#$Id$
#
# An example of how to migrate from a Netscape server
# (which uses a slash as a separator and which does
# not allow subfolders under the INBOX, only next to it)
# to a Cyrus server (which uses a dot (.) as a separator
# and which requires subfolders to be under "INBOX").
# There are also some allowed-character differences taken
# into account but this is by no means complete AFAIK.
#
# This is an example. If you are doing mail migrations 
# then this may in fact be a very helpful example but
# it is unlikely to work 100% correctly as-is. 
# A good place to start is by testing a rather large-volume
# transfer of actual mail from the source server with the
# -v option turned on and redirect output to a file for
# perusal. Examine the output carefully for unexpected
# results, such as a number of messages being skipped because
# they're already in the target folder when you know darn
# well this is the first time you ran the script. This
# would indicate an incompatibility with the logic for
# detecting duplicates, unless for some reason the source 
# mailbox contains a lot of duplicate messages to begin with.
# (The latter case is an example of why you should use an
# actual mailbox stuffed with actual mail for test; if you
# generate test messages and then test migrating those you
# will only prove that your test messages are migratable.
# 
# Also, you may need to play with the rules
# for translating folder names based on what kind of
# names your target server and source server support.
#
# You may also need to play with the logic that determines
# whether or not a message has already been migrated, 
# especially if your source server has messages that 
# did not come from an SMTP gateway or something like that.
#
# Some servers allow folders to contain mail and subfolders,
# some allow folders to only contain either mail or subfolders.
# If you are migrating from a "mixed use" type to a "single use"
# type server then you'll have to figure out how to deal 
# with this. (This script deals with this by creating folders like
# "/blah_mail", "/blah/blah_mail", and "/blah/blah/blah_mail"
# to hold mail if the source folder contains mail and subfolders
# and the target server supports only single-use folders. 
# You may not choose a different strategy.)
#
# Finally, it's possible that in some server-to-server
# copies, the source server supports messages that the
# target server considers unacceptable. For example, some
# but not all IMAP servers flat out refuse to accept 
# messages with "base newlines", which is to say messages
# whose lines are match the pattern /[^\r]\n$/. There is
# no logic in this script that deals with the situation;
# you will have to identify it if it exists and figure
# out how you want to handle it.
# 
# This is probably not an exhaustive list of issues you'll
# face in a migration, but it's a start.
#
# If you're just migrating from an old version to a newer
# version of the same server then you'll probably have
# a much easier time of it.
#
#

use Mail::IMAPClient;
use Data::Dumper;
use IO::File;
use File::Basename ;
use Getopt::Std;
use strict;
use vars qw/ 	$opt_B $opt_D $opt_T $opt_U
		$opt_W $opt_b $opt_d $opt_h
		$opt_t $opt_u $opt_w $opt_v
		$opt_s $opt_S $opt_W $opt_p
		$opt_P $opt_f $opt_F $opt_m 
		$opt_M
/;

getopts('vs:S:u:U:dDb:B:f:F:w:W:p:P:t:T:hm:M:');

if ( $opt_h ) {
	print STDERR <<"HELP";

$0 - 	an example script demonstrating the use of the Mail::IMAPClient's
	migrate method.

Syntax:
	$0 -s source_server -u source_user -w source_password -p source_port \
	   -d debug_source -f source_debugging_file -b source_buffsize       \
	   -t source_timeout -m source_auth_mechanism                        \
	   -S target_server -U target_user -W target_password -P target_port \
	   -D debug_target -F target_debugging_file -B target_buffsize       \
	   -T target_timeout -M target_auth_mechanism                        \
	   -v

where "source" refers to the "copied from" mailbox, target is the 
"copied to" mailbox, and -v turns on verbose output.
Authentication mechanisms default to "PLAIN".

HELP
	exit;
}
$opt_v and ++$|;
print "$0: Started at ",scalar(localtime),"\n" if $opt_v;

$opt_p||=143;
$opt_P||=143;

# Make a connection to the source mailbox:
my $imap = Mail::IMAPClient->new(
				Server  => $opt_s,
				User    => $opt_u,
				Password=> $opt_w,
				Uid	=> 1,	
				Port	=> $opt_p,	
				Debug	=> $opt_d||0,
				Buffer	=> $opt_b||4096,
				Fast_io	=> 1,
				( $opt_m ? ( Authmechanism => $opt_m) : () ),
				Timeout	=> $opt_t,	 
				($opt_f ? ( Debug_fh=>IO::File->new(">$opt_f" )) : ()),
) or die "$@";

# Make a connection to the target mailbox:
my $imap2 = Mail::IMAPClient->new(	
				Server  => $opt_S,
				User    => $opt_U,
				Password=> $opt_W,
				Port	=> $opt_P,
				Uid	=> 1,	
				Debug	=> $opt_D||0,
				( $opt_M ? ( Authmechanism => $opt_M) : () ),
				($opt_F ? ( Debug_fh=>IO::File->new(">$opt_F")) : ()),
				Buffer	=> $opt_B||4096,
				Fast_io	=> 1,
				Timeout	=> $opt_T,			 # True value
) or die "$@";

# Turn off buffering on debug files:
$imap->Debug_fh->autoflush;
$imap2->Debug_fh->autoflush;

# Get folder hierarchy separator characters from source and target:
my $sep1 = $imap->separator;
my $sep2 = $imap2->separator;

# Find out if source and target support subfolders inside INBOX:
my $inferiorFlag1 = $imap->is_parent("INBOX");
my $inferiorFlag2 = $imap2->is_parent("INBOX");

# Set up a test folders to see if the source and target support mixed-use
# folders (i.e. folders with both subfolders and mail messages):
my $testFolder1 = "Migrate_Test_$$" ;		# Ex: Migrate_Test_1234
$testFolder1 = $inferiorFlag2 ? 
		"INBOX" . $sep2 . $testFolder1  : 
		$testFolder1 ;

# The following folder will be a subfolder of $testFolder1:
my $testFolder2 = "Migrate_Test_$$" . $sep2 . "Migrate_test_subfolder_$$" ;
$testFolder2 = $inferiorFlag2 ? "INBOX" . $sep2 . $testFolder2  : $testFolder2 ;

$imap2->create($testFolder2) ;	# Create the subfolder first; RFC2060 dictates that
				# the parent folder should be created at the same time


# The following line inspired the selectable method.  It was also made obsolete by it,
# but I'm leaving it as is to demonstrate use of lower-level method calls:
my $mixedUse2 = grep(/NoSelect/i,$imap2->list("",$testFolder1))? 0 : 1;

# Repeat the above with the source mailbox:
$testFolder2 = "Migrate_Test_$$" . $sep1 . "Migrate_test_subfolder_$$" ;
$testFolder2 = $inferiorFlag1 ? "INBOX" . $sep1 . $testFolder1  : $testFolder1 ;

$imap->create($testFolder2) ;

my $mixedUse1 = grep(/NoSelect/i,$imap->list("",$testFolder1))? 0 : 1;

print 	"Imap host $opt_s:$opt_p uses a '$sep1' as a separator and ",
	( defined($inferiorFlag1) ? "allows " : "does not allow "), 
	"children in the INBOX. It supports ",
	($mixedUse1?"mixed use ":"single use "), "folders.\n" if $opt_v;

print 	"Imap host $opt_S:$opt_P uses a '$sep2' as a separator and ",
	( defined($inferiorFlag2) ? "allows " : "does not allow "), 
	"children in the INBOX. It supports ",
	($mixedUse2?"mixed use ":"single use "), "folders.\n" if $opt_v;

for ($testFolder1,$testFolder2) {$imap->delete($_); $imap2->delete($_);}

my($totalMsgs, $totalBytes) = (0,0);

# Now we will migrate the folder. Here we are doing one message at a time
# so that we can do more granular status reporting and error checking.
# A lazier way would be to do all the messages in one migrate method call
# (specifying "ALL" as the message number) but then we wouldn't be able
# to print out which message we were migrating and it would be a little 
# bit tougher to control checking for duplicates and stuff like that.
# We could also check the size of the message on the target right after 
# the migrate as an extra safety check if we wanted to but I didn't bother
# here. (I saved as an exercise for the reader. Yeah! That's it! An exercise!)

# Iterate over all the folders in the source mailbox:
for my $f ($imap->folders) { 
	# Select the folder on the source side:
	$imap->select($f) ; 

	# Massage the foldername into an acceptable target-side foldername:
	my $targF = "";
	my $srcF = $f;
	$srcF =~ s/^INBOX$sep1//i;
	if ( $inferiorFlag2 ) {
		$targF = $srcF eq "INBOX" ? "INBOX" : "INBOX.$f" ;
	} else {
		$targF = $srcF ;
	}

	$targF =~ s/$sep1/$sep2/go unless $sep1 eq $sep2;
	$targF =~ tr/#\$\& '"/\@\@+_/;
	if ( $imap->is_parent($f) and !$mixedUse2 ) {
		$targF .= "_mail" ;
	}
	print "Migrating folder $f to $targF\n" if $opt_v;

	# Create the (massaged) folder on the target side:
	unless ( $imap2->exists($targF) ) {
		$imap2->create($imap2->Massage($targF))
			or warn "Cannot create $targF on " . $imap2->Server . ": $@\n" and next;
	}

	# ... and select it
	$imap2->select($imap2->Massage($targF))  
			or warn "Cannot select $targF on " . $imap2->Server . ": $@\n" and next;

	# now that we know the target folder is selectable, we can close it again:
	$imap2->close; 
	my $count = 0;
	my $expectedTotal = $imap->message_count($f) ;

	# Now start iterating over all the messages on the source side...
	for my $msg ($imap->messages) {
		++$count; 
		my $h = "";
		# Get some basic info about the message:
		eval { $h = ($imap->parse_headers($msg,"Message-id")||{})->{'Message-id'}[0]};
		my $tsize = $imap->size($msg);
		my $ret = 0 ; my $h2 = [];

		# Make sure we didn't already migrate the message in a previous pass:
		$imap2->select($targF);
		if ( 	$tsize and $h and $h2 = $imap2->search( 
					HEADER 	=> 'Message-id'	=> $imap2->Quote($h),
					NOT 	=>  SMALLER 	=> $tsize, 
					NOT	=>  LARGER	=> $tsize
			)
		) {
			print  	
				"Skipping $f/$msg to $targF. ",
				"One or more messages (" ,join(", ",@$h2),
				") with the same size and message id ($h) ",
				"is already on the server. ",
				"\n" 
			if $opt_v;
			$imap2->close;

		} else {

			print  	
				"Migrating $f/$msg to $targF. ",
				"Message #$count of $expectedTotal has ",
				$tsize , " bytes.",
				"\n" if $opt_v;
			$imap2->close;

			# Migrate the message:
			my $ret = $imap->migrate($imap2,$msg,"$targF") ;
			$ret and ( $totalMsgs++ , $totalBytes += $tsize);
			$ret or warn "Cannot migrate $f/$msg to $targF on " . $imap2->Server . ": $@\n" ;
		}
	}
}

print "$0: Finished migrating $totalMsgs messages and $totalBytes bytes at ",scalar(localtime),"\n" 
	if $opt_v;
exit;


=head1 AUTHOR 
	
David J. Kernen

The Kernen Group, Inc.

imap@kernengroup.com

=head1 COPYRIGHT

This example and Mail::IMAPClient are Copyright (c) 2003 
by The Kernen Group, Inc. All rights reserved.

This example is distributed with Mail::IMAPClient and 
subject to the same licensing requirements as Mail::IMAPClient.

imtest is a utility distributed with Cyrus IMAP server, 
Copyright (c) 1994-2000 Carnegie Mellon University.  
All rights reserved. 

=cut

#$Log: migrate_mail2.pl,v $
#Revision 19991216.4  2003/06/12 21:38:33  dkernen
#
#Preparing 2.2.8
#Added Files: COPYRIGHT
#Modified Files: Parse.grammar
#Added Files: Makefile.old
#	Makefile.PL Todo sample.perldb
#	BodyStructure.pm
#	Parse.grammar Parse.pod
# 	range.t
# 	Thread.grammar
# 	draft-crispin-imapv-17.txt rfc1731.txt rfc2060.txt rfc2062.txt
# 	rfc2221.txt rfc2359.txt rfc2683.txt
#