#!/usr/local/bin/perl -w # convert a user from mbox to maildir # uses mb2md.pl # read carefully before using. # Phil Hollenback, hollenba@schrodinger.com use Getopt::Std; use File::stat; use File::Copy; use File::Temp qw/ tempfile /; use File::Path; use Term::ReadKey; use Quota; ### Variables # Note that a number of variables are defined in the main section of # the script below as they are based on computed values. $DateString=`date +%Y%m%d`; chop $DateString; $SMTPSERVER="smtp.example.com"; $IMAPSERVER="imap.example.com"; $MAILDOMAIN="example.com"; # see http://batleth.sapienti-sat.org/projects/mb2md/ $MbConvertScript="/usr/local/bin/mb2md-3.20.pl"; # use the user editor or default of vi. $EDITOR = $ENV{'EDITOR'} or $EDITOR="vi"; # percentage extra space we need in user quota. $QuotaOverhead = 10; ### End Variables # Subroutines # Make sure we have some slack under the user quota limit as # conversion from mbox to maildir makes quota usage grow slightly. sub CheckQuota{ my $UserHome = shift @_; my $UserId = shift @_; my $QuotaDev = ""; my $BlockCurrent = ""; my $BlockHard = ""; $QuotaDev = Quota::getqcarg($UserHome); ($BlockCurrent,$dummy,$BlockHard) = Quota::query($QuotaDev, $UserId); if ($BlockHard == 0){ # either no quotas in effect or unlimited quota. We can skip all this. print "no quota found, skipping rest of quota size check\n"; return 1; } print "$UserName is using $BlockCurrent blocks of a quota of $BlockHard blocks.\n"; print " "; print $BlockHard - $BlockCurrent . " blocks remaining.\n"; if($BlockCurrent + ($BlockCurrent * ($QuotaOverhead / 100)) > $BlockHard){ die("conversion to maildir will likely exceed user quota, thus I quit.\n"); } return 1; } # Make sure we aren't root and we have a valid username and uid. sub ValidUserOrExit{ my $UserId = $>; my $UserName = getpwuid($UserId); $UserId > 0 or die("don't run this script as root\n"); $UserName or die("failed to determine username\n"); return ($UserName,$UserId); } # Determine if we can read a given directory or file sub ReadableOrExit{ my $Item = shift @_; -r $Item or die("$Item not readable\n"); return 1; } # Check if a directory or file is writable. sub WritableOrExit{ my $Item = shift @_; -w $Item or die("$Item not writable\n"); return 1; } # Determine if a file is executable. sub ExecutableOrExit{ my $Item = shift @_; -x $Item or die("$Item not executable\n"); return 1; } # Get user home dir from system sub GetUserHome{ my $UserName = shift @_; ($dummy, $dummy, $dummy, $dummy, $dummy, $dummy, $dummy, $UserHome) = getpwnam($UserName) or die("can't get user home directory\n"); return $UserHome; } sub ValidUserHomeOrExit{ my $UserHome = shift @_; -d $UserHome or die("$UserHome not a directory\n"); WritableOrExit($UserHome); ReadableOrExit($UserHome); return 1; } # Archive existing mail for safekeeping. sub ArchiveMail{ my $UserName = shift @_; my $ArchiveDir="/tmp/$UserName"; # Note there is a race condition here. mkpath($ArchiveDir,0,0700); $ArchiveFile="$ArchiveDir/mail-backup-$UserName-$DateString.cpio.gz"; # make sure archive location exists WritableOrExit($ArchiveDir); print "archiving user mail to\n $ArchiveFile\n"; system("find $UserSpoolFile $UserMboxDir -print0 | cpio -Hcrc -o0 | gzip - >$ArchiveFile") == 0 or die("archive of old mail to $ArchiveFile failed\n"); # make sure randoms can't read backup mail. chmod 0600,$ArchiveFile; return $ArchiveDir; } # Move user mbox dir to archive area temporarily so we are sure to have enough space # in homedir for the conversion. sub MoveMbox{ my $UserMboxDir = shift @_; my $ArchiveDir = shift @_; my $MbConvertScript = shift @_; my $UserSpoolFile = shift @_; my $UserMaildir = shift @_; print "moving $UserMboxDir to $ArchiveDir/mail\n"; system("cd $UserMboxDir && find . -print0 | cpio -pd0 $ArchiveDir/mail") == 0 or die("cpio of $UserMboxDir to $ArchiveDir failed\n"); rmtree($UserMboxDir, 0, 1) or die("rm of $UserMboxDir failed\n"); # Begin the conversion! First, the spoolfile: system("$MbConvertScript -s $UserSpoolFile -d $UserMaildir") == 0 or die("$MbConvertScript failed converting $UserSpoolFile\n"); # Next, everything else. system("$MbConvertScript -R -s $ArchiveDir/mail") == 0 or die("$MbConvertScript failed converting $UserMboxDir\n"); return 1; } # Generate the password file entry for dovecot. sub GenDovecotPasswdEntry{ my $UserName = shift @_; my $UserHome = shift @_; my $UserGroupId = ""; my $UserId = ""; my $UserPasswd = ""; # Extract the user data from whatever authentication method you # use (passwd files, NIS, LDAP, etc). Assign values to # $UserGroupId, $UserId, and $UserPasswd. # Adjust the $UserPasswd entry depending on what format you use. # For example, use {plain} for plaintext and {md5} for regular # linux passwd file entries. See # http://wiki.dovecot.org/AuthDatabase/PasswdFile for details. print "add the following entry to dovecot.passwd:\n"; print "$UserName:{crypt}$UserPasswd:$UserId:${UserGroupId}::$UserHome::userdb_mail=maildir:~/Maildir\n"; return 1; } # Check if a .procmail file exists and/or create a new one with # appropriate templates. sub ProcessProcmail{ my $ProcmailFile = shift @_; my $UserHome = shift @_; my ($TempProcmailFH,$TempProcmailFile) = tempfile() or die("failed to open temporary procmail file\n"); if ( ! -f "$ProcmailFile" ){ print "User .procmailrc not found. Creating default one.\n"; CreateTempProcmail($UserHome,*$TempProcmailFH); }else{ # Make a backup of old .procmailrc, just in case. print "existing .procmailrc found, backing up to"; print " $ProcmailFile.$DateString\n"; copy("$ProcmailFile","$ProcmailFile.$DateString") or die("failed to backup old user .procmailrc\n"); # Add new stuff to existing .procmailrc ConvertProcmail($UserHome,*$TempProcmailFH,$ProcmailFile); } print "press enter to edit new $ProcmailFile\n"; ReadKey 0; close $TempProcmailFH; # Give user an opportunity to edit the resulting file. system("$EDITOR $TempProcmailFile") == 0 or die("problem editing $TempProcmailFile\n"); copy($TempProcmailFile,$ProcmailFile) or die("failed to copy $TempProcmailFile over $ProcmailFile\n"); unlink $TempProcmailFile; # Make sure .procmail has correct ownership and permissions. chmod 0644,"$ProcmailFile"; # Make sure we have correct procmail dirs to match new .procmailrc. mkpath("$UserHome/.procmail/backup",0,0700); WritableOrExit("$UserHome/.procmail/backup"); # make sure only user can read their backup messages. chmod 0700,"$UserHome/.procmail/backup"; return 1; } # Create a temporary .procmail file for user if one doesn't already exist. # This will be copied over to the user home dir later. sub CreateTempProcmail{ my $UserHome = shift @_; my $TempProcMailFH = shift @_; print $TempProcMailFH <){ print $TempProcmailFH $_; } close PROCMAILFILE; print $TempProcmailFH <){ if (s/^(smtp-server=.*)/#$1/){ print $TempPineRc $_; print $TempPineRc "smtp-server=$SMTPSERVER\n"; $FoundServer=1; } elsif (s/^(inbox-path=.*)/#$1/){ print $TempPineRc $_; print $TempPineRc "inbox-path={$IMAPSERVER/ssl/user=$UserName}INBOX\n"; $FoundInbox=1; } elsif (s/^(user-domain=.*)/#$1/){ print $TempPineRc $_; print $TempPineRc "user-domain=$MAILDOMAIN\n"; $FoundUserDomain=1; } elsif (s/^(folder-collections=.*)/#$1/){ print $TempPineRc $_; print $TempPineRc "folder-collections={$IMAPSERVER/ssl/user=$UserName}\${IMAPHOME}\[\]\n"; $FoundCollection = 1; } elsif (s/^(default-fcc=.*)/#$1/){ print $TempPineRc $_; print $TempPineRc "default-fcc={$IMAPSERVER/ssl/user=$UserName}Sent\n"; $FoundDefaultFcc = 1; } elsif (s/^(default-saved-msg-folder=.*)/#$1/){ print $TempPineRc "default-saved-msg-folder={$IMAPSERVER/ssl/user=$UserName}Saved\n"; $FoundSaved = 1; } elsif (s/^(postponed-folder=.*)/#$1/){ print $TempPineRc "postponed-folder={$IMAPSERVER/ssl/user=$UserName}Drafts\n"; $FoundPostponed = 1; } else{ print $TempPineRc $_; } } close PINERC; print $TempPineRc "\n# mods for mbox conversion (if any) below this line\n"; $FoundServer or print $TempPineRc "smtp-server=$SMTPSERVER\n"; $FoundInbox or print $TempPineRc "inbox-path={$IMAPSERVER/ssl/user=$UserName}INBOX\n"; $FoundCollection or print $TempPineRc "folder-collections={$IMAPSERVER/ssl/user=$UserName}\${IMAPHOME}\[\]\n"; $FoundDefaultFcc or print $TempPineRc "default-fcc={$IMAPSERVER/ssl/user=$UserName}Sent\n"; $FoundSaved or print $TempPineRc "default-saved-msg-folder={$IMAPSERVER/ssl/user=$UserName}Saved\n"; $FoundPostponed or print $TempPineRc "postponed-folder={$IMAPSERVER/ssl/user=$UserName}Drafts\n"; $FoundUserDomain or print $TempPineRc "user-domain=$MAILDOMAIN\n"; # reset file pointer on temporary and copy temporary over final. # Do this instead of a filesystem copy to preserve permissions, etc. seek($TempPineRc,0,0); open(PINERC,">$PineRc") or die("failed to open $PineRc for writing\n"); while(<$TempPineRc>){ print PINERC; } close PINERC; close $TempPineRc; return 1; } # Convert the subscription file by doing some text munging and making # sure certain folders are pre-subscribed. sub ConvertSubscriptions { my $ArchiveDir = shift @_; my $UserMaildir = shift @_; my $OldSubFile="$ArchiveDir/mail/.subscriptions"; my $NewSubFile="$UserMaildir/subscriptions"; if (! -r $OldSubFile){ # There doesn't seem to be an old subscription file. Create # one to keep everyone happy. print "creating empty subscription file\n"; open(OLDSUBFILE, ">$OldSubFile") && close OLDSUBFILE; } open(OLDSUBFILE,"<$OldSubFile") or die("failed to open $OldSubFile for reading\n"); open(NEWSUBFILE,">$NewSubFile") or die("failed to open $NewSubFile for writing\n"); # Loop through the file and change it, tracking if we find a Junk # folder subscription. $FoundJunk = 0; while(){ m|^Junk$| and $FoundJunk = 1; s/\./_/g; s/\//./g; print NEWSUBFILE; } close OLDSUBFILE; # If the spam folder isn't subscribed, add it. if (! $FoundJunk){ print NEWSUBFILE "Junk\n"; } close NEWSUBFILE; return 1; } # Main getopts('hv',\%opts) or die("this script takes no arcuments\n"); if ( $opts{h} ) { die("this script takes no arguments\n"); } if ($#ARGV > 0 ) { die("this script takes no arguments\n"); } # Make sure this is a valid user. ($UserName,$UserId) = ValidUserOrExit(); # Make sure we have the perl conversion script. -x $MbConvertScript or die("can't find mbox to maildir conversion script\n"); # Get the user homedir and perform various sanity checks. $UserHome=GetUserHome($UserName); ReadableOrExit($UserHome); WritableOrExit($UserHome); $UserMboxDir="$UserHome/mail"; ReadableOrExit($UserMboxDir); $UserSpoolFile="/var/mail/$UserName"; ReadableOrExit($UserSpoolFile); $UserMaildir="$UserHome/Maildir"; # Purposefully fail if $UserMaildir already exists. mkdir $UserMaildir or die("failed to create $UserMaildir\n"); $ProcmailFile="$UserHome/.procmailrc"; # Time to make the doughnuts... # Are we at risk of going over-quota with this conversion? CheckQuota($UserHome,$UserId); # Convert (or create) user .procmail first so that new mail starts # being delivered to maildir before we do anything else. ProcessProcmail($ProcmailFile,$UserHome); # Ok now we should have a working .procmailrc that is delivering to # the user's $HOME/Maildir. Now it is time to archive and convert the # old mail. $ArchiveDir = ArchiveMail($UserName); # now that we have a backup of the user mail we need to move old # UserMboxDir to the archive area and run the conversion from there. # This protects us from running over quota during the conversion. MoveMbox($UserMboxDir,$ArchiveDir,$MbConvertScript,$UserSpoolFile,$UserMaildir); # The mail is converted. Happy, joy. Next, convert the # subscription file ConvertSubscriptions($ArchiveDir,$UserMaildir); print "\n"; # Remove the old spoolfile as we are no longer using it. print "removing old spool file $UserSpoolFile\n"; unlink $UserSpoolFile; # Convert user .pinerc, if it exists. ConvertPinerc($UserHome); # Generate the dovecot user line for display. GenDovecotPasswdEntry($UserName,$UserHome); print "Don't forget your archived mail in\n $ArchiveFile\n"; rmtree("$ArchiveDir/mail", 0, 1); print "all done at `date`";