diff --git a/doc/additional/migrating.rst b/doc/additional/migrating.rst
index 448e7fdd1..4f1e8f71c 100644
--- a/doc/additional/migrating.rst
+++ b/doc/additional/migrating.rst
@@ -6,7 +6,7 @@ Migrating from other software
PostfixAdmin
************
-Since version 0.8.5, Modoboa provides a simple script to migrate an
+Modoboa provides a simple script to migrate an
existing `PostfixAdmin (version 2.3.3+)
`_ database to a Modoboa one.
@@ -21,40 +21,37 @@ connection named ``pfxadmin`` into the ``DATABASES`` variable
corresponding to your PostfixAdmin setup::
DATABASES = {
- "default" : {
- # default connection definition
- },
- "pfxadmin" : {
- "ENGINE" : "",
- "NAME" : "",
- "USER" : "",
- "PASSWORD" : "",
- }
+ "default" : {
+ # default connection definition
+ },
+ "pfxadmin" : {
+ "ENGINE" : "",
+ "NAME" : "",
+ "USER" : "",
+ "PASSWORD" : "",
+ }
}
This connection should correspond to the one defined in PostfixAdmin's
configuration file.
-You are now ready to start the migration. Enter Modoboa's root
-directory and execute the following command::
+Then, uncomment the line containing
+``'modoboa.tools.pfxadmin_migrate'`` inside the ``MODOBOA_APPS``
+variable and save your changes.
- $ PYTHONPATH=$PWD/.. DJANGO_SETTINGS_MODULE=modoboa.settings \
- ./tools/pfxadmin_migrate/migrate.py -r -p
+You are now ready to start the migration so run the following commands::
+
+ $ cd
+ $ python manage.py migrate_from_postfixadmin -r -p
Depending on how many domains/mailboxes your existing setup contains,
the migration can be long. Just wait for the script's ending.
-Once the migration has succeed, go the *Admin > Configuration* panel,
-click on the *admin* row and modify the value of ``MAILDIR_ROOT`` as
-follow::
+.. note::
- MAILDIR_ROOT =
-
-The corresponding field must be empty. Don't touch other fields except
-``PASSWORD_SCHEME``, if needed. (set it to the same method as the one
-used by PostfixAdmin, check its configuration file if you're not sure)
-
-Click on the *Save* button.
+ The rename operation is not synchronous. If you choose this option,
+ you must enable **Handle mailboxes on filesystem** option. See
+ :ref:`fs_operations`.
The procedure is over, edit the :file:`settings.py` file and:
diff --git a/modoboa/tools/pfxadmin_migrate/management/__init__.py b/modoboa/tools/pfxadmin_migrate/management/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/modoboa/tools/pfxadmin_migrate/management/commands/__init__.py b/modoboa/tools/pfxadmin_migrate/management/commands/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/modoboa/tools/pfxadmin_migrate/management/commands/migrate_from_postfixadmin.py b/modoboa/tools/pfxadmin_migrate/management/commands/migrate_from_postfixadmin.py
new file mode 100644
index 000000000..9d338742e
--- /dev/null
+++ b/modoboa/tools/pfxadmin_migrate/management/commands/migrate_from_postfixadmin.py
@@ -0,0 +1,296 @@
+# coding: utf-8
+
+"""Automatic postfixadmin to Modoboa migration
+===========================================
+
+This script provides an easy way to migrate an existing postfixadmin
+database to a Modoboa one.
+
+As the two products do not share the same schema, some information
+will be lost on the new database. Here is the list:
+ * Description, transport and backup MX for domains
+ * Logs
+ * Fetchmail table
+
+Mailboxes organisation on the filesystem will change. PostfixAdmin
+uses the following layout::
+
+ /domain.tld/user@domain.tld/
+
+Whereas Modoboa uses the following::
+
+ /domain.tld/user/
+
+To rename directories, you'll need the appropriate permissions.
+
+Domain administrators that manage more than one domain will not be
+completly restored. They will only be administrator of the domain that
+corresponds to their username.
+
+Vacation messages will not be migrated by this script. Users will have
+to define new messages by using Modoboa's autoreply plugin. (if
+activated)
+
+One last point about passwords. PostfixAdmin uses different hash
+algorithms to store passwords so this script does not modify
+them. They are copied "as is" in the new database. In order to use
+them, use the ``--passwords-scheme`` to tell Modoboa which scheme was
+used by PostfixAdmin.
+
+.. note::
+
+ Actually, it appears that the ``crypt`` scheme is compatible with
+ PostfixAdmin ``md5crypt`` algorithm...
+
+"""
+from optparse import make_option
+import os
+import sys
+
+from django.contrib.auth.models import Group
+from django.core.management.base import BaseCommand
+from django.db import transaction
+from django.db.models import Q
+
+import modoboa.core.models as core_models
+import modoboa.extensions.admin.models as admin_models
+from modoboa.lib.emailutils import split_mailbox
+
+from . import models as pf_models
+
+
+class Command(BaseCommand):
+
+ """Migration from postfixadmin."""
+
+ help = "This command eases the migration from a postfixadmin database."
+ option_list = BaseCommand.option_list + (
+ make_option(
+ "-f", "--from", dest="_from", default="pfxadmin",
+ help="Name of postfixadmin db connection declared in settings.py"
+ "(default is pfxadmin)"
+ ),
+ make_option(
+ "-t", "--to", dest="_to", default="default",
+ help="Name of the Modoboa db connection declared in settings.py"
+ " (default is default)"
+ ),
+ make_option(
+ "-r", "--rename-dirs", action="store_true", default=False,
+ help="Rename mailbox directories (default is no)"
+ ),
+ make_option(
+ "-p", "--mboxes-path", default=None,
+ help="Path where directories are stored on the filesystem"
+ "(used only if --rename-dirs)"
+ ),
+ make_option(
+ "-s", "--passwords-scheme", default="crypt",
+ help="Scheme used to crypt user passwords"
+ )
+ )
+
+ def _migrate_dates(self, oldobj):
+ """Creates a new ObjectDates instance.
+
+ Due to Django limitations, we only retrieve the creation date. The
+ modification date will be set to 'now' because the corresponding
+ field in Modoboa's schema has the 'auto_now' attribute to True.
+ (see https://docs.djangoproject.com/en/dev/ref/models/fields/#django.db.models.DateField).
+
+ :param oldobj: the PostfixAdmin record which is beeing migrated.
+ """
+ dates = admin_models.base.ObjectDates()
+ dates.save()
+
+ dates.creation = oldobj.created
+ dates.save()
+ return dates
+
+ def _migrate_domain_aliases(self, domain, options, creator):
+ """Migrate aliases of a single domain."""
+ print "\tMigrating domain aliases"
+ old_domain_aliases = pf_models.AliasDomain.objects.using(
+ options["_from"]
+ ).filter(target_domain=domain.name)
+ for old_da in old_domain_aliases:
+ try:
+ target_domain = admin_models.Domain.objects \
+ .using(options["_to"]).get(name=old_da.target_domain)
+ new_da = admin_models.DomainAlias()
+ new_da.name = old_da.alias_domain
+ new_da.target = target_domain
+ new_da.enabled = old_da.active
+ new_da.dates = self._migrate_dates(old_da)
+ new_da.save(using=options["_to"])
+ new_da.post_create(creator)
+ except core_models.Domain.DoesNotExist:
+ print ("Warning: target domain %s does not exists, "
+ "not creating alias domain %s"
+ % old_da.target_domain, old_da.alias_domain)
+ continue
+
+ def _migrate_mailbox_aliases(self, domain, options, creator):
+ """Migrate mailbox aliases of a single domain."""
+ print "\tMigrating mailbox aliases"
+ old_aliases = pf_models.Alias.objects \
+ .using(options["_from"]).filter(domain=domain.name)
+ for old_al in old_aliases:
+ if old_al.address == old_al.goto:
+ continue
+ new_al = admin_models.Alias()
+ local_part, tmp = split_mailbox(old_al.address)
+ if local_part is None or not len(local_part):
+ if tmp is None or not len(tmp):
+ print (
+ "Warning: skipping alias %s (cannot retrieve local "
+ "part). You will need to recreate it manually."
+ % old_al.address
+ )
+ continue
+ new_al.address = "*"
+ else:
+ new_al.address = local_part
+ new_al.domain = domain
+ new_al.enabled = old_al.active
+ extmboxes = []
+ intmboxes = []
+ for goto in old_al.goto.split(","):
+ try:
+ mb = admin_models.Mailbox.objects \
+ .using(options["_to"]).get(user__username=goto)
+ except admin_models.Mailbox.DoesNotExist:
+ extmboxes += [goto]
+ else:
+ intmboxes += [mb]
+ new_al.dates = self._migrate_dates(old_al)
+ new_al.save(
+ int_rcpts=intmboxes, ext_rcpts=extmboxes, creator=creator,
+ using=options["_to"]
+ )
+
+ def _migrate_mailboxes(self, domain, options, creator):
+ """Migrate mailboxes of a single domain.
+
+ .. note::
+
+ Mailboxes rename will be done later by the
+ handle_mailboxes_operation script.
+
+ """
+ print "\tMigrating mailboxes"
+ old_mboxes = pf_models.Mailbox.objects \
+ .using(options["_from"]).filter(domain=domain.name)
+ for old_mb in old_mboxes:
+ new_user = core_models.User()
+ new_user.username = old_mb.username
+ new_user.first_name = old_mb.name.partition(' ')[0]
+ new_user.last_name = old_mb.name.partition(' ')[2]
+ new_user.email = old_mb.username
+ new_user.is_active = old_mb.active
+ new_user.date_joined = old_mb.created
+ new_user.password = "{%s}%s" % \
+ (options["passwords_scheme"].upper(), old_mb.password)
+ new_user.dates = self._migrate_dates(old_mb)
+ new_user.save(creator=creator, using=options["_to"])
+ new_user.set_role("SimpleUsers")
+
+ new_mb = admin_models.Mailbox()
+ new_mb.user = new_user
+ new_mb.address = old_mb.local_part
+ new_mb.domain = domain
+ new_mb.dates = self._migrate_dates(old_mb)
+ new_mb.set_quota(old_mb.quota / 1024000, override_rules=True)
+ new_mb.save(creator=creator, using=options["_to"])
+
+ if options["rename_dirs"]:
+ oldpath = os.path.join(
+ options["mboxes_path"], domain.name, old_mb.maildir)
+ new_mb.rename_dir(oldpath)
+
+ def _migrate_domain(self, old_dom, options, creator):
+ """Single domain migration."""
+ print "Migrating domain %s" % old_dom.domain
+ newdom = admin_models.Domain()
+ newdom.name = old_dom.domain
+ newdom.enabled = old_dom.active
+ newdom.quota = old_dom.maxquota
+ newdom.dates = self._migrate_dates(old_dom)
+ newdom.save(creator=creator, using=options["_to"])
+ self._migrate_mailboxes(newdom, options, creator)
+ self._migrate_mailbox_aliases(newdom, options, creator)
+ self._migrate_domain_aliases(newdom, options, creator)
+
+ def _migrate_admins(self, options, creator):
+ """Administrators migration."""
+ print "Migrating administrators"
+
+ dagroup = Group.objects.using(options["_to"]).get(name="DomainAdmins")
+ for old_admin in pf_models.Admin.objects.using(options["_from"]).all():
+ local_part, domname = split_mailbox(old_admin.username)
+ try:
+ query = Q(username=old_admin.username) & \
+ (Q(domain="ALL") | Q(domain=domname))
+ creds = pf_models.DomainAdmins.objects \
+ .using(options["_from"]).get(query)
+ except pf_models.DomainAdmins.DoesNotExist:
+ print (
+ "Warning: skipping useless admin %s" % (old_admin.username)
+ )
+ continue
+ try:
+ domain = admin_models.Domain.objects \
+ .using(options["_to"]).get(name=domname)
+ except admin_models.Domain.DoesNotExist:
+ print (
+ "Warning: skipping domain admin %s, domain not found"
+ % old_admin.username
+ )
+ continue
+ try:
+ user = core_models.User.objects \
+ .using(options["_to"]).get(username=old_admin.username)
+ except core_models.User.DoesNotExist:
+ user = core_models.User()
+ user.username = old_admin.username
+ user.email = old_admin.username
+ user.password = old_admin.password
+ user.is_active = old_admin.active
+ user.save(creator=creator, using=options["_to"])
+
+ user.date_joined = old_admin.modified
+ if creds.domain == "ALL":
+ user.is_superuser = True
+ else:
+ user.groups.add(dagroup)
+ domain.add_admin(user)
+ user.save(using=options["_to"])
+
+ def _do_migration(self, options, creator_username='admin'):
+ """Run the complete migration."""
+ pf_domains = pf_models.Domain.objects.using(options["_from"]).all()
+ creator = core_models.User.objects.using(options["_to"]).get(
+ username=creator_username)
+ for pf_domain in pf_domains:
+
+ if pf_domain.domain == "ALL":
+ continue
+
+ try:
+ # Skip this domain if it's an alias
+ is_domain_alias = pf_models.AliasDomain.objects \
+ .using(options["_from"]).get(alias_domain=pf_domain.domain)
+ print "Info: %s looks like an alias domain, skipping it" \
+ % pf_domain.domain
+ continue
+ except pf_models.AliasDomain.DoesNotExist:
+ self._migrate_domain(pf_domain, options, creator)
+ self._migrate_admins(options, creator)
+
+ def handle(self, *args, **options):
+ """Command entry point."""
+ if options["rename_dirs"] and options["mboxes_path"] is None:
+ print "Error: you must provide the --mboxes-path option"
+ sys.exit(1)
+ with transaction.commit_on_success():
+ self._do_migration(options)
diff --git a/modoboa/tools/pfxadmin_migrate/migrate.py b/modoboa/tools/pfxadmin_migrate/migrate.py
deleted file mode 100644
index 6824d3787..000000000
--- a/modoboa/tools/pfxadmin_migrate/migrate.py
+++ /dev/null
@@ -1,247 +0,0 @@
-#!/usr/bin/env python
-# coding: utf-8
-
-"""
-Automatic postfixadmin to Modoboa migration
-===========================================
-
-This script provides an easy way to migrate an existing postfixadmin
-database to a Modoboa one.
-
-As the two products do not share the same schema, some informations
-will be lost on the new database. Here is the list:
- * Description, transport and backup MX for domains
- * Logs
- * Fetchmail table
-
-Mailboxes organisation on the filesystem will change. PostfixAdmin
-uses the following layout::
-
- /domain.tld/user@domain.tld/
-
-Whereas Modoboa uses the following::
-
- /domain.tld/user/
-
-To rename directories, you'll need the appropriate permissions.
-
-Domain administrators that manage more than one domain will not be
-completly restored. They will only be administrator of the domain that
-corresponds to their username.
-
-Vacation messages will not be migrated by this script. Users will have
-to define new messages by using Modoboa's autoreply plugin. (if
-activated)
-
-One last point about passwords. PostfixAdmin uses different hash
-algorithms to store passwords so this script does not modify
-them. They are copied "as is" is the new database. In order to use
-them, you'll have to adapt the ``PASSWORD_SCHEME`` parameter in the
-admin. panel. Currently, it appears that the ``crypt`` scheme is
-compatible with PostfixAdmin ``md5crypt`` algorithm...
-
-"""
-import os
-import sys
-from django.db import transaction
-from django.db.utils import ConnectionDoesNotExist
-from django.db.models import Q
-from django.contrib.auth.models import Group
-import modoboa.extensions.admin.models as admin_models
-import modoboa.core.models as core_models
-import models as pf_models
-from modoboa.lib.sysutils import exec_cmd
-from modoboa.lib.emailutils import split_mailbox
-
-def migrate_dates(oldobj):
- """Creates a new ObjectDates instance
-
- Due to Django limitations, we only retrieve the creation date. The
- modification date will be set to 'now' because the corresponding
- field in Modoboa's schema has the 'auto_now' attribute to True.
- (see https://docs.djangoproject.com/en/dev/ref/models/fields/#django.db.models.DateField).
-
- :param oldobj: the PostfixAdmin record which is beeing migrated.
- """
- dates = admin_models.base.ObjectDates()
- dates.save()
-
- dates.creation = oldobj.created
- dates.save()
- return dates
-
-def migrate_domain_aliases(domain, options, creator):
- print "\tMigrating domain aliases"
- old_domain_aliases = pf_models.AliasDomain.objects.using(options._from).filter(target_domain=domain.name)
- for old_da in old_domain_aliases:
- try:
- target_domain = admin_models.Domain.objects.using(options._to).get(name=old_da.target_domain)
- new_da = admin_models.DomainAlias()
- new_da.name = old_da.alias_domain
- new_da.target = target_domain
- new_da.enabled = old_da.active
- new_da.dates = migrate_dates(old_da)
- new_da.save(using=options._to)
- new_da.post_create(creator)
- except core_models.Domain.DoesNotExist as e:
- print "Warning: target domain %s does not exists, not creating alias domain %s" \
- % old_da.target_domain, old_da.alias_domain
- continue
-
-def migrate_mailbox_aliases(domain, options, creator):
- print "\tMigrating mailbox aliases"
- old_aliases = pf_models.Alias.objects.using(options._from).filter(domain=domain.name)
- for old_al in old_aliases:
- if old_al.address == old_al.goto:
- continue
- new_al = admin_models.Alias()
- local_part, tmp = split_mailbox(old_al.address)
- if local_part is None or not len(local_part):
- if tmp is None or not len(tmp):
- print """Warning: skipping alias %s (cannot retrieve local part).
-You will need to recreate it manually.
-""" % old_al.address
- continue
- new_al.address = "*"
- else:
- new_al.address = local_part
- new_al.domain = domain
- new_al.enabled = old_al.active
- extmboxes = []
- intmboxes = []
- for goto in old_al.goto.split(","):
- try:
- mb = admin_models.Mailbox.objects.using(options._to).get(user__username=goto)
- except admin_models.Mailbox.DoesNotExist:
- extmboxes += [goto]
- else:
- intmboxes += [mb]
- new_al.dates = migrate_dates(old_al)
- new_al.save(int_rcpts=intmboxes, ext_rcpts=extmboxes, creator=creator, using=options._to)
-
-def migrate_mailboxes(domain, options, creator):
- print "\tMigrating mailboxes"
- old_mboxes = pf_models.Mailbox.objects.using(options._from).filter(domain=domain.name)
- for old_mb in old_mboxes:
- new_user = core_models.User()
- new_user.username = old_mb.username
- new_user.first_name = old_mb.name.partition(' ')[0]
- new_user.last_name = old_mb.name.partition(' ')[2]
- new_user.email = old_mb.username
- new_user.is_active = old_mb.active
- new_user.date_joined = old_mb.created
- new_user.password = old_mb.password
- new_user.dates = migrate_dates(old_mb)
- new_user.save(creator=creator, using=options._to)
- new_user.set_role("SimpleUsers")
-
- new_mb = admin_models.Mailbox()
- new_mb.user = new_user
- new_mb.address = old_mb.local_part
- new_mb.domain = domain
- new_mb.dates = migrate_dates(old_mb)
- if old_mb.quota:
- new_mb.quota = old_mb.quota / 1024000
- else:
- new_mb.quota = 0
-
- if options.rename_dirs:
- oldpath = os.path.join(options.mboxes_path, domain.name, old_mb.maildir)
- newpath = os.path.join(options.mboxes_path, domain.name, new_mb.path)
- code, output = exec_cmd("mv %s %s" % (oldpath, newpath))
- if code:
- print "Error: cannot rename mailbox directory\n%s" % output
- sys.exit(1)
-
- new_mb.save(creator=creator, using=options._to)
-
-def migrate_domain(old_dom, options, creator):
- print "Migrating domain %s" % old_dom.domain
- newdom = admin_models.Domain()
- newdom.name = old_dom.domain
- newdom.enabled = old_dom.active
- newdom.quota = old_dom.maxquota
- newdom.dates = migrate_dates(old_dom)
- newdom.save(creator=creator, using=options._to)
- migrate_mailboxes(newdom, options, creator)
- migrate_mailbox_aliases(newdom, options, creator)
- migrate_domain_aliases(newdom, options, creator)
-
-def migrate_admins(options, creator):
- print "Migrating administrators"
-
- dagroup = Group.objects.using(options._to).get(name="DomainAdmins")
- for old_admin in pf_models.Admin.objects.using(options._from).all():
- local_part, domname = split_mailbox(old_admin.username)
- try:
- query = Q(username=old_admin.username) & \
- (Q(domain="ALL") | Q(domain=domname))
- creds = pf_models.DomainAdmins.objects.using(options._from).get(query)
- except pf_models.DomainAdmins.DoesNotExist:
- print "Warning: skipping useless admin %s" % (old_admin.username)
- continue
- try:
- user = core_models.User.objects.using(options._to).get(username=old_admin.username)
- except core_models.User.DoesNotExist:
- try:
- domain = admin_models.Domain.objects.using(options._to).get(name=domname)
- except admin_models.Domain.DoesNotExist:
- print "Warning: skipping domain admin %s, domain not found" \
- % old_admin.username
- continue
- user = core_models.User()
- user.username = old_admin.username
- user.email = old_admin.username
- user.password = old_admin.password
- user.is_active = old_admin.active
- user.save(creator=creator, using=options._to)
-
- user.date_joined = old_admin.modified
- if creds.domain == "ALL":
- user.is_superuser = True
- else:
- user.groups.add(dagroup)
- user.save(using=options._to)
-
-@transaction.commit_on_success
-def do_migration(options, creator_username='admin'):
- pf_domains = pf_models.Domain.objects.using(options._from).all()
- creator = core_models.User.objects.using(options._to).get(username=creator_username)
- for pf_domain in pf_domains:
-
- if pf_domain.domain == "ALL":
- continue
-
- try:
- #Skip this domain if it's an alias
- is_domain_alias = pf_models.AliasDomain.objects.using(options._from).get(alias_domain=pf_domain.domain)
- print "Info: %s looks like an alias domain, skipping it" \
- % pf_domain.domain
- continue
- except pf_models.AliasDomain.DoesNotExist:
- migrate_domain(pf_domain, options, creator)
- migrate_admins(options, creator)
-
-if __name__ == "__main__":
- from optparse import OptionParser
-
- parser = OptionParser()
- parser.add_option("-f", "--from", dest="_from", default="pfxadmin",
- help="Name of postfixadmin db connection declared in settings.py (default is pfxadmin)")
- parser.add_option("-t", "--to", dest="_to", default="default",
- help="Name of the Modoboa db connection declared in settings.py (default is default)")
- parser.add_option("-r", "--rename-dirs", action="store_true",
- help="Rename mailbox directories (default is no)")
- parser.add_option("-p", "--mboxes-path", default=None,
- help="Path where directories are stored on the filesystem (used only if --rename-dirs)")
-
- options, args = parser.parse_args()
-
- if options.rename_dirs and options.mboxes_path is None:
- print "Error: you must provide the --mboxes-path option"
- sys.exit(1)
-
- try:
- do_migration(options)
- except ConnectionDoesNotExist, e:
- print e