Alternc  latest
Alternc logiel libre pour l'hébergement
courier-dovecot-migrate.pl
Go to the documentation of this file.
1 #!/usr/bin/perl
2 # Copyright 2008-2002 Timo Sirainen
3 # Last updated: 2012-07-28
4 
5 # NOTE: Requires Dovecot v2.0.13+ for POP3 'O' entries
6 
7 # Based largely on courier-dovecot-migrate.pl v1.1.7:
8 # cpanel12 - maildir-migrate Copyright(c) 2008 cPanel, Inc.
9 # All Rights Reserved.
10 # copyright@cpanel.net http://cpanel.net
11 
12 # Redistribution and use in source and binary forms, with or without
13 # modification, are permitted provided that the following conditions are met:
14 # * Redistributions of source code must retain the above copyright
15 # notice, this list of conditions and the following disclaimer.
16 # * Redistributions in binary form must reproduce the above copyright
17 # notice, this list of conditions and the following disclaimer in the
18 # documentation and/or other materials provided with the distribution.
19 # * Neither the name of the cPanel, Inc. nor the
20 # names of its contributors may be used to endorse or promote products
21 # derived from this software without specific prior written permission.
22 #
23 # THIS SOFTWARE IS PROVIDED BY CPANEL, INC. "AS IS" AND ANY
24 # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
25 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
26 # DISCLAIMED. IN NO EVENT SHALL CPANEL, INC BE LIABLE FOR ANY
27 # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
28 # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
29 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
30 # ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
31 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
32 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
33 
34 use strict;
35 use warnings;
36 use Getopt::Long ();
37 
38 # Key files in maildirs
39 my $courier_imap_uidfile = 'courierimapuiddb';
40 my $courier_pop3_uidfile = 'courierpop3dsizelist';
41 my $courier_subscriptions_file = 'courierimapsubscribed';
42 my $courier_keywords_dir = 'courierimapkeywords/';
43 my $courier_keywords_file = 'courierimapkeywords/:list';
44 my $dovecot_uidfile = 'dovecot-uidlist';
45 my $dovecot_uidvalidity_file = 'dovecot-uidvalidity';
46 my $dovecot_subscriptions_file = 'subscriptions';
47 my $dovecot_keywords_file = 'dovecot-keywords';
48 
49 # Globals
50 my $do_conversion = 0;
51 my $quiet = 0;
52 my $recursive = 0;
53 my $overwrite = 0;
54 
55 my $depth = 1;
56 my $maildir_subdirs = -1;
57 my $global_error_count = 0;
58 my $global_mailbox_count = 0;
59 my $global_user_count = 0;
60 my $uidlist_write_count = 0;
61 my $convert_to_dovecot = 0;
62 my $convert_to_courier = 0;
63 my $help = 0;
64 my $maildir_name = 'Maildir';
65 
66 # Argument processing
67 my %opts = (
68  'convert' => \$do_conversion,
69  'quiet' => \$quiet,
70  'overwrite' => \$overwrite,
71  'recursive' => \$recursive,
72  'to-dovecot' => \$convert_to_dovecot,
73  'to-courier' => \$convert_to_courier,
74  'help' => \$help,
75 );
76 
77 Getopt::Long::GetOptions(%opts);
78 usage() if $help;
79 
80 my $mailroot = shift @ARGV || '.';
81 
82 my $conversion_type;
83 if ( $convert_to_dovecot && $convert_to_courier ) {
84  print "It is not possible to convert to both Dovecot and Courier formats simultaneously!\n";
85  exit 1;
86 }
87 else {
88  print $do_conversion ? 'Converting' : 'Testing conversion' unless $quiet;
89 
90  if ($convert_to_courier) {
91  print " to Courier format\n" unless $quiet;
92  $conversion_type = 'courier';
93  }
94  elsif ($convert_to_dovecot) {
95  print " to Dovecot format\n" unless $quiet;
96  $conversion_type = 'dovecot';
97  }
98  else {
99  print " based on newest timestamps\n" unless $quiet;
100  $conversion_type = 'auto';
101  }
102 }
103 
104 # Check/Convert maildirs
105 print "Finding maildirs under $mailroot\n" if ( !$quiet );
106 if ( is_maildir($mailroot) ) {
107  check_maildir($mailroot);
108 }
109 elsif ( -d "$mailroot/$maildir_name" ) {
110  if ( !is_maildir("$mailroot/$maildir_name") ) {
111  print STDERR "$mailroot/$maildir_name doesn't seem to contain a valid Maildir\n";
112  }
113  else {
114  check_maildir("$mailroot/$maildir_name");
115  }
116 }
117 elsif ($recursive) {
118  if ( $depth > 0 || !userdir_check($mailroot) ) {
119  $depth-- if ( $depth > 0 );
120  if ( !depth_check( $mailroot, $depth ) ) {
121  print STDERR "No maildirs found\n";
122  exit;
123  }
124  }
125 }
126 
127 # Totals
128 if ( !$quiet ) {
129  print "\nTotal: $global_mailbox_count mailboxes / $global_user_count users\n";
130  print " $global_error_count errors\n";
131 
132  if ( !$do_conversion ) {
133  print "No actual conversion done, use --convert parameter\n";
134  }
135  else {
136  print "$uidlist_write_count $dovecot_uidfile files written\n";
137  }
138  print "\nWARNING: Badly done migration will cause your IMAP and/or POP3 clients to re-download all mails. Read http://wiki.dovecot.org/Migration carefully.\n";
139 }
140 
141 sub scan_maildir {
142  my ( $dir, $map ) = @_;
143 
144  my @scan_maildir_files;
145  if ( opendir my $scan_maildir_dh, $dir ) {
146  @scan_maildir_files = readdir($scan_maildir_dh);
147  closedir $scan_maildir_dh;
148  }
149  foreach my $real_filename (@scan_maildir_files) {
150  next if ( $real_filename eq "." || $real_filename eq ".." );
151 
152  my $base_filename;
153  if ( $real_filename =~ /^([^:]+):2,/ ) {
154  $base_filename = $1;
155  }
156  else {
157  $base_filename = $real_filename;
158  }
159  $$map{$base_filename} = $real_filename;
160  }
161 }
162 
163 sub read_courier_pop3 {
164  my ( $dir ) = @_;
165 
166  my ( $pop3_uidv, $pop3_nextuid ) = ( -1, 0 );
167  my $filename_map = {};
168 
169  my $f;
170  my $pop3_fname = "$dir/$courier_pop3_uidfile";
171  open( $f, $pop3_fname ) || die $!;
172  my $pop3_hdr = <$f>;
173  if ( $pop3_hdr =~ /^\/2 (\d+) (\d+)$/ ) {
174  # /2 <next uid> <uidvalidity>
175  $_ = <$f>;
176  }
177  elsif ( $pop3_hdr =~ /^\/1 (\d+)$/ ) {
178  # /1 <next uid>
179  $_ = <$f>;
180  }
181  elsif ( $pop3_hdr !~ /^\// ) {
182  # version 0: no UIDs
183  $_ = $pop3_hdr;
184  }
185  else {
186  print STDERR "$pop3_fname: Broken header: $pop3_hdr\n";
187  close $f;
188  return $filename_map;
189  }
190 
191  my $line = 0;
192  do {
193  chomp $_;
194  $line++;
195 
196  my ( $full_fname, $fsize, $uid, $uidv );
197 
198  if ( /^([^ ]+) (\d+) (\d+):(\d+)$/ ) {
199  # v2
200  ( $full_fname, $fsize, $uid, $uidv ) = ( $1, $2, $3, $4 );
201  }
202  elsif ( /^([^ ]+) (\d+) (\d+)$/ ) {
203  # v1
204  ( $full_fname, $fsize, $uid ) = ( $1, $2, $3 );
205  $uidv = 0;
206  }
207  elsif ( /^([^ ]+) (\d+)$/ ) {
208  # v0
209  ( $full_fname, $fsize ) = ( $1, $2 );
210  $uid = 0;
211  $uidv = 0;
212  }
213  else {
214  $global_error_count++;
215  print STDERR "$pop3_fname: Broken line: $_\n";
216  next;
217  }
218 
219  # get base filename
220  my $fname = $full_fname;
221  $fname =~ s/^([^:]+).*$/$1/;
222 
223  my $uidl = "";
224  if ( $uid == 0 ) {
225  # use filename
226  foreach (split(//, $fname)) {
227  if (ord($_) < 0x21 || ord($_) > 0x7e || $_ eq "'" || $_ eq '"' || $_ eq "+") {
228  $uidl .= sprintf("+%02X", ord($_));
229  } else {
230  $uidl .= $_;
231  }
232  }
233  }
234  elsif ($uidv != 0) {
235  $uidl = "UID$uid-$uidv";
236  }
237  else {
238  $uidl = "UID$uid";
239  }
240  $filename_map->{$fname} = [ 0, $uidl, $fsize, $full_fname, $line ];
241  } while (<$f>);
242  close $f;
243  return $filename_map;
244 }
245 
246 sub read_courier_imap {
247  my ( $dir, $filename_map ) = @_;
248 
249  # check if we can preserve IMAP UIDs
250  my $imap_fname = "$dir/$courier_imap_uidfile";
251  if ( !-f $imap_fname ) {
252  print "$imap_fname: OK\n" if ( !$quiet );
253  return;
254  }
255 
256  my $f;
257  open( $f, $imap_fname ) || die $!;
258  my $imap_hdr = <$f>;
259  if ( $imap_hdr !~ /^1 (\d+) (\d+)$/ ) {
260  $global_error_count++;
261  print STDERR "$imap_fname: Broken header: $imap_hdr\n";
262  close $f;
263  return;
264  }
265  my ( $uidv, $nextuid ) = ( $1, $2 );
266 
267  my %found_files;
268  my $found_files_looked_up = 0;
269  while (<$f>) {
270  chomp $_;
271 
272  if (/^(\d+) (.*)$/) {
273  my ( $uid, $full_fname ) = ( $1, $2 );
274 
275  # get the base filename
276  my $fname = $full_fname;
277  $fname =~ s/^([^:]+).*$/$1/;
278 
279  if ( defined $filename_map->{$fname} ) {
280  $filename_map->{$fname}->[0] = $uid;
281  }
282  else {
283  # not in pop3 list
284  $filename_map->{$fname} = [ $uid, "", 0, $full_fname, 0 ];
285  }
286  $nextuid = $uid + 1 if ($uid >= $nextuid);
287  }
288  else {
289  $global_error_count++;
290  print STDERR "$imap_fname: Broken header\n";
291  }
292  }
293  close $f;
294 
295  return ( $uidv, $nextuid, $filename_map );
296 }
297 
298 sub write_dovecot_uidlist {
299  my ( $dir, $uidv, $nextuid, $owner_uid, $owner_gid, $filename_map ) = @_;
300 
301  my $uidlist_fname = "$dir/$dovecot_uidfile";
302  if ( !$overwrite && -f $uidlist_fname ) {
303  print "$uidlist_fname already exists, not overwritten\n" if ( !$quiet );
304  return;
305  }
306  return if (scalar keys %{$filename_map} == 0);
307 
308  return if ( !$do_conversion );
309 
310  if ($uidv <= 0) {
311  # only pop3 UIDLs, generate a new uidvalidity
312  $uidv = time();
313  }
314 
315  # POP3 clients may want to get POP3 UIDLs in the same order always.
316  # Preserve the order with O flag supported in dovecot 2.0.13+ and dont change order of IMAP UIDs.
317  my %order_map;
318  foreach my $fname ( keys %{$filename_map} ) {
319  my $order = $filename_map->{$fname}->[4];
320  $filename_map->{$fname}->[5]=$order;
321  }
322 
323  my $prev_uid = 0;
324  foreach my $order ( sort { $a <=> $b } keys %order_map ) {
325  my $file_ar = $filename_map->{ $order_map{$order} };
326  if ($file_ar->[0] == 0) {
327  $file_ar->[0] = $nextuid;
328  $nextuid++;
329  }
330 
331  if ($file_ar->[0] <= $prev_uid) {
332  $file_ar->[0] = 0;
333  } else {
334  $prev_uid = $file_ar->[0];
335  }
336  }
337 
338  # sort all messages by their imap UID
339  my @all = sort {
340  ($filename_map->{$a}[0] || 'inf') <=> ($filename_map->{$b}[0] || 'inf')
341  } keys %$filename_map;
342 
343  $prev_uid = 0;
344  my %uidlist_map;
345  foreach my $fname ( @all ) {
346  my $file_ar = $filename_map->{ $fname };
347  my $uid = $file_ar->[0];
348  if ($uid == 0 # only in pop3 list or marked for new UID
349  # OR: other ill-ordered UID message
350  || $uid <= $prev_uid)
351  {
352  # Assign a new IMAP UID:
353  $uid = $nextuid;
354  $nextuid++;
355  }
356  $prev_uid = $uid;
357  $uidlist_map{$uid} = $fname;
358  }
359 
360  open( my $dovecot_uidlist_fh, '>', $uidlist_fname ) || die $!;
361  print $dovecot_uidlist_fh "3 V$uidv N$nextuid\n";
362  foreach my $uid ( sort { $a <=> $b } keys %uidlist_map ) {
363  my $file_ar = $filename_map->{ $uidlist_map{$uid} };
364  print $dovecot_uidlist_fh "$uid ";
365  if ($file_ar->[5]) {
366  print $dovecot_uidlist_fh "O".$file_ar->[5]." ";
367  }
368  print $dovecot_uidlist_fh 'P' . $file_ar->[1] . ' ' if ( $file_ar->[1] ne "" );
369  print $dovecot_uidlist_fh 'W' . $file_ar->[2] . ' ' if ( $file_ar->[2] > 0 );
370  print $dovecot_uidlist_fh ':' . $file_ar->[3] . "\n";
371  }
372  close $dovecot_uidlist_fh;
373  chown $owner_uid, $owner_gid, $uidlist_fname;
374  $uidlist_write_count++;
375 }
376 
377 sub convert_keywords {
378  my ( $dir, $owner_uid, $owner_gid ) = @_;
379 
380  my $courier_mtime = ( stat("$dir/$courier_keywords_file") )[9] || 0;
381  my $dovecot_mtime = ( stat("$dir/$dovecot_keywords_file") )[9] || 0;
382 
383  # No need to convert if there are no keywords files
384  return unless ( $courier_mtime || $dovecot_mtime );
385 
386  # If we're doing auto-conversion, find the newest keywords file
387  my $convert_to = $conversion_type;
388  if ( $convert_to eq 'auto' ) {
389  $convert_to = $dovecot_mtime > $courier_mtime ? 'courier' : 'dovecot';
390  }
391 
392  if ( $convert_to eq 'dovecot' ) {
393  # Courier to Dovecot keyword conversion
394  my $keyword_dir = "$dir/courierimapkeywords";
395  my $dovecot_keyfname = "$dir/dovecot-keywords";
396 
397  if ( !-f "$keyword_dir/:list" ) {
398 
399  # no keywords
400  return;
401  }
402 
403  if ( !$overwrite && -f $dovecot_keyfname ) {
404  print "$dovecot_keyfname already exists, not overwritten\n" if ( !$quiet );
405  return;
406  }
407 
408  my ( %keywords, %files );
409  my $f;
410  open( $f, "$keyword_dir/:list" ) || die $!;
411 
412  # read keyword names
413  while (<$f>) {
414  chomp $_;
415 
416  last if (/^$/);
417  $keywords{$_} = scalar keys %keywords;
418  }
419 
420  # read filenames -> keywords mapping
421  while (<$f>) {
422  if (/([^:]+):([\d ]+)$/) {
423  my $fname = $1;
424  foreach ( sort { $a <=> $b } split( " ", $2 ) ) {
425  $files{$fname} .= chr( 97 + $_ );
426  }
427  }
428  else {
429  print STDERR "$keyword_dir/:list: Broken entry: $_\n";
430  }
431  }
432  close $f;
433 
434  # read updates from the directory
435  my %updates;
436  my @update_files;
437  if ( opendir my $kw_dh, $keyword_dir ) {
438  @update_files = readdir($kw_dh);
439  closedir $kw_dh;
440  }
441  foreach (@update_files) {
442  next if ( $_ eq ":list" || $_ eq "." || $_ eq ".." );
443 
444  my $fname = $_;
445  if (/^\.(\d+)\.(.*)$/) {
446  my ( $num, $base_fname ) = ( $1, $2 );
447  if ( !defined $updates{$fname} ) {
448  $updates{$fname} = $num;
449  }
450  else {
451  my $old = $updates{$fname};
452  if ( $old >= 0 && $num > $old ) {
453  $updates{$fname} = $num;
454  }
455  }
456  }
457  else {
458 
459  # "fname" overrides .n.fnames
460  $updates{$fname} = -1;
461  }
462  }
463 
464  # apply the updates
465  foreach ( keys %updates ) {
466  my $base_fname = $_;
467  my $num = $updates{$_};
468 
469  my $fname;
470  if ( $num < 0 ) {
471  $fname = $base_fname;
472  }
473  else {
474  $fname = ".$num.$base_fname";
475  }
476 
477  my @kw_list;
478  open( $f, "$keyword_dir/$fname" ) || next;
479  while (<$f>) {
480  chomp $_;
481  my $kw = $_;
482  my $idx;
483 
484  if ( defined $keywords{$kw} ) {
485  $idx = $keywords{$kw};
486  }
487  else {
488  $idx = scalar keys %keywords;
489  $keywords{$kw} = $idx;
490  }
491  $kw_list[ scalar @kw_list ] = $idx;
492  }
493  close $f;
494 
495  $files{$fname} = "";
496  foreach ( sort { $a <=> $b } @kw_list ) {
497  $files{$fname} .= chr( 97 + $_ );
498  }
499  }
500 
501  return if ( !$do_conversion );
502 
503  # write dovecot-keywords file
504  open( $f, ">$dovecot_keyfname" ) || die $!;
505  foreach ( sort { $keywords{$a} <=> $keywords{$b} } keys %keywords ) {
506  my $idx = $keywords{$_};
507  print $f "$idx $_\n";
508  }
509  close $f;
510  chown $owner_uid, $owner_gid, $dovecot_keyfname;
511 
512  # update the maildir files
513  my $cur_dir = "$dir/cur";
514  my @cur_files;
515  if ( opendir my $cur_dir_dh, $cur_dir ) {
516  @cur_files = readdir($cur_dir_dh);
517  closedir $cur_dir_dh;
518  }
519  foreach (@cur_files) {
520  my $fname = $cur_dir . '/' . $_;
521 
522  my ( $base_fname, $flags, $extra_flags );
523  if (/^([^:]+):2,([^,]*)(,.*)?$/) {
524  ( $base_fname, $flags, $extra_flags ) = ( $1, $2, $3 );
525  $extra_flags = "" if ( !defined $extra_flags );
526  }
527  else {
528  $base_fname = $fname;
529  $flags = "";
530  $extra_flags = "";
531  }
532 
533  if ( defined $files{$base_fname} ) {
534 
535  # merge old and new flags
536  my %newflags;
537  foreach ( sort split( "", $files{$base_fname} ) ) {
538  $newflags{$_} = 1;
539  }
540  foreach ( sort split( "", $flags ) ) {
541  $newflags{$_} = 1;
542  }
543  $flags = "";
544  foreach ( sort keys %newflags ) {
545  $flags .= $_;
546  }
547  my $new_fname = "$cur_dir/$base_fname:2,$flags$extra_flags";
548  if ( $fname ne $new_fname ) {
549  rename( $fname, $new_fname )
550  || print STDERR "rename($fname, $new_fname) failed: $!\n";
551  }
552  }
553  }
554  }
555  else {
556 
557  # Dovecot to Courier keywords conversion
558  return unless $dovecot_mtime;
559 
560  if ( !$overwrite && -f "$dir/$courier_keywords_file" ) {
561  print "$courier_keywords_file already exists, not overwritten\n" if ( !$quiet );
562  return;
563  }
564 
565  # Read Dovecot keywords list into memory
566  open my $dovecot_kw_fh, '<', "$dir/$dovecot_keywords_file" || die $!;
567  my %keywords;
568  while ( my $line = readline($dovecot_kw_fh) ) {
569  chomp $line;
570  if ( $line =~ /(\d+)\s+(.+)/ ) {
571 
572  # Number then Keyword
573  $keywords{$1} = $2;
574  }
575  }
576  close $dovecot_kw_fh;
577 
578  # Scan files in cur for keywords
579  my $cur_dir = "$dir/cur";
580  my %file_keyword_map;
581 
582  my @cur_files;
583  if ( opendir my $cur_dir_dh, $cur_dir ) {
584  @cur_files = readdir($cur_dir_dh);
585  closedir $cur_dir_dh;
586  }
587  foreach my $basename (@cur_files) {
588  my $flags;
589  my $extra_flags;
590  my $keywords = '';
591 
592  # Split out and process flags
593  if ( $basename =~ /^([^:]+):2,([^,]*)(,.*)?$/ ) {
594  ( $basename, $flags, $extra_flags ) = ( $1, $2, $3 );
595  $extra_flags = "" unless ( defined $extra_flags );
596  }
597  else {
598  $basename = "";
599  $flags = "";
600  $extra_flags = "";
601  }
602  foreach my $key ( sort split( //, $flags ) ) {
603  my $val = ord($key) - 97;
604  next unless ( $val >= 0 && $val < 26 );
605  next unless ( defined $keywords{$val} );
606  $keywords .= ' ' . $val;
607  }
608  if ($keywords) {
609  $keywords =~ s/^\s+//;
610  $file_keyword_map{$basename} = $keywords;
611  }
612  }
613 
614  return unless ($do_conversion);
615 
616  # Make courier keywords directory if necessary
617  my $key_dir = "$dir/$courier_keywords_dir";
618  unless ( -d $key_dir ) {
619  unlink $key_dir;
620  mkdir $key_dir;
621  chown $owner_uid, $owner_gid, $key_dir;
622  }
623 
624  # Remove any old courier keywords files
625  my @courier_keywords_files;
626  if ( opendir my $courier_keywords_dh, $key_dir ) {
627  @courier_keywords_files = readdir($courier_keywords_dh);
628  closedir $courier_keywords_dh;
629  }
630  foreach my $file (@courier_keywords_files) {
631  $file = $key_dir . $file;
632  next unless -f $file;
633  unlink $file;
634  }
635 
636  # Write courier keywords list
637  return unless ( scalar %keywords );
638  open my $courier_kw_fh, '>', "$dir/$courier_keywords_file" || die $!;
639  foreach my $num ( sort keys %keywords ) {
640  print $courier_kw_fh $keywords{$num} . "\n";
641  }
642  print $courier_kw_fh "\n";
643  foreach my $file ( sort keys %file_keyword_map ) {
644  print $courier_kw_fh $file . ':' . $file_keyword_map{$file} . "\n";
645  }
646  close $courier_kw_fh;
647  chown $owner_uid, $owner_gid, "$dir/$courier_keywords_file";
648  }
649 }
650 
651 sub convert_subscriptions {
652  my ( $dir, $owner_uid, $owner_gid ) = @_;
653 
654  my $courier_mtime = ( stat("$dir/$courier_subscriptions_file") )[9] || 0;
655  my $dovecot_mtime = ( stat("$dir/$dovecot_subscriptions_file") )[9] || 0;
656 
657  # No need to convert if there is no subscriptions files
658  return unless ( $courier_mtime || $dovecot_mtime );
659 
660  # If we're doing auto-conversion, find the newest subscription file
661  my $convert_to = $conversion_type;
662  if ( $convert_to eq 'auto' ) {
663  $convert_to = $dovecot_mtime > $courier_mtime ? 'courier' : 'dovecot';
664  }
665 
666  my $src_file = "$dir/$dovecot_subscriptions_file";
667  my $dst_file = "$dir/$courier_subscriptions_file";
668  my $src_mtime = $dovecot_mtime;
669  my $dst_mtime = $courier_mtime;
670  if ( $convert_to eq 'dovecot' ) {
671  $src_file = "$dir/$courier_subscriptions_file";
672  $dst_file = "$dir/$dovecot_subscriptions_file";
673  $src_mtime = $courier_mtime;
674  $dst_mtime = $dovecot_mtime;
675  }
676 
677  # Sanity checks..
678  if ( $dst_mtime && !$overwrite ) {
679  print "$dst_file already exists, not overwritten\n" if ( !$quiet );
680  return;
681  }
682  if ( $dst_mtime && !-f $dst_file ) {
683  print "$dst_file already exists as something other than a file\n" if ( !$quiet );
684  return;
685  }
686  unless ($src_mtime) {
687  return;
688  }
689  unless ( -f $src_file ) {
690  print "$src_file isn't a regular file\n" if ( !$quiet );
691  return;
692  }
693 
694  return unless ($do_conversion);
695 
696  open( my $src_fh, '<', $src_file ) || die $!;
697  open( my $dst_fh, '>', $dst_file ) || die $!;
698  while ( my $line = readline($src_fh) ) {
699  chomp $line;
700  if ( $line =~ /^INBOX$/i ) {
701  print $dst_fh "INBOX\n";
702  }
703  elsif ( $convert_to eq 'dovecot' ) {
704  if ( $line =~ /^INBOX\.(.*)$/i ) {
705  print $dst_fh "$1\n";
706  }
707  else {
708 
709  # Unknown. The dovecot migrate script leaves these as-is...
710  print $dst_fh "$line\n";
711  }
712  }
713  else {
714 
715  # converting to Courier INBOX namespace
716  if ( $line =~ /\S/ ) {
717  print $dst_fh "INBOX.$line\n";
718  }
719  }
720  }
721  close $src_fh;
722  close $dst_fh;
723  chown $owner_uid, $owner_gid, $dst_file;
724 }
725 
726 sub check_maildir_single {
727  my ( $dir, $childbox ) = @_;
728 
729  $dir =~ s{^\./}{}g;
730 
731  my $owner_uid;
732  my $owner_gid;
733 
734  # Store the relevant stats()
735  my @courier_pop_stat = ();
736  @courier_pop_stat = stat("$dir/$courier_pop3_uidfile") unless $childbox;
737  my @courier_imap_stat = stat("$dir/$courier_imap_uidfile");
738  my @dovecot_stat = stat("$dir/$dovecot_uidfile");
739 
740  # Gather mtimes
741  my $courier_pop_mtime = ( scalar @courier_pop_stat > 0 ) ? $courier_pop_stat[9] : 0;
742  my $courier_imap_mtime = ( scalar @courier_imap_stat > 0 ) ? $courier_imap_stat[9] : 0;
743  my $dovecot_mtime = ( scalar @dovecot_stat > 0 ) ? $dovecot_stat[9] : 0;
744 
745  # Determine conversion type
746  my $convert_uidl_to = $conversion_type;
747 
748  if ( $convert_uidl_to eq 'auto' ) {
749  $convert_uidl_to = $dovecot_mtime > $courier_pop_mtime && $dovecot_mtime > $courier_imap_mtime ? 'courier' : 'dovecot';
750  }
751 
752  # Convert UIDLs
753 
754  if ( $convert_uidl_to eq 'dovecot' ) {
755 
756  # To Dovecot
757  unless ( $courier_pop_mtime || $courier_imap_mtime ) {
758  print "$dir: No imap/pop3 uidlist files\n" if ( !$quiet && !$childbox );
759  return;
760  }
761 
762  $owner_uid = $courier_pop_mtime ? $courier_pop_stat[4] : $courier_imap_stat[4];
763  $owner_gid = $courier_pop_mtime ? $courier_pop_stat[5] : $courier_imap_stat[5];
764 
765  my $uidv = -1;
766  my $nextuid = 1;
767  my $filename_map;
768 
769  if ( $courier_pop_mtime) {
770  $filename_map = read_courier_pop3( $dir );
771  }
772 
773  if ($courier_imap_mtime) {
774  ( $uidv, $nextuid, $filename_map ) = read_courier_imap( $dir, $filename_map );
775  }
776  $global_mailbox_count++;
777  write_dovecot_uidlist( $dir, $uidv, $nextuid, $owner_uid, $owner_gid, $filename_map );
778  remove_dovecot_caches($dir) if ($overwrite);
779  }
780  else {
781 
782  # To Courier
783  unless ($dovecot_mtime) {
784  print "$dir: No imap/pop3 uidlist files\n" if ( !$quiet && !$childbox );
785  return;
786  }
787 
788  $owner_uid = $dovecot_stat[4];
789  $owner_gid = $dovecot_stat[5];
790  my ( $uidv, $nextuid, $msguids ) = read_dovecot_uidfile($dir);
791  if ($uidv) {
792  write_courier_pop3( $dir, $uidv, $nextuid, $owner_uid, $owner_gid, $msguids );
793  write_courier_imap( $dir, $uidv, $nextuid, $owner_uid, $owner_gid, $msguids );
794  }
795  }
796 
797  # If we get here we did a UIDL conversion. Now convert subscriptions and keywords
798 
799  convert_subscriptions( $dir, $owner_uid, $owner_gid );
800  convert_keywords( $dir, $owner_uid, $owner_gid );
801 }
802 
803 sub remove_dovecot_caches {
804  my $dir = shift;
805  foreach my $file ( qw(dovecot.index dovecot.index.cache dovecot.index.log dovecot.index.log2), $dovecot_uidvalidity_file ) {
806  unlink $dir . '/' . $file;
807  }
808  unlink glob( $dir . '/' . $dovecot_uidvalidity_file . '.*' );
809 }
810 
811 sub read_dovecot_uidfile {
812  my $dir = shift;
813  my $dovecot_uidfile = "$dir/$dovecot_uidfile";
814 
815  my $uidv;
816  my $nextuid = 1;
817  my $dovecot_uid_version;
818  my @msguids;
819 
820  if ( !-f $dovecot_uidfile ) {
821  print "$dovecot_uidfile: OK\n" if ( !$quiet );
822  return;
823  }
824 
825  my $dovecot_uid_fh;
826  open( $dovecot_uid_fh, '<', $dovecot_uidfile ) || die $!;
827  my $dovecot_hdr = readline($dovecot_uid_fh);
828  if ( $dovecot_hdr =~ /^3\s+(.+)$/ ) {
829  my $options = $1;
830  $dovecot_uid_version = 3;
831  foreach my $part ( split( /\s+/, $options ) ) {
832  if ( $part =~ /(\w)(.+)/ ) {
833  my $type = $1;
834  my $val = $2;
835  if ( $type eq 'V' ) {
836  $uidv = $val;
837  }
838  elsif ( $type eq 'N' ) {
839  $nextuid = $val;
840  }
841  }
842  }
843 
844  unless ($uidv) {
845  $global_error_count++;
846  print STDERR "$dovecot_uidfile: Broken header: $dovecot_hdr\n";
847  close $dovecot_uid_fh;
848  return;
849  }
850  }
851  elsif ( $dovecot_hdr =~ /^1\s+(\S+)\s+(\S+)$/ ) {
852  $dovecot_uid_version = 1;
853  $uidv = $1;
854  $nextuid = $2;
855  }
856  else {
857  $global_error_count++;
858  print STDERR "$dovecot_uidfile: Broken header: $dovecot_hdr\n";
859  close $dovecot_uid_fh;
860  return;
861  }
862 
863  while ( my $line = readline($dovecot_uid_fh) ) {
864 
865  chomp $line;
866  my @prts = split( /\s+/, $line );
867 
868  if ( $dovecot_uid_version eq '3' ) {
869  next unless ( scalar @prts >= 2 );
870  my $msgnum = shift @prts;
871  my $filename = pop @prts;
872  $filename =~ s/^\://;
873  my $msgsize;
874 
875  # Dovecot may or may not store the sizes for each message in the uidl file
876  # S# is the size with UNIX newlines, W# is the size with windows newlines
877  SIZE_LOOP:
878  foreach my $subprt (@prts) {
879  if ( $subprt =~ s/^W// ) {
880  $msgsize = $subprt;
881  last SIZE_LOOP;
882  }
883  }
884  push @msguids, [ $msgnum, $filename, $msgsize ];
885  $nextuid = $msgnum + 1 if ( $msgnum >= $nextuid );
886  }
887  else {
888 
889  # process V1 data
890  next unless ( scalar @prts == 2 );
891  push @msguids, [ $prts[0], $prts[1], undef ];
892  $nextuid = $prts[0] + 1 if ( $prts[0] >= $nextuid );
893  }
894  }
895 
896  return ( $uidv, $nextuid, \@msguids );
897 }
898 
899 sub write_courier_pop3 {
900  my $dir = shift;
901  my $uidv = shift;
902  my $nextuid = shift;
903  my $owner_uid = shift;
904  my $owner_gid = shift;
905  my $msguids = shift;
906 
907  # Check file/overwrite/conversion
908  my $uidlist_fname = "$dir/$courier_pop3_uidfile";
909  if ( !$overwrite && -f $uidlist_fname ) {
910  print "$uidlist_fname already exists, not overwritten\n" if ( !$quiet );
911  return;
912  }
913 
914  return if ( !$do_conversion );
915 
916  # Check that all entries have sizes.. We don't write the pop3 file if we can't do so correctly
917  foreach my $msg_ar ( @{$msguids} ) {
918  unless ( defined $msg_ar->[2] ) {
919  print "Missing some sizes for $uidlist_fname, skipping\n" if ( !$quiet );
920  return;
921  }
922  }
923 
924  # Write file
925  open( my $courier_pop_fh, '>', $uidlist_fname ) || die $!;
926  print $courier_pop_fh "/2 $nextuid $uidv\n";
927  foreach my $msg_ar ( @{$msguids} ) {
928  print $courier_pop_fh $msg_ar->[1] . ' ' . $msg_ar->[2] . ' ' . $msg_ar->[0] . ':' . $uidv . "\n";
929  }
930  close $courier_pop_fh;
931  chown $owner_uid, $owner_gid, $uidlist_fname;
932  $uidlist_write_count++;
933 
934 }
935 
936 sub write_courier_imap {
937  my $dir = shift;
938  my $uidv = shift;
939  my $nextuid = shift;
940  my $owner_uid = shift;
941  my $owner_gid = shift;
942  my $msguids = shift;
943 
944  # Check file/overwrite/conversion
945  my $uidlist_fname = "$dir/$courier_imap_uidfile";
946  if ( !$overwrite && -f $uidlist_fname ) {
947  print "$uidlist_fname already exists, not overwritten\n" if ( !$quiet );
948  return;
949  }
950 
951  return if ( !$do_conversion );
952 
953  # Write file
954  open( my $courier_imap_fh, '>', $uidlist_fname ) || die $!;
955  print $courier_imap_fh "1 $uidv $nextuid\n";
956  foreach my $msg_ar ( @{$msguids} ) {
957  my $filename = $msg_ar->[1];
958  $filename =~ s/\:2.*$//;
959  print $courier_imap_fh $msg_ar->[0] . ' ' . $filename . "\n";
960  }
961  close $courier_imap_fh;
962  chown $owner_uid, $owner_gid, $uidlist_fname;
963  $uidlist_write_count++;
964 
965 }
966 
967 sub check_maildir {
968  my ($dir) = @_;
969 
970  my $orig_mailboxes = $global_mailbox_count;
971 
972  check_maildir_single( $dir, 0 );
973  my @check_maildir_files;
974  if ( opendir my $check_maildir_dh, $dir ) {
975  @check_maildir_files = readdir($check_maildir_dh);
976  closedir $check_maildir_dh;
977  }
978  foreach my $file (@check_maildir_files) {
979  next unless ( $file =~ /^\./ );
980  next if ( $file =~ /^\.?\.$/ );
981  $file = $dir . '/' . $file;
982  next if ( -l $file );
983  check_maildir_single( $file, 1 );
984  }
985 
986  $global_user_count++ if ( $orig_mailboxes != $global_mailbox_count );
987 }
988 
989 sub is_maildir {
990  my ($dir) = @_;
991 
992  # Do we need to check for the courier specific files here or is it enough to assume every maildir will have a cur directory?
993  return ( -f "$dir/$courier_pop3_uidfile" || -f "$dir/$courier_imap_uidfile" || -d "$dir/cur" );
994 }
995 
996 sub userdir_check {
997  my ($dir) = @_;
998  my $found = 0;
999 
1000  my @userdir_check_files;
1001  if ( opendir my $userdir_dh, $dir ) {
1002  @userdir_check_files = readdir($userdir_dh);
1003  closedir $userdir_dh;
1004  }
1005  foreach my $userdir (@userdir_check_files) {
1006  $userdir = $dir . '/' . $userdir;
1007  next if ( -l $userdir );
1008  next if ( !-d $userdir );
1009 
1010  if ( $maildir_subdirs == -1 ) {
1011 
1012  # unknown if we want $maildir_name/ or not
1013  if ( -d "$userdir/$maildir_name" && is_maildir("$userdir/$maildir_name") ) {
1014  $maildir_subdirs = 1;
1015  }
1016  elsif ( is_maildir($userdir) ) {
1017  $maildir_subdirs = 0;
1018  }
1019  else {
1020  next;
1021  }
1022  }
1023 
1024  if ( $maildir_subdirs == 1 ) {
1025  if ( is_maildir("$userdir/$maildir_name") ) {
1026  check_maildir("$userdir/$maildir_name");
1027  $found = 1;
1028  }
1029  }
1030  elsif ( $maildir_subdirs == 0 ) {
1031  if ( is_maildir($userdir) ) {
1032  check_maildir($userdir);
1033  $found = 1;
1034  }
1035  }
1036  }
1037  return $found;
1038 }
1039 
1040 sub depth_check {
1041  my ( $dir, $depth ) = @_;
1042  my $found = 0;
1043 
1044  my @depth_check_files;
1045  if ( opendir my $depth_check_dh, $dir ) {
1046  @depth_check_files = readdir($depth_check_dh);
1047  closedir $depth_check_dh;
1048  }
1049  foreach my $subdir (@depth_check_files) {
1050  next if ($subdir eq '.' || $subdir eq '..');
1051  $subdir = $dir . '/' . $subdir;
1052  next if ( !-d $subdir );
1053 
1054  if ( $depth > 0 ) {
1055  $found = 1 if ( depth_check( $subdir, $depth - 1 ) );
1056  }
1057  else {
1058  $found = 1 if ( userdir_check($subdir) );
1059  }
1060  }
1061  return $found;
1062 }
1063 
1064 sub usage {
1065  print "Usage: maildir-migrate [options] <maildir>\n\n";
1066  print "Options:\n";
1067  print " --convert Perform conversion\n";
1068  print " --quiet Silence output\n";
1069  print " --overwrite Overwrite existing files\n";
1070  print " --recursive Recursively look through maildir for subaccounts\n";
1071  print " --to-dovecot Conversion is from Courier to Dovecot\n";
1072  print " --to-courier Conversion is from Dovecot to Courier\n";
1073  exit 0;
1074 }
1075