#!/usr/bin/perl # Licence: GPL 2006, Samage # Author: Mart van Santen # Latest version on: http://samage.net/dev/ # # This scrips converts maildir boxes to cyrus boxes, # please BE VERY CAREFULL WITH EXECUTION OF THIS SCRIPT, # it is made for our internal use and some options # are a littlebit dangerous. # # What it does is this: # # - Takes a Maildir box en copies its email to a cyrus box # - Read flags from user Maildir boxes and write them in # the cyrus database # - Creates cyrus mailboxes # # Some code works on the cyrus databases directly, without # using the cyrus commands. Please be very carefull if # you are running an other version then we did # (that was, 2.2.13-8 from debian etch/testing) # It is possible that the database format has been changed. # # Also be sure cyrus is not running when converting # # It alters these files directly (some files also indirectly): # 1. /var/lib/cyrus/mailboxes.db (cyrus skiplist file) # 2. /var/lib/cyrus/user/$char/$user.seen (skiplist file) # 3. /var/lib/cyrus/user/$char/$user.sub (plaintext file) # 4. /var/spool/cyrus/mail/$char/user/$mailbox/cyrus.header # 5. /var/spool/cyrus/mail/$char/user/$mailbox/cyrus.index # # This is what is done: # For file 1. Add user mailboxes and subboxes # For file 2. Write list of 'seen' messages # For file 3. Write list of subscribed mailboxes # For file 4. Read mailbox id & add custom flags # For file 5. Write per message system & custom flags # # ALL FILES CREATED BY THIS SCRIPT ARE WORLD READABLE/WRITABLE, # CHANGE FILE MODES AFTER CONVERSION # # Thanks to & parts used from: # http://www.codepoets.co.uk/docs/courier2cyrus (David Goodwin) # http://loophole.morpheus.net/linux/ (index_dump_pl.txt) # # This file is distributed under GPL, please send all # changes/additions to me. # # usage: maildir2cyrus maildir_username cyrus_username # # be sure cyrus is NOT running when executing # Usefull include for debugging use Data::Dumper; # More output, use fast to $DEBUG = 1; $FAST = 0; $user = $ARGV[0]; $to = $ARGV[1]; $c = substr($to, 0, 1); ##################################################### # Change these items according your local config $source = "/mail/$user/Maildir"; $destination = "/mail/cyrus/mail/$c/user/$to"; $CYRUS_UID = 501; $CYRUS_GID = 8; $CYRUS_USER = "cyrus"; $CYRUS_RECONSTRUCT = "/usr/sbin/cyrreconstruct"; $CYRUS_DBCONVERT = "/usr/sbin/cvt_cyrusdb"; $CYRUS_ROOT = "/var/lib/cyrus"; # End: Local Config ##################################################### print "Handling user: $user => $to\n"; # Some ugly global, used to map maildirfile to their # cyrus index number %map; # Handel all handleall($source, $destination, $to, $to); # Main function, takes a userbox and convert it, # also handle subfolders sub handleall { my $src = $_[0]; my $dst = $_[1]; my $user = $_[2]; my $user2 = $_[3]; my %flags; my $c = 1; # Flush GLOBAL map undef(%map); $c = 1; $t = 1; print " * Processing mainfolder:\n" if $DEBUG; # Create userbox for this user createmb($user2, ""); # Create destination dir $tmp = $dst; $tmp =~ s/'/'\\''/g; system("mkdir -p '$tmp'"); chown $CYRUS_UID, $CYRUS_GID, $dst; chmod 0777, $dst; my ($uid_validity, $uid_last, $uid_map)=getuiddb($src); # Handele cur/new dirs of maildir $c = handledir("$src/cur",$dst, $c, $uid_map); $c = handledir("$src/new",$dst, $c, undef); # Reconstruct folder system("su $CYRUS_USER -c '$CYRUS_RECONSTRUCT user.$user'"); # Add userflags $userflags = getuserflags($src); my $mailbox_id = handlecyrusheader($dst, $userflags); # Handle index: write flags handlecyrusindex($dst,$uid_validity,$c); # Handle seen database handlecyrusseen($mailbox_id, $user2); # Read subdits opendir(DIR, $src); @files = readdir(DIR); closedir(DIR); $t = ($c - 1); foreach $dir (@files) { next if $dir eq "."; next if $dir eq ".."; next if (! -d "$src/$dir"); next if (substr($dir, 0, 1) ne "."); print " * Processing subfolder: $dir\n" if $DEBUG; $c = 1; undef(%map); $src2 = $src . "/$dir"; my ($uid_validity, $uid_last, $uid_map)=getuiddb($src2); $dir = substr($dir, 1); $dst_dir = $dir; $folder = $dir; $dst_dir =~ s/\./\//g; $dst2 = $dst . "/" . $dst_dir; $tmp = $dst2; $tmp =~ s/'/'\\''/g; system("mkdir -p '$tmp'"); chmod 0777, $dst2; chown $CYRUS_UID, $CYRUS_GID, $dst2; createmb($user, $folder); $c = handledir("$src2/cur",$dst2, $c, $uid_map); $c = handledir("$src2/new",$dst2, $c, undef); $t += ($c - 1); $userflags = getuserflags($src2); $folder =~ s/'/'\\''/g; open(OUT, ">/tmp/cyrus-convert.sh"); print OUT "#!/bin/sh\n"; print OUT "$CYRUS_RECONSTRUCT 'user.$user.$folder'\n"; close(OUT); chmod 0777, "/tmp/cyrus-convert.sh"; system("su $CYRUS_USER -c /tmp/cyrus-convert.sh"); my $mailbox_id = handlecyrusheader($dst2, $userflags); print "MAILBOX: $mailbox_id\n"; handlecyrusindex($dst2,$uid_validity,$c); handlecyrusseen($mailbox_id, $user); } # New, add junk mailbox to every account createmb($user, "Spam"); system("mkdir -p $dst/Spam"); chmod 0777, "$dst/Spam"; chown $CYRUS_UID, $CYRUS_GID, "$dst/Spam"; open(OUT, ">/tmp/cyrus-convert.sh"); print OUT "#!/bin/sh\n"; print OUT "$CYRUS_RECONSTRUCT 'user.$user.Spam'\n"; close(OUT); system("su $CYRUS_USER -c /tmp/cyrus-convert.sh"); updatesubscriptions($user, $src); print " * Handled $t mails, finished\n"; } sub updatesubscriptions { my $user = $_[0]; my $src = $_[1]; if (! -f "$src/courierimapsubscribed") { return; } print " * Updating subscribtions\n" if $DEBUG; $l = substr($user, 0, 1); open(IN, "<$src/courierimapsubscribed"); open(OUT, ">$CYRUS_ROOT/user/$l/${user}.sub"); $junk = 0; while() { $_ =~ s/^INBOX/user.$user/; $_ =~ s/$/\t/; print OUT $_; if ($_ =~ m/^user.$user.Spam/) { $junk = 1; } } if ($junk == 0) { print OUT "user.$user.Spam\t\n"; } close(IN); close(OUT); chown $CYRUS_UID, $CYRUS_GID, "$CYRUS_ROOT/user/$l/${user}.sub"; chmod 0666, "$CYRUS_ROOT/user/$l/${user}.sub"; return; } sub createmb { my $user = $_[0]; my $folder = $_[1]; my $mb = $user; if ($folder ne "") { $mb .= "." . $folder; } unlink("/tmp/cyrus-mailbox"); unlink("/tmp/cyrus-mailbox.out"); system("$CYRUS_DBCONVERT $CYRUS_ROOT/mailboxes.db skiplist /tmp/cyrus-mailbox flat"); open(IN, "/tmp/cyrus-mailbox.out"); $found = 0; while() { if (m/^user.$mb\t/) { $found = 1; } print OUT $_; } if ($found == 0) { print " - Add mailbox $mb\n" if $DEBUG; print OUT "user.$mb\t0 default $user\tlrswipcda\t\n"; } close(IN); close(OUT); if ($found == 0) { unlink("$CYRUS_ROOT/mailboxes.db"); system("$CYRUS_DBCONVERT /tmp/cyrus-mailbox.out flat $CYRUS_ROOT/mailboxes.db skiplist"); chown $CYRUS_UID, $CYRUS_GID, "$CYRUS_ROOT/mailboxes.db"; chmod 0666, "$CYRUS_ROOT/mailboxes.db"; } } sub handlecyrusseen { my $mailbox = $_[0]; my $user = $_[1]; print " - Fixing seen index file ($user)...\n" if $DEBUG; my @seen; foreach $file_id (keys %map) { if ($map{$file_id}{'systemflags'}{'seen'}) { push @seen, $map{$file_id}{'cyrusid'}; } } @seen = sort {$a <=> $b} @seen; $seen_string = ""; $last = -1; $range = 0; foreach $cyrus_id (@seen) { if ($cyrus_id == $last + 1) { $range = 1; } else { if ($range == 1) { $seen_string .= ":$last"; } $range = 0; $seen_string .= ",$cyrus_id"; } $last = $cyrus_id; } if ($range == 1) { $seen_string .= ":$last"; } if ($seen_string eq "") { print " - Empty mailbox, skipping\n" if $DEBUG; return; } $seen_string = substr($seen_string, 1); $stamp = time; $line = "$mailbox\t1 $stamp $last $stamp $seen_string"; open(OUT, ">/tmp/cyrus-seen.out"); $l = substr($user, 0, 1); $src = "$CYRUS_ROOT/user/$l/$user.seen"; unlink("/tmp/cyrus-seen"); my $written = 0; if (-f $src) { system("$CYRUS_DBCONVERT $src skiplist /tmp/cyrus-seen flat"); open(IN, ") { if ($_ =~ m/^$mailbox/) { print OUT $line . "\n"; $written = 1; } else { print OUT $_; } } close(IN); } if ($written == 0) { print OUT $line . "\n"; } close(OUT); system("$CYRUS_DBCONVERT /tmp/cyrus-seen.out flat $src skiplist"); chown $CYRUS_UID, $CYRUS_GID, $src; chmod 0666, $src; # DB format: # SPSPSPSP } sub handlecyrusindex { print " - Fixing mailbox index file...\n" if $DEBUG; my $dst = $_[0]; my $uid_validity = $_[1]; my $uid_last = $_[2]; my $index_file = $dst . '/cyrus.index'; my @field_names = ( "uid", "internal date", "sent date", "size", "header size", "content offset", "cache offset", "last updated", "system flags", "user 1", "user 2", "user 3", "user 4", "unknown 1", "unknown 2"); my %flags = ( "answered" => 1<<0, "flagged" => 1<<1, "deleted" => 1<<2, "draft" => 1<<3 ); sysopen(IDX,$index_file, 2) or return "Failed to open index.\n"; #define OFFSET_LAST_UID 28 $data = pack("N1", $uid_last); die "unable to seek!" unless sysseek(IDX, 28, 0); die "unable to write!" unless syswrite(IDX, $data, 4); #define OFFSET_UIDVALIDITY 44 $data = pack("N1", $uid_validity); die "unable to seek!" unless sysseek(IDX, 44, 0); die "unable to write!" unless syswrite(IDX, $data, 4); # Skip header.. return "unable to seek $!" unless sysseek(IDX,76,0); my $seek = 76; my $i = 1; my $r = sysread(IDX,$doi,60); while($r){ # First 8, system fields, # Next 5, flags, # Last 2, unknown my @data = unpack("N8N5N2",$doi); my $j = 0; foreach $field_name (@field_names){ $record{$field_name} = $data[$j]; $j++; } # Seek position $record{'seek'} = $seek; $records{$record{'uid'}} = { %record }; $seek += 60; $r = sysread(IDX,$doi,60); $i++; } # Index file read (cyrus) foreach $file_id (keys %map) { $cyrus_id = $map{$file_id}{'cyrusid'}; if ($records{$cyrus_id}) { $offset = $records{$cyrus_id}{'seek'} + 32; $data = 0; $data = $data | 1<<0 if $map{$file_id}{'systemflags'}{'replied'}; $data = $data | 1<<1 if $map{$file_id}{'systemflags'}{'flagged'}; # $data = $data | 1<<2 if $map{$file_id}{'systemflags'}{'deleted'}; $data = $data | 1<<3 if $map{$file_id}{'systemflags'}{'draft'}; # Write system flags $data = pack("N1", $data); die "unable to seek!" unless sysseek(IDX, $offset, 0); die "unable to write!" unless syswrite(IDX, $data, 4); # Write custom flags $flag_string = $map{$file_id}{'customflags'}; @custom_flags = split /\ /, $flag_string; $data = 0; foreach $flag (@custom_flags) { $data = $data | 1<<$flag } $data = pack("N4", $data); die "unable to seek!" unless sysseek(IDX, $offset + 4, 0); die "unable to write!" unless syswrite(IDX, $data, 16); } else { print "No cyrus-index entry found for maildir file: " . $file_id . "\n"; } } close(IDX); } sub handlecyrusheader { my $dst = $_[0]; my $customflags = $_[1]; my $header = $dst . '/cyrus.header'; print " - Fixing mailbox header file...\n" if $DEBUG; open(IN, "<$header"); $data = ""; while() { $data .= $_; } close(IN); # Header = 115 chars # Header + tab = 116 my $offset = 116; my $eol = index($data, "\n",$offset); my $neol = index($data, "\n",$eol+1); my $mailbox = substr($data, $offset, $eol - $offset); my $start = substr($data, 0, $eol+1); my $end = substr($data, $neol); open(OUT, ">$header"); print OUT $start; foreach my $key (@{$customflags}) { print OUT "$key "; } print OUT $end; close(OUT); return $mailbox; } sub getuserflags { my $src = $_[0]; my @flags; return \@flags if (! -d "$src/courierimapkeywords"); return \@flags if (! -f "$src/courierimapkeywords/:list"); open(IN, "<$src/courierimapkeywords/:list") or die ("Error opening"); my $header_done = 0; my $k = 0; while() { if ($_ eq "\n") { $header_done = 1; next; }; chop; if ($header_done == 0) { $flags[$k] = $_; $k++; } else { my $file_id_pos = index($_, ":"); my $file_id = substr($_, 0, $file_id_pos); # Check if file is in map, else no need to add to index if ($map{$file_id}) { $flag_string = substr($_, $file_id_pos + 1); $map{$file_id}{'customflags'} = $flag_string; } } } close(IN); return \@flags; } sub getuiddb { my $src = $_[0]; my $version; my %uid_map; my $uid_validity; my $uid_last; my $uid_next; return undef if (! -f "$src/courierimapuiddb"); open(IN, "<$src/courierimapuiddb") or die ("Error opening"); # 1 1089373752 890 ($version,$uid_validity,$uid_next)=split(/ /,); $uid_last=($uid_next>1)?($uid_next-1):0; printf("Maildir: %s, uid_validity=%d, uid_next=%d, uid_last=%d\n",$src,$uid_validity,$uid_next,$uid_last); # 577 1161107238.V39704I486200.fqdn.example.org # 578 1161107816.V39704I486201.fqdn.example.org while() { chomp($_); my ($uid,$filename) = split(/ /); #printf("uid=%d, filename=%s\n", $uid,$filename); $uid_map{$filename}=$uid; if($uid>$uid_last) { $uid_last=$uid; } } close(IN); return ($uid_validity, $uid_last, \%uid_map); } sub handledir { my $src = $_[0]; my $dst = $_[1]; my $i = $_[2]; my $uid_map = $_[3]; my $file; $j = 0; if (! -d $src) { return $i; } opendir(DIR, $src); my @files = readdir(DIR); closedir(DIR); foreach $file (@files) { next if $file eq "."; next if $file eq ".."; $j++; next if ($FAST && $j > 25); if (-f "$src/$file") { $file_src = "$src/$file"; my $file_id_pos = index($file, ":"); my $file_id = ($file_id_pos)?substr($file, 0, $file_id_pos):$file; $i=$uid_map->{$file_id} if defined($uid_map->{$file_id}); #printf("file_id=%s, uid=%d\n", $file_id, $i); $file_dst = $dst . "/" . $i . "."; # If file already exists, increase number while (-f $file_dst) { $i++; $file_dst = $dst . "/" . $i . "."; } open(IN, "<$file_src"); $data = ""; while() { $data .= $_; } $data =~ s/\n/\r\n/g; close(IN); open(OUT, ">$file_dst"); print OUT $data; close(OUT); chown $CYRUS_UID, $CYRUS_GID, $file_dst; chmod 0666, $file_dst; my $file_id_pos = index($file, ":2,"); my $file_id = substr($file, 0, $file_id_pos); $map{$file_id}{'cyrusid'} = $i; $systemflags = getsystemflags($file_src); # TODO: Add dovecot custom flags $map{$file_id}{'cyrusid'} = $i; $map{$file_id}{'systemflags'} = $systemflags; $i++; } if ($i % 250 == 0) { print "Handled $i messages so far...\n" if $DEBUG; } } return $i; } sub getsystemflags { # R: replied # S: seen # T: deleted/junk # D: draft # F: flagged # P: Not used, implemented as user-flag my $file = $_[0]; my $fs = index($file, ":2,"); my $flag_string = substr($file, $fs+3); my %flags; $flags{'seen'} = 1 if index($flag_string, 'S') >= 0; $flags{'replied'} = 1 if index($flag_string, 'R') >= 0; $flags{'junk'} = 1 if index($flag_string, 'T') >= 0; $flags{'draft'} = 1 if index($flag_string, 'D') >= 0; $flags{'flagged'} = 1 if index($flag_string, 'F') >= 0; return \%flags; }