#! /usr/bin/perl -w
#
# Maildir++2IMAPdir.pl (v0.2) - convert Maildir++ depots to IMAPdir depots
# Copyright (C) 2004  Henry Baragar mailto:Henry.Baragar@Instantiated.ca
# 
# 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., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
#
# WARNING:
#	All activity on the Maildir must be stopped before running this
#	script.  MAIL MAY BE LOST if you do not.  This includes both MTA's
#	(e.g. qmail, postfix) and MUA's (e.g. bincimap).
#
# WARNING:
#	There is no attempt to convert .qmail* files, .mailfilter or any 
# 	other files that affect mail delivery:  that is YOUR RESPONSIBILITY.
#
# This script converts a Maildir++ depot to an IMAPdir depot.  It is 
# invoked as follows:
#	Maildir++2IMAPdir Maildir IMAPdir
# where "-d" is used for printing debuggin information.
# Running this script produces the following directories:
#	Maildir.bak	- The original Maildir (untouched)
#	IMAPdir		- the new IMAPdir mail depot 
#	Maildir		- a new diretory with links into IMAPdir
# The purpose of the new Maildir directory is allow the conversion of 
# .qmail* and related scripts to be postponed until a less stressful time.
#
# The Maildir.bak directory can be deleted once you are comfortable that the
# IMAPdir is operational.  The Maildir directory can be removed once you are
# comfortable that you have converted all the mail delivery files.
#
# NOTE:
#	When run invoked from root, this script changes ID to the
#	owner of the IMAPdir as soon as possible.  That is, most
#	of the script is not run as root.
#

#
# We start by making sure that we are called correctly.
# Note that "Maildir" and "IMAPdir" will be supplied if they are missing.
#
$USAGE = "Usage: $0 [-d] [Maildir [IMAPdir]]\n";
$DEBUG = shift if @ARGV && $ARGV[0] eq "-d";
die $USAGE if grep /^-/, @ARGV;
$Maildir = shift || "Maildir";
$IMAPdir = shift || "IMAPdir";
die $USAGE if $ARGV[0];

#
# Make sure the environment is ready for conversion:
# - drop root privileges
# - make sure we don't clobber anything
# - make sure that we don't get messed up by a previous run that was aborted
#
@stats  = stat $Maildir or die "Can't stat $Maildir: $!";
$)=$stats[5];			# Change gid  (when root) 
$>=$stats[4];			# Change user (must do after setgid)
$mode = 07777 & $stats[2];
maildir("$Maildir")		or  die "$Maildir not a Maildir";
-e $IMAPdir			and die "$IMAPdir exists";
$Maildir_bak = "$Maildir.bak";
-e $Maildir_bak 		and die "$Maildir_bak exists";
$SHADOWdir = "$Maildir-conv-$$";
-e $SHADOWdir 			and die "$SHADOWdir exists";

#
# Find the source folders that need migrating and determine their targets.
# NB.  Linked folders need to be migrated after real folders since we might
#      need to create a real folder before creating the link 
#
opendir MAILDIR, $Maildir;
for my $folder (readdir MAILDIR) {
    next if $folder =~ /^[.]{2}$/;		# skip ".."
    next unless $folder =~ /^[.]/;
    my($source) = "$Maildir/$folder";
    my($target) = $IMAPdir."/".mapname($folder);
    next unless maildir($source);
    if (-l $source)	{ $linked{$source}  = $target; }
    else		{ $IMAPdir{$source} = $target; }
    $SHADOW{$target} = "$SHADOWdir/$folder";
    }
close MAILDIR;
if ($DEBUG) {
    print "IMAPdirs:\n";  print "\t$_ -> $IMAPdir{$_}\n" for sort keys %IMAPdir;
    print "Links:\n";     print "\t$_ <- $linked{$_}\n" for sort keys %linked;
    print "Shadow:\n";    print "\t$_ <- $SHADOW{$_}\n" for sort keys %SHADOW;
    }

#
# Migrate the folders, create the links and copy the .subscribe 
#
mkdir $SHADOWdir 		or  die "Can't make $SHADOWdir: $!";
mkdir $IMAPdir			or  die "Can't make $IMAPdir: $!";
migratefolder($_,$IMAPdir{$_}) for keys %IMAPdir;
linkfolder($_,$linked{$_}) for keys %linked;
symlink(abspath($_),$SHADOW{$_}) for keys %SHADOW;
symlink(abspath("$IMAPdir/INBOX/$_"),"$SHADOWdir/$_") for ("tmp","new","cur");
cpfiles(<$Maildir/.subscribed>,$IMAPdir);
chmod $mode, $SHADOWdir		or  die "Can't chmod $mode $SHADOWdir: $!";
chmod $mode, $IMAPdir		or  die "Can't chmod $mode $IMAPdir: $!";

#
# Success! (since we would have died if we had an error)
#
rename $Maildir, $Maildir_bak;
rename $SHADOWdir, $Maildir;

#
# maildir returns true if the suplied directory looks like IMAPdir
#
sub maildir {
    my ($dir) = shift;
    return unless -d $dir;
    return unless -d "$dir/tmp" && -d "$dir/new" && -d "$dir/cur";
    return $dir;
    }

#
# mapname maps a Maildir++ folder name to a IMAPdir folder name
#
sub mapname {
    my ($folder) = shift;
    return "INBOX" if $folder =~ /^[.]?$/;
    return "INBOX$folder" if $folder =~ /^[.]/;
    return "BOTCHED.$folder";
    }
#
#
# migratefolder migrates a Maildir folder from a Maildir++ depot to
# an IMAPdir depot
#
# Note that linkfolder should be used if the Maildir++ Maildir is a
# symbolic link
#
# Note that we link data files to preserve space (since they do not change).
#
sub migratefolder {
    my ($source,$target) = @_;
    mkdir $target || die "Can't create $target: $!";
    for my $subdir (("tmp","cur","new")) {
	my $tsub = "$target/$subdir";
	my $ssub = "$source/$subdir";
	mkdir $tsub or die "Can't create $tsub: $!";
	opendir SUBDIR, $ssub;
	for $file (readdir SUBDIR) {
	    next if $file =~ /^[.]{1,2}$/;		# skip "." and ".."
	    my ($sfile) = "$ssub/$file";
	    my ($tfile) = "$tsub/$file";
	    next unless -f $sfile;
	    link($sfile, $tfile) or die "Can't create link $sfile $tfile: $!";
	    }
	closedir SUBDIR;
	copyperms($ssub,$tsub);
	}
    cpfiles(bincfiles($source), "$target/");
    copyperms($source,$target);
    }

#
# copyperms copies the perms of one maildir to another
#
sub copyperms {
    my ($template,$target) = @_;
    my ($mode)  = 0777& (stat $template)[2] or die "stat'ing $template: $!";
    chmod $mode, $target;
    return if -l $target;
    chmod $mode, "$target/tmp", "$target/new", "$target/cur";
    }

#
# linkfolder
#
# Doing the right thing for symbolic links is tricky.
# There are three cases:
#   1.  The link points to another folder in the source's parents directory 
#	(i.e. the Maildir); in which case we need to make it point to the
#	new folder in the IMAPdir directory
#   2.	The link points to a subsubdirectory (or deeper) of the source's 
#	parent directory; in which case we need to copy the directory to
#	the IMAPdir directory (effectively eliminating the link)
#   3.	The link points somewhere else outside the source's parent directory;
#       in which case we simply make a new link to point to that directory
#
sub linkfolder {
    my ($source,$target) = @_;
    my ($abspath) = abspath($source);
    my ($sparent) = "$source/";
    $sparent =~ s{[^/]*/$}{};
    $sparent = abspath($sparent);
    print "$source->$target:\n\t$abspath\n\t$sparent\n\t" if $DEBUG;
    if ($abspath =~ m{^$sparent}) {
        my($relpath) = $abspath =~ m{^$sparent/(.*)};
	$relpath = "." unless $relpath;
	print "$relpath\n\t" if $DEBUG;
	if ($relpath eq "" or $relpath =~ m{^[.][^/]*$}) {
	    print "  case 1  \n" if $DEBUG;
	    symlink mapname($relpath)||"." , $target;
	    }
	else {
	    print "  case 2  \n" if $DEBUG;
	    migratefolder($source,$target);
	    }
	}
    else {
	print "  case 3  \n" if $DEBUG;
	symlink $abspath, $target;
	}
    }

#
# abspath determines the absolute path of a path, particular one that is 
# a symbolic link
#
sub abspath {
    my ($path) = @_;
    my ($cwd) = `pwd`;
    chomp $cwd;
    chdir $path or die "Can't cd to $path: $!";
    my ($abspath) = `pwd`;
    chomp $abspath;
    chdir $cwd or die "Can't cd to $cwd: $!";
    return $abspath;
    }

#
# cpfiles just calls the system cp
#
sub cpfiles {
    return if @_ < 2;
    system "cp", "-p", @_;
    }

#
# binfiles returns the list of binc* files in the supplied directory
#
sub bincfiles {
    my ($dir) = shift;
    return unless -d $dir;
    opendir DIR, $dir;
    my @bincfiles = grep /^binc/, readdir DIR;
    close DIR;
    return grep {s{^}{$dir/}} @bincfiles;
    }