Benutzer-Werkzeuge

Webseiten-Werkzeuge


tachtler:dovecot_migration_-_cyrus2dovecot

Dovecot Migration - cyrus2dovecot

Diese Dokumentation ist nach dem Kurs: Dovecot bei www.heinlein-support.de - Peer Heinlein entstanden. Hier noch einmal meinen Dank für die Informationen und das ☛ Buch: Dovecot

:!: HINWEIS - Die Nachfolgende Konfiguration von Dovecot setzt eine lauffähige Installation von Dovecot voraus, wie unter nachfolgendem internen Link beschrieben !!!

Dovecot ist ein Open-Source-IMAP-und POP3-E-Mail-Server für Linux bzw. UNIX-ähnlichen Systeme, entwickelt mit dem Hauptaugenmerk auf Sicherheit. Dovecot ist eine ausgezeichnete Wahl für kleine und große Installationen. Dovecot ist schnell und einfach zu installieren, erfordert keine besonderen Voraussetzungen und ist Ressourcenschonend.

Dovecot wird von Timo Sirainen entwickelt.

Beschreibung Externer Link
Homepage http://dovecot.org
Dokumentation http://dovecot.org/documentation.html
Wiki Dovecot2 http://wiki2.dovecot.org/

Ab hier werden root-Rechte zur Ausführung der nachfolgenden Befehle benötigt. Um root zu werden geben Sie bitte folgenden Befehl ein:

$ su -
Password: 

Vorbereitung

Es gibt wie immer verschiedene Möglichkeiten wie von einem zum anderen IMAP-Server eine Migration durchgeführt werden kann.

Nachfolgend sollen einige dieser Möglichkeiten aufgezeigt werden:

  • Migration auf Dateiebene
  • Migration auf Basis von IMAP-(Befehlen) wie z.B. einem Client der an beide IMAP-Server angebunden ist.
  • Migration mit doveadm oder anderen Werkzeugen wie z.B. imapsync

Nachfolgende Gegebenheiten sollten bei einer Migration jedoch beachtet werden:

  • UID'S sollten beibehalten werden
    • Falls die UID's nicht erhalten bleiben, werden alle Clients sich erneut versuchen sich zu synchronisieren, was bei POP3 sicherlich problematischer wäre als bei IMAP ist.
  • IMAP-Namesräume sollten sich nicht ändern
    • Wie sind die Postfächer (Mailboxes) aufgebaut, mit . (Punkt) oder / (Schrägstrich) als Hirarchietrenner.
  • Kann eine offline oder muss eine online Migration durchgeführt werden
    • Bei kleineren Installation ist ggf. offline möglich

Migrationsbeispiel

  1. Kleine Installation die offline durchgeführt werden kann
  2. Es soll von einem Cyrus IMAPd zu Dovecot migriert werden
  3. Die Migration erfolgt vom Cyrus IMAPd IP: 192.168.0.180 zu Dovecot IP: 192.168.0.80
  4. Die Migration erfolgt auf Dateiebene
  5. Es wird nachfolgendes Skript der Freie Universität Berlin Cyrus2Dovecot verwendet

Voraussetzungen

Als Voraussetzung für die Installation des nachfolgenden Skriptes der Freie Universität Berlin Cyrus2Dovecot, ist folgende Komponente erforderlich:

  • Ein installierte Version der Script-Sprache Perl ab Version 5.006

Herunterladen

Wie bereits erwähnt soll ein Skript das an der Freie Universität Berlin entwickelt wurde zur Migration verwendet werden.

Dieses Skript kann unter nachfolgendem externen Link heruntergeladen werden:

oder nachfolgender kompletter Quell-Code kann auf Textbasis kopiert werden.

#!/usr/bin/env perl
#
# $Id: cyrus2dovecot,v 1.2 2008/09/24 09:52:33 holger Exp $
#
# Convert Cyrus folders to Dovecot.
#
# Written by Holger Weiss <holger@ZEDAT.FU-Berlin.DE> at Freie Universitaet
# Berlin, Germany, Zentraleinrichtung fuer Datenverarbeitung (ZEDAT).
#
# ------------------------------------------------------------------------------
# Copyright (c) 2008 Freie Universitaet Berlin.
# All rights reserved.
#
# This program is free software; you can redistribute it and/or modify it under
# the same terms as Perl itself.  See perlartistic(1).  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.
# ------------------------------------------------------------------------------
#
require 5.006;	# We need Perl >= 5.6.0 for our open() calls.
 
use warnings;
use strict;
use Data::Dumper;
use File::Basename;
use Fcntl qw(:seek);
use Getopt::Long qw(:config gnu_getopt auto_help auto_version);
use Sys::Hostname;
 
#
# Default settings (can be overridden on the command line).
#
# Within pathnames, any occurrence of "%u" will be replaced by the current user
# name, any occurrence of "%<n>u" will be replaced by the <n>'th character of
# that user name, any occurrence of "%h" will be replaced by Cyrus' directory
# "hash" character for that user name (i.e., "%h" is equivalent to "%1u" if the
# first character of the user name is a lowercase letter), and any occurrence of
# "%x" will be replaced by Cyrus' "fulldirhash" character for that user name.
#
# See "perldoc cyrus2dovecot" for details.
#
my %DEFAULT = (
	dovecot_inbox => '/tmp/dovecot/%u/Maildir',
	cyrus_inbox => '/var/spool/imap/user/%u',
	cyrus_sub => '/var/imap/user/%h/%u.sub',
	cyrus_seen => '/var/imap/user/%h/%u.seen',
	cyrus_quota => undef,
	cyrus_quota_format => 1,        # Cyrus quota format (1 for legacy).
	dovecot_uidlist_format => 3,    # Create this dovecot-uidlist format.
	dovecot_host => hostname,       # The host name for Maildir++ filenames.
	dovecot_crlf => 0,              # Use CR+LF instead of LF in Dovecot?
	default_quota => 0,             # Use this quota as a fallback.
	dump_meta => 0,                 # Print the metadata structure?
	debug => 0,                     # Print debug output?
	quiet => 0,                     # Suppress the standard output?
	edit_foldernames => []          # List of folder name substitutions.
);
 
#
# Plan of attack: Basically, we convert the folders of a user in two steps.
#
# 1) We call c_read_mailbox() which reads Cyrus' metadata for all folders of the
#    user and creates a data structure such as the following.  In this example,
#    the user has the folder "ac" and the subfolder "ac/dc" next to his INBOX,
#    he is subscribed to these folders, and there are two e-mails per folder.
#    The user also defined a few IMAP keywords (a.k.a. user flags).  Both the
#    system flags and user keywords are saved as bitmasks for each e-mail.
#
#    	$meta->{subscriptions} = [ 'ac', 'ac/dc' ]
#    	     ->{quota} = 2147483648
#    	     ->{box}->{'INBOX'}->{uidvalidity} = 1107601073
#    	                       ->{uidnext} = 3
#    	                       ->{nonrecent} = 2	# Last non-recent UID.
#    	                       ->{keywords}  = [ 'Junk', '$Label1' ]
#    	                       ->{mail}->{1}->{internaldate} = 1107601472
#    	                                    ->{sysflags} = $sysmask
#    	                                    ->{usrflags} = $usrmask
#    	                                 {2}->{internaldate} = 1107601543
#    	                                    ->{sysflags} = $sysmask
#    	                                    ->{usrflags} = $usrmask
#    	            ->{'ac'}   ->{uidvalidity} = 1108639232
#    	                       ->{uidnext} = 3
#    	                       ->{nonrecent} = 1	# Last non-recent UID.
#    	                       ->{keywords}  = [ 'Private', 'Work' ]
#    	                       ->{mail}->{1}->{internaldate} = 1108639290
#    	                                    ->{sysflags} = $sysmask
#    	                                    ->{usrflags} = $usrmask
#    	                                 {2}->{internaldate} = 1108639299
#    	                                    ->{sysflags} = $sysmask
#    	                                    ->{usrflags} = $usrmask
#    	            ->{'ac/dc'}->{uidvalidity} = 1109821442
#    	                       ->{uidnext} = 3
#    	                       ->{nonrecent} = 1	# Last non-recent UID.
#    	                       ->{keywords}  = [ 'Rock', 'Pop' ]
#    	                       ->{mail}->{1}->{internaldate} = 1109821455
#    	                                    ->{sysflags} = $sysmask
#    	                                    ->{usrflags} = $usrmask
#    	                                 {2}->{internaldate} = 1109821500
#    	                                    ->{sysflags} = $sysmask
#    	                                    ->{usrflags} = $usrmask
#
# 2) We call d_write_mailbox() which creates a Dovecot Maildir++ directory
#    including all subfolders, writes the metadata, and converts the actual
#    e-mails.
#
sub main ();
sub usage ();
sub debug (@);
sub info (@);
sub warning (@);
sub error (@);
sub fatal (@);
sub message ($@);
sub makedir ($);
sub readint ($$);
sub xread ($$$);
sub slurp ($);
sub fixpath ($@);
sub c_fulldirhash ($);
sub c_read_skiplist ($);
sub c_read_mailbox ($$$$$$);
sub d_write_mailbox ($$$);
 
#
# IMAP flags.
#
use constant FLAG_ANSWERED => (1 << 0);	# As stored in Cyrus' index database.
use constant FLAG_FLAGGED  => (1 << 1);	# As stored in Cyrus' index database.
use constant FLAG_DELETED  => (1 << 2);	# As stored in Cyrus' index database.
use constant FLAG_DRAFT    => (1 << 3);	# As stored in Cyrus' index database.
use constant FLAG_SEEN     => (1 << 4);	# Stored in Cyrus' seen database.
 
#
# Cyrus skiplist database constants.
#
use constant INORDER           => 1;
use constant ADD               => 2;
use constant DELETE            => 4;
use constant COMMIT            => 255;
use constant DUMMY             => 257;
use constant HEADER_SIZE       => 28;
use constant HEADER_MAGIC_SIZE => 20;
use constant HEADER_MAGIC      => "\241\002\213\015skiplist file\0\0\0";
 
#
# Cyrus mailbox header constants.
#
use constant MAILBOX_HEADER_MAGIC_SIZE => 115;
use constant MAILBOX_HEADER_MAGIC      =>
    "\241\002\213\015Cyrus mailbox header\n" .
    "\"The best thing about this system was that it had lots of goals.\"\n" .
    "\t--Jim Morris on Andrew\n";	# Gesundheit!
 
#
# Miscellaneous constants.
#
use constant UINT32_MAX => 4294967295;
 
our $VERSION = sprintf('%d.%d (%04d-%02d-%02d)', q$Revision: 1.2 $ =~ /(\d+)/g,
    q$Date: 2008/09/24 09:52:33 $ =~ /(\d{4})\/(\d{2})\/(\d{2})/);
 
my $MYSELF = basename($0);
my ($FOLDERS, $MAILS, $SIZE, $USER, $FROM_STDIN, $QUOTADATA, %CONF);
 
GetOptions(
	\%CONF,
	'edit_foldernames|edit-foldernames|E=s@',
	'cyrus_inbox|cyrus-inbox|C=s',
	'cyrus_quota|cyrus-quota|Q=s',
	'cyrus_quota_format|cyrus-quota-format|O=i',
	'cyrus_seen|cyrus-seen|S=s',
	'cyrus_sub|cyrus-sub|U=s',
	'dovecot_host|dovecot-host|H=s',
	'dovecot_inbox|dovecot-inbox|D=s',
	'default_quota|default-quota|N=s',
	'dovecot_uidlist_format|dovecot-uidlist-format|F=i',
	'dovecot_crlf|dovecot-crlf|c',
	'dump_meta|dump-meta|m',
	'debug|d',
	'quiet|q',
	'h',
	'v'
) or usage;
 
if ($CONF{h}) {
	exec($0, '--help') or die;
}
if ($CONF{v}) {
	exec($0, '--version') or die;
}
 
foreach my $opt (keys %DEFAULT) {
	$CONF{$opt} = $DEFAULT{$opt} if not exists($CONF{$opt});
}
 
fatal('Option dovecot-uidlist-format must be set to: 1 or 3')
    if ($CONF{dovecot_uidlist_format} != 1 and
        $CONF{dovecot_uidlist_format} != 3);
 
$SIG{__WARN__} = sub { fatal('Caught exception:', @_) };	# Perl warnings.
$FROM_STDIN = (@ARGV == 0) ? 1 : 0;
$QUOTADATA = c_read_skiplist($CONF{cyrus_quota})
    if ($CONF{cyrus_quota} and $CONF{cyrus_quota_format} != 1);
 
main;
exit 0;
 
# ----- Generic subroutines. ---------------------------------------------------
 
#
# Loop over the specified users and convert their e-mails.  This is done within
# a subroutine in order to make it callable from error().
#
sub main () {
	while (my $user = $FROM_STDIN ? <STDIN> : shift(@ARGV)) {
		chomp($user);
 
		my $start = time;
		my $meta = {};
		my $dovecot_inbox = $CONF{dovecot_inbox};
		my $cyrus_inbox = $CONF{cyrus_inbox};
		my $cyrus_seen = $CONF{cyrus_seen};
		my $cyrus_sub = $CONF{cyrus_sub};
		my $cyrus_quota = $CONF{cyrus_quota};
 
		debug("Converting the e-mail folders of $user.");
 
		# (Re)set "global" variables.
		$USER = $user;
		$FOLDERS = $MAILS = $SIZE = 0;
 
		# Resolve "%u", "%h", "x", and "%<n>u" within pathnames.
		fixpath($user, $dovecot_inbox, $cyrus_inbox, $cyrus_seen,
		    $cyrus_sub);
		fixpath($user, $cyrus_quota)
		    if ($CONF{cyrus_quota} and $CONF{cyrus_quota_format} == 1);
 
		# Do the actual conversion.
		c_read_mailbox($meta, $user, $cyrus_inbox, $cyrus_seen,
		    $cyrus_sub, $cyrus_quota);
		d_write_mailbox($meta, $cyrus_inbox, $dovecot_inbox);
 
		# Give some feedback.
		print Dumper($meta) if $CONF{dump_meta};
		info(sprintf('%u messages in %u folders (%.1f MiB, %u s)',
		    $MAILS, $FOLDERS, $SIZE / 1024 / 1024, time - $start))
		    unless $CONF{quiet};
 
		debug("Done converting the e-mail folders of $user.");
	}
}
 
#
# Print usage information to the standard error output and die.
#
sub usage () {
	exec("$0 --help >&2") or die;
}
 
#
# Print a message to the standard output if we were called with "--debug", do
# nothing otherwise.
#
sub debug (@) {
	return unless $CONF{debug};
	my @message = @_;
 
	info(@message);
}
 
#
# Print a message to the standard output.
#
sub info (@) {
	my @message = @_;
 
	message(\*STDOUT, @message);
}
 
#
# Print a message to the standard error output.
#
sub warning (@) {
	my @message = @_;
 
	unshift(@message, '(warning)');
	message(\*STDERR, @message);
}
 
#
# Print a message to the standard error output.  Then, try to continue with the
# next user (if any).  When done, exit >0.
#
sub error (@) {
	my @message = @_;
 
	unshift(@message, '(error)');
	message(\*STDERR, @message);
	main;	# Continue with the next user (if any).
	exit 1;
}
 
#
# Print a message to the standard error output and exit >0 immediately.
#
sub fatal (@) {
	my @message = @_;
 
	unshift(@message, '(fatal)');
	message(\*STDERR, @message);
	exit 1;
}
 
#
# Print a message.
#
sub message ($@) {
	my $handle = shift;
	my @message = @_;
	my $prefix = $MYSELF;
 
	$prefix .= " [$USER]" if defined($USER);
	chomp(@message);
	print $handle "$prefix: @message\n";
}
 
#
# Create the specified directory, and recursively create parent directories as
# needed.  Die on error.
#
sub makedir ($) {
	my $dir = shift;
 
	unless (-d $dir) {
		my $parent = dirname($dir);
 
		makedir($parent) if not -d $parent;
		mkdir($dir) or error("Cannot create directory $dir: $!");
		debug('Created directory:', $dir);
	}
}
 
#
# Read and return a 32-bit integer which is in "network" (big-endian) order.
# Die on error.
#
sub readint ($$) {
	my ($file, $handle) = @_;
	my $buf = xread($file, $handle, 4);
 
	return unpack('N', $buf);
}
 
#
# Read the specified number of bytes or die.
#
sub xread ($$$) {
	my ($file, $handle, $size) = @_;
	my ($buf, $n);
 
	defined($n = read($handle, $buf, $size))
	    or error("Cannot read $file: $!");
	error("Read $n instead of $size bytes from $file.") if $n != $size;
	return $buf;
}
 
#
# Read (the rest of) a file into memory and return the read content.
#
sub slurp ($) {
	my $handle = shift;
	local $/;	# Slurp mode.
 
	return <$handle>;
}
 
#
# Resolve "%u", "%h", "x", and "%<n>u" within pathnames.  Modify the specified
# arguments directly (as opposed to returning the new pathnames).
#
sub fixpath ($@) {
	my $user = shift;
	my @char = split(//, $user);
	my $hash = $char[0];
	my $fullhash = c_fulldirhash($user);
 
	if ($hash !~ /^[a-z]$/) {
		# This is how Cyrus "hashes" non-[a-z]-characters.
		$hash = ($hash =~ /^[A-Z]$/) ? lc($hash) : 'q';
	}
 
	for (@_) {
		s/%u/$user/g;
		s/%h/$hash/g;
		s/%x/$fullhash/g;
		s/%(\d+)u/$char[$1-1]/g;
	}
}
 
# ----- Cyrus subroutines. -----------------------------------------------------
 
sub _c_read_folders ($$$$);
sub _c_read_header ($);
sub _c_read_index ($);
sub _c_read_old_seen ($);
sub _c_read_legacy_quota ($);
sub _c_read_skiplist_item ($$);
sub _c_parse_seendata ($);
sub _c_seen ($$);
sub _c_make_uid ($$);
 
#
# Return Cyrus' "fulldirhash" character for the given user name.  See also the
# "dir_hash_c" subroutine in tools/rehash.
#
sub c_fulldirhash ($) {
	my $user = shift;
	my $n = 0;
 
	$n = (($n << 3) ^ ($n >> 5)) ^ ord($_) for split(/ */, $user);
	return chr(ord('A') + ($n % 23));
}
 
#
# Read a "skiplist" database file (or a flat text file) and return a reference
# to a hash containing the records.  Die on error.
#
sub c_read_skiplist ($) {
#
# | /*
# |  * disk format; all numbers in network byte order
# |  *
# |  * there's the data file, consisting of the multiple records of "key",
# |  * "data", and "skip pointers", where skip pointers are the record number of
# |  * the data pointer.  [...]
# |  */
# |
# | /*
# |    header "skiplist file\0\0\0"
# |    version (4 bytes)
# |    version_minor (4 bytes)
# |    maxlevel (4 bytes)
# |    curlevel (4 bytes)
# |    listsize (4 bytes)
# |      in active items
# |    log start (4 bytes)
# |      offset where log records start, used mainly to tell when to compress
# |    last recovery (4 bytes)
# |      seconds since unix epoch
# |
# |    1 or more skipnodes, one of:
# |
# |      record type (4 bytes) [DUMMY, INORDER, ADD]
# |      key size (4 bytes)
# |      key string (bit string, rounded to up to 4 byte multiples w/ 0s)
# |      data size (4 bytes)
# |      data string (bit string, rounded to up to 4 byte multiples w/ 0s)
# |      skip pointers (4 bytes each)
# |        least to most
# |      padding (4 bytes, must be -1)
# |
# |      record type (4 bytes) [DELETE]
# |      record ptr (4 bytes; record to be deleted)
# |
# |      record type (4 bytes) [COMMIT]
# |
# |    record type is either
# |      DUMMY (first node is of this type)
# |      INORDER
# |      ADD
# |      DELETE
# |      COMMIT (commit the previous records)
# | */
# |
# | enum {
# |     INORDER = 1,
# |     ADD = 2,
# |     DELETE = 4,
# |     COMMIT = 255,
# |     DUMMY = 257
# | };
# |
# | #define HEADER_MAGIC ("\241\002\213\015skiplist file\0\0\0")
# | #define HEADER_MAGIC_SIZE (20)
#
# [ lib/cyrusdb_skiplist.c ]
#
	my $file = shift;
	my $skiplist = {};
	my ($buf, $n);
 
	if (not -e $file) {
		#
		# The seen or subscription database will not be created until
		# the user saw an e-mail or subscribed a folder.  So, we just
		# return an empty skiplist.
		#
		debug("$file doesn't exist, I'll pretend it's empty.");
		return $skiplist;
	}
 
	debug('Reading:', $file);
	open(my $handle, '<', $file) or error("Cannot open $file: $!");
 
	# Read and check the header magic.
	defined($n = read($handle, $buf, HEADER_MAGIC_SIZE))
	    or error("Cannot read $file: $!");
 
	if ($n == HEADER_MAGIC_SIZE and $buf eq HEADER_MAGIC) {
		# Read the actual header.
		$buf = xread($file, $handle, HEADER_SIZE);
		my @header = unpack('N7', $buf);
		error('Unknown skiplist DB version:', $header[0])
		    if $header[0] != 1;
		debug('Minor skiplist DB version:', $header[1]);
 
		# Read the records.
		while ($n = read($handle, $buf, 4)) {
			error("Read $n instead of 4 bytes from $file.")
			    if $n != 4;
			my $rectype = unpack('N', $buf);
 
			# Parse the record type.
			if ($rectype == COMMIT) {
				debug('Record type: COMMIT');
				next;
			} elsif ($rectype == DELETE) {
				debug('Record type: DELETE');
				seek($handle, 4, SEEK_CUR)
				    or error('Cannot seek in:', $file);
				next;
			} elsif ($rectype == INORDER) {
				debug('Record type: INORDER');
			} elsif ($rectype == ADD) {
				debug('Record type: ADD');
			} elsif ($rectype == DUMMY) {
				debug('Record type: DUMMY');
			} else {
				error('Unknown record type:', $rectype);
			}
 
			# Read and save the key and the data, if any.
			my $key = _c_read_skiplist_item($file, $handle);
			my $data = _c_read_skiplist_item($file, $handle);
 
			if (defined($key)) {
				$skiplist->{$key} = $data;
				$data = '(undef)' if not defined($data);
				debug("Saved skiplist record: $key = $data");
			}
 
			#
			# Skip the "skip pointers" (4 bytes each), terminated by
			# 4 bytes of -1 padding.
			#
			1 while readint($file, $handle) != 0xFFFFFFFF;
		}
	} else {
		# Assume it's a flat text file if the header magic is missing.
		debug('Parsing as a flat text file:', $file);
		seek($handle, 0, SEEK_SET) or error('Cannot seek in:', $file);
		while (<$handle>) {
			my ($key, $data) = /(.*)\t(.*)/;
 
			error("Cannot parse $file.")
			    if not (defined($key) and defined($data));
			$skiplist->{$key} = $data;
			debug("Saved flat file record: $key = $data");
		}
	}
	close($handle) or error("Cannot close $file: $!");
	debug('Done reading:', $file);
	return $skiplist;
}
 
#
# Read all global and folder-specific metadata of a user into the given data
# structure.  Die on error.
#
sub c_read_mailbox ($$$$$$) {
	my ($meta, $user, $rootdir, $seenfile, $subfile, $quotafile) = @_;
	my (%existing, @subscriptions);
	my $seendata = ($seenfile eq 'cyrus.seen') ? undef :
	    c_read_skiplist($seenfile);
 
	debug('Reading Cyrus metadata.');
	_c_read_folders($meta, $rootdir, $rootdir, $seendata);
 
	#
	# Cyrus' subscription skiplist keys are in the form "user.<name>.<box>",
	# where <name> is the user name and <box> the name of the subscribed
	# folder.  We only save the latter (or "INBOX" for "user.<name>").  Note
	# that the subscription skiplist values are empty.  Cyrus doesn't clean
	# up subscriptions of folders which no longer exist (as per RFC 3501,
	# 6.3.6.).  While at it, we check whether a folder exist before adding
	# it to the list in order to straighten the subscriptions up.
	#
	foreach my $folder (keys %{ $meta->{box} }) {
		$folder =~ s/\//./g;
		$existing{$folder} = 1;
	}
	foreach my $folder (keys %{ c_read_skiplist($subfile) }) {
		$folder =~ s/^user\.$user$/INBOX/;
		$folder =~ s/^user\.[^\.]+\.(.+)$/$1/;
		$folder =~ s/\//./g;	# Probably not necessary.
		push(@subscriptions, $folder) if $existing{$folder};
	}
	$meta->{subscriptions} = \@subscriptions;
	debug('Subscribed folders:', @subscriptions);
 
	#
	# Save the user's quota limit, either from Cyrus or using the specified
	# default quota.
	#
	if ($quotafile) {
		if ($CONF{cyrus_quota_format} == 1) {
			$meta->{quota} = _c_read_legacy_quota($quotafile);
		} elsif (defined($QUOTADATA->{"user.$user"})) {
			$meta->{quota} = $QUOTADATA->{"user.$user"};
			$meta->{quota} =~ s/^\d+\s+(\d+)$/$1/;
			error('Cannot parse quota:', $QUOTADATA->{"user.$user"})
			    if $meta->{quota} !~ /^\d+$/;
		}
		warning('No quota information available.')
		    if (not $meta->{quota} and not $CONF{default_quota});
	}
	if ($meta->{quota}) {
		$meta->{quota} *= 1024;	# Kilobytes to bytes,
	} else {
		$meta->{quota} = $CONF{default_quota} || 0;
	}
	debug('Quota:', $meta->{quota});
	debug('Done reading Cyrus metadata.');
}
 
#
# Read and save the folder-specific metadata for the given mailbox into the
# given data structure.  Recurse into subdirectories.  Die on error.
#
sub _c_read_folders ($$$$) {
	my ($meta, $rootdir, $boxpath, $seendata) = @_;
	my $box = ($boxpath eq $rootdir) ? 'INBOX' : $boxpath;
	my ($mailfolder, $index, $seen);
 
	#
	# Currently, we wouldn't be able to distinguish user.foo.INBOX from
	# user.foo.  While this could be fixed, a Maildir/.INBOX folder could
	# not be accessed via Dovecot, anyway.
	#
	error('Cannot convert non-INBOX folder named INBOX.')
	    if $box =~ /^$rootdir\/+INBOX$/;
 
	# Let $box hold the path relative to the root directory (or "INBOX").
	$box =~ s/^$rootdir\/+//;
 
	debug('Looking at folder:', $box);
 
	if (-e "$boxpath/cyrus.header" and -e "$boxpath/cyrus.index") {
		# Collect this folder's metadata.
		my $header = _c_read_header($boxpath);
		$index = _c_read_index($boxpath);
		$header->{folderuid} = _c_make_uid($index->{uidvalidity}, $box)
		    if not $header->{folderuid};
		$seen = defined($seendata) ?
		    _c_parse_seendata($seendata->{$header->{folderuid}}) :
		    _c_parse_seendata(_c_read_old_seen($boxpath));
 
		# Autovivify "mail" in case this folder contains no e-mails.
		$meta->{box}->{$box}->{mail} = {};
		$meta->{box}->{$box}->{nonrecent} = $seen->{nonrecent};
		$meta->{box}->{$box}->{uidvalidity} = $index->{uidvalidity};
		$meta->{box}->{$box}->{uidnext} = $index->{lastuid} + 1;
		$meta->{box}->{$box}->{keywords} =
		    [ split(/\s+/, $header->{userflags}) ];
 
		debug('The UIDVALIDITY is:', $index->{uidvalidity});
		debug('The last message UID is:', $index->{lastuid});
		debug('The last non-recent message UID is:', $seen->{nonrecent});
		$mailfolder = 1;
	} else {
		#
		# The folder we're in is not in a Cyrus mailbox.  However, we
		# don't simply return here because this folder might contain
		# other folders which might be Cyrus mailboxes (unless we're in
		# the INBOX, in which case something is going wrong).
		#
		error('No Cyrus INBOX at:', $boxpath) if $box eq 'INBOX';
		debug("Skipping $boxpath as it's not a Cyrus mailbox.");
		$mailfolder = 0;
	}
 
	opendir(my $handle, $boxpath) or error("Cannot open $boxpath: $!");
	while (my $file = readdir($handle)) {
		next if $file =~ /^\.\.?$/;
		next if $file =~ /^cyrus\.(?:header|index|cache|seen)$/;
		my $path = "$boxpath/$file";
 
		if (-d $path) {
			# Recurse into subfolders.
			_c_read_folders($meta, $rootdir, $path, $seendata);
		} elsif ($mailfolder and $file =~ /^(\d+)\.$/) {
			my $uid = $1;
			my $attr;
 
			if ($index->{$uid}) {
				# Save the e-mail's flags and its INTERNALDATE.
				debug("Saving attributes of: $box/$file");
				$attr->{usrflags} = $index->{$uid}->{usrflags};
				$attr->{sysflags} = $index->{$uid}->{sysflags};
				$attr->{internaldate} =
				    $index->{$uid}->{internaldate};
			} else {
				my @statlist = stat($path);
 
				warning("Index record missing for: $box/$file");
				error('Cannot stat(2) message file:', $path)
				    if not defined($statlist[9]);
 
				$attr->{usrflags} = 0;
				$attr->{sysflags} = 0;
				$attr->{internaldate} = $statlist[9];
			}
			$attr->{sysflags} |= FLAG_SEEN if _c_seen($uid, $seen);
			$meta->{box}->{$box}->{mail}->{$uid} = $attr;
		} else {
			warning("Skipping $box/$file, dunno what it is.");
		}
	}
	closedir($handle) or error("Cannot close $boxpath: $!");
	debug('Done with folder:', $box);
}
 
#
# Read a mailbox header file and return a reference to a hash which holds the
# data.  Die on error.  See doc/internal/mailbox-format.html for details on the
# format of the mailbox header file.
#
sub _c_read_header ($) {
#
# | This file contains mailbox-wide information that does not change that often.
# | Its format:
# |
# | <Mailbox Header Magic String>
# | <Quota Root>\t<Mailbox Unique ID String>\n
# | <Space-separated list of user flags>\n
# | <Mailbox ACL>\n
#
# [ doc/internal/mailbox-format.html ]
#
	my $boxpath = shift;
	my $file = "$boxpath/cyrus.header";
	my ($header, $buf);
 
	debug('Reading:', $file);
	open(my $handle, '<', $file) or error("Cannot open $file: $!");
 
	# Read and check the header magic.
	$buf = xread($file, $handle, MAILBOX_HEADER_MAGIC_SIZE);
	error("Cannot parse $file: Mailbox header magic incorrect")
	    if $buf ne MAILBOX_HEADER_MAGIC;
 
	# Slurp the rest of the file into memory and close it.
	$buf = slurp($handle);
	close($handle) or error("Cannot close $file: $!");
 
	# Guess the header file format and save the data into a hash.
	if ($buf =~ /^([^\t\n]*)\t([^\n]+)\n([^\n]*)\n([^\n]*)\n$/) {
		$header->{quotaroot} = $1;
		$header->{folderuid} = $2;
		$header->{userflags} = $3;
		$header->{folderacl} = $4;
	} elsif ($buf =~ /^([^\n]*)\n([^\n]*)\n([^\n]*)\n$/) {
		$header->{quotaroot} = $1;
		$header->{folderuid} = 0;	# No mailbox UID provided.
		$header->{userflags} = $2;
		$header->{folderacl} = $3;
	} else {
		error("Cannot parse $file: One or more fields missing");
	}
	$header->{userflags} =~ s/\s+$//;	# Cyrus adds a trailing space.
 
	debug('Mailbox UID:', $header->{folderuid});
	debug('Quota root:', $header->{quotaroot});	# Unused.
	debug('Mailbox ACL:', $header->{folderacl});	# Unused.
	debug('User-defined keywords:', $header->{userflags});
	debug('Done reading:', $file);
	return $header;
}
 
#
# Read a mailbox index file and return a reference to a hash which holds the
# interesting data.  Die on error.  See doc/internal/mailbox-format.html for
# details on the format of the mailbox index file.  See also:
#
# imap/mailbox.h and imap/mailbox.c:mailbox_read_index_header()
#
sub _c_read_index ($) {
	my $boxpath = shift;
	my $file = "$boxpath/cyrus.index";
	my ($data, $buf, $n);
 
	debug('Reading:', $file);
	open(my $handle, '<', $file) or error("Cannot open $file: $!");
 
	# Read and save the interesting header fields.
	seek($handle, 8, SEEK_SET) or error('Cannot seek in:', $file);
 
	my $version = readint($file, $handle);
	my $headersize = readint($file, $handle);
	my $recordsize = readint($file, $handle);
 
	debug('Index format version:', $version);
 
	seek($handle, 8, SEEK_CUR) or error('Cannot seek in:', $file);
	$data->{lastuid} = readint($file, $handle);
 
	# Skip 4 additional bytes for 64-bit quotas in Cyrus 2.2 and newer.
	seek($handle, ($version < 6) ? 8 : 12, SEEK_CUR)
	    or error('Cannot seek in:', $file);
	$data->{uidvalidity} = readint($file, $handle);
 
	#
	# As we try to parse future index file formats (i.e., we don't bail out
	# if the $version is unknown), we do at least a dumb consistency check:
	# Cyrus sets the UIDVALIDITY of a folder to its creation date, so let's
	# make sure its value is >= 600000000 (1989-01-05 11:40:00).
	#
	error("Cannot parse $file: UIDVALIDITY is", $data->{uidvalidity})
	    if $data->{uidvalidity} < 600000000;
 
	seek($handle, $headersize, SEEK_SET) or error('Cannot seek in:', $file);
 
	# Read and save the interesting fields of all records.
	while ($n = read($handle, $buf, 4)) {
		error("Read $n instead of 4 bytes from $file.") if $n != 4;
		my $uid = unpack('N', $buf);
 
		debug('Reading index record for message UID:', $uid);
 
		# Read, save, and "check" (see above) the INTERNALDATE.
		$data->{$uid}->{internaldate} = readint($file, $handle);
		error("Cannot parse $file: INTERNALDATE is",
		    $data->{$uid}->{internaldate})
		    if $data->{$uid}->{internaldate} < 600000000;
 
		#
		# Read and save the system and user flags.  Note that the size
		# of the user flags bitmask is MAX_USER_FLAGS / 32.  At least in
		# Cyrus 2.2.12, MAX_USER_FLAGS is 128 by default, so we'll read
		# 4 bytes.  If this is ever changed, the number of bytes to skip
		# at the end of this loop (currently $recordsize - 40) must be
		# adjusted accordingly.
		#
		seek($handle, 24, SEEK_CUR) or error('Cannot seek in:', $file);
		$data->{$uid}->{sysflags} = readint($file, $handle);
		$data->{$uid}->{usrflags} = readint($file, $handle);
 
		# Skip the rest of the record.
		seek($handle, $recordsize - 40, SEEK_CUR)
		    or error('Cannot seek in:', $file);
	}
	close($handle) or error("Cannot close $file: $!");
	debug('Done reading:', $file);
	return $data;
}
 
#
# Read a Cyrux 1.x seen file and return the seen UIDs string or undef if the
# seen file is empty.  Die on error.
#
sub _c_read_old_seen ($) {
	my $boxpath = shift;
	my $file = "$boxpath/cyrus.seen";
 
	debug('Reading:', $file);
	open(my $handle, '<', $file) or error("Cannot open $file: $!");
	my @seen = <$handle>;
	close($handle) or error("Cannot close $file: $!");
	chomp(@seen);
 
	error("Cannot parse $file: File contains multiple lines") if @seen > 1;
	debug('Done reading:', $file);
	return $seen[0];
}
 
#
# Read a legacy quota file and return the quota limit (specified in kilobytes)
# or undef if the specified file does not exist.  Die on error.
#
sub _c_read_legacy_quota ($) {
	my $file = shift;
 
	if (not -e $file) {
		debug('Legacy quota file does not exist:', $file);
		return undef;
	}
 
	debug('Reading:', $file);
	open(my $handle, '<', $file) or error("Cannot open $file: $!");
	my @quota = <$handle>;
	close($handle) or error("Cannot close $file: $!");
	chomp(@quota);
 
	error("Cannot parse $file: Not in legacy quota format")
	    if (@quota != 2 or $quota[1] !~ /^\d+$/);
	debug('Done reading:', $file);
	return $quota[1];
}
 
#
# Read a skiplist string and return the string or undef if the string's length
# is zero.  Die on error.
#
sub _c_read_skiplist_item ($$) {
	my ($file, $handle) = @_;
 
	# Read the item size and the actual item.
	my $size = readint($file, $handle);
	my $item = xread($file, $handle, $size);
 
	# Skip four-byte-alignment padding, if any.
	seek($handle, (($size + 3) & 0xFFFFFFFC) - $size, SEEK_CUR)
	    or error("Cannot seek skiplist (item size: $size).");
 
	return ($size > 0) ? $item : undef;
}
 
#
# Parse the seen data for a mailbox and return a reference to a hash which holds
# the seen message UIDs (saved in a format optimized for fast _c_seen() lookups)
# and the last non-recent message UID.  Die on error.
#
sub _c_parse_seendata ($) {
#
# The third field of the seen data contains the last non-recent message UID, and
# the fifth field contains a seen UIDs string which is in the following format
# (see doc/internal/database-formats.html for a description of all seen data
# fields):
#
# | /*
# |  * Format of the seenuids string:
# |  *
# |  * no whitespace, n:m indicates an inclusive range (n to m), otherwise
# |  * list is comma separated of single messages, e.g.:
# |  *
# |  * 1:16239,16241:17015,17019:17096,17098,17100
# |  */
#
# [ imap/index.c ]
#
# See also: imap/seen_db.c:seen_readit()
#
	my $seendata = shift;
	my $seen;
 
	if (defined($seendata)) {
		my @fields = split(/\s+/, $seendata, 5);
 
		debug('Parsing seen data:', $seendata);
		error('Cannot parse seen data:', $seendata)
		    if @fields != 5;
		$fields[4] =~ s/\s+//g;	# Cyrus sometimes adds a trailing tab.
		foreach my $uid (split(/,/, $fields[4])) {
			debug('Parsing seen UID(s):', $uid);
			if ($uid =~ /^\d+$/) {
				$seen->{$uid} = 1;
			} elsif ($uid =~ /^(\d+):(\d+)$/) {
				my ($n, $m) = ($2 > $1) ? ($1, $2) : ($2, $1);
				push(@{ $seen->{ranges} },
				    { min => $n, max => $m });
			} else {
				error('Cannot parse seen UID(s):', $uid);
			}
		}
		$seen->{nonrecent} = $fields[2];
	} else {
		$seen->{nonrecent} = 0;
		$seen->{ranges} = [];
	}
	return $seen;
}
 
#
# Return true if the message with the given UID is seen, false otherwise.
#
sub _c_seen ($$) {
	my ($uid, $seen) = @_;
 
	return 1 if $seen->{$uid};
	foreach my $range (@{ $seen->{ranges} }) {
		return 1 if ($uid >= $range->{min} and $uid <= $range->{max});
	}
	return 0;
}
 
#
# Calculate and return Cyrus' internal mailbox UID.  See also:
#
# imap/mailbox.c:mailbox_make_uniqueid()
#
sub _c_make_uid ($$) {
	my ($uidvalidity, $box) = @_;
	my $hash = 0;
 
	if ($box eq 'INBOX') {
		$box = "user.$USER";
	} else {
		$box =~ s/\//./g;
		$box = "user.$USER.$box";
	}
 
	foreach my $character (split(//, $box)) {
		$hash *= 251;
		$hash += ord($character);
		$hash %= 2147484043;
	}
 
	my $uid = sprintf("%08lx%08lx", $hash, $uidvalidity);
	debug("Calculated mailbox UID for $box: $uid");
	return $uid;
}
 
# ----- Dovecot subroutines. ---------------------------------------------------
 
sub _d_make_maildir ($);
sub _d_touch_maildirfolder ($);
sub _d_write_maildirsize ($$);
sub _d_write_subscriptions ($$);
sub _d_write_keywords ($$);
sub _d_create_filename ($$$$);
 
#
# Create the Maildir++ folder including all subfolders, write the metadata, and
# convert the actual e-mails.
#
sub d_write_mailbox ($$$) {
	my ($meta, $c_rootdir, $d_rootdir) = @_;
 
	#
	# The Maildir++ filenames we create include a random number.  We call
	# srand(3) in order to re-create the same filenames if the conversion
	# is repeated for some reason.
	#
	srand($meta->{box}->{'INBOX'}->{uidvalidity});
 
	debug('Writing Dovecot folders.');
	foreach my $c_box (keys %{ $meta->{box} }) {
		my ($c_boxpath, $d_boxpath);
		my $d_box = $c_box;
 
		if ($d_box eq 'INBOX') {
			$c_boxpath = $c_rootdir;
		} else {
			$c_boxpath = "$c_rootdir/$c_box";
			$d_box =~ s/\//./g;
			$d_box = ".$d_box";
		}
 
		# Edit the Maildir++ folder name if desired.
		foreach my $operation (@{ $CONF{edit_foldernames} }) {
			debug("Editing folder name $d_box: $operation");
			my $r = eval("\$d_box =~ $operation");
			error("Cannot evaluate $operation: $@") if $@;
			error("Cannot evaluate $operation.") if not defined($r);
			debug('New folder name:', $d_box);
		}
 
		$d_boxpath = ($d_box eq 'INBOX') ?
		    $d_rootdir : "$d_rootdir/$d_box";
 
		# Create an empty Maildir.
		_d_make_maildir($d_boxpath);
		_d_touch_maildirfolder($d_boxpath) unless $d_box eq 'INBOX';
 
		my $box = $meta->{box}->{$c_box};
		my $uidfile = "$d_boxpath/dovecot-uidlist";
 
		# Open the dovecot-uidlist file.
		open(my $ufh, '>', $uidfile)
		    or error("Cannot open $uidfile: $!");
 
		# Write the dovecot-uidlist header line.
		if ($CONF{dovecot_uidlist_format} == 1) {
			print $ufh "1 $box->{uidvalidity} $box->{uidnext}\n"
			    or error("Cannot write $uidfile: $!");
		} elsif ($CONF{dovecot_uidlist_format} == 3) {
			print $ufh "3 V$box->{uidvalidity} N$box->{uidnext}\n"
			    or error("Cannot write $uidfile: $!");
		} else {
			error('Unknown dovecot-uidlist format:',
			    $CONF{dovecot_uidlist_format});
		}
 
		# Handle all e-mails in this folder.
		foreach my $uid (sort {$a <=> $b} keys %{ $box->{mail} }) {
			my $c_mailpath = "$c_boxpath/$uid.";
			my $d_temppath = "$d_boxpath/tmp/$MYSELF.$$.$MAILS";
 
			# Convert the e-mail.
			open(my $cfh, '<', $c_mailpath)
			    or error("Cannot open $c_mailpath: $!");
			open(my $dfh, '>', $d_temppath)
			    or error("Cannot open $d_temppath: $!");
			while (<$cfh>) {
				s/\r\n/\n/ unless $CONF{dovecot_crlf};
				print $dfh $_
				    or error("Cannot write $d_temppath: $!");
			}
			close($cfh) or error("Cannot close $c_mailpath: $!");
			close($dfh) or error("Cannot close $d_temppath: $!");
 
			#
			# Create the Maildir++ e-mail filename.  Include the
			# size fields used by Dovecot:
			#
			# | ,S=<size>: <size> contains the file size.  Getting
			# | the size from the filename avoids doing a stat(),
			# | which may improve the performance.  This is
			# | especially useful with Maildir++ quota.
			# |
			# | ,W=<vsize>: <vsize> contains the file's RFC822.SIZE,
			# | i.e. the file size with linefeeds being CR+LF
			# | characters.  If the message was stored with CR+LF
			# | linefeeds, <size> and <vsize> are the same.  Setting
			# | this may give a small speedup because now Dovecot
			# | doesn't need to calculate the size itself.
			#
			# [ http://wiki.dovecot.org/MailboxFormat/Maildir ]
			#
			my @c_stat = stat($c_mailpath);
			my @d_stat = stat($d_temppath);
 
			error('Cannot stat(2) message file:', $c_mailpath)
			    if not defined($c_stat[7]);
			error('Cannot stat(2) message file:', $d_temppath)
			    if not defined($d_stat[7]);
 
			my $attr = $box->{mail}->{$uid};
			my $size = $d_stat[7];	# File size with LF.
			my $vsize = $c_stat[7];	# File size with CR+LF.
			my $subdir = ($uid > $box->{nonrecent}) ? 'new' : 'cur';
			my $d_mail = _d_create_filename($attr, $size, $vsize,
			    $#{ $box->{keywords} });
			my $d_mailpath = "$d_boxpath/$subdir/$d_mail";
 
			error("Cannot rename $d_temppath to: $d_mailpath")
			    if not rename($d_temppath, $d_mailpath);
 
			# Set the e-mail's last access and modification times.
			utime($attr->{internaldate}, $attr->{internaldate},
			    $d_mailpath);
 
			# Add the e-mail to the dovecot-uidlist.
			if ($CONF{dovecot_uidlist_format} == 1) {
				print $ufh "$uid $d_mail\n"
				    or error("Cannot write $uidfile: $!");
			} elsif ($CONF{dovecot_uidlist_format} == 3) {
				print $ufh "$uid :$d_mail\n"
				    or error("Cannot write $uidfile: $!");
			} else {
				error('Unknown dovecot-uidlist format:',
				    $CONF{dovecot_uidlist_format});
			}
			$SIZE += $size;
			$MAILS++;
		}
		close($ufh) or error("Cannot close $uidfile: $!");
		_d_write_keywords($box->{keywords}, $d_boxpath);
		$FOLDERS++;
	}
	_d_write_subscriptions($meta->{subscriptions}, $d_rootdir);
	_d_write_maildirsize($d_rootdir, $meta->{quota}) if $meta->{quota};
	debug('Done writing Dovecot folders.');
}
 
#
# Create the specified Maildir, including the "new, "cur", and "tmp" Maildir
# subdirectories as well as all parent directories as needed.  Die on error.
#
sub _d_make_maildir ($) {
	my $maildir = shift;
	my $new = "$maildir/new";
	my $cur = "$maildir/cur";
	my $tmp = "$maildir/tmp";
 
	debug('Creating Maildir:', $maildir);
	makedir($_) for $maildir, $new, $cur, $tmp;
}
 
#
# Create an empty "maildirfolder" file within the given Maildir.  Die on error.
#
sub _d_touch_maildirfolder ($) {
	my $maildir = shift;
	my $file = "$maildir/maildirfolder";
 
	open(my $handle, '>', $file) or error("Cannot touch $file: $!");
	close($handle) or error("Cannot close $file: $!");
	debug('Touched:', $file);
}
 
#
# Write the maildirsize file.  Die on error.  See also:
#
# http://www.inter7.com/courierimap/README.maildirquota.html
#
sub _d_write_maildirsize ($$) {
	my ($rootdir, $quota) = @_;
	my $file = "$rootdir/maildirsize";
 
	debug('Writing:', $file);
	open(my $handle, '>', $file) or error("Cannot open $file: $!");
	print $handle $quota . "S\n";
	print $handle $SIZE . " $MAILS\n";
	close($handle) or error("Cannot close $file: $!");
	debug('Done writing:', $file);
}
 
#
# Write the subscriptions file.  Die on error.
#
sub _d_write_subscriptions ($$) {
	my ($subscriptions, $rootdir) = @_;
	my $file = "$rootdir/subscriptions";
 
	debug('Writing:', $file);
	open(my $handle, '>', $file) or error("Cannot open $file: $!");
	foreach my $subscription (@$subscriptions) {
		# Add a leading dot for "--edit-foldernames".
		$subscription = ".$subscription"
		    unless $subscription eq 'INBOX';	# Usually not necessary.
		# Edit the subscribed folder name if desired.
		foreach my $operation (@{ $CONF{edit_foldernames} }) {
			debug("Editing subscription $subscription: $operation");
			my $r = eval("\$subscription =~ $operation");
			error("Cannot evaluate $operation: $@") if $@;
			error("Cannot evaluate $operation.") if not defined($r);
			debug('New subscription:', $subscription);
		}
		# Remove the leading dot we added (if there still is one).
		$subscription =~ s/^\.//;
		print $handle "$subscription\n"
		    or error("Cannot write $file: $!");
		debug('Subscribed:', $subscription);
	}
	close($handle) or error("Cannot close $file: $!");
	debug('Done writing:', $file);
}
 
#
# Write the dovecot-keywords file.  Die on error.
#
sub _d_write_keywords ($$) {
	my ($keywords, $boxpath) = @_;
	my $file = "$boxpath/dovecot-keywords";
 
	return if @$keywords == 0;	# No keywords defined.
 
	debug('Writing:', $file);
	open(my $handle, '>', $file) or error("Cannot open $file: $!");
	for (my $i = 0; $i <= $#{ $keywords }; $i++) {
		print $handle "$i $keywords->[$i]\n"
		    or error("Cannot write $file: $!");
		debug('Added keyword:', $keywords->[$i]);
	}
	close($handle) or error("Cannot close $file: $!");
	debug('Done writing:', $file);
}
 
#
# Create and return a Maildir++ e-mail filename including the flags and the size
# fields used by Dovecot.
#
sub _d_create_filename ($$$$) {
	my ($attr, $size, $vsize, $maxkeyword) = @_;
	my @alphabet = ('a' .. 'z');
	my $filename = sprintf('%u.R%08xQ%u.%s,S=%u,W=%u:2,',
	    $attr->{internaldate}, int(rand(UINT32_MAX)), $MAILS + 1,
	    $CONF{dovecot_host}, $size, $vsize);
 
	$filename .= 'S' if $attr->{sysflags} & FLAG_SEEN;
	$filename .= 'R' if $attr->{sysflags} & FLAG_ANSWERED;
	$filename .= 'F' if $attr->{sysflags} & FLAG_FLAGGED;
	$filename .= 'T' if $attr->{sysflags} & FLAG_DELETED;
	$filename .= 'D' if $attr->{sysflags} & FLAG_DRAFT;
 
	$maxkeyword = $#alphabet if $maxkeyword > $#alphabet;
	for (my $i = 0; $i <= $maxkeyword; $i++) {
		$filename .= $alphabet[$i] if $attr->{usrflags} & (1 << $i);
	}
 
	debug('Created new Maildir++ filename:', $filename);
	return $filename;
}
 
__END__
 
=head1 NAME
 
cyrus2dovecot - convert Cyrus folders to Dovecot
 
=head1 SYNOPSIS
 
B<cyrus2dovecot>
[B<-cdmq>]
S<[B<-C> I<cyrus-inbox>]>
S<[B<-D> I<dovecot-inbox>]>
S<[B<-E> I<edit-foldernames>]>
S<[B<-F> I<dovecot-uidlist-format>]>
S<[B<-H> I<dovecot-host>]>
S<[B<-N> I<default-quota>]>
S<[B<-O> I<cyrus-quota-format>]>
S<[B<-Q> I<cyrus-quota>]>
S<[B<-S> I<cyrus-seen>]>
S<[B<-U> I<cyrus-sub>]>
[I<user> ...]
 
B<cyrus2dovecot>
B<-h> E<verbar> B<-v>
 
=head1 DESCRIPTION
 
B<cyrus2dovecot> converts the e-mails of one or more I<user>s from Cyrus
format to Dovecot Maildir++ folders.  If no I<user> is specified, the
I<user> names are read from the standard input, one per line.  Message
C<UID>s, C<INTERNALDATE>s, IMAP folder subscriptions, the C<UIDVALIDITY>
and C<UIDNEXT> values for each folder, as well as all IMAP flags
(including the first 26 user-defined keywords) are preserved during the
conversion.  The generated e-mail filenames include the Maildir++
extensions C<S=E<lt>sizeE<gt>> and C<W=E<lt>vsizeE<gt>> (which are used
by Dovecot for better performance).  Optionally, Maildir++
F<maildirsize> files are created.
 
=head1 OPTIONS
 
Within the specified I<PATH>s, any occurrence of C<%u> will be replaced
by the current I<user> name, any occurrence of C<%I<n>u> will be
replaced by the I<n>'th character of that I<user> name, any occurrence
of C<%h> will be replaced by Cyrus' directory "hash" character for that
I<user> name (i.e., C<%h> is equivalent to C<%1u> if the first character
of the I<user> name is a lowercase letter), and any occurrence of C<%x>
will be replaced by Cyrus' "fulldirhash" character for that I<user>
name.  However, within the specified B<--cyrus-quota> I<PATH> (if any),
these replacements will only be done if the B<--cyrus-quota-format>
I<VERSION> is set to C<1>.
 
The default settings can be found (and modified) at the top of the
B<cyrus2dovecot> script.
 
=over 8
 
=item B<-C>, B<--cyrus-inbox=>I<PATH>
 
Use this I<PATH> to the I<user>'s INBOX folder in Cyrus.
 
=item B<-c>, B<--dovecot-crlf>
 
Store e-mails with C<CR+LF> instead of plain C<LF>.  This flag should be
specified if the C<mail_save_crlf> option is set to C<yes> in the
Dovecot configuration.
 
=item B<-D>, B<--dovecot-inbox=>I<PATH>
 
Use this I<PATH> to the I<user>'s INBOX folder in Dovecot.
 
=item B<-d>, B<--debug>
 
Print information which is usually only useful for debugging to the
standard output.
 
=item B<-E>, B<--edit-foldernames=>I<SUBSTITUTION>
 
Apply the specified I<SUBSTITUTION> to the name of each Maildir++ folder
and subscription using Perl code such as
S<C<eval('$name=~'.$substitution)>>, where $name holds either the string
F<INBOX> (which denotes the main Maildir) or the full Maildir++ folder
name (e.g., F<.sub.folder>), and $substitution holds the specified
I<SUBSTITUTION>.  The resulting $name will be used as the Maildir++
folder's name.  This option may be specified multiple times, in which
case each of the I<SUBSTITUTION>s will be applied to each Maildir++
folder name in the order specified on the command line.  Note that while
Dovecot stores the subscribed folder names without the leading "." of
Maildir++ subfolders, B<cyrus2dovecot> adds a leading "." to each
subscribed subfolder name before applying the specified
I<SUBSTITUTION>(s) and removes it afterwards (if it still exists) in
order to simplify the matching.
 
=item B<-F>, B<--dovecot-uidlist-format=>I<VERSION>
 
Create the F<dovecot-uidlist> files using this format I<VERSION>.  For
Dovecot releases older than 1.0.2, I<VERSION> 1 must be specified;
otherwise, I<VERSION> 3 can be used.
 
=item B<-H>, B<--dovecot-host=>I<NAME>
 
Use this host I<NAME> for the Maildir++ e-mail file's basename.
 
=item B<-h>, B<--help>
 
Print usage information to the standard output and exit.
 
=item B<-m>, B<--dump-meta>
 
Print a dump of the data structure which holds the metadata gathered
from scanning the Cyrus folders of a user to the standard output.
 
=item B<-N>, B<--default-quota=>I<BYTES>
 
Create a Maildir++ F<maildirsize> file for each I<user>, and set the
quota limit to the specified number of I<BYTES> unless B<--cyrus-quota>
is also specified, in which case a I<user>-specific quota would override
the B<--default-quota> limit.  Specifying C<0> I<BYTES> disables the
creation of F<maildirsize> files unless B<--cyrus-quota> is also
specified.
 
=item B<-O>, B<--cyrus-quota-format=>I<VERSION>
 
Expect the quota database file specified via B<--cyrus-quota> to be
present in this format I<VERSION>, where I<VERSION> C<1> denotes the
"quotalegacy" format and I<VERSION> C<2> denotes the "skiplist" or the
"flat" text format (B<cyrus2dovecot> will autodetect which of those two
formats is used if I<VERSION> 2 is specified).  This option is ignored
if B<--cyrus-quota> is not specified.
 
=item B<-Q>, B<--cyrus-quota=>I<PATH>
 
Use this I<PATH> to the quota database file in Cyrus, and create a
Maildir++ F<maildirsize> file for each I<user> whose quota limit is
found in that file.
 
=item B<-q>, B<--quiet>
 
Suppress the line usually printed to the standard output for each
I<user> whose e-mails were successfully converted.  Error messages, if
any, will still be printed to the standard error output.
 
=item B<-S>, B<--cyrus-seen=>I<PATH>
 
Use this I<PATH> to the I<user>'s seen database file in Cyrus.  If
F<cyrus.seen> is specified as the I<PATH>, B<cyrus2dovecot> expects an
old-style F<cyrus.seen> file in every Cyrus folder.
 
=item B<-U>, B<--cyrus-sub=>I<PATH>
 
Use this I<PATH> to the I<user>'s subscription database file in
Cyrus.
 
=item B<-v>, B<--version>
 
Print version information to the standard output and exit.
 
=back
 
=head1 RETURN VALUE
 
B<cyrus2dovecot> exits 0 on success.  If a non-fatal error occurs,
B<cyrus2dovecot> prints a message to the standard error output and then
tries to convert the e-mails of the remaining I<user>s (if any), but it
exits E<gt>0 regardless of whether or not those conversions succeed.  If
a fatal error occurs, B<cyrus2dovecot> exits E<gt>0 immediately.
 
=head1 EXAMPLES
 
Given that the default settings specified at the top of the
B<cyrus2dovecot> script are correct and that F</tmp/users> holds the
names of all I<user>s whose e-mails should be converted (one per line),
the following command would convert all e-mails of those I<user>s from
Cyrus to Dovecot:
 
	cyrus2dovecot < /tmp/users
 
Given that the path to the INBOX in Cyrus is F</var/spool/imap/user/%u>
(where C<%u> denotes the I<user> name), that Cyrus stores the seen and
subscription databases within the directory F</var/imap/user/%h>, and
that Cyrus stores "quotalegacy" files within the directory
F</var/imap/quota/%h> (where C<%h> denotes Cyrus' directory "hash"
character for that I<user> name, respectively), the following command
would convert all e-mails of the I<user>s "bill" and "george" from Cyrus
to Dovecot, and the result would be stored below F</tmp/dovecot>
(including F<maildirsize> files for both users if their quota limits are
found):
 
	cyrus2dovecot --cyrus-inbox /var/spool/imap/user/%u     \
	              --cyrus-seen /var/imap/user/%h/%u.seen    \
	              --cyrus-sub /var/imap/user/%h/%u.sub      \
	              --cyrus-quota /var/imap/quota/%h/user.%u  \
	              --cyrus-quota-format 1                    \
	              --dovecot-inbox /tmp/dovecot/%u/Maildir   \
	              bill george
 
A script such as the following could be used in order to convert all
e-mails of all I<user>s (of course, the pathnames and the desired quota
limit may have to be adjusted, and if the C<hashimapspool> option is
enabled in the Cyrus configuration, F</?> must be appended to the $in
path):
 
	#!/bin/sh
 
	in=/var/spool/imap/user         # Cyrus INBOXes.
	db=/var/imap/user/?             # Cyrus seen/subscription files.
	out=/tmp/dovecot                # Dovecot Maildirs.
	log=/tmp/conversion.log         # Log of successful conversions.
	err=/tmp/error.log              # Log of conversion errors.
	quota=2147483648                # 2 GiB quota (for maildirsize).
 
	for u in `find $in/. \! -name . -prune -exec basename \{\} \;`
	do
		cyrus2dovecot --cyrus-inbox $in/$u              \
			      --cyrus-seen $db/$u.seen          \
			      --cyrus-sub $db/$u.sub            \
			      --default-quota $quota            \
			      --dovecot-inbox $out/$u/Maildir   \
			      $u 2>&1 >>$log | tee -a $err >&2
	done
 
In order to create all folders (except for the INBOX) as subfolders of
the INBOX in Dovecot, the following argument could be added to the
B<cyrus2dovecot> command line:
 
	--edit-foldernames 's/^\./.INBOX./'
 
Cyrus transparently replaces any "." character in folder names with a
"^" character.  Dovecot supports "." characters in Maildir++ folder
names if the "listescape" plugin is used, which replaces any "."
character in folder names with the string "\2e".  The following argument
could be added to the B<cyrus2dovecot> command line in order to replace
any "^" character in Cyrus folder names with "\2e" for the Maildir++
folder name:
 
	--edit-foldernames 's/\^/\\2e/g'
 
Dovecot 1.1 and newer support using folders such as
F<Maildir/sub/folder> (as opposed to F<Maildir/.sub.folder>) if
C<:LAYOUT=fs> was added to the C<mail_location> in the Dovecot
configuration.  The following B<cyrus2dovecot> arguments could be
specified in order to create such folders by removing the leading dot
from Maildir++ subfolder names and then substituting any following dots
with slashes:
 
	--edit-foldernames 's/^\.//'    \
	--edit-foldernames 's/\./\//g'
 
If the seen states, subscriptions, or quotas are stored in Berkeley
databases, they must first be converted for B<cyrus2dovecot> using a
command such as the following:
 
	cvt_cyrusdb /var/imap/user/b/bill.seen berkeley \
	            /tmp/imap/user/b/bill.seen skiplist
 
=head1 CAVEATS
 
B<cyrus2dovecot> assumes that the user has no e-mails in Dovecot yet and
that neither his Cyrus folders nor his Dovecot folders will be accessed
by another process during the conversion.
 
If C<%I<n>u> is specified within any I<PATH> on the command line, all
I<user> names must have a length of at least I<n> characters.
Otherwise, B<cyrus2dovecot> will die with an exception.
 
If folder name substitutions are specified via B<--edit-foldernames>,
the resulting Maildir++ folder names must be unique.
 
=head1 RESTRICTIONS
 
Cyrus' seen and subscription databases must be present either in the
"skiplist" format or in the "flat" text format, and Cyrus' quota
database(s) (if any) must be present either in one of those formats or
in the "quotalegacy" format, as B<cyrus2dovecot> doesn't support
Berkeley databases.  However, Berkeley databases can be converted to one
of the supported formats using cvt_cyrusdb(8), see the L</EXAMPLES>.
 
In F<maildirsize> files created by B<cyrus2dovecot>, no limit for the
number of messages is specified (as such a limit does not seem useful).
 
Cyrus' ACL settings are not converted.
 
=head1 COMPATIBILITY
 
B<cyrus2dovecot> is supposed to work with all Cyrus releases up to (at
least) version 2.3.x.  So far, it has been tested with Cyrus 1.4,
2.1.18, 2.2.12, and 2.3.12p2.
 
=head1 SEE ALSO
 
Other tools for converting e-mails from Cyrus to Dovecot can be found at
L<http://wiki.dovecot.org/Migration/Cyrus>.
 
=head1 AUTHOR
 
Written by Holger WeiE<szlig> E<lt>holger@ZEDAT.FU-Berlin.DEE<gt> at
Freie UniversitE<auml>t Berlin, Germany, Zentraleinrichtung fE<uuml>r
Datenverarbeitung (ZEDAT).
 
=head1 COPYRIGHT AND LICENSE
 
Copyright (c) 2008 Freie UniversitE<auml>t Berlin.
All rights reserved.
 
This program is free software; you can redistribute it and/or modify it
under the same terms as Perl itself.  See L<perlartistic>.  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.
 
=head1 HISTORY
 
	$Log: cyrus2dovecot,v $
	Revision 1.2  2008/09/24 09:52:33  holger
	Message seen states are now parsed more efficiently with regard
	to performance and memory usage.  Apart from that, minor code
	cleanups have been applied.
 
	Revision 1.1  2008/09/22 08:36:44  holger
	Initial release.

Installation

:!: WICHTIG - Die Installation soll auf dem Server/Host

erfolgen!

/tmp/cyrus2dovecot

Nach dem erfolgreichen herunterladen oder kopieren des Skriptes, sollte das Skript unter nachfolgendem Verzeichnis zu finden sein, oder angelegt werden:

  • /tmp/cyrus2dovecot

da das Skript nur zur Migration benötigt wird.

Um das Skript nun einfach ausführen zu können, ist es erforderlich die entsprechenden Dateirechte zu setzen, was mit nachfolgendem Befehl durchgeführt werden kann:

# chmod +x /tmp/cyrus2dovecot

Ein Überprüfung, ob dies erfolgreich war, kann mit nachfolgendem Befehl durchgeführt werden:

# ll /tmp/cyrus2dovecot 
-rwxr-xr-x 1 root root 52213 Apr 24 16:35 /tmp/cyrus2dovecot

Konfiguration

Nachfolgende Ausführungen beziehen sich auf eine Cyrus IMAPd Installation unter dem Betriebssystem CentOS ab Verison 6.x voraus, wie z.B. unter nachfolgendem internen Link beschrieben:

* Abweichende oder ältere Cyrus IMAPd oder CentOS Installation sind ähnlich, bitte entsprechende Verzeichnisangaben prüfen !

Nachfolgender Befehl listet die Parameter für das Skript Cyrus2Dovecot auf:

# /tmp/cyrus2dovecot -h
Usage:
    cyrus2dovecot [-cdmq] [-C *cyrus-inbox*] [-D *dovecot-inbox*]
    [-E *edit-foldernames*] [-F *dovecot-uidlist-format*]
    [-H *dovecot-host*] [-N *default-quota*] [-O *cyrus-quota-format*]
    [-Q *cyrus-quota*] [-S *cyrus-seen*] [-U *cyrus-sub*] [*user* ...]

    cyrus2dovecot -h | -v

Usage:
    cyrus2dovecot [-cdmq] [-C *cyrus-inbox*] [-D *dovecot-inbox*]
    [-E *edit-foldernames*] [-F *dovecot-uidlist-format*]
    [-H *dovecot-host*] [-N *default-quota*] [-O *cyrus-quota-format*]
    [-Q *cyrus-quota*] [-S *cyrus-seen*] [-U *cyrus-sub*] [*user* ...]

    cyrus2dovecot -h | -v

Hier die Parameter im Überblick:

Parameter Defaultwert Beschreibung
dovecot_inbox /tmp/dovecot/%u/Maildir Verzeichnisbaum der Maildir-Struktur
cyrus_inbox /var/spool/imap/user/%u Cyrus: Postfach (Mailbox) des Benutzer
cyrus_sub /var/imap/user/%h/%u.sub Cyrus: Ordnerstruktur-Datei des Benutzers
cyrus_seen /var/imap/user/%h/%u.seen Cyrus: Gelesen-Flag-Datei des Benutzers
cyrus_quota <undef> Cyrus: Default Quota-Wert
cyrus_quota_format 1 Cyrus: Quota Format
dovecot_uidlist_format 3 Dovecot: Erstellen des dovecot-uidlist Format
dovecot_host hostname Dovecot: Hostname für Maildir++ Dateinamen
dovecot_crlf 0 Dovecot: CR+LF anstelle von LF in Dovecot
default_quota 0 Quota-Wert als Standard
dump_meta 0 Ausgabe der META-Daten Struktur
debug 0 Debug-Meldungen Ausgabe
quiet 0 Standard Ausgaben unterdrücken
edit_foldernames [LISTE] Liste der Ordnernamen Umbenennungen

%u = Benutzername | %h = Erster Buchstabe des Benutzernamens (wie %1u)

:!: HINWEIS - Die Defaultwerte stimmen NICHT für eine Cyrus IMAPd Installation unter CentOS ab Verison 6.x !!!

Skript Parameter

Nachfolgende Aufruf-Parameter stellen die benötigten Parameter für einen Skript Aufruf für eine Cyrus IMAPd Installation unter CentOS ab Verison 6.x dar:

Parameter Wert Beschreibung
cyrus_inbox /var/spool/imap/%h/user/%u Cyrus: Postfach (Mailbox) des Benutzers
cyrus_seen /var/lib/imap/user/%h/%u.seen Cyrus: Gelesen-Flag-Datei des Benutzers
cyrus_sub /var/lib/imap/user/%h/%u.sub Cyrus: Ordnerstruktur-Datei des Benutzers
cyrus_quota /var/lib/imap/quota/%h/user.%u Cyrus: Default Quota-Wert des Benutzers
cyrus_quota_format 2 Cyrus: Quota Format
dovecot_host rechner80.dmz.tachtler.net Dovecot: Hostname für Maildir++ Dateinamen
dovecot_inbox /tmp/dovecot/%u/Maildir Verzeichnisbaum der Maildir-Struktur
user klaus Anmeldename des Benutzers

%u = Benutzername | %h = Erster Buchstabe des Benutzernamens (wie %1u)

:!: WICHTIG - Bevor es zur Ausführung kommen sollte, ist es dringend notwendig die Verzeichnisse und Dateinamen zu überprüfen:

Nachfolgende Tabelle zeigt noch einmal die Umsetzung der Verzeichnispfade und Dateinamen:

Parameter Wert Verzeichnisumsetzung
cyrus_inbox /var/spool/imap/%h/user/%u /var/spool/imap/k/user/klaus
cyrus_seen /var/lib/imap/user/%h/%u.seen /var/lib/imap/user/k/klaus.seen
cyrus_sub /var/lib/imap/user/%h/%u.sub /var/lib/imap/user/k/klaus.sub
cyrus_quota /var/lib/imap/quota/%h/user.%u /var/lib/imap/quota/k/user.klaus

Nachfolgende Befehle sollten der Reihe nach ausgeführt werden, um die Verzeichnispfade und Dateinamen zu überprüfen:

# ls -l /var/spool/imap/k/user/klaus
total 71712
-rw-------  1 cyrus mail    17748 May  1  2012 1.
...
-rw-------  1 cyrus mail     2749 May  1  2012 8976.
-rw-------  1 cyrus mail   448512 Apr 24 10:30 cyrus.cache
-rw-------  1 cyrus mail      208 Mar 23 21:40 cyrus.header
-rw-------  1 cyrus mail    24912 Apr 24 10:30 cyrus.index
-rw-------  1 cyrus mail  1518101 Apr 24 16:55 cyrus.squat
drwx------  2 cyrus mail     4096 Apr 24 16:55 Drafts
drwx------  2 cyrus mail     4096 Apr 24 16:56 Sent
drwx------  2 cyrus mail     4096 Apr 24 16:56 Junk
drwx------  2 cyrus mail     4096 Apr 24 17:01 Trash

# ls -l /var/lib/imap/user/k/klaus.seen
-rw------- 1 cyrus mail 10708 Apr 24 17:52 /var/lib/imap/user/k/klaus.seen

# ls -l /var/lib/imap/user/k/klaus.sub
-rw------- 1 cyrus mail 991 Mar  9 09:26 /var/lib/imap/user/k/klaus.sub

# ls -l /var/lib/imap/quota/k/user.klaus
-rw------- 1 cyrus mail 17 Apr 24 17:52 /var/lib/imap/quota/k/user.klaus

Skript Aufruf

Nachfolgend der Befehlsaufruf:

# /tmp/cyrus2dovecot --cyrus-inbox /var/spool/imap/%h/user/%u --cyrus-seen /var/lib/imap/user/%h/%u.seen --cyrus-sub /var/lib/imap/user/%h/%u.sub --cyrus-quota /var/lib/imap/quota/%h/user.%u --cyrus-quota-format 2 --dovecot-host rechner80.dmz.tachtler.net --dovecot-inbox /tmp/dovecot/%u/Maildir klaus
cyrus2dovecot [klaus]: (warning) Skipping Junk/cyrus.squat, dunno what it is.
cyrus2dovecot [klaus]: (warning) Skipping INBOX/cyrus.squat, dunno what it is.
cyrus2dovecot [klaus]: (warning) Skipping Sent/cyrus.squat, dunno what it is..
cyrus2dovecot [klaus]: (warning) Skipping Drafts/cyrus.squat, dunno what it is.
cyrus2dovecot [klaus]: (warning) Skipping Trash/cyrus.squat, dunno what it is.
cyrus2dovecot [klaus]: (warning) No quota information available.
cyrus2dovecot [klaus]: 960 messages in 5 folders (72.1 MiB, 2 s)

:!: HINWEIS - Die Meldungen (warning) können ignoriert werden, da diese von nachfolgender Konfiguration des Cyrus IMAPd stammen, siehe nachfolgenden internen Link

Squatter (warning)

Nachfolgend die relevante Erklärung zu den (warning) Ausgaben:

Im Bereich EVENTS{} des Cyrus IMAPd, sind Dienste und Hilfsprogramme definiert, welche in regelmäßigen Abständen ausgeführt werden.

Hier wurde in diesem Beispiel ein Dienst bzw. ein Hilfsprogramm hinzugefügt, welches sich squatter nennt.

Dieses Programm legt für jede Mailbox einen sogenannten „Squat-Volltext-Index“ an. Dadurch kann der Mail-Client bei einer Suche die betreffende Nachricht innerhalb der Mailbox schneller finden.

Nachfolgend der relevante Ausschnitt des Cyrus IMAPd aus der Konfigurationsdatei

  • /etc/cyrus.conf

(Nur relevanter Ausschnitt)

...
EVENTS {
  # this is required
  checkpoint    cmd="ctl_cyrusdb -c" period=30
 
  # this is only necessary if using duplicate delivery suppression,
  # Sieve or NNTP
  delprune      cmd="cyr_expire -E 3" at=0400
 
  # this is only necessary if caching TLS sessions
  tlsprune      cmd="tls_prune" at=0400
 
  # Tachtler
  # this enables to build a squat-index, for faster search results
  # for better performance start with a high nice value 
  squatter      cmd="/bin/nice -n 19 /usr/lib/cyrus-imapd/squatter -r *" period=180
}
...

Maildir Verzeichnis

Nach dem Aufruf des Skriptes Cyrus2Dovecot sollte nun eine Verzeichnisstruktur unterhalb des Verzeichnisses

  • /tmp

mit nachfolgendem Inhalt einstanden sein, was mit nachfolgendem Befehl überprüft werden kann:

# ls -l /tmp/
total 4
-rwxr-xr-x  1 root  root  104423 Apr 23 14:23 cyrus2dovecot
drwxr-xr-x  3 root  root    4096 Apr 24 18:04 dovecot

Die Hierarchie des Ordners /tmp/dovecot sollte wie folgt skizziert aussehen:

/tmp/dovecot/
 +-----------> klaus
               +-----> Maildir

Um in die das Verzeichnis /tmp/dovecot/klaus/Maildir zu wechseln kann nachfolgender Befehl genutzt werden:

cd /tmp/dovecot/klaus/Maildir

Ob das Maildir-Verzeichnis korrekt aufgebaut wurde, kann mit einer Auflistung des Verzeichnisses /tmp/dovecot/klaus/Maildir mit nachfolgendem Befehl überprüft werden:

# ll -la
total 84
drwxr-xr-x 30 root root  4096 Apr 24 18:04 .
drwxr-xr-x  3 root root  4096 Apr 24 18:04 ..
drwxr-xr-x  2 root root 36864 Apr 24 18:04 cur
-rw-r--r--  1 root root    30 Apr 24 18:04 dovecot-keywords
-rw-r--r--  1 root root 21817 Apr 24 18:04 dovecot-uidlist
drwxr-xr-x  5 root root  4096 Apr 24 18:04 .Drafts
drwxr-xr-x  2 root root  4096 Apr 24 18:04 new
drwxr-xr-x  5 root root  4096 Apr 24 18:04 .Sent
drwxr-xr-x  5 root root  4096 Apr 24 18:04 .Junk
-rw-r--r--  1 root root   569 Apr 24 18:04 subscriptions
drwxr-xr-x  2 root root  4096 Apr 24 18:04 tmp
drwxr-xr-x  5 root root  4096 Apr 24 18:04 .Trash

Die gelesenen Nachrichten sollten nun im Verzeichnis

  • /tmp/dovecot/klaus/Maildir/cur

im Dovecot spezifischen Dateiformat, was mit nachfolgendem Befehl überprüft werden kann:

(Nur beispielhafter kurzer Ausschnitt)

# ls -l /tmp/dovecot/klaus/Maildir/cur
total 68756
-rw-r--r-- 1 root root    17202 May  1  2012 1335866770.R4c6b2e68Q116.rechner80.dmz.tachtler.net,S=17202,W=17748:2,S
-rw-r--r-- 1 root root     2684 May  1  2012 1335866775.Rd9f3e513Q117.rechner80.dmz.tachtler.net,S=2684,W=2749:2,SR
...
-rw-r--r-- 1 root root    16059 Apr 22 09:20 1398151257.Rcf454617Q396.rechner80.dmz.tachtler.net,S=16059,W=16401:2,S
-rw-r--r-- 1 root root     6745 Apr 22 10:13 1398154391.R7bbf55c8Q397.rechner80.dmz.tachtler.net,S=6745,W=6927:2,SR

Übertrag Dovecot-Server

Nachdem das Postfach des Benutzers vom Cyrus IMAPd spezifischen Dateiformat in das Dovecot spezifischen Dateiformat umgewandelt wurde, muss nun noch das so entstandene Maildir-Verzeichnis

  • vom Cyrus IMAPd-Server, hier mit der IP: 192.168.0.180
  • auf den Dovecot-Server, hier mit der IP: 192.168.0.80

übertragen werden.

Da dies alles hier offline geschieht, ist eine Möglichkeit dies mittels

  1. Packen des Maildir-Verzeichnisses in eine Datei
  2. Übertragung der Datei auf den neuen Server und
  3. Entpacken des Maildir-Verzeichnisses aus der gepackten Datei

zu realisieren.

Mit nachfolgenden Befehlen kann der relevante Teil des unter dem Verzeichnis /tmp entstandenen Verzeichnisses /tmp/dovecot in eine Datei gepackt werden. Hierbei sollen die Besitzrechte, Dateirechte, das Datum und die Uhrzeit erhalten bleiben.

Vorbereitend hierzu kann mit nachfolgendem Befehl in den relevanten Teil des Verzeichnisses /tmp/dovecot gewechselt werden:

# cd /tmp/dovecot

Mit nachfolgendem Befehl wird nun alles innerhalb von /tmp/dovecot/klaus in eine *.tar.gz-Datei gepackt:

# tar -cvzf klaus-Maildir.tar.gz klaus/ --atime-preserve --preserve-permissions

Nun kann z.B. via scp (Secure Copy) die so erhaltene gepackte Datei mit nachfolgendem Befehl auf den neuen Dovecot-Server ebenfalls in das Verzeichnis /tmp transferiert werden:

# scp -p /tmp/dovecot/klaus-Maildir.tar.gz 192.168.0.80:/tmp

Abschließend kann dann auf dem neuen Dovecot-Server die Datei mit nachfolgendem Befehl gleich in das Zielverzeichnis

  • /var/spool/vmail/tachtler.net/

entpackt werden:

tar -xvzf /tmp/klaus-Maildir.tar.gz -C /var/spool/vmail/tachtler.net/ --atime-preserve --preserve-permissions

:!: WICHTIG - Nachfolgend müssen nach dem entpacken noch die Besitzrechte und die Dateirechte angepasst werden !!!

Nachfolgendes Beispiel setzt voraus, dass ein Authentifizierungsbenutzer wie unter nachfolgendem internen Link beschrieben

verwendet wird.

Mit nachfolgendem Befehl, werden die Besitzrechte, für das so entstandene Verzeichnis

  • /var/spool/vmail/tachtler.net/klaus

auf den Dummy-Benutzer gesetzt:

# chown -R vmail:vmail /var/spool/vmail/tachtler.net/klaus

Mit nachfolgendem Befehlen, werden die Dateirechte ebenfalls, für das so entstandene Verzeichnis

  • /var/spool/vmail/tachtler.net/klaus

richtig gesetzt:

# chmod -R o-rx /var/spool/vmail/tachtler.net/klaus
# chmod -R g-rx /var/spool/vmail/tachtler.net/klaus

Abschließend kann mit nachfolgenden Befehlen, die korrekte Erstellung und Rechtevergabe überprüft werden, welche eine Ausgabe wie die nachfolgende zurückgeben sollte:

# ls -l /var/spool/vmail/tachtler.net/klaus
total 4
drwx------ 30 vmail vmail 4096 Apr 24 18:04 Maildir
# ls -la /var/spool/vmail/tachtler.net/klaus/*
total 112
drwx------ 30 vmail vmail  4096 Apr 24 18:04 .
drwx------  3 vmail vmail  4096 Apr 24 18:04 ..
drwx------  2 vmail vmail 36864 Apr 24 18:04 cur
-rw-------  1 vmail vmail    30 Apr 24 18:04 dovecot-keywords
-rw-------  1 vmail vmail 21817 Apr 24 18:04 dovecot-uidlist
drwx------  5 vmail vmail  4096 Apr 24 18:04 .Drafts
drwx------  2 vmail vmail  4096 Apr 24 18:04 new
drwx------  5 vmail vmail  4096 Apr 24 18:04 .Sent
drwx------  5 vmail vmail  4096 Apr 24 18:04 .Junk
-rw-------  1 vmail vmail   569 Apr 24 18:04 subscriptions
drwx------  2 vmail vmail  4096 Apr 24 18:04 tmp
drwx------  5 vmail vmail  4096 Apr 24 18:04 .Trash

Neustart

Bevor der der dovecot-Daemon/Dienst neu gestartet werden soll, ist eine Überprüfung der korrekten Konfiguration durch nachfolgenden Befehl, zu empfehlen

# doveconf -n
# 2.2.10: /etc/dovecot/dovecot.conf
# OS: Linux 2.6.32-431.11.2.el6.x86_64 x86_64 CentOS release 6.5 (Final) 
auth_debug = yes
auth_master_user_separator = *
auth_mechanisms = plain digest-md5 cram-md5 login
auth_verbose = yes
listen = *
mail_debug = yes
mail_location = maildir:~/Maildir
mail_plugins = " quota acl zlib mail_log notify"
managesieve_notify_capability = mailto
managesieve_sieve_capability = fileinto reject envelope encoded-character vacation subaddress
comparator-i;ascii-numeric relational regex imap4flags copy include variables body enotify environment
mailbox date ihave
mbox_write_locks = fcntl
namespace {
  list = children
  location = maildir:%%h/Maildir:INDEX=%h/shared/%%u:CONTROL=%h/shared/%%u
  prefix = shared/%%u/
  separator = /
  subscriptions = yes
  type = shared
}
namespace inbox {
  inbox = yes
  location = 
  mailbox Drafts {
    auto = subscribe
    special_use = \Drafts
  }
  mailbox Junk {
    auto = subscribe
    special_use = \Junk
  }
  mailbox Sent {
    auto = subscribe
    special_use = \Sent
  }
  mailbox "Sent Messages" {
    special_use = \Sent
  }
  mailbox Trash {
    auto = subscribe
    special_use = \Trash
  }
  prefix = INBOX/
  separator = /
}
passdb {
  args = /etc/dovecot/master-users
  driver = passwd-file
  master = yes
  pass = yes
}
passdb {
  args = /etc/dovecot/dovecot-sql.conf.ext
  driver = sql
}
plugin {
  acl = vfile
  acl_shared_dict = file:/var/lib/dovecot/db/shared-mailboxes.db
  mail_log_fields = uid box msgid size from
  quota = maildir:User quota
  quota_grace = 10%%
  quota_rule = *:storage=1G
  quota_rule2 = INBOX/Trash:storage=+100M
  quota_status_nouser = DUNNO
  quota_status_overquota = 552 5.2.2 Mailbox is over quota
  quota_status_success = DUNNO
  quota_warning = storage=95%% quota-warning 95 %u
  quota_warning2 = storage=80%% quota-warning 80 %u
  sieve = ~/.dovecot.sieve
  sieve_dir = ~/sieve
  zlib_save = gz
  zlib_save_level = 6
}
protocols = imap lmtp sieve
service auth {
  unix_listener auth-userdb {
    group = vmail
    user = vmail
  }
}
service imap-login {
  process_min_avail = 1
  service_count = 0
}
service lmtp {
  inet_listener lmtp {
    address = 192.168.0.80
    port = 24
  }
}
service managesieve-login {
  inet_listener sieve {
    port = 4190
  }
  inet_listener sieve_deprecated {
    port = 2000
  }
}
service quota-status {
  client_limit = 1
  executable = quota-status -p postfix
  inet_listener {
    address = 192.168.0.80
    port = 12340
  }
}
service quota-warning {
  executable = script /usr/local/bin/quota-warning.sh
  user = vmail
}
ssl_cert = </etc/pki/dovecot/certs/CAcert-class3-wildcard_all_in_one.crt
ssl_cipher_list = ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA
ssl_key = </etc/pki/dovecot/private/tachtler.net.key
ssl_prefer_server_ciphers = yes
ssl_protocols = !SSLv2 !SSLv3
userdb {
  driver = prefetch
}
userdb {
  args = /etc/dovecot/dovecot-sql.conf.ext
  driver = sql
}
verbose_proctitle = yes
protocol lmtp {
  mail_plugins = " quota acl zlib mail_log notify sieve"
}
protocol imap {
  mail_max_userip_connections = 30
  mail_plugins = " quota acl zlib mail_log notify imap_quota imap_acl imap_zlib"
}
protocol sieve {
  mail_max_userip_connections = 30
}

:!: HINWEIS - die Konfiguration des dovecot-Daemon/Dienst konnte korrekt gelesen werden, wenn die Konfiguration erscheint, was letztendlich zwar nicht bedeutet, das Sie auch korrekt ist, aber syntaktische Fehler ausschließt !!!

Danach kann der dovecot-Server mit nachfolgendem Befehle neu gestartet werden:

# service dovecot restart
Stopping Dovecot Imap:                                     [  OK  ]
Starting Dovecot Imap:                                     [  OK  ]

Login-Test mit telnet

Um zu Überprüfen, ob eine Anmeldung als Benutzer von einem entfernten Rechner möglich ist, kann nachfolgender Befehl genutzt werden:

# telnet 192.168.0.80 143
Trying 192.168.0.80...
Connected to 192.168.0.80.
Escape character is '^]'.
* OK [CAPABILITY IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE IDLE STARTTLS AUTH=PLAIN AUTH=DIGEST-MD5
AUTH=CRAM-MD5 AUTH=LOGIN] Dovecot ready.
a1 login klaus@tachtler.net geheim
a1 OK [CAPABILITY IMAP4rev1 LITERAL+ SASL-IR LOGIN-REFERRALS ID ENABLE IDLE SORT SORT=DISPLAY
THREAD=REFERENCES THREAD=REFS THREAD=ORDEREDSUBJECT MULTIAPPEND URL-PARTIAL CATENATE UNSELECT CHILDREN
NAMESPACE UIDPLUS LIST-EXTENDED I18NLEVEL=1 CONDSTORE QRESYNC ESEARCH ESORT SEARCHRES WITHIN CONTEXT=SEARCH
LIST-STATUS SPECIAL-USE BINARY MOVE COMPRESS=DEFLATE QUOTA ACL RIGHTS=texk] Logged in
a2 list "" "*"
* LIST (\HasChildren) "/" INBOX
* LIST (\HasNoChildren \Junk) "/" INBOX/Junk
* LIST (\HasNoChildren \Sent) "/" INBOX/Sent
* LIST (\HasNoChildren \Trash) "/" INBOX/Trash
* LIST (\HasNoChildren \Drafts) "/" INBOX/Drafts
a2 OK List completed.
a3 logout
* BYE Logging out
a3 OK Logout completed.
Connection closed by foreign host.

Erforderliche Benutzereingaben:

  1. telnet 192.168.0.80 143
  2. a1 login klaus@tachtler.net geheim
  3. a2 list "" "*"
  4. a3 logout
Cookies helfen bei der Bereitstellung von Inhalten. Durch die Nutzung dieser Seiten erklären Sie sich damit einverstanden, dass Cookies auf Ihrem Rechner gespeichert werden. Weitere Information
tachtler/dovecot_migration_-_cyrus2dovecot.txt · Zuletzt geändert: 2015/05/21 12:53 von klaus