# Copyright 1999-2012. Parallels IP Holdings GmbH. All Rights Reserved.
package Agent;

#
# Agent.pm module implements Agent interface
#

use strict;
use warnings;

use XmlNode;
use CommonXmlNodes;

use AgentConfig;
use Dumper;
use Storage::Storage;
use ContentDumperBase;
use DatabaseContentDumper;
use ContentDumper;

use FormatConverter;
use LimitsAndTemplates;
use SpamAssassinCfg;
use EncodeBase64;
use IDN;
use DateUtils;
use CustomLogging;
use ConfixxConfig;

use Preferences;
use PreMigration;
use PreMigrationChecks;

use constant DOMAIN_MAIL_CONTENT => 1;
use constant DOMAIN_HOSTING_CONTENT => 2;

#
# Begin interface methods
#

my $do_gzip = undef;
my $agentWorkDir;
my $contentDumperBase = ContentDumperBase->new(Storage::Storage::createFileStorage($do_gzip, getWorkDir()));

sub enableCompression {
  $do_gzip = 1;
}

sub getCompressionStatus {
  return $do_gzip;
}

sub setWorkDir {
  my $workDir = shift;
  $agentWorkDir = $workDir;
}

sub getWorkDir {
  return $agentWorkDir || AgentConfig::cwd();
}

sub getContentTransportDescription{
  return $contentDumperBase->getContentTransportDescription();
}

sub getAgentName {
  return "Confixx";
}

sub getAgentVersion {
  return '10.4'
}

sub getAgentStatus {
  Logging::trace("Getting status...");

  # TODO: check confixx version, pre-requirements, etc
  return XmlNode->new('agent-status');
}

sub getAdminPreferences {
  my $adminPersonalInfo = Dumper::getAdminPersonalInfo();
  my $adminInfo = Dumper::getAdminInfo();
  
  $adminPersonalInfo->{'language'} = $adminInfo->{'language'};

  return XmlNode->new('preferences',
    'children' => [_makePinfoNodes(_convertUserPersonalInfo(undef, $adminPersonalInfo))]
  )
}

# Should return an array of reseller identifiers, that could be a number, name or guid.
# Should be unique through migration dump.
sub getResellers {
  Logging::trace("Getting resellers...");
  return Dumper::getResellers();
}

sub getReseller {
  # reseller identifier from 'getResellers' result
  my $resellerId = shift;

  Logging::trace("Getting reseller dump for '$resellerId' reseller...");

  my $resellerInfo = Dumper::getResellerInfo($resellerId);

  my @templatesNodes = map {
    CommonXmlNodes::domainTemplate($_->{'name'}, $_->{'items'} )
  } LimitsAndTemplates::getClientTemplates($resellerId);

  PreMigrationChecks::checkReseller($resellerId, $resellerInfo);

  my $numberOfSystemDomains = scalar(Dumper::getClients($resellerId));    # is equal number of clients
  
  return XmlNode->new('reseller',
    'attributes' => {
      'id' => $resellerId,
      'name' => $resellerId,
      'guid' => ConfixxGuidGenerator::getResellerGuid($resellerId),
    },
    'children' => [
      XmlNode->new('content', 
        'children' => [ 
          ContentDumper::getResellerOrAdminVirtualHostTemplateContent(
            $contentDumperBase, $resellerId, 
            $resellerInfo->{'indexfile'}, $resellerInfo->{'indexcode'}
          ) 
        ]
      ),
      XmlNode->new('preferences',
        'children' => [
          _makePinfoNodes(_convertUserPersonalInfo($resellerId, $resellerInfo)),
          @templatesNodes
        ]
      ),
      _makeUserPropertiesNode($resellerInfo->{'longpw'}, !$resellerInfo->{'gesperrt'}),
      CommonXmlNodes::limitsAndPermissions(
        LimitsAndTemplates::getResellerLimits($resellerId, $numberOfSystemDomains),
        LimitsAndTemplates::getResellerPermissions($resellerId)
      ),
      CommonXmlNodes::ipPool(getResellerIPPool($resellerId))
    ]
  );
}

# Should return an array of clients identifiers, that could be a number, name or guid.
# Should be unique through migration dump
sub getClients {
  my $owner = shift;

  unless (Dumper::isResellerId($owner)) {
    # clients are owned by resellers only
    return ();
  }

  Logging::trace("Getting clients list for $owner reseller...");
  
  return Dumper::getClients($owner);
}

sub _getContactString {
  my $contactInfo = shift;
  my @contactList = ();
  foreach my $field ( 'firstname', 'name', 'firma' ) {
    if ( exists $contactInfo->{ $field } and 
       defined $contactInfo->{ $field } and 
       $contactInfo->{ $field } !~ m/^\s*$/ ) {
      my $part = $contactInfo->{ $field };
      $part = '(' . $part . ')' if($field eq 'firma');
      push @contactList, $part;
    }
  }
  return @contactList;
}

sub getClientContactString {
  my $clientId = shift;
  my $contactInfo = Dumper::getClientInfo($clientId);
  my @contactList = _getContactString($contactInfo);

  my $rv = "";
  $rv ||= join( ' ', @contactList );
  return $rv;
}

sub getResellerContactString {
  my $resellerId = shift;
  my $contactInfo = Dumper::getResellerInfo($resellerId);
  my @contactList = _getContactString($contactInfo);

  my $rv = "";
  $rv ||= join( ' ', @contactList );
  return $rv;
}

sub getClient {
  my ($clientId, $dumpOptions) = @_;

  # client identifier from 'getClients' result,
  Logging::trace("Getting client dump for '$clientId' client...");

  my $clientInfo = Dumper::getClientInfo($clientId);
  
  PreMigrationChecks::checkClient($clientId, $clientInfo);

  my $resellerId = Dumper::getResellerForClient($clientId);
  return XmlNode->new('client',
    'attributes' => {
      'id' => $clientId, 
      'name' => $clientId,
      'guid' => ConfixxGuidGenerator::getClientGuid($clientId),
      'vendor-guid' => ConfixxGuidGenerator::getResellerGuid($resellerId),
    },
    'children' => [ 
      XmlNode->new('preferences',
        'children' => [_makePinfoNodes(_convertUserPersonalInfo($clientId, $clientInfo))]
      ),
      _makeUserPropertiesNode($clientInfo->{'longpw'}, !$clientInfo->{'gesperrt'}),
      CommonXmlNodes::ipPool(getClientIPPool($clientId)),
      XmlNode->new('domains', 
        'children' => [ _getSystemDomainNode($clientId, $dumpOptions) ]
      ),
      getClientUsersNode($clientId),
    ]
  );
}

sub _generateRandomPassword($) {
  my ($length) = @_;
  my @chars = ('a'..'z', 'A'..'Z', '0'..'9', '+', '-', '=', '/', '$', '%', '#');
  my $result = join(''), map {$chars[rand @chars]} 1..$length;
  return $result;
}

sub getClientUsersNode {
  my ($clientId) = @_;
  my $roleName = "Application User";

  my $usersNode = XmlNode->new('users');

  my $mailMapping = _mappingMailUsers($clientId);
  foreach my $siteName (keys %{$mailMapping}) {
    my $mailUsers = $mailMapping->{$siteName};
    if (defined($mailUsers) && %{$mailUsers}) {
      while (my ($emailPrefix, $emailSettings) = each %{$mailUsers}) {
        if (exists($emailSettings->{'mbox'}) and defined($emailSettings->{'mbox'})) {
          my $mboxName = $emailSettings->{'mbox'};
          my $emailAddress = $mboxName . '@' . $siteName;

          # madness ahead!
          $usersNode->addChild(
            XmlNode->new('user',
              'attributes' => {
                'name' => $emailAddress,
                'email' => $emailAddress,
                'subscription-name' => getClientSystemDomainName($clientId),
                'contact' => '',
                'is-domain-admin' => 'false',
              },
              'children' => [
                XmlNode->new('properties', 'children' => [
                  # Here we should provide a real mailuser's password. But in this 
                  # particular place hashed passwords are not supported by Plesk. 
                  # So we'd better generate them ourselves asking users to choose
                  # "forgot password" later, thus avoiding tons of warnings during
                  # restore.
                  CommonXmlNodes::password('plain', _generateRandomPassword(15)),
                  CommonXmlNodes::status('enabled'),
                ]),
                XmlNode->new('limits-and-permissions', 'children' => [
                  XmlNode->new('role-ref', 'content' => $roleName),
                ]),
                XmlNode->new('preferences', 'children' => [
                  XmlNode->new('pinfo', 'attributes' => {'name' => 'email'}, 'content' => $emailAddress),
                ]),
              ],
            )
          );
        }
      }
    }
  }
  my $roleNode = XmlNode->new('roles', 
    'children' => [
      XmlNode->new('role',
        'attributes' => {
          'name' => $roleName,
        },
        'children' => [
          XmlNode->new('limits-and-permissions'),
        ],
      ),
    ],
  );
  return ($usersNode, $roleNode);
}

# In current implementation domains are not separated from clients.
#
# Reason: it is not clear how to migrate databases, user's content and other objects
# that are related to a customer, not a domain. The problem rises
# when you try to implement the shallow dump, with the following strategy:
# - if some element should be removed, all its children should go to its parent element
# - if parent element should be removed too, all the children should go to a parent of the parent
# - and so on.
#
# For example user selected a domain, but has not selected a customer. Should the content and databases
# go to a reseller? And if no domains are selected and customer is not selected? Should we dump
# the content? And there are a lot of other such questions, which could not be answered in a simple way.
sub getDomains {
  my ($clientId) = @_;
  
  if (Dumper::isClientId($clientId)) {
    return ($clientId);
  } else {
    return ();
  }
}

sub getPreMigrationDomains {
  my ($client) = @_;
  
  unless (Dumper::isClientId($client)) {
    return ();
  }

  return Dumper::getDomains($client);
}

# <doc>
# == IP migration ==
# 
# * Each server IP can be:
# ** Standard server IP - a default shared IP. There is one such IP addreess per Confixx installation.
#    Migration mapping: added to a pool of each reseller that have no assigned IPs; added to reseller's IP pool in case, if there is a customer which uses that IP.
# ** Reseller's shared IP - a shared IP for customers of the reseller.
#    Migration mapping: added to reseller's IP pool as a shared IP; added to customer's IP pool as a shared IP.
# ** Customer's dedicated IP. Once reseller's free IP is assigned to one of the customers, it become a customer's dedicated IP.
#    Migration mapping: added to reseller's IP pool as dedicated IP; added to customer's IP pool as dedicated IP.
# ** A free IP address - not used by anyone.
# </doc>
sub getResellerIPPool {
  my ($reseller) = @_;

  my @ips;
  my $resellerInfo = Dumper::getResellerInfo($reseller);
  push @ips, {'type' => 'shared', 'ip' => $resellerInfo->{'standardip'}};
  for my $ip (Dumper::getResellerExclusiveIPs($reseller)) {
    my $ip_type = $ip->{'kunde'} eq '' ? 'shared' : 'exclusive'; 
    unless ($ip_type eq 'shared' && $ips[0]->{'ip'} eq $ip->{'ip'}) {
      push @ips, 
        {
          'type' => $ip_type,
          'ip' => $ip->{'ip'}
        } 
    }
  }
  return \@ips;
}

# Client's IP pool always consists of one IP 
sub getClientIPPool {
  my ($client) = @_;

  return [getClientIP($client)];
}

sub getServerNode {
  my $adminInfo = Dumper::getAdminInfo();

  return XmlNode->new('server', 
    'children' => [
      XmlNode->new('content', 
        'children' => [ 
          ContentDumper::getResellerOrAdminVirtualHostTemplateContent(
            $contentDumperBase, undef, 
            $adminInfo->{'indexfile'}, $adminInfo->{'indexcode'}
          )
        ],
      ),
      _makeDnsSettingsNode()
    ]
  );
}

#
# End of interface methods
#

sub _getDomainCapabilitiesNode($) {
  my $clientId = shift;
  my $capabilities = _getDomainCapabilities($clientId);

  my @partNodes = ();
  while (my ($part, $hash) = each %{$capabilities}) {
    if ($part eq 'resources') {
      my @paramNodes = ();
      while (my ($paramName, $paramValue) = each %{$hash}) {
        my $paramNode = XmlNode->new( 'resource', 'children' => [
          XmlNode->new( 'name', 'content' => $paramName ),
          XmlNode->new( 'value', 'content' => $paramValue ),
        ]);
        push @paramNodes, $paramNode;
      }
      push @partNodes, XmlNode->new('resource-usage', 'children' => \@paramNodes);
    }
  }
  my $capabilitiesNode = XmlNode->new('capability-info', 'children' => \@partNodes);
  return $capabilitiesNode;
}

sub _getDomainCapabilities($) {
  my $clientId = shift;
  my $clientInfo = Dumper::getClientInfo($clientId);

  my $capabilities = {};

  my $resources = {};

  $resources->{'diskusage.vhost'} = $clientInfo->{'kbhomedir'} * 1024;
  $resources->{'diskusage.db'} = $clientInfo->{'kbdb'} * 1024;
  $resources->{'diskusage.mail'} = $clientInfo->{'kbpop'} * 1024;

  $capabilities->{'resources'} = $resources;

  return $capabilities;  
}

# cache is also needed for emitting "no system domain" warning only once per client
my %systemDomainNameCache = ();

sub getClientSystemDomainName {
  my ($clientName) = @_;
  my $systemDomainName = undef;
  if (not exists $systemDomainNameCache{$clientName}) {
    $systemDomainName = Dumper::getClientSystemDomain($clientName);
    unless (defined($systemDomainName)) {
      my $domainCounter = 0;
      $systemDomainName = "$clientName." . ConfixxConfig::getValue('hostname');     # create special fake domain
      while (Dumper::checkExistsDomain($systemDomainName)) {
        $domainCounter++;
        $systemDomainName = "$domainCounter.$clientName." . ConfixxConfig::getValue('hostname');
      }
      PreMigration::message('CUSTOMER_HAS_NO_SYSTEM_DOMAIN', {'user' => $clientName, 'systemDomain' => $systemDomainName});
    }
    $systemDomainNameCache{$clientName} = $systemDomainName;
  } else {
    $systemDomainName = $systemDomainNameCache{$clientName};
  }
  return $systemDomainName;
}

sub getClientDomainNames {
  my ($clientName) = @_;
  return _removeAlias('www.', Dumper::getDomains($clientName));
}

sub _getDomainLimitsAndPermissionsNode {
  my ($clientName) = @_;
  my $numberOfSites = 1 + scalar(getClientDomainNames($clientName)); # '1' is for systemdomain

  return CommonXmlNodes::limitsAndPermissions(
    LimitsAndTemplates::getClientLimits($clientName, $numberOfSites),
    LimitsAndTemplates::getClientPermissions($clientName)
  );
}

sub _getDomainTrafficNode {
  my ($clientName) = @_;
  return CommonXmlNodes::traffic(getTraffic($clientName));
}

sub _getSystemDomainNode {
  my ($clientName, $dumpOptions) = @_;

  my $systemDomainName = getClientSystemDomainName($clientName);

  my @domainNames = getClientDomainNames($clientName);
  PreMigration::assert(scalar(@domainNames) == 0, 'CUSTOMER_WITH_NO_DOMAINS', {'user' => $clientName, 'systemDomain' => $systemDomainName});

  Logging::trace("Getting dump of a system domain '$systemDomainName' of a '$clientName' client");

  my $domainNode = XmlNode->new('domain',
    'attributes' => {
      'name' => $systemDomainName,
      'guid' => ConfixxGuidGenerator::getDomainGuid($systemDomainName),
      'www' => 'false',
      'vendor-guid' => ConfixxGuidGenerator::getResellerGuid(Dumper::getResellerForClient($clientName)),
    });

  my @domainElementsList = (
    [DOMAIN_HOSTING_CONTENT, \&_getSystemDomainPreferencesNode],
    [DOMAIN_HOSTING_CONTENT, \&_getSystemDomainPropertiesNode],
    [DOMAIN_HOSTING_CONTENT, \&_getDomainLimitsAndPermissionsNode],
    [DOMAIN_MAIL_CONTENT,    \&_getMailSystemForSubscription],
    [DOMAIN_HOSTING_CONTENT, \&_makeDatabaseListNode],
    [DOMAIN_MAIL_CONTENT,    \&_makeDomainMaillistsNode],
    [DOMAIN_HOSTING_CONTENT, \&_getDomainTrafficNode],
    [DOMAIN_HOSTING_CONTENT, \&_makeSslNode],
    [DOMAIN_HOSTING_CONTENT, \&_makeDomainHostingNode],
  );

  my %dumpContentTypes = ();

  if ($dumpOptions->{'onlyMail'}) {
    $dumpContentTypes{DOMAIN_MAIL_CONTENT()} = 1;
  } elsif ($dumpOptions->{'onlyHosting'}) {
    $dumpContentTypes{DOMAIN_HOSTING_CONTENT()} = 1;
  } else {
    $dumpContentTypes{DOMAIN_MAIL_CONTENT()} = 1;
    $dumpContentTypes{DOMAIN_HOSTING_CONTENT()} = 1;
  } 

  foreach my $domainElement (@domainElementsList) {
    my ($contentType, $callback) = @{$domainElement};
    if (exists($dumpContentTypes{$contentType})) {
      $domainNode->addChild($callback->($clientName, $dumpOptions));
    }
  }

  return $domainNode;
}

# NB: we don't want to break the order here
sub _removeAlias {
  my ($prefix, @domains) = @_;
  my %domainLookup = map { $_ => 1 } @domains;
  my $prefixLen = length $prefix;

  my @result = ();

  foreach my $domain (@domains) {
    unless (substr($domain, 0, $prefixLen) eq $prefix and exists($domainLookup{substr($domain, $prefixLen)})) {
      push @result, $domain;
    }
  }
  return @result;
}

use constant MAIL_MAPPING_LOG_FILE => 'Confixx-mail-mapping.txt';

sub _getSystemDomainPreferencesNode {
  return XmlNode->new('preferences');
}

sub _getSystemDomainPropertiesNode {
  my ($clientName) = @_;
  my $systemDomainName = getClientSystemDomainName($clientName);

  Logging::trace("Making properties for system domain of client '$clientName' ... ");
  my $clientInfo = Dumper::getClientInfo($clientName);
  my $clientIP = getClientIP($clientName);
  return XmlNode->new('properties', 
    'children' => [
      CommonXmlNodes::ip($clientIP->{'ip'}, $clientIP->{'type'}),
      CommonXmlNodes::status(!$clientInfo->{'gesperrt'}, 'admin'),
      _makeDomainDnsZoneNode(Dumper::getDomainInfo($systemDomainName))
    ]
  );
}

#
# create <mailsystem> node
#
sub _getMailSystem {
  my ($clientName, $siteName) = @_;
  my $clientInfo = Dumper::getClientInfo($clientName);
  my $domainMailUsers = _mappingMailUsers($clientName);
  my $systemDomainName = getClientSystemDomainName($clientName);
  my $mailUsers = $domainMailUsers->{$siteName};

  Logging::trace("Getting emails for domain '$siteName' ...");

  my $mailSystemNode = XmlNode->new('mailsystem',
    'children' => [
        XmlNode->new('properties', 'children' => [
            CommonXmlNodes::status(1, 'admin')
        ])
    ]
  );
  my $preferences = _getMailPreferences($siteName, $systemDomainName, $clientInfo);
  if (defined($mailUsers) && %{$mailUsers}) {
    my $mailUsersNode = XmlNode->new('mailusers');
    while (my ($emailPrefix, $emailSettings) = each %{$mailUsers}) {
        $mailUsersNode->addChild(_getMailUserNode($siteName, $clientInfo, $emailPrefix, $emailSettings, $systemDomainName));
        _logEmailMapping($emailPrefix, $siteName, $emailSettings, $clientInfo);
    }
    $mailSystemNode->addChild($mailUsersNode);
  }

  $mailSystemNode->addChild($preferences);
  return $mailSystemNode;
}

sub _getMailSystemForSubscription {
  my ($clientName) = @_;
  return _getMailSystem($clientName, getClientSystemDomainName($clientName));
}

sub _getMailPreferences {
  my ($siteName, $systemDomainName, $clientInfo) = @_;
  if ($siteName eq $systemDomainName) {
    # Only the system (aka 'main') domain can have 'site/mailsystem/preferences' node in Plesk
    my $webmailNodeContent = $clientInfo->{'webmail'} ? 'horde' : 'none';
    return XmlNode->new('preferences',
      'children' => [
        _getCatchAll($clientInfo->{'kunde'}, $siteName),
        XmlNode->new('web-mail', 'content' => $webmailNodeContent),
      ]
    );
   } else {
    return XmlNode->new('preferences');
  }
}

# Generate XML node '/mailsystem/preferences/catch-all'
#
# Unlike in Confixx, Plesk catch-all is a per-subscription setting, not per-domain.
# In Plesk, catch-all mailbox receives email from all domains in the subscription.
# To be completely on safe side, we should disable catch-all in all cases but a single one:
# there is exactly one domain in the subscription.
#
# As of now, we relax the above statement a bit: we migrate catch-all only if 
# there is exactly one email, to which catch-all mail is delivered, no matter
# how many domains are there in the subscription.
# (We assume that the catch-all address is an admin-controlled email)
sub _getCatchAll {
  my ($client, $domainName) = @_;
  my $node = XmlNode->new('catch-all');
  my @addresses = Dumper::getCatchAllAddresses($client);
  my $nodeText;
  if (1 == @addresses) {
    my $addr = pop @addresses;
    $nodeText = ($addr =~ m/@/) ? $addr : "$addr\@$domainName";
  } elsif (@addresses > 1) {
    PreMigration::message('MAIL_CATCH_ALL', {'email_list' => join(', ', @addresses), 'client' => $client});
    $nodeText = 'reject';
  } else {
    $nodeText = 'reject';
  }
  $node->setText($nodeText, 'UTF-8');
  return $node;
}

##
# Return hash of <mailusers> xml nodes.
# Format of this table is:
#   $mailUsers->  {'domain'}->{'email'}                     - E-mail Address in Plesk (mailuser node in dump.xml).
#                                                             The 'email' is e-mail prefix here (username before @domain)
#   $mailUsers->  {'domain'}->{'email'}->{'mbox'}           - name of POP3 box in Confixx ('email' can be without mbox)
#   @{$mailUsers->{'domain'}->{'email'}->{'alias'}}         - ARRAY of E-mail Address aliases
#   @{$mailUsers->{'domain'}->{'email'}->{'forwarding'}}    - ARRAY of E-mail Address forwarding
#   $mailUsers->  {'domain'}->{'email'}->{'isspecialemail'} - indicate, that this mailuser is specially generated
#                                                             (for logging in _logEmailMapping)
##

sub _mappingMailUsers {
  my ($client) = @_;
  my $clientSystemDomain = getClientSystemDomainName($client);

  my $mailUsers;

  #
  # In special system(fake) domain:
  # create special emails(mailusers) for mboxes without emails
  #
  foreach my $mboxWithoutEmails (Dumper::getMboxesWithoutEmails($client)) {
    $mailUsers->{$clientSystemDomain}->{$mboxWithoutEmails}->{'mbox'}           = $mboxWithoutEmails;
    $mailUsers->{$clientSystemDomain}->{$mboxWithoutEmails}->{'isspecialemail'} = 1;
  }

  my $mboxToEmailMap = Dumper::getMboxToEmailMap($client);

  #
  # first loop: fill %mailUsers from email table for this domain
  #             main mapping actions(selections) here
  #
  foreach my $emailForward (Dumper::getEmailPrefixToDestinationTable($client)) {
      my $emailPrefix = $emailForward->{'prefix'};
      my $emailDomain = $emailForward->{'domain'};
      my $destination = $emailForward->{'pop3'};
      my $mailUserTable = $mailUsers->{$emailDomain}->{$emailPrefix};

      if ($destination =~ m/@/) {
        Logging::debug("Destination '$destination' is an email address; created a 'forwarding'.");
        push @{$mailUserTable->{'forwarding'}}, $destination;
      } else {  # $destination is mbox here
        if ("$emailPrefix\@$emailDomain" ne $mboxToEmailMap->{$destination}) {
          if ('*' eq $emailPrefix) {
            $mailUserTable->{'mbox'} = $destination;
          } else {
              Logging::debug("Email is not main/first email for mbox '$destination' - created forward to main email: '". $mboxToEmailMap->{$destination}."'");
            push @{$mailUserTable->{'forwarding'}}, $mboxToEmailMap->{$destination};
          }
        } else {
          if (defined($mailUserTable->{'mbox'})) {
              Logging::debug("Found more than one mbox for this email. Creating a new Plesk E-Mail Address for this mbox: '$destination'");
              # 
              my $newEmailPrefix = _generateEmailPrefixName($destination, $mailUsers, $emailDomain);
              $mailUsers->{$emailDomain}->{$newEmailPrefix}->{'mbox'} = $destination;

              push @{$mailUserTable->{'forwarding'}}, "$newEmailPrefix\@$emailDomain";
          } else {
              Logging::debug("Defined a Confixx POP3 mbox for this email: '$destination'");
              $mailUserTable->{'mbox'} = $destination;
          }
        }
      }

      if (!defined($mailUserTable->{'autoresponder'})
        && (my $autoresponder = Dumper::getEmailAutoresponder($emailPrefix, $emailDomain))) {
          $mailUserTable->{'autoresponder'} = $autoresponder;
      }


      if (defined($mailUserTable)) {
          $mailUsers->{$emailDomain}->{$emailPrefix} = $mailUserTable;
      }
  }

  # bug #68368 "full mesh mailmaping with two email and two mailbox"
  # next loop for resolving it: remove duplicate 'forwarding' entries
  #
  while (my ($emailDomain, $domainMailUsers) = each %{$mailUsers}) {
    while (my ($emailPrefix, $emailSettings) = each %{$domainMailUsers}) {
      next unless(defined($emailSettings->{'forwarding'}));

      my %uniqueEmailForwardings = map { $_ => 1 } @{$emailSettings->{'forwarding'}};
      @{$emailSettings->{'forwarding'}} = keys %uniqueEmailForwardings;
    }
  }

  #
  # next loop:
  #     Create aliases (from $mailUsers{}->{'forwardings'})
  # collapse emails forwardings into aliases by the following conditions:
  # - this email has only one <forwarding>
  # - this <forwarding> contains same $domainName
  # - this <forwarding> pointer to existing mailuser
  # - this email has not mbox
  # - this email has not autoresponders
  #
  while (my ($emailDomain, $domainMailUsers) = each %{$mailUsers}) {
    while (my ($emailPrefix, $emailSettings) = each %{$domainMailUsers}) {
        if (defined($emailSettings->{'forwarding'})                                 # check exist forwarding
          && !defined($emailSettings->{'mbox'})                                     # check not exist mbox
          && !defined($emailSettings->{'autoresponder'})                            # check not exist autoresponder
          && scalar(@{$emailSettings->{'forwarding'}}) == 1                         # check one forwarding
          && @{$emailSettings->{'forwarding'}}[0] =~ m/^([^@]+)\@$emailDomain$/) {  # check same domain
            
            Logging::debug("Collapsing forwardings into aliases for '$emailPrefix', '$1'.");
            my $alias = $1;     # get prefix of this forwarding email

            if (!defined($mailUsers->{$emailDomain}->{$alias})) {                   # check exist mailUsers
                next;
            }
            push @{$mailUsers->{$emailDomain}->{$alias}->{'alias'}}, $emailPrefix;  # create alias
            delete($mailUsers->{$emailDomain}->{$emailPrefix});                     # remove this mailuser with simple forwarding
        }
    }
  }

  return $mailUsers;
}

# create unique EmailPrefix for this Confixx mbox
sub _generateEmailPrefixName {
  my ($mboxname, $mailUsers, $domainName) = @_;
  use constant MAIL_USERNAME_LENGTH_LIMIT => 99;
  use constant MAIL_USERNAME_INDEX_LENGTH => 4;
  my $newMboxname = $mboxname;
  my $index = 0;

  while (defined($mailUsers->{$domainName}->{$newMboxname}) || Dumper::checkExistEmail($newMboxname, $domainName)) {
    $newMboxname = _getStringIndexWithSuffix("${mboxname}_mb", ++$index, MAIL_USERNAME_LENGTH_LIMIT, MAIL_USERNAME_INDEX_LENGTH);
  }

  $mailUsers->{$domainName}->{$newMboxname}->{'isspecialemail'} = 1;

  return $newMboxname;
}

sub _getStringIndexWithSuffix {
  my ($string, $valueIndex, $maxLength, $indexStrLength) = @_;

  return undef if ($valueIndex >= 10 ** $indexStrLength);

  my $strIndex = sprintf("%0${indexStrLength}d", $valueIndex);
  return (substr($string, 0, $maxLength - $indexStrLength) . $strIndex);
}

sub _getMailUserNode {
  my ($domainName, $clientInfo, $emailPrefix, $emailSettings, $systemDomainName) = @_;
  my $mailBoxNode;
  my $mboxName = $emailSettings->{'mbox'};
  my %attributes = (
    'name' => $emailPrefix,
    'forwarding-enabled' => (defined($emailSettings->{'forwarding'}) && @{$emailSettings->{'forwarding'}}) ? 'true' : 'false'
  );

  my $password;
  if (defined($mboxName)) {
    # XXX content should be placed under system subscription, not under domain
    my $contentNode = ContentDumper::getMailBoxContent($contentDumperBase, $mboxName, $domainName, $systemDomainName);

    $mailBoxNode = XmlNode->new('mailbox',
      'attributes' => {
        'type'      => 'mdir',
        'enabled'   => 'true'
      },
      'children'   => [
        $contentNode
      ]
    );

    my $mboxInfo = Dumper::getMboxInfo($mboxName);
    $password = $mboxInfo->{'longpw'};
    $password =~ s/^!//;            # remove password lock

    $attributes{'mailbox-quota'} = _getMboxQuota($domainName, $mboxInfo->{'maxkbhard'}, $mboxName);
  }

  my @aliasNodes = map { XmlNode->new('alias', 'content' => $_) } @{$emailSettings->{'alias'}};
  my @forwardingNodes = map {
    my $forwardingNode = XmlNode->new('forwarding');
    $forwardingNode->setText(_convertEmailAddressToUtf8($_), 'UTF-8');
    $forwardingNode;
  } @{$emailSettings->{'forwarding'}};

  return XmlNode->new('mailuser',
    'attributes' => \%attributes,
    'children' => [
      XmlNode->new('properties', 'children' => [
        defined($password) ? CommonXmlNodes::encryptedPassword($password) : CommonXmlNodes::emptyPassword()
      ]),
      XmlNode->new('preferences', 'children' => [
        $mailBoxNode,
        @aliasNodes,
        @forwardingNodes,
        _getEmailAutoresponders($emailSettings->{'autoresponder'}),
        defined($mboxName) ? _getEmailAddressBook($mboxName) : undef,
        defined($mboxName) && ConfixxConfig::getValue('spamassassin_support') ? 
          CommonXmlNodes::spamassassin(_getSpamassassinOptions($domainName, $mboxName, $clientInfo)) : ()
      ])
    ]
  );
}

# Convert e-mail address from latin1 and punycode to UTF-8
sub _convertEmailAddressToUtf8 {
  my ($emailAddress) = @_;

  # we use an ungreedy match to match the last '@' symbol 
  # as according to RFC 822 there could be a '@' in a local-part of e-mail address,
  # but '@' is not allowed in domain name
  # this could be also written as /^(.*)@([^@]*)$/ if it is more clear for you
  if ($emailAddress =~ /^(.*)@(.*?)$/s) { 
    # local part is called $emailPrefix here
    my ($emailPrefix, $domain) = ($1, $2);
    my ($canConvertIdnDomain, $reason) = IDN::canConvert();
    if ($canConvertIdnDomain && IDN::isIdnDomain($domain)) {
      return Encoding::encode($emailPrefix) . '@' . IDN::idnToUtf8($domain);
    } else {
      return Encoding::encode($emailAddress);
    }
  } else {
    return Encoding::encode($emailAddress);
  }
}

sub _getMboxQuota {
  my ($domainName, $mboxQuota, $mboxName) = @_;

  my $adminInfo = Dumper::getAdminInfo();
  my $globalMboxQuota = LimitsAndTemplates::convertBytesLimitValue($adminInfo->{'popmaxkbhard'});
  $mboxQuota = LimitsAndTemplates::convertBytesLimitValue($mboxQuota);

  if (!ConfixxConfig::getValue('mail_quota')) {
    return LimitsAndTemplates::PLESK_RESOURCE_UNLIMITED;
  }

  if ($mboxQuota == LimitsAndTemplates::PLESK_RESOURCE_UNLIMITED || $mboxQuota == 0) { # sometimes, in Confixx DB can be zero values for mbox quotas
    return $globalMboxQuota;
  }

  if ($globalMboxQuota != LimitsAndTemplates::PLESK_RESOURCE_UNLIMITED && $mboxQuota > $globalMboxQuota) {
    PreMigration::message('GLOBAL_MAILBOX_QUOTA_IS_LESS', {
        'domain' => $domainName,
        'mboxQuotaValue' => $mboxQuota, 
        'mboxName' => $mboxName, 
        'globalQuotaValue' => $globalMboxQuota
    });
    $mboxQuota = $globalMboxQuota;
  }

  return $mboxQuota;
}

sub _getEmailAutoresponders {
  my ($autoresponder) = @_;

  if (!defined($autoresponder)) {
    return undef;
  }

  my $text      = defined($autoresponder->{'text'})     ? $autoresponder->{'text'} : "";
  my $fromName  = defined($autoresponder->{'fromname'}) ? $autoresponder->{'fromname'} : "";
  my $fromEmail = defined($autoresponder->{'fromemail'})? $autoresponder->{'fromemail'} : "";
  my $subject   = defined($autoresponder->{'subject'})  ? $autoresponder->{'subject'} : "";

  if ( $fromName ne "" || $fromEmail ne "") {
    $text .= "\n--\n$fromName";
    if ($fromEmail ne "") {
        $text .= " <$fromEmail>";
    }
  }

  return XmlNode->new('autoresponders',
    'children' => [
      XmlNode->new('autoresponder',
        'attributes' => {
          'content-type' => 'text/plain',
          'subject' => EncodeBase64::encode($subject),
          'status' => 'on'
        },
        'children' => [
          XmlNode->new('text',
            'content' => EncodeBase64::encode($text))
        ]
      )
    ]
  );
}

sub _getEmailAddressBook {
  my ($mboxName) = @_;
  my @addressBook = Dumper::getEmailAddressBook($mboxName);

  unless (@addressBook) {
    return undef;
  }


  # TODO: make correct attributes
  return XmlNode->new('addressbook',
    'children' => [
      map {
        XmlNode->new('addressbook-contact',
          'attributes' => {
            'alias'         => EncodeBase64::encode($_->{'name'}),                   # We can not use 'name' attribute for it, because we do not know format of this attribute.
            'email'         => EncodeBase64::encode($_->{'email'}),
            'fax'           => EncodeBase64::encode($_->{'tel_fax'}),
            'home-phone'    => EncodeBase64::encode($_->{'tel_home'}),
            'id'            => "some_required_string_${mboxName}_" . $_->{'ident'} , # It is useless string, but must be unique for every record for work.
            'mobile-phone'  => EncodeBase64::encode($_->{'tel_mobile'}),
            'notes'         => EncodeBase64::encode("Address: " . $_->{'address'}),  # We can not use 'work-address' for it, because we do not know format of this attribute.
            'work-phone'    => EncodeBase64::encode($_->{'tel_work'})
          }
        )
      } @addressBook
    ]
  );
}

sub _logEmailMapping {
  my ($emailPrefix, $domainName, $emailSettings, $clientInfo) = @_;

  my $domainNameUtf8 = _convertEmailAddressToUtf8($domainName);
  my $confixxEmailUtf8 = _convertEmailAddressToUtf8("$emailPrefix\@$domainName");
  my $mboxName = $emailSettings->{'mbox'};
  # TODO probably this logging should be removed in favour of pre-migration log
  my $writeToLog = sub { CustomLogging::addToLog(MAIL_MAPPING_LOG_FILE, $_[0] . "\n") };

  $clientInfo->{'domain'} = $domainName;
  if (defined($emailSettings->{'isspecialemail'})) {
    if (defined($mboxName)) {
      $writeToLog->("Confixx's POP3 box '$mboxName' is migrated into Plesk's e-mail address '$confixxEmailUtf8'.");
      PreMigration::message('MAIL_MAPPING_MBOX_TARGET', {'mboxName' => $mboxName, 'domain' => $domainName, 'destinationEmail' => $confixxEmailUtf8});
    } else {
      # XXX ('isspecialemail' && ! $mboxname) --> 'catch-all' ?
      $writeToLog->("Confixx's '*' e-mail addresses are migrated into Plesk's e-mail address '$confixxEmailUtf8'.");
      PreMigration::message('MAIL_MAPPING_CATCH_ALL_EMAIL_TARGET', {'domain' => $domainName, 'destinationEmail' => $confixxEmailUtf8}, $clientInfo);
    }
  } else {
    my $mboxDescription = defined($mboxName) ? " with mailbox '$mboxName'" : "";
    $writeToLog->("Confixx's e-mail address '$confixxEmailUtf8' is migrated as is$mboxDescription.");
    if (defined($mboxName)) {
      PreMigration::message('MAIL_MAPPING_EMAIL_MIGRATED_WITH_MAILBOX', {'domain' => $domainName, 'email' => $confixxEmailUtf8, 'mboxName' => $mboxName});
    } else {
      PreMigration::message('MAIL_MAPPING_EMAIL_MIGRATED_AS_IS', {'domain' => $domainName, 'email' => $confixxEmailUtf8});
    }
  }

  my @forwardingsUtf8 = map { _convertEmailAddressToUtf8($_); } @{$emailSettings->{'forwarding'}};
  foreach my $forwarding (@forwardingsUtf8) {
    $writeToLog->("Additionally, e-mail address '$forwarding' is added to forwardings of Plesk's e-mail address '$confixxEmailUtf8'.");
    PreMigration::message('MAIL_MAPPING_FORWARDING', {'domain' => $domainName, 'email' => $forwarding, 'destinationEmail' => $confixxEmailUtf8});
  }

  my @aliasesUtf8 = map { _convertEmailAddressToUtf8($_); } @{$emailSettings->{'alias'}};
  foreach my $alias (@aliasesUtf8) {
    $writeToLog->("Confixx's e-mail address '$alias\@$domainNameUtf8' is migrated into an alias of Plesk's e-mail address '$confixxEmailUtf8'.");
    PreMigration::message('MAIL_MAPPING_ALIAS', {'domain' => $domainName, 'email' => "$alias\@$domainNameUtf8", 'destinationEmail' => $confixxEmailUtf8});
  }
}

# <doc>
# = Mail =
# == Spamassassin ==
# Custom spamassassin settings (that are edited in "For advanced users" mode) are not migrated. 
# ''Reason'': there is no such feature in Plesk.
#
# Action is always 'mark', other actions ('delete' and 'move') are not migrated because action is set
# outside of spamassassin config, and Confixx has no control on this: it's difficult to determine 
# which of them is enabled as it requires at least parsing procmailrc. Moreover it is not possible
# to determine if procmailrc is used, or if there is another solution.
#
# Server-wide spamassassin settings are not migrated due to PMM limitations. However if 'hits' or 
# 'subj-text' are not set in user spam preferences, they are taken from spamassassin global configuration.
#
# </doc>

use constant SPAMASSASSIN_ACTION_MARK => 'mark';

use constant SPAMASSASSIN_MAPPING_LOG_FILE => 'Confixx-spamassassin-mapping.txt';

sub _getSpamassassinOptions {
  my ($domainName, $mailbox, $clientInfo) = @_;
  my %options;
  my %duplicateMessagesIssued;
  
  my @spamPreferences = Dumper::getMboxSpamPreferences($mailbox);

  $options{'status'} = $clientInfo->{'spamfilter'};
  $options{'action'} = SPAMASSASSIN_ACTION_MARK;
  $options{'blacklist'} = [];
  $options{'whilelist'} = [];

  foreach my $preference (@spamPreferences) {
    my ($name, $value) = ($preference->{'preference'}, $preference->{'value'});

    if ($name eq 'rewrite_header') {
      if (defined($options{'subj-text'})) {
        if (!defined($duplicateMessagesIssued{'subj-text'})) {
          PreMigration::message('SPAMASSASSIN_DUPLICATE_SETTING',
              {'domain' => $domainName, 'mboxName' => $mailbox, 'param_name' => 'rewrite_header'},
          );
          $duplicateMessagesIssued{'subj-text'} = 1;
        }
      }
      $options{'subj-text'} = $value;
    } elsif ($name eq 'required_score') {
      if (defined($options{'hits'})) {
        if (!defined($duplicateMessagesIssued{'hits'})) {
          PreMigration::message('SPAMASSASSIN_DUPLICATE_SETTING',
              {'domain' => $domainName, 'mboxName' => $mailbox, 'param_name' => 'required_score'},
          );
          $duplicateMessagesIssued{'hits'} = 1;
        }
      }
      $options{'hits'} = $value;
    } elsif ($name eq 'blacklist_from') {
      push(@{$options{'blacklist'}}, $value);
    } elsif ($name eq 'whitelist_from') {
      push(@{$options{'whitelist'}}, $value);
    } else {
      PreMigration::message('SPAMASSASSIN_NO_SUCH_OPTION_IN_PLESK', {'domain' => $domainName, 'mboxName' => $mailbox, 'param_name' => $name, 'param_value' => $value});
      # TODO probably this logging should be removed in favour of pre-migration log
      CustomLogging::addToLog(SPAMASSASSIN_MAPPING_LOG_FILE, "Mailbox '$mailbox': SpamAssassin option '$name' with value '$value' was not migrated as there is no equivalent option in Plesk.\n");
    }
  }

  # use default system-wide options if user options are not set
  my %serverWideOptions = _getServerWideSpamassassinOptions();

  if (!defined($options{'hits'}) && defined($serverWideOptions{'hits'})) {
    $options{'hits'} = $serverWideOptions{'hits'};
  }
  
  if (!defined($options{'subj-text'}) && defined($serverWideOptions{'subj-text'})) {
    $options{'subj-text'} = $serverWideOptions{'subj-text'};
  }

  return %options;
}

sub _getServerWideSpamassassinOptions {
  my %result;

  my $config = SpamAssassinCfg::parseConfig(
    ConfixxConfig::getValue('spamassassinConfig'),
    undef # we do not pass home dir as it is not clear what is the home directory
  ); 

  my $hits = SpamAssassinCfg::getConfigRequireScore($config);
  $result{'hits'} = $hits if (defined $hits);

  my $rewriteHeaderArray = SpamAssassinCfg::getConfigRewriteHeadr($config);
  my $rewriteHeaderText;
  if (defined($rewriteHeaderArray) && @{$rewriteHeaderArray} == 2) {
    my ($argumentRewrite, $textRewrite) = @{$rewriteHeaderArray};

    $result{'subj-text'} = $textRewrite if ($argumentRewrite =~ m/subject/i);
  }

  return %result;
}

sub getClientIP {
  my ($client) = @_;

  my $clientInfo = Dumper::getClientInfo($client);
  my $clientIP = $clientInfo->{'ip'};
  my @dedicatedIPs = Dumper::getClientExclusiveIPs($client); 

  if (@dedicatedIPs == 1 && $dedicatedIPs[0] eq $clientIP) {
    return {'type' => 'exclusive', 'ip' => $clientIP};
  } else {
    if (@dedicatedIPs == 1 && $dedicatedIPs[0] ne $clientIP) { # fall back to shared IP
      Logging::warning("Expected that the only exclusive IP is the same as IP from client properties");
    } elsif (@dedicatedIPs > 1) { # fall back to shared IP
      Logging::warning("Expected that every client has 0 or 1 exclusive IPs, but client $client has " . scalar(@dedicatedIPs) . " IPs");
    }
    return {'type' => 'shared', 'ip' => $clientIP };
  }
}


# <doc>
# = Personal information =
# Gender, Customer ID and all definable fields are not mapped.
# ''Reason'': all non-mapped information could be mapped to comment field according to XSD,
# but there is no place in UI where this comment is shown or could be edited.
# 
# For all fields except name, language and locale, no convesion is made.
#
# Full country name should be used as a country field value. Otherwise it will not be migrated. ''Reason'': Plesk supports only a set of fixed values for country field.
#
# All the other fields (company name, phone, fax, address, city, zip code, e-mail) must 
# conform to Plesk format. Otherwise they will not be migrated and Plesk will issue a warning (however migration completes).
# </doc>
sub _convertUserPersonalInfo {
  my ($userId, $userInfo) = @_;

  my %result;

  my $name = _convertUserName($userId, $userInfo->{'firstname'}, $userInfo->{'name'});
  if (defined($name)) {
    $result{'name'} = $name;
  }

  my %usersMap = (
    'firma' => 'company',
    'telefon'=> 'phone',
    'fax' => 'fax',
    'anschrift' => 'address',
    'plzort' => 'city',
    'plz' => 'zip',
    'emailadresse' => 'email' 
  );
  
  while (my ($confixxKey, $pleskKey) = each %usersMap) {
    if ($userInfo->{$confixxKey} ne '') {
      $result{$pleskKey} = $userInfo->{$confixxKey};
    }
  }

  if ($result{'phone'} && $result{'phone'} =~ /\//) {
    $result{'phone'} =~ s/\///g # see bug 76853
  }
  if ($result{'fax'} && $result{'fax'}  =~ /\//) {
    $result{'fax'} =~ s/\///g if ($result{'fax'}); # see bug 76853
  }

  if ($userInfo->{'land'} ne '') {
    my $countryCode = FormatConverter::convertCountry($userInfo->{'land'});
    if (defined($countryCode)) {
      $result{'country'} = $countryCode;
    }
  }

  my $locale = FormatConverter::convertLocale($userInfo->{'language'});

  if (defined($locale)) {
    $result{'locale'} = $locale;
  }

  return \%result;
}

# <doc>
# == First and last names ==
# Plesk name is a combination of a first name and a last name, if some of them is set.
# If both the first name and the last name are not set - use user id (something like "res5", or "web8") as a name.
# </doc>
sub _convertUserName {
  my ($userId, $firstName, $lastName) = @_;

  if ($firstName ne '' && $lastName ne '') { 
    my @nameComponents;
    push @nameComponents, $firstName if ($firstName ne '');
    push @nameComponents, $lastName if ($lastName ne '');
    return join ' ', @nameComponents;
  } else {
    return $userId;
  }
}

# Users CP password and enabled/disabled status is migrated
sub _makeUserPropertiesNode {
  my ($password, $isEnabled) = @_;
  return XmlNode->new('properties', 
    'children' => [
      CommonXmlNodes::encryptedPassword($password),
      CommonXmlNodes::status($isEnabled, 'admin')
    ]
  );
}

# TODO migrator to Plesk9 also does the following questionable things:
# - add a webmail record manually: makeDnsRecord($dnsZoneNode, 'A', "webmail.$src", $dst);
# - read NS record values from reseller's or admin's settings instead of zone file
sub _makeDomainDnsZoneNode {
  my ( $domainInfo ) = @_;
  my $domainName = $domainInfo->{'domain'};
  Logging::trace("DNS status for domain $domainName: ".$domainInfo->{'dns'});

  # generate admin e-mail for domain the same way Confixx does it
  my $reseller  = Dumper::getResellerInfo($domainInfo->{'anbieter'});
  my $emailHost = $reseller->{'pns'};
  # take the domain part of the name: ns.example.com -> example.com
  $emailHost    =~ s/^[^\.]+\.//;
  my $email     = 'hostmaster.'.('' ne $emailHost ? $emailHost : $domainName);

  my @rawDnsRecords = Dumper::parseDnsRecords(Dumper::getDnsRecords($domainName));
  my $status     = $domainInfo->{'dns'};

  # fix single word domain in DNS
  my $domainNameSingleWordFixed = _fixSingleWordDomain($domainName);
  if ($domainNameSingleWordFixed ne $domainName) {
    foreach my $record (@rawDnsRecords) {
      $record->[0] =~ s/\Q$domainName.\E$/$domainNameSingleWordFixed./;
      my $dst = pop @$record;
      $dst =~ s/\Q$domainName.\E$/$domainNameSingleWordFixed./;
      push @$record, $dst;
    }
  }
  
  return _makeDnsZoneNode($domainName, $email, $status, \@rawDnsRecords);
}
  
sub _makeDnsZoneNode {
  my ($domainName, $email, $status, $dnsRecords) = @_;

  # create a SOA record with the same (hard-coded in confixx_updatescript.pl) parameters as Confixx does
  my @soaRecord = (
      "$domainName.",       # SRC
      86400,                # TTL
      'IN',                 # CLASS
      'SOA',                # TYPE
      '',                   # NS (not used by Plesk migrator)
      '',                   # admin (not used by Plesk migrator)
      '',                   # serial (not used by Plesk migrator)
      10800,                # refresh
      3600,                 # retry
      604800,               # expire
      86400                 # minimum
    );

  XmlNode->new('dns-zone', (
    'attributes' => {
        'email'         => $email,
        'serial-format' => 'YYYYMMDDNN',
        'type'          => 'master',
    },
    'children' => [
        CommonXmlNodes::status($status, 'admin'),
        CommonXmlNodes::dnsZoneParams(@soaRecord),
        # note that DNS records will be found only if DNS is enabled for domain
        map { CommonXmlNodes::dnsRecord(@$_) } @$dnsRecords
    ] 
  ));
}

sub _makeDomainHostingNode {
  my ($clientName) = @_;
  my $customerInfo_ptr = Dumper::getClientInfo($clientName);
  my $domainName = getClientSystemDomainName($clientName);
  my @domainNames = getClientDomainNames($clientName);
  my $mailUsers = _mappingMailUsers($clientName);
  my $siteInfo = Dumper::getDomainInfo($domainName);

  my %pDirs = Dumper::getProtectedDirectories($customerInfo_ptr, '/');

  my $pHostingNode = XmlNode->new('phosting',
    'attributes'  => {
      'webstat' => ($customerInfo_ptr->{'statistik'} == 1 ? 'webalizer' :
                    $customerInfo_ptr->{'awstats'} == 1 ? 'awstats' : 'none'),
      'errdocs' => $customerInfo_ptr->{'fehlerseiten'} ? 'true' : 'false',
      'wu_script' => 'true',
      'guid' => '',
      'owner-guid' => '',
      'www-root' => 'httpdocs',
      'cgi_bin_mode' => 'www-root'
    },
    'children' => [
      ContentDumper::getPHostingContent($contentDumperBase, $customerInfo_ptr->{'kunde'}, $domainName, '/', \%pDirs, $siteInfo->{'id'}),
      _makeHostingPreferencesNode($customerInfo_ptr, \%pDirs, $domainName),
      XmlNode->new('limits-and-permissions', 'children' => [ _makeScriptingNode($customerInfo_ptr) ]),
    ]
  );

  my @ftpUserNodes = _makeFtpUserNodes($customerInfo_ptr->{'kunde'});
  if (@ftpUserNodes) {
    $pHostingNode->addChild(XmlNode->new('ftpusers', 'children' => [ @ftpUserNodes ] ));
  }

  my @siteNodes = _getDomainSites($customerInfo_ptr, \@domainNames, $mailUsers, $domainName, $clientName);
  if (@siteNodes) {
    $pHostingNode->addChild(XmlNode->new('sites', 'children' => [ @siteNodes ] ));
  }

  return $pHostingNode;
}

sub _makeFtpUserNodes {
  my ($clientName) = @_;

  return map {
    XmlNode->new('ftpuser',
      'attributes' => {
        'name' => $_->{'account'}
      },
      'children' => [
        CommonXmlNodes::sysuser($_->{'account'}, $_->{'longpw'}, 'httpdocs/' . $_->{'pfad'}),
        XmlNode->new('permission', 'content' => 'upload'),
        XmlNode->new('permission', 'content' => 'download')
      ]
    );
  } Dumper::getFtpRecords($clientName);
}

sub _getDomainSites {
  my ($clientInfo, $siteNames, $mailUsers, $systemDomainName, $clientName) = @_;

  my @result;
  for my $siteName (@$siteNames) {
    push @result, _makeSiteNode($clientInfo, $siteName, undef, $mailUsers, $systemDomainName, $clientName);
    
    for my $wildcardSiteName (Dumper::getWildcardSubdomains($siteName)) {
      push @result, _makeSiteNode($clientInfo, $wildcardSiteName, $siteName, undef, $systemDomainName, $clientName);
    }
  }

  return @result;
}

sub _makeSiteNode {
  my ($clientInfo, $siteName, $parentSiteName, $mailUsers, $systemDomainName, $clientName) = @_;

  my $siteInfo = Dumper::getDomainInfo($siteName);
  my $siteNode = XmlNode->new('site',
    'attributes' => {
      'guid' => '',
      'www' => 'false'
    },
    'children' => [
      XmlNode->new('preferences'), # there are no own preferences, but this node is required by schema
      XmlNode->new('properties', 
        'children' => [
          CommonXmlNodes::status(!$clientInfo->{'gesperrt'}, 'admin'),
          _makeDomainDnsZoneNode($siteInfo)
        ]
      ),
      # $mailUser could be undef for wildcard subdomains as they do not have e-mail
      defined($mailUsers) ? _getMailSystem($clientName, $siteName) : undef, 
      defined($mailUsers) ? _makeSiteMaillistsNode($clientInfo, $siteName) : undef,
      _makeSiteHostingNode($clientInfo, $siteInfo, $systemDomainName)
    ]
  );

  my ($canConvertIdnDomain, $reason) = IDN::canConvert();
  if (IDN::isIdnDomain($siteName) && !$canConvertIdnDomain) {
    PreMigration::message('CAN_NOT_CONVERT_IDN_DOMAIN', {'domain' => $siteName, 'reason' => $reason});
  }

  my $siteNameSingleWordFixed = _fixSingleWordDomain($siteName);
  if ($siteName ne $siteNameSingleWordFixed) {
    PreMigration::message('SINGLE_WORD_DOMAIN', {'user' => $clientInfo->{'kunde'}, 'domain' => $siteName, 'newDomain' => $siteNameSingleWordFixed});
  }

  $siteNode->setAttribute('name', IDN::idnToUtf8($siteNameSingleWordFixed), 'UTF-8'); # domain name in UTF-8, for resulting XML 
  if (defined($parentSiteName)) { # mainly for wildcard subdomains
    $siteNode->setAttribute('parent-domain-name', IDN::idnToUtf8(_fixSingleWordDomain($parentSiteName)), 'UTF-8');
  }

  return $siteNode;
}

# if domain is a single-word, it is not allowed in Plesk, so we add Confixx hostname as a suffix
# otherwise return domain as-is
sub _fixSingleWordDomain {
  my ($domain) = @_;

  if ($domain !~ /\./) { # single-word domain
    return "$domain.single-word." . ConfixxConfig::getValue('hostname');
  } else {
    return $domain;
  }
}

sub _makeSiteHostingNode {
  my ($customerInfo_ptr, $domainInfo_ptr, $systemDomain) = @_;

  if ($domainInfo_ptr->{'pfad'} =~ /^http(s)?:/) {
    return XmlNode->new('shosting',
      'children' => [
        XmlNode->new('url', 'content' => $domainInfo_ptr->{'pfad'})
      ]
    );
  }

  my $domainName = $domainInfo_ptr->{'domain'};

  my $vhostFileContent = _getHttpdSpecialsVhostConfigurationText($domainName);

  if ($vhostFileContent ne '') {
    PreMigration::message('DOMAIN_HTTPD_SPECIALS', {'domain' => $domainName});
  }

  # not HTTPD specials, but a nice redirect added into the same vhostFileContent to migrate Confixx's http->https feature
  use constant CSSL_NONE => 0;
  use constant CSSL_HTTPS => 1;
  use constant CSSL_HTTPS_PLUS_REDIRECT => 2;
  if ($domainInfo_ptr->{'cssl'} == CSSL_HTTPS_PLUS_REDIRECT) {
    $vhostFileContent .= "\nRedirect / https://" . $domainName . "/\n";
  }
  Logging::debug("vhost.conf content: \n$vhostFileContent");

  my %pDirs = Dumper::getProtectedDirectories($customerInfo_ptr, $domainInfo_ptr->{'pfad'});

  my $pHostingNode = XmlNode->new('phosting',
    'attributes'  => {
      'www-root' => 'httpdocs' . $domainInfo_ptr->{'pfad'},
      'https' => $domainInfo_ptr->{'cssl'} == CSSL_NONE  ? 'false' : 'true',
      'cgi_bin_mode' => 'www-root'
    },
    'children' => [
      ContentDumper::getSitePHostingContent($contentDumperBase, $domainName, $vhostFileContent, $systemDomain, $domainInfo_ptr->{'id'}),
      XmlNode->new('preferences',
        'children' => [
            @{_makePDirNodes(\%pDirs)}
        ]
      ),
      XmlNode->new('limits-and-permissions', 'children' => [ _makeScriptingNode($customerInfo_ptr) ])
    ]
  );

  return $pHostingNode;
}

# <doc>
# == HTTPD specials ==
# HTTPD specials are migrated into vhost.conf. Plesk doesn't provide the functionality that
# exists in Confixx: there is no UI for editing HTTPD settings, reseller can't edit HTTPD settings. 
# However existing HTTPD settings are migrated to vhost.conf, without substitution of variables and
# commented by default (see Preferences.pm for more details).
# </doc>
sub _getHttpdSpecialsVhostConfigurationText {
  my ($domainName) = @_;

  # There are two types of HTTPD specials:
  # 1) items that could be selected from a list of values: for example, PHP safe_mode with values 'on' and 'off',
  # 2) advanced HTTPD specials: just a plain text that is put into HTTPD config.
  my @httpdSpecials;

  # put the 1st type
  push @httpdSpecials, Dumper::getDomainSelectableHttpdSpecials($domainName);

  # put the 2nd type
  my $advancedModeHttpdSpecials = Dumper::getDomainAdvancedModeHttpdSpecials($domainName);
  if (defined($advancedModeHttpdSpecials)) {
    push @httpdSpecials, $advancedModeHttpdSpecials; 
  }

  if ($Preferences::commentHttpdSpecials) {
    @httpdSpecials = map { _commentHttpdConfigText($_) } @httpdSpecials;
  }

  unless (@httpdSpecials) {
    return '';
  }

  return join "\n", ( 
    '# BEGIN HTTPD special options from Confixx',
    '',
    @httpdSpecials,
    '', 
    '# END HTTPD special options from Confixx'
  ); 
}

sub _commentHttpdConfigText {
  my ($configText) = @_;

  return
    join "\n", 
      map { "# $_"} 
        split /\n/, $configText;
}


sub _makeHostingPreferencesNode {
  my ($customerInfo_ptr, $pDirs_ptr, $systemDomainName) = @_;
  my $clientName = $customerInfo_ptr->{'kunde'};
  my @cronJobs = Dumper::getCronJobs($clientName);

  my $sysUserNode = CommonXmlNodes::sysuser($clientName, $customerInfo_ptr->{'longpw'}, "/var/www/vhosts/". $systemDomainName, \@cronJobs);
  $sysUserNode->setAttribute('quota', $customerInfo_ptr->{'maxkb'} * 1024) if ($customerInfo_ptr->{'maxkb'} > 0);
  $sysUserNode->setAttribute('shell', $customerInfo_ptr->{'shell'}) if ($customerInfo_ptr->{'shell'});

  return
    XmlNode->new('preferences',
      'children' => [
        $sysUserNode,
        @{_makePDirNodes($pDirs_ptr)},
      ]
    );
}

sub _makeScriptingNode {
  my ($scriptingInfo_ptr) = @_;

  my $scriptingNode = XmlNode->new('scripting',
    'attributes' => {
      'ssi' => $scriptingInfo_ptr->{'ssi'} ? 'true' : 'false',
      'php' => $scriptingInfo_ptr->{'php'} ? 'true' : 'false',
      'cgi' => $scriptingInfo_ptr->{'perl'} ? 'true' : 'false',
      'perl' => $scriptingInfo_ptr->{'perl'} ? 'true' : 'false'
    }
  );

  return $scriptingNode;
}

sub _makeSslNode {
  my ($clientName) = @_;

  my @csslRecords = Dumper::getClientSSLRecords($clientName);
  unless (@csslRecords) {
    return undef;
  }
  my $csslInfo_ptr = $csslRecords[0];
  unless ($csslInfo_ptr->{'crt'} && $csslInfo_ptr->{'privatekey'}) {
    return undef;
  }

  my $certificatesNode = XmlNode->new('certificates',
    'children' => [
      XmlNode->new('certificate',
        'attributes' => {
          'default' => 'true'
        },
        'children' => [
          XmlNode->new('certificate-data', 'content' => $csslInfo_ptr->{'crt'}),
          XmlNode->new('private-key', 'content' => $csslInfo_ptr->{'privatekey'})
        ]
      )
    ]
  );

  return $certificatesNode;
}

sub _makePDirNodes {
  my ($pDirs_ptr) = @_;

  my @result;
  while (my ($path, $properties) = each %{$pDirs_ptr}) {
    # determine the area (cgi-bin, nonssl) and adjust path relatively to this area
    my $isCgiDir = 0;
    if ($path =~ /^\/cgi-bin$/) { # /cgi-bin itself
      $isCgiDir = 1;
      $path = '/';
    } elsif ($path =~ /^\/cgi-bin\//) { # directories living under /cgi-bin
      $isCgiDir = 1;
      $path =~ s/^\/cgi-bin//;
    }
    my $pDirNode = XmlNode->new('pdir',
      'attributes' => {
        'name' => $path,
        'nonssl' => $isCgiDir ? 'false' : 'true',
        'cgi' => $isCgiDir ? 'true' : 'false'
      }
    );
    $pDirNode->setAttribute('title', $properties->{'title'}) if $properties->{'title'};
    my %usersMap = %{$properties->{'users'}} if (ref($properties->{'users'}) eq 'HASH');
    while (my ($username, $password) = each %usersMap) {
      $pDirNode->addChild(XmlNode->new('pduser',
        'attributes' => {
          'name' => $username
        },
        'children' => [
          CommonXmlNodes::encryptedPassword($password)
        ]
      ));
    } # for each user
    push @result, $pDirNode;
  } # for each protected directory

  return \@result;
}

sub _makePinfoNodes {
  my ($pinfoHash) = @_;

  my @result = map { 
    XmlNode->new('pinfo', 
      'attributes' => {'name' => $_},
      'content' => $pinfoHash->{$_}
    ) 
  } keys %$pinfoHash;

  return @result;
}

sub _makeDatabaseListNode {
  my ($client, $dumpOptions) = @_;
  my $domainName = getClientSystemDomainName($client);
  my $dbType = 'mysql';
  my $adminDb = 'mysql';

  my $dbAdminLogin = ConfixxConfig::getValue('mysqlUserUser');
  my $dbAdminPassword = ConfixxConfig::getValue('mysqlUserPw');

  my @databases = Dumper::getClientDatabaseNames($client);
  unless (@databases) {
    return;
  }

  my %userDatabases = ();
  my $dbCon = Db::DbConnect::getDbConnect($dbType, $dbAdminLogin, $dbAdminPassword, $adminDb);
  foreach my $database ( @databases ) {
       # Skip 'root' user during DB users dump.
       my @users = Db::DbConnect::getDbUsers( $database, $dbCon, ['root'] );
       foreach my $user ( @users ) {
               $userDatabases{ $user } = [] if not exists $userDatabases{ $user };
               push @{ $userDatabases{ $user } }, $database;
       }
  }

  my $dbUsersNode = XmlNode->new( 'dbusers' );
  foreach my $user ( keys %userDatabases ) {
       if( scalar( @{ $userDatabases{ $user } } ) == scalar( @databases ) ) {
               Db::DbConnect::addDbUser($dbUsersNode, $user, $dbCon);
       }
  }
  $dbCon->{'DISCONNECT'}->();

  my @childDatabaseElements = map { _makeDatabaseNode($dbAdminLogin, $dbAdminPassword, $_, $domainName, $dumpOptions) } @databases;

  return XmlNode->new('databases',
      'children' => [
           @childDatabaseElements,
           $dbUsersNode,
      ]);
}

sub _makeDatabaseNode {
  my ($dbLogin, $dbPassword, $dbName, $domainName, $dumpOptions) = @_;
  my $dbType  = 'mysql';
  my $adminDb = 'mysql';

  my $domainInfo = Dumper::getDomainInfo($domainName);
  my $domainId = $domainInfo->{'id'};

  my $dbServerNode = Db::DbConnect::getDefaultDbServerNode();
  my $dbNode = XmlNode->new('database',
      'attributes' => {
            'name'     => $dbName,
            'type'     => $dbType,
            'version'  => Db::MysqlUtils::getVersion()
      },
      'children' => [
            DatabaseContentDumper::getDatabaseContent($contentDumperBase, $dbLogin, $dbPassword, $dbName, $dbType, $domainName, $domainId, $dumpOptions),
            $dbServerNode,
      ]
  );

  return $dbNode;
}

sub _makeDomainMaillistsNode {
  my ($clientName) = @_;
  my $clientInfo_ptr = Dumper::getClientInfo($clientName);

  return XmlNode->new('maillists',
    'children' => [
      XmlNode->new('properties', 
        'children' => [ CommonXmlNodes::status($clientInfo_ptr->{'maxmaillist'} != 0, 'admin') ]
      )
    ]
  );
}

sub _makeSiteMaillistsNode {
  my ($clientInfo_ptr, $domainName) = @_;

  my $maillistInfos_ptr = Dumper::getMaillists($domainName);
  unless (@{$maillistInfos_ptr}) {
    return undef;
  }

  return XmlNode->new('maillists',
    'children' => [
      XmlNode->new('properties', 
        'children' => [ CommonXmlNodes::status($clientInfo_ptr->{'maxmaillist'} != 0, 'admin') ]
      ),
      map { _makeMaillistNode($_) } @{$maillistInfos_ptr}
    ]
  );
}

sub _makeMaillistNode {
  my ($maillistInfo_ptr) = @_;

  my $passwordNode = CommonXmlNodes::password('plain', $maillistInfo_ptr->{'pwd'}) if $maillistInfo_ptr->{'pwd'};

  my $maillistNode = XmlNode->new('maillist',
    'attributes' => {
      'name' => $maillistInfo_ptr->{'name'}
    },
    'children' => [
      CommonXmlNodes::status(! $maillistInfo_ptr->{'gesperrt'}, 'admin'),
      XmlNode->new('owner', 'content' => $maillistInfo_ptr->{'owner_mail'}),
      $passwordNode,
      map { XmlNode->new('recipient', 'content' => $_) } @{$maillistInfo_ptr->{'recipients'}}
    ]
  );
  
  return $maillistNode;
}

sub getTraffic {
  my ($client) = @_;

  my @result;

  # <doc>
  # It is still not clear if 'pop', 'other', 'geloescht' and 'log' fields from 'transfer' table
  # are still used in Confixx and what's their meaning, so we do not migrate them.
  # </doc>
  my %mapping = (
    'web' => 'http',
    # <doc>
    # There is no way to recognize if mail traffic was POP3/IMAP traffic or SMTP traffic,
    # so we put all Confixx mail traffic into Plesk POP3/IMAP traffic.
    # </doc>
    'email' => 'pop3-imap',
    'ftp' => 'ftp'
  );

  for my $trafficItem (Dumper::getTraffic($client)) {
    my ($day, $month, $year, $type, $direction, $amount);
    
    ($day, $month, $year) = ($trafficItem->{'day'}, $trafficItem->{'month'}, $trafficItem->{'year'});

    # <doc>
    # Confixx sumarizes traffic that is older than 3 months, we move summarized records to the last day of the month.
    # </doc>
    if ($day == 0) {
      $day = DateUtils::lastMonthDay($year, $month);
    }

    for my $confixxTrafficType (keys %mapping) {
      my $pleskTrafficType = $mapping{$confixxTrafficType};

      if ($trafficItem->{$confixxTrafficType} > 0) {
        push @result, {
          'day' => $day,
          'month' => $month,
          'year' => $year,
          'type' => $pleskTrafficType,
          # <doc>
          # There is no way to recognize direction of traffic in Confixx, so we always set it to 'in'.
          # </doc>
          'direction' => 'in',
          # In Confixx traffic is stored in kilobytes, convert to bytes
          'amount' => $trafficItem->{$confixxTrafficType} * 1024
        };
      }
    }
  }

  return @result;
}

sub _makeDnsSettingsNode {
  my $adminInfo  = Dumper::getAdminInfo();
  my @rawDnsRecords = map { s/##(\w+)##/<$1>/g; $_; } split "\n", $adminInfo->{'dnstemplate'};

  # add a Plesk standard NS template; weed out Confixx NS templates ##ns1## and ##ns2##
  push @rawDnsRecords, '<domain>. 86400 IN NS ns.<domain>';
  my @dnsRecords = Dumper::parseDnsRecords(grep { !/<ns[\d+]>/ } @rawDnsRecords);

  my $domainName = $adminInfo->{'hostname'};
  my $email      = "hostmaster.$domainName";
  my $status     = 1; # enabled

  return XmlNode->new('dns-settings', 
    'attributes' => {
        'recursion' => 'localnets' # Plesk default setting
    },
    'children' => [
        _makeDnsZoneNode($domainName, $email, $status, \@dnsRecords)
    ]
  );
}

1;
