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

#
# Agent.pm module implements Agent interface
#

use strict;
use warnings;

use Logging;
use XmlNode;
use AgentConfig;
use Db::Connection;
use Db::MysqlUtils;
use Dumper;
use DomainDumper;
use MySQLServer;
use DumperUtils;
use Storage::Storage;
use EncodeBase64;
use SpamAssassinCfg;
use CommonXmlNodes;
use PreMigration;
use PreMigrationChecks;
use EnsimGuidGenerator;
use NameMapper;
use PleskLimits;

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

my $agentWorkDir;

my $do_gzip = undef;
sub getCompressionStatus {
  Logging::debug("Compression is '".( $do_gzip ? 'enabled':'disabled')."'.");
  return $do_gzip;
}

#
# Begin interface methods
#
sub setWorkDir {
  my $workDir = shift;
  $agentWorkDir = $workDir;
}

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

sub getAgentName {
  return "PPCPL";
}

sub getAgentVersion {
  return '10.4';
}

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

  return Dumper::getAgentStatus();
}

sub getAdminPreferences {
  return undef;
}

# 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 $resellerName = shift;

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

  my $generalInfo = Dumper::getResellerGeneralInfo($resellerName);

  my %limits = _getResellerLimits($resellerName);
  my %permissions = _getResellerPermissions($resellerName);

  my $outResellerName = Dumper::mangleResellerName($resellerName);

  if ($resellerName ne $outResellerName) {
    Logging::info("Reseller '" . $resellerName . "' was renamed to '" . $outResellerName . "'");
  }

  return XmlNode->new('reseller', (
    'attributes' => {
      'id' => $outResellerName,
      'name' => $outResellerName,
      'contact' => $generalInfo->{'fullname'}, 
      'guid' => EnsimGuidGenerator::getResellerGuid($resellerName),
    },
    'children' => [
      _makePreferencesNode($generalInfo->{'email'}),
      XmlNode->new('properties', (
        'children' => [
          CommonXmlNodes::encryptedPassword($generalInfo->{'password'}),
          CommonXmlNodes::status($generalInfo->{'enabled'} eq '1', 'admin')
        ]
      )),
      CommonXmlNodes::limitsAndPermissions(\%limits, \%permissions),
      CommonXmlNodes::ipPool(getResellerIPPool($resellerName))
    ]
  ));
}

# Should return an array of clients identifiers, that could be a number, name or guid.
# Should be unique through migration dump
sub getClients {
  # reseller that owns the clients returned.
  # Could be 'undef' for default reseller ('root' or 'admin')
  my $owner = shift;

  Logging::trace(
    "Getting clients list for '" . ( defined($owner) ? $owner : 'undef' ) . "' reseller ... " );

  return Dumper::getClientIdsForReseller($owner);
}

# @param clientName - consists of domain name alone
sub getClient {
  my ( $clientName, $dumpOptions ) = @_;

  # client identifier from 'getClients' result,
  Logging::trace("Getting client dump for '$clientName' client...");
  
  my $domain = $clientName;
  my $usersInfoMapPtr = DomainDumper::getSiteUsersInfoMap($domain);
  my $clientAdminLogin = DomainDumper::getSiteAdminLogin($domain);

  PreMigrationChecks::checkClient($domain);

  return XmlNode->new('client',
    'attributes' => {
      'id'   => DomainDumper::getSiteId($domain),
      'name' => $domain,
      'guid' => EnsimGuidGenerator::getClientGuid($domain),
      'vendor-guid' => DomainDumper::getVendorGuid($domain),
    },
    'children' => [ 
      _makePreferencesNode(
        DomainDumper::getSiteContactEmail($domain)
      ),
      XmlNode->new('properties',
        'children' => [
          CommonXmlNodes::encryptedPassword(
            $usersInfoMapPtr->{$clientAdminLogin}{'password'}
          ),
          CommonXmlNodes::status(DomainDumper::isEnabled($clientName), 'admin')
        ]
      ),
      CommonXmlNodes::ipPool(getClientIPPool($clientName)),
      XmlNode->new('domains', 
        'children' => [ 
          _getDomainNode($domain, $dumpOptions)
        ]
      )
    ]
  );
}

sub getDomains {
  my ($entityId, $entityType) = @_;
  if (defined($entityType) and ($entityType eq 'client')) {
    return ($entityId);
  }
  return ();
}

sub getPreMigrationDomains {
  my ($entityId, $entityType) = @_;
  if (defined($entityType) and ($entityType eq 'client')) {
    return ($entityId);
  }
  return ();
}

sub getServerNode {
  my @children;
  my $maxSpamassassinThreads = Dumper::getSpamassassinMaxThreads();
  if (defined($maxSpamassassinThreads)) {
    push @children,
      XmlNode->new('spamassassin',
        'attributes' => {
          'max-spam-threads' => $maxSpamassassinThreads
        }
      );
  }

  if (@children) {
    return XmlNode->new('server', 'children' => \@children);
  } else {
    return undef;
  }
}

sub getResellerIPPool {
  my ($resellerName) = @_;

  my @clientNames = getClients($resellerName);
  return [ map { @{getClientIPPool($_)} } @clientNames ];
}

# @param clientId - consists of domain name alone
sub getClientIPPool {
  my ($domainName) = @_;

  return [ _getSiteIPInfo($domainName) ];
}

sub getSiteDiskUsage {
  my $domainName = shift @_;
  my $adminUsername = DomainDumper::getAdminSystemLogin($domainName);
  my @quotaReport = _getQuotaReport('/home/', $adminUsername);
  return @quotaReport > 1 ? $quotaReport[1] : undef;
}

#
# End of interface methods
#

sub _getDomainCapabilitiesNode($) {
  my $clientId = shift;
  return () if not defined($clientId);
  my $capabilities = _getDomainCapabilities($clientId);

  my %capabilityTypeChilds = (
    'resource-usage' => 'resource',
    'components' => 'component',
  );

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

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

  my $capabilities = {};

  my $resources = {};

  my $quotaUsed = _getDomainDiskQuotaUsed($clientId);
  if (defined($quotaUsed)) {
    $resources->{'diskusage.vhost'} = $quotaUsed;
  }

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

  my $webstat = _getDomainStatisticEngineForTransfer($clientId); 
  if ($webstat ne 'none') {
    $capabilities->{'components'} = {};
    $capabilities->{'components'}->{$webstat} = 'true';
  }
  return $capabilities;  
}

sub _getDomainDiskQuotaUsed {
  my $clientId = shift;

  my @sitelookupCommand = ('/usr/local/bin/sitelookup', '-d', $clientId, 'wp_user,site_root');
  Logging::debug("Site lookup command: " . join(',', @sitelookupCommand));
  open FH, '-|', @sitelookupCommand;
  my $line = <FH>;
  close FH;

  if (defined($line)) {
    chomp $line;
    my ($unixUser, $siteRoot) = split(',', $line);
    my @quotaLines = _getQuotaReport($siteRoot, $unixUser);
    my $currentQuota = $quotaLines[1];

    if (defined($currentQuota)) {
      return $currentQuota * 1024;
    }
  }
  return undef;
}

sub _getQuotaReport {
    my ($directory, $unixUser) = @_;
    my (undef, undef, undef, $unixGroupId) = getpwnam($unixUser);
    my @quotareportCommand = ('/usr/local/bin/quota_report', '-d', $directory, '-g', $unixGroupId);
    Logging::debug("Site quota report command: " . join(',', @quotareportCommand));
    open FH, '-|', @quotareportCommand;
    my @quotaLines = <FH>;
    close FH;
    return @quotaLines;
}

sub getDomainLimitsAndPermissionsNode {
  my ($domainName) = @_;

  my %limits = _getDomainLimits($domainName);
  my %permissions = _getDomainPermissions($domainName);

  return CommonXmlNodes::limitsAndPermissions(\%limits, \%permissions);
}

sub _getDomainNode {
  my ($domainName, $dumpOptions) = @_;

  Logging::trace("Getting dump of '$domainName' domain");

  my $domainNode = XmlNode->new('domain');
  $domainNode->setAttribute( 'name', $domainName );
  $domainNode->setAttribute( 'guid', EnsimGuidGenerator::getDomainGuid($domainName) );
  $domainNode->setAttribute( 'vendor-guid', DomainDumper::getVendorGuid($domainName) );

  my @domainElementsList = (
    [DOMAIN_HOSTING_CONTENT, \&_getDomainPreferencesNode],
    [DOMAIN_HOSTING_CONTENT, \&_getDomainPropertiesNode],
    [DOMAIN_HOSTING_CONTENT, \&getDomainLimitsAndPermissionsNode],
    [DOMAIN_MAIL_CONTENT,    \&_getMailSystemNode],
    [DOMAIN_HOSTING_CONTENT, \&_getDatabasesNode],
    [DOMAIN_MAIL_CONTENT,    \&_getMaillistsNode],
    [DOMAIN_HOSTING_CONTENT, \&_getSslCertificatesNode],
    [DOMAIN_HOSTING_CONTENT, \&_getDomainPHostingNode],
  );

  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->($domainName));
    }
  }

  return $domainNode;
}

sub _getDomainPreferencesNode {
  my ($domainName) = @_;
  my $preferencesNode = XmlNode->new('preferences');
  my @domainAliases = DomainDumper::getDomainAliases($domainName);
  foreach my $alias (@domainAliases) {
    my $domainAliasNode =  XmlNode->new('domain-alias', 
      'attributes' => { 
        'name' => $alias,
        # see "Customer's Guide, PPP 11.5", "Adding Domain Aliases" for explanation
        'mail' => 'true',
        'web'  => 'true',
        'dns'  => 'false',
      },
    );
    $domainAliasNode->addChild(CommonXmlNodes::status('enabled'));
    $domainAliasNode->addChild(_makeDnsZoneNode($domainName, $alias));
    $preferencesNode->addChild($domainAliasNode);
  }

  return $preferencesNode;
}

sub _getIpNodes {
  my ($domainName) = @_;
  my @result = ();

  my $ipInfo = _getSiteIPInfo($domainName);
  if (defined($ipInfo)) {
    push @result, CommonXmlNodes::ip($ipInfo->{'ip'}, $ipInfo->{'type'});
  }
  return @result; 
}

sub _getDomainPropertiesNode {
  my ($domainName) = @_;

  Logging::trace("Getting properties for '$domainName' domain ... ");

  my $domainPropertiesNode = XmlNode->new('properties');

  my $isEnabled = DomainDumper::isEnabled($domainName);
  $domainPropertiesNode->addChildren(_getIpNodes($domainName));
  $domainPropertiesNode->addChild(CommonXmlNodes::status($isEnabled, 'admin'));
  $domainPropertiesNode->addChild(_makeDnsZoneNode($domainName));

  return $domainPropertiesNode;
}

sub _getDomainStatisticEngineForTransfer {
  my ($domainName) = @_;

  my $webstat;
  my @statsEngines =  grep { $_ ne 'analog' } DomainDumper::getEnabledWebstatEngines($domainName);
  if (scalar(@statsEngines) == 1) {
    $webstat = $statsEngines[0];
  } elsif (scalar(@statsEngines) == 0) {
    $webstat = 'none';
  } else {
    # WA for PPP-2994, as advised by PMM team
    $webstat = 'webalizer';
  } 
  return $webstat;
}

sub _getDomainPHostingNode {
  my ($domainName) = @_;
  my $usersInfoMapPtr = DomainDumper::getSiteUsersInfoMap($domainName);

  Logging::trace("Getting phosting for domain '$domainName' ... ");
  my $webstat = _getDomainStatisticEngineForTransfer($domainName);

  my $pHostingNode = XmlNode->new('phosting',
    'attributes' => {
      'wu_script' =>      'true',
      'https' =>          DomainDumper::isNameBased($domainName) ? 'false' : 'true',
      'fp' =>             DomainDumper::isFrontpageEnabled($domainName) ? 'true' : 'false',
      'fpssl' =>          DomainDumper::isFrontpageEnabled($domainName) ? 'true' : 'false',
      'fpauth' =>         DomainDumper::isFrontpageEnabled($domainName) ? 'true' : 'false',
      'webstat' =>        $webstat,
      'errdocs' =>        'false',
      'shared-content' => 'true',
      'www-root' =>	  'httpdocs'
    }
  );

  $pHostingNode->addChild(_makeHostingPreferencesNode($domainName, $usersInfoMapPtr));
  $pHostingNode->addChild(XmlNode->new('properties', children => [
    _getIpNodes($domainName)]
  ));


  $pHostingNode->addChild(_pHostingScriptingNode($domainName));
  my $webuserNodes = _getWebUsers($domainName);
  if (@$webuserNodes) {
    $pHostingNode->addChild(
      XmlNode->new('webusers',
        'children' => $webuserNodes 
      )
    );
  }

  my ($sitesArray, $ftpusersArray) = _makeSitesAndFtpusersNodes($domainName,
    $pHostingNode, $usersInfoMapPtr);

  if (defined($ftpusersArray) and @{$ftpusersArray}) {
    $pHostingNode->addChild(XmlNode->new('ftpusers',
        'children' => [ @{$ftpusersArray} ]
    ));
  }

  if (defined($sitesArray) and @{$sitesArray}) {
    $pHostingNode->addChild(XmlNode->new('sites',
        'children' => [ @{$sitesArray} ]
    ));
  }

  return $pHostingNode;
}

sub getFtpUsernameTemplate {
  return join('_', @_);
}

sub getFtpUsernameNamespace {
  return join(':', @_);
}

sub _makeSitesAndFtpusersNodes {
  my ($domainName, $pHostingNode, $usersInfoMapPtr) = @_;

  my $nameMapping = NameMapper->instance();

  my $listOfSubdomains = DomainDumper::getListOfSubdomains($domainName) || return undef;
  my $siteAdminLogin = DomainDumper::getAdminLogin($domainName);
  my %usedUsernames = map { $_ => 1 } keys %{$listOfSubdomains};
  my (@sites, @ftpusers);

  while (my ($subdomainName, $subdomainInfo) = each %{$listOfSubdomains}) {
    my $owner = $subdomainInfo->{owner};
    my $subdomainDir = "subdomains_wwwroot/$subdomainName";

    $owner = $siteAdminLogin if (!defined($owner) || $owner eq "");

    if ($owner ne $siteAdminLogin) {
      my $namespace = getFtpUsernameNamespace($domainName, $subdomainName);
      my $usernameTemplate = getFtpUsernameTemplate($owner, $subdomainName);
      my $ftpusername = $nameMapping->getMappedName($namespace, $usernameTemplate);

        push @ftpusers, XmlNode->new('ftpuser',
            'attributes' => {
                'name' => $ftpusername
            },
            'children' => [
                CommonXmlNodes::sysuser($ftpusername, $usersInfoMapPtr->{$owner}{'password'}, "/$subdomainDir"),
            ]
        );
    }

	my $fullSubdomainName = "$subdomainName.$domainName";
    push @sites, XmlNode->new('site', (
        'attributes' => {
                'name'             => $fullSubdomainName,
				'parent-domain-name' => $domainName,
                'id'               => '',
				'guid' => EnsimGuidGenerator::getDomainGuid($fullSubdomainName)
        },
        'children' => [
				XmlNode->new('preferences'),
				XmlNode->new('properties', 'children' => [CommonXmlNodes::status(1, undef)]),
				XmlNode->new('phosting', 
					'attributes' => {
						'https'  =>  DomainDumper::isNameBased($domainName) ? 'false' : 'true', # get value from parent domain
						'shared-content'   => "true",
						'www-root'         => "$subdomainDir",
						####
						## PPCPL does not support frontpage for subdomains
						'fp'               => "false",
						'fpssl'            => "false",
						'fpauth'           => "false"
					},
					'children' => [
						XmlNode->new('preferences'),
						XmlNode->new('limits-and-permissions', 
							'children' => [
								_makeSubdomainScriptingNode($subdomainInfo, $domainName)
							]
						)
				]),
        ]
    ));
  }

  return (\@sites, \@ftpusers);
}

sub _getSslCertificatesNode {
  my ($domainName) = @_;

  return unless DomainDumper::isSslEnabled($domainName);

  my $httpdConfDir = DomainDumper::getFilesystemRoot($domainName) . "/etc/httpd/conf";
  my @certInfoMap = (
    { 'name' => 'certificate-data', 'file' => $httpdConfDir . "/ssl.crt/server.crt"},
    { 'name' => 'signing-request', 'file' => $httpdConfDir . "/ssl.csr/server.csr"},
    { 'name' => 'private-key', 'file' => $httpdConfDir . "/ssl.key/server.key"}
  );
  my @certFileNodes;
  for my $certInfo (@certInfoMap) {
    my $certificate = DumperUtils::readFile($certInfo->{'file'});
    next unless (defined $certificate);
    push @certFileNodes, XmlNode->new($certInfo->{'name'}, 'content' => $certificate);
  }
  unless (@certFileNodes) {
    return undef;
  }

  return XmlNode->new('certificates',
    'children' => [
      XmlNode->new('certificate',
        'attributes' => {
          'name' => $domainName
        },
        'children' => \@certFileNodes
      )
    ]
  );
}

sub _getWebUsers {
  my ($domainName) = @_;
  my $nameMapping = NameMapper->instance();

  my @result;
  my $webUsers = DomainDumper::getWebUsers($domainName);
  while (my ($username, $password) = each (%$webUsers)) {
	my $webuserDir = DomainDumper::getFilesystemRoot($domainName) . "/home/$username/public_html";
    # if a user has no web content, it is not a web user, skip it
	next unless (-d $webuserDir);

    my $uniqueUserName = $nameMapping->getMappedName($domainName, $username);
    if ($uniqueUserName ne $username) {
      Logging::info("Web user '$username' was renamed to '$uniqueUserName'");
    }
    my $webUserNode = XmlNode->new('webuser',
      'attributes' => {
        'name' => $uniqueUserName
      },
      'children' => [
        CommonXmlNodes::sysuser($uniqueUserName, $password, "/web_users/$uniqueUserName"),
        _makeWebUserScriptingNode($domainName)
      ]
    );
    push @result, $webUserNode;
  }

  return \@result;
}

sub _getDatabaseUserNode {
  my ($userName) = @_;

  Logging::debug("Dumping mysql user '$userName'");

  my $outUserName = Dumper::mangleDbUserName($userName);

  if ($outUserName ne $userName) {
    Logging::info("DB user '" . $userName . "' was renamed to '" . $outUserName . "'");
  }

  my ($password, $accessHostsList) = MySQLServer::getDatabaseUserData($userName);

  my $node = XmlNode->new('dbuser', 'attributes' => {'name' => $outUserName});
  $node->addChild(CommonXmlNodes::encryptedPassword($password)); 
  foreach my $accessHost (@{$accessHostsList}) {
    $node->addChild(XmlNode->new('accesshost', 'content' => $accessHost));
  }
  $node->addChild(CommonXmlNodes::defaultDbServerNode());
  
  return $node; 
}

sub _getDatabasesNode {
  my ($domainName) = @_;
  my $dbType = 'mysql';
  my $adminDb = 'mysql';

  Logging::trace("Getting databases for domain '$domainName' ...");

  my @databases = DomainDumper::getDatabaseNames($domainName);
  if (!@databases) {
    return;
  }

  # PPCPL has one user to rule them all
  my $dbUsersNode = undef;
  my @users = MySQLServer::getDatabaseUsers( $databases[0], ['root'] );
  if (@users) {
    $dbUsersNode = XmlNode->new( 'dbusers' );
    $dbUsersNode->addChild(_getDatabaseUserNode($users[0]));
  }

  my @childDatabaseElements = map { _getDatabaseNode($_, $domainName) } @databases;

  if(defined($dbUsersNode)) {
    push @childDatabaseElements, $dbUsersNode;
  }
  my $databasesNode = XmlNode->new('databases', 'children' => [
    @childDatabaseElements,
  ]);

  return $databasesNode;
}

sub _getDatabaseNode {
  my ($dbName, $domainName) = @_;

  my ($dbAdminLogin, $dbAdminPassword) = Dumper::getDbAdminCredentials();
  my $dbType = 'mysql';

  my $dbNode = XmlNode->new('database');
  $dbNode->setAttribute('name', $dbName);
  $dbNode->setAttribute('type', $dbType);
  $dbNode->setAttribute('version', Db::MysqlUtils::getVersion());

  $dbNode->addChild(CommonXmlNodes::defaultDbServerNode());

  return $dbNode;
}

sub _getSiteIPInfo {
  my ($domainName) = @_;
  my %ipinfo = DomainDumper::getIpInfo($domainName);

  unless ( $ipinfo{'enabled'} ) {
    # It means that ip information is not available (maybe configuration file is absent)
    return;
  }

  if ($ipinfo{'namebased'} eq '1') {
    $ipinfo{'nbaddrs'} =~ /\[\'(.+)\'\]/;
    return {'ip' => $1, 'type' => 'shared'};
  } else {
    $ipinfo{'ipaddrs'} =~ /\[\'(.+)\'\]/;
    return {'ip' => $1, 'type' => 'exclusive'};
  }
}

# various lc() calls are here because of case-sensitive mail implementation in PPCPL
# please pay special attention here
sub _getMailSystemNode {
  my ($domainName) = @_;

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

  return unless DomainDumper::isSendmailEnabled($domainName);

  my $aliases = DomainDumper::getMailAliases(
    $domainName, 
    # maillist aliases are removed as some of them are recreated by Plesk when creating maillists
    DomainDumper::getMaillistsAliases($domainName)
  );

  my %mailUsers = ();

  my $siteUsersInfo = DomainDumper::getSiteUsersInfoMap($domainName);
  # create mailboxes first
  while (my ($userName, $userInfo) = each %{$siteUsersInfo}) {
    my $meta = {
      'password' => $userInfo->{'password'},
      'mailbox' => 'true',
      'original_name' => $userName,
      'mailbox_enabled' => 'true',
      '_userInfo' => $userInfo,
    };

    # check if mail forwarding is enabled for this mailuser
    my @forwarding = DomainDumper::getMailboxForwarding($domainName, $userName);
    if (@forwarding) {
      $meta->{'forwardlist'} = \@forwarding;
    }

    $mailUsers{lc($userName)} = $meta;
  }

  my $catchAll = 'reject';

  while (my ($aliasName, $aliasInfo) = each %{$aliases}) {
    # if it's a catch-all, raise a flag
    if ($aliasName eq 'catch-all') {
      $catchAll = $aliasName . '@' . $domainName;
    }
    # for each alias create a mailuser without mailbox
    # (if it's not already a mailbox)
    my $meta = undef;
    if (exists($mailUsers{lc($aliasName)})) {
      $meta = $mailUsers{lc($aliasName)};
      # usually folks in PPCPL create mixed-case aliases for lc-only 
      # mailbox names. We'll keep the beauty here
      $meta->{'original_name'} = $aliasName;
    } else {
      $meta = {
        'mailbox' => 'false',
        'original_name' => $aliasName,
      };
      $mailUsers{lc($aliasName)} = $meta;
    }

    my @forwardList = ();
    foreach my $localAddress (@{$aliasInfo->{'localDelivery'}}) {
      if (lc($localAddress) ne lc($aliasName)) { # alias to myself means this recipient has a mailbox
        push @forwardList, $localAddress . '@' . $domainName;
      }
    }
    foreach my $remoteAddress (@{$aliasInfo->{'redirects'}}) {
      push @forwardList, $remoteAddress;
    }

    if (scalar(@forwardList)) {
      $meta->{'forwardlist'} = [] if not exists($meta->{'forwardlist'});
      push @{$meta->{'forwardlist'}}, @forwardList;
    }
    if (scalar(@{$aliasInfo->{'responders'}})) {
      $meta->{'responders'} = $aliasInfo->{'responders'};
    }
  }

  my $mailSystemNode = XmlNode->new('mailsystem', 'children' => [
    # TODO what status should mailsystem have ? maybe domain's one ?
    XmlNode->new('properties', 'children' => [CommonXmlNodes::status(1, 'admin')])
  ]);

  my $mailUsersNode = XmlNode->new('mailusers');

  while (my ($mailboxName, $mailboxMeta) = each %mailUsers) {
    $mailUsersNode->addChild(_getMailUserNode($domainName, $mailboxName, $mailboxMeta, $mailboxMeta->{'_userInfo'}));
  }

  $mailSystemNode->addChild($mailUsersNode);

  $mailSystemNode->addChild(XmlNode->new('preferences', 'children' => [
    XmlNode->new('catch-all', 'content' => $catchAll),
    XmlNode->new('web-mail', 'content' => 'horde'),
    XmlNode->new('grey-listing', 'content' => 'on')
  ]));

  return $mailSystemNode;
}

sub _getAutoresponderNode {
  my ($subject, $text) = @_;
  
  return XmlNode->new(
    'autoresponder',
    'children' => [
      XmlNode->new('text', 'content' => EncodeBase64::encode($text)),
    ],
    'attributes' => {
      'subject' => EncodeBase64::encode($subject)
    }
  );
}

sub _splitHash {
  my ($hash, $conditionFunction) = @_;
  my (%hashTrue, %hashFalse);

  while (my ($key, $value) = each %{$hash}) {
    if ($conditionFunction->($value)) {
      $hashTrue{$key} = $value;
    } else {
      $hashFalse{$key} = $value;
    }
  } 

  return (\%hashTrue, \%hashFalse);
}

sub _getMailUserNode {
  my ($domainName, $userName, $meta, $userInfo) = @_;

  my $mailuserAttributes = {
    'name' => exists($meta->{'original_name'}) ? $meta->{'original_name'} : $userName, # only case difference
  };
  my $mailuserChildren = [];
  my $mailuserPreferences = [];

  if (exists($meta->{'password'})) {
    push @{$mailuserChildren}, _getMailuserPropertiesNode($meta->{'password'});
  } else {
    push @{$mailuserChildren}, _getMailuserPropertiesNode();
  }

  # mailbox
  if (exists($meta->{'mailbox'}) and $meta->{'mailbox'} eq 'true' ) {
    my $mailboxEnabled = $meta->{'mailbox_enabled'} || 'true';
    my $mailboxNode = XmlNode->new('mailbox',
      'attributes' => {'type' => 'mdir', 'enabled' => $mailboxEnabled},
    );
    push @{$mailuserPreferences}, $mailboxNode;
    $mailuserAttributes->{'mailbox-quota'} = _getMboxQuota($domainName, $userName);
  }

  # forwarding
  $mailuserAttributes->{'forwarding-enabled'} = 'false'; # default, since it's a required field
  if (exists($meta->{'forwardlist'})) {
    # filter forwardlist
    my @forwardList = ();
    {
      # kill unneeded duplicates, case-insensitive..
      my %seen = map { lc($_) => $_ } @{$meta->{'forwardlist'}};
      # ...and reference for myself, if any
      my $fullAddress = lc($userName . '@' . $domainName);
      if (exists($seen{$fullAddress})) {
        delete $seen{$fullAddress};
      }

      @forwardList = values %seen;
    }

    if (scalar(@forwardList)) {
      $mailuserAttributes->{'forwarding-enabled'} = 'true';
      my @forwardingNodes = map { XmlNode->new('forwarding', 'content' => $_) } @forwardList;	
      push @{$mailuserPreferences}, @forwardingNodes;
    }
  }

  # autoresponders
  if (exists($meta->{'responders'})) {
    my @autoresponderNodes = map { _getAutoresponderNode($_->{'subject'}, $_->{'body'}) } @{$meta->{'responders'}};
    my $autorespondersNode = XmlNode->new(
      'autoresponders',
      'children' => \@autoresponderNodes,
    );
    push @{$mailuserPreferences}, $autorespondersNode;
  }

  # spamassassin
  my $domainSiteDir = DomainDumper::getFilesystemRoot($domainName);
  my $spamassassinNode = _makeSpamassassinNode("$domainSiteDir/home/$userName/.spamassassin/user_prefs", "$domainSiteDir/home/$userName");
  if (!defined($spamassassinNode)) {
      # use default site/domain settings
      $spamassassinNode = _makeSpamassassinNode("$domainSiteDir/etc/mail/spamassassin/local.cf", $domainSiteDir);
  }
  push @{$mailuserPreferences}, $spamassassinNode;

  push @{$mailuserChildren}, new XmlNode('preferences', 'children' => $mailuserPreferences);

  my $mailUserNode = XmlNode->new('mailuser', 'attributes' => $mailuserAttributes, 'children' => $mailuserChildren );
  return $mailUserNode;
}

use constant PP_MAILQUOTA_UNLIMITED => -1;
# Ensim has no separate mail quota. We use total disk quota as an upper bound value for mail quota.
sub _getMboxQuota {
  my $quota = _getDiskQuota(@_);
  return $quota ? $quota*1024 : PP_MAILQUOTA_UNLIMITED; # 'quota_report' unit is 'MB'; in PPCPL, quota == '0' means 'unlimited quota'
}

sub _getDiskQuota {
    my ($domainName, $domainUserName) = @_;
    my $domainSiteDir = DomainDumper::getFilesystemRoot($domainName);
    my $foundUid;
    if (open PASSWD, "<", "$domainSiteDir/etc/passwd") {
      while (<PASSWD>) {
        my ($username, $fullname, $uid) = split ':';
        if ($username eq $domainUserName) {
          $foundUid = $uid;
          last;
        }
      }
      close PASSWD;
    } 
    unless ($foundUid) {
        Logging::error("Cannot find '$domainUserName''s user ID in '$domainSiteDir/etc/passwd'");
        return undef;
    }
    open FH, '-|', "/usr/local/bin/quota_report -d /home -u $foundUid";
    my @values = <FH>;
    close FH;
    my $quotaLineNumber = 3;  # '/usr/local/bin/quota_report' line 3 contains 'absolute limit on disk space (in blocks)'
    if (@values > $quotaLineNumber) {
      chomp(my $quota = $values[$quotaLineNumber]);
      return $quota;
    } else {
      Logging::error("Unable to read '$domainUserName''s disk quota report.");
      return undef;
    }
}

sub _getMailuserPropertiesNode {
  my ($userPassword) =  @_;
  return XmlNode->new('properties',
    'children' => [ 
      defined($userPassword) ? CommonXmlNodes::encryptedPassword($userPassword) : CommonXmlNodes::emptyPassword()
    ]
  );
}

sub _getMaillistsNode {
  my ($domainName) = @_;

  my $maillists = DomainDumper::getMaillists($domainName);

  my $maillistsNode = XmlNode->new('maillists',
    'children' => [
      XmlNode->new('properties',
        'children' => [ CommonXmlNodes::status(keys %{$maillists} > 0, 'admin') ]
      )
    ]
  );

  while (my ($maillistName, $maillistInfo) = each %{$maillists}) {
    $maillistsNode->addChild(_getMaillistNode($maillistName, $maillistInfo));
  }

  return $maillistsNode;
}

sub _getMaillistNode {
  my ($maillistName, $maillistInfo) = @_;

  return XmlNode->new('maillist',
    'attributes' => {
      'name' => $maillistName
    },
    'children' => [
      CommonXmlNodes::status(1, 'admin'),
      XmlNode->new('owner', 'content' => $maillistInfo->{'owner'}),
      _makePlainPasswordNode($maillistInfo->{'adminPassword'}),
      map { XmlNode->new('recipient', 'content' => $_); } @{$maillistInfo->{'members'}}
    ]
  );
}

sub _makeSpamassassinNode {
  my ($configFile, $configHomeDir) = @_;
  use constant DEFAULT_SPAMASSASSIN_REQUIRE_SCORE => 5;

  unless (-r "$configFile") {
      return undef;
  }

  my $spamassassinConfig = SpamAssassinCfg::parseConfig($configFile, $configHomeDir);
  return undef unless defined($spamassassinConfig);

  my $spamassassinConfigRequireScore = SpamAssassinCfg::getConfigRequireScore($spamassassinConfig);
  my $spamassassinConfigRewriteHeaderArray = SpamAssassinCfg::getConfigRewriteHeadr($spamassassinConfig);
  my $spamassassinConfigRewriteHeaderText;
  my $spamassassinConfigAction = Dumper::getProcmailrcAction("$configHomeDir/.procmailrc");

  if (defined($spamassassinConfigRewriteHeaderArray) && @{$spamassassinConfigRewriteHeaderArray} == 2) {
    my ($argumentRewrite, $textRewrite) = @{$spamassassinConfigRewriteHeaderArray};

    $spamassassinConfigRewriteHeaderText = $textRewrite if ($argumentRewrite =~ m/subject/i);
  }

  if (!defined($spamassassinConfigRewriteHeaderText) && $spamassassinConfigAction eq "mark") {
      return undef;
  }
  $spamassassinConfigRequireScore = DEFAULT_SPAMASSASSIN_REQUIRE_SCORE unless defined($spamassassinConfigRequireScore);

  my ($spamassassinNode) = XmlNode->new('spamassassin',
    'attributes' => {
            'status'    => 'on',
            'hits'      => $spamassassinConfigRequireScore,
            'action'    => $spamassassinConfigAction,
    },
    'children'   => [
        _makeSpamassassinListNodes('blacklist-member', SpamAssassinCfg::getConfigBlackList($spamassassinConfig)),
        _makeSpamassassinListNodes('whitelist-member', SpamAssassinCfg::getConfigWhiteList($spamassassinConfig)),
        _makeSpamassassinListNodes('unblacklist-member', SpamAssassinCfg::getConfigUnBlackList($spamassassinConfig)),
        _makeSpamassassinListNodes('unwhitelist-member', SpamAssassinCfg::getConfigUnWhiteList($spamassassinConfig))
    ]
  );
  if (defined($spamassassinConfigRewriteHeaderText)) {
      $spamassassinNode->setAttribute('subj-text', $spamassassinConfigRewriteHeaderText);
  }

  return $spamassassinNode;
}

sub _makeSpamassassinListNodes {
    my ($nodeName, $contentArrayPtr) = @_;

    return undef unless defined($contentArrayPtr);

    return map { XmlNode->new($nodeName, 'content' => $_) } @{$contentArrayPtr};
}

use constant RESOURCE_UNLIMITED => -1;

sub _getResellerLimits {
  my ( $resellerId ) = @_;

  my %limits = Dumper::getResellerLimits($resellerId);

  my %convertedLimits = _convertLimitsHash( 
    \%limits,
    { 
      'disk_space' => 'diskquota',
      # maximum number of users is mapped to maximum number of mailboxes
      # as there is no exact mapping 
      # and every user in PPCPL has exactly one mailbox
      'max_box' => 'users',
      'max_traffic' => 'bandwidth'
    }
  );

  if(exists($convertedLimits{'max_box'}) and defined($convertedLimits{'max_box'}) and $convertedLimits{'max_box'} != -1){
    $convertedLimits{'max_box'} += 1; # admin user is not included in limit
  }

  if (defined($limits{'ip_based_sites'}) && defined($limits{'name_based_sites'})) {
    $convertedLimits{'max_cl'} = $limits{'ip_based_sites'} + $limits{'name_based_sites'};
  } else {
    $convertedLimits{'max_cl'} = RESOURCE_UNLIMITED;
  }

  return %convertedLimits;
}

sub _getResellerPermissions {
  my ( $resellerId ) = @_;

  return (
    'manage_dns' => Dumper::canResellerManageDNS($resellerId) eq '1',
    'create_clients' => '1', # PPCPL reseller is able to create client
    'create_domains' => '1'  # with domain
  );
}

sub _getDomainLimits {
  my ( $domainName ) = @_;

  my %limits = Dumper::getSiteLimits($domainName);
  
  my %convertedLimits = _convertLimitsHash( 
    \%limits,
    { 
      'disk_space' => 'diskquota',
      'max_box' => 'users',
      'max_traffic' => 'bandwidth'
    }
  );

  if(exists($convertedLimits{'max_box'}) and defined($convertedLimits{'max_box'}) and $convertedLimits{'max_box'} != -1){
    $convertedLimits{'max_box'} += 1; # admin user is not included in limit
  }

  $convertedLimits{'max_site'} = '1';

  return %convertedLimits;
}

sub _convertLimitsHash {
  my ($srcHash, $map) = @_;

  my %result;

  while (my ($dstKey, $srcKey) = each %{$map}) {
    if (defined($srcHash->{$srcKey})) {
      $result{$dstKey} = $srcHash->{$srcKey}
    } else {
      $result{$dstKey} = RESOURCE_UNLIMITED; 
    }
  }

  return %result;
}

sub _getDomainPermissions {
  my ( $domainName ) = @_;
  return (
    'manage_dns' => DomainDumper::hasManageDnsPermission($domainName) eq '1'
  );
}

sub _makePlainPasswordNode {
  my ($password) = @_;

  return XmlNode->new('password',
    'attributes' => {'type' => 'plain'},
    'content' => $password
  );
}

sub _makeHostingPreferencesNode {
  my ($domainName, $usersInfoMapPtr) = @_;
  my $nameMapping = NameMapper->instance();

  my $siteAdminLogin = DomainDumper::getAdminLogin($domainName);
  my $uniqueSiteAdminLogin = $nameMapping->getMappedName($domainName, $siteAdminLogin);

  if ($uniqueSiteAdminLogin ne $siteAdminLogin) {
    Logging::info("System user '$siteAdminLogin' was renamed to '$uniqueSiteAdminLogin'");
  }

  return
    XmlNode->new('preferences', 
      'children' => [
        CommonXmlNodes::sysuser($uniqueSiteAdminLogin, $usersInfoMapPtr->{$siteAdminLogin}{'password'}, "/var/www/vhosts/$domainName"),
        _makeLogrotationNode($domainName, $siteAdminLogin),
        _makeAnonFtpNode($domainName),
        @{_makePDirNodes($domainName)},
      ]
    );
}

sub _makeLogrotationNode {
  my ($domainName, $siteAdminLogin) = @_;

  my %settings = DomainDumper::getLogrotateSettings($domainName) or return;

  my $rotationNode;
  if (defined $settings{'size'} && $settings{'size'} != 0) {
    $rotationNode = XmlNode->new('logrotation-maxsize',
      'content' => $settings{'size'}
    );
  } else {
    $rotationNode = XmlNode->new('logrotation-period',
      'attributes' => {
        'period' => $settings{'period'}
      }
    );
  }

  my $logrotateNode = XmlNode->new('logrotation',
    'attributes' => {
      'enabled' => 'true',
      'max-number-of-logfiles' => $settings{'number-of-rotated-files'},
      'compress' => 'true',
      'email' => "$siteAdminLogin". "@". "$domainName"
    },
    'children' => [
      $rotationNode
    ]
  );

  return $logrotateNode;
}

sub _makeAnonFtpNode {
  my ($domainName) = @_;

  # we need anonftp access enabled when it's enabled in PPCPL,
  # but since Plesk complains that it can't enable anonftp for name-based sites,
  # let's ask for anonftp access enabled only if site is ip-based.
  my $enableAnonftp = DomainDumper::isAnonymousFtpEnabled($domainName) && (!DomainDumper::isNameBased($domainName));

  if ($enableAnonftp) {
    return XmlNode->new('anonftp',
      'attributes' => {
        'pub' => $enableAnonftp ? 'true' : 'false',
        'incoming' => $enableAnonftp ? 'true' : 'false',
        'display-login' => 'false'
      },
    );
  } else {
    return undef;
  }
}

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

  my @result;
  my @pDirInfoList = DomainDumper::getProtectedDirectories($domainName);
  for my $pDirInfo_ptr ( @pDirInfoList ) {
    my $pDirNode = XmlNode->new('pdir',
      'attributes' => {
        'name' => $pDirInfo_ptr->{'name'},
        'nonssl' => $pDirInfo_ptr->{'cgi'} eq 'true' ? 'false' : 'true',
        'cgi' => $pDirInfo_ptr->{'cgi'}
      }
    );
    $pDirNode->setAttribute('title', $pDirInfo_ptr->{'title'}) if $pDirInfo_ptr->{'title'};
    my %usersMap = %{$pDirInfo_ptr->{'users'}};
    for my $username ( keys %usersMap ) {
      $pDirNode->addChild(XmlNode->new('pduser',
        'attributes' => {
          'name' => $username
        },
        'children' => [
          CommonXmlNodes::encryptedPassword($usersMap{$username})
        ]
      ));
    } # for each user
    push @result, $pDirNode;
  } # for each protected directory

  return \@result;
}

sub _makePreferencesNode {
  my ( $email ) = @_;

  return XmlNode->new('preferences', (
    'children' => [
      XmlNode->new('pinfo', (
        'attributes' => { 'name' => 'email' },
        'content' => $email 
      ))
    ]
  ));
}

sub _pHostingScriptingNode {
  my ($domainName) = @_;

  my $scriptingNode = _makeBasicScriptingNode($domainName);
  $scriptingNode->setAttribute('php_handler_type', 'isapi');
  $scriptingNode->setAttribute('php_safe_mode', 'false');
  $scriptingNode->setAttribute('cgi', DomainDumper::isCgiEnabled($domainName) ? 'true' : 'false');
  $scriptingNode->setAttribute('asp_dot_net', 'false');
  $scriptingNode->setAttribute('coldfusion', 'false');
  $scriptingNode->setAttribute('miva', DomainDumper::isMivaEnabled($domainName) ? 'true' : 'false');
  $scriptingNode->setAttribute('fp', DomainDumper::isFrontpageEnabled($domainName) ? 'true' : 'false'); # enable FrontPage
  $scriptingNode->setAttribute('fp_enable', DomainDumper::isFrontpageEnabled($domainName)  ? 'true' : 'false'); # enable FrontPage authoring
  my $phpMode = DomainDumper::getSitePhpMode($domainName);
  if (defined($phpMode)) {
    $scriptingNode->setAttribute('php_handler_type', $phpMode);
  }

  return XmlNode->new('limits-and-permissions', 'children' => [ $scriptingNode ]);
}

sub _makeWebUserScriptingNode {
  my ($domainName) = @_;

  my $result = _makeBasicScriptingNode($domainName);
  $result->setAttribute('cgi', DomainDumper::isCgiEnabled($domainName) ? 'true' : 'false');
  $result->setAttribute('asp_dot_net', 'false');
  $result->setAttribute('fp_enable', DomainDumper::isFrontpageEnabled($domainName) ? 'true' : 'false');

  return $result;
}

sub _makeSubdomainScriptingNode {
  my ($subdomainInfo, $domainName) = @_;

  my $result = _makeBasicScriptingNode($domainName);
  $result->setAttribute('miva', DomainDumper::isMivaEnabled($domainName) ? 'true' : 'false');
  $result->setAttribute('php_handler_type', 'isapi');
  $result->setAttribute('coldfusion', 'false');
  $result->setAttribute('cgi', (
            (defined($subdomainInfo->{cgi}) && "_$subdomainInfo->{cgi}" eq "_1") ? "true" : "false"));
  my $phpMode = DomainDumper::getSitePhpMode($domainName);
  if (defined($phpMode)) {
    $result->setAttribute('php_handler_type', $phpMode);
  }

  return $result;
}

sub _makeBasicScriptingNode {
  my ($domainName) = @_;

  return XmlNode->new('scripting',
    'attributes' => {
      'ssi'       => DomainDumper::isSsiEnabled($domainName) ? 'true' : 'false',
      'php'       => 'true',
      'perl'      => DomainDumper::isPerlEnabled($domainName) ? 'true' : 'false',
      'asp'       => 'false',
      'python'    => 'false',
      'fastcgi'   => 'true'
    }
  );
}

sub _makeDnsZoneNode {
  my ( $domainName, $alias ) = @_;

  my $dnsZoneDomainName = defined $alias ? $alias : $domainName;
    
  my $isDnsEnabled = DomainDumper::isDnsEnabled($domainName);
  my $isLocalNamedAvailable = Dumper::getLocalNamedStatus();
  if ($isDnsEnabled and $isLocalNamedAvailable) {
    return XmlNode->new('dns-zone', (
      'attributes' => {
          # nobody cares about 'email' field in Plesk (sp@PMM)
          'serial-format' => 'YYYYMMDDNN',
          'type'          => 'master',
      },
      'children' => [
          # translate PPCPL "Add to DNS" setting to Plesk 'enabled'/'disabled'
          CommonXmlNodes::status($isDnsEnabled, 'admin'),
          _makeDnsZoneProperties($dnsZoneDomainName),
      ],
    ));
  } else {
    return undef;
  }
}

use constant BIND_BINARY_ROOT => '/usr/lib/opcenter/bind';

sub getDnsZoneSoa {
  my ($domainName) = (@_);

  my $listSoaBinary = File::Spec->catfile(BIND_BINARY_ROOT, 'list_soa');
  if (open(SOA, "$listSoaBinary $domainName |")) {
    my %soa = ();
    while (my $line = <SOA>) {
      chomp($line);
      my ($name, $value) = split(' ', $line);
      $soa{$name} = $value;
    }
    close(SOA);
    return \%soa;
  } else {
    Logging::warning("Cannot get SOA for domain '$domainName': $!");
    return;
  }
}

sub getDnsZoneRecords {
  my ($domainName) = (@_);

  my $listDnsRecordsBinary = File::Spec->catfile(BIND_BINARY_ROOT, 'list_domain');
  if (open(DNSREC, "$listDnsRecordsBinary -all $domainName |")) {
    my @records = ();
    while (my $line = <DNSREC>) {
      chomp($line);
      my ($name, $type, @remain) = split(' ', $line);
      push @records, [$name, $type, @remain];
    }
    close(DNSREC);
    return @records;
  } else {
    Logging::warning("Cannot get resource records for domain '$domainName': $!");
    return;
  }
}

sub _makeDnsZoneProperties {
  my ( $domainName ) = @_;
  my $soa = getDnsZoneSoa($domainName);
  my @records = getDnsZoneRecords($domainName);

  if (scalar(@records) and $soa) {
    my @dnsZoneNodes = ();
    push @dnsZoneNodes, _makeDnsZoneParams(%$soa);
  
    for my $rec (@records) {
      my $src = shift @$rec;
      my $type = shift @$rec;
      push @dnsZoneNodes, CommonXmlNodes::dnsRecord($src, undef, undef, $type, @$rec);
    }
    return @dnsZoneNodes;
  } else {
    return;
  }
}

# parse SOA record, create <dns-zone-param> elements
sub _makeDnsZoneParams {
  my %soa = @_;
  my @parameterNameList = qw(refresh retry expire ttl);
  my @records = ();

  for my $parameterName (@parameterNameList) {
    if (exists($soa{$parameterName}) and defined($soa{$parameterName})) {
      my $record = XmlNode->new('dns-zone-param',
        'attributes' => {
          # 'ttl' from PPCPL is actually 'minimum' (5th SOA number)
          'name'  => $parameterName eq 'ttl' ? 'minimum' : $parameterName,
          'unit'  => 'second',
          'value' => $soa{$parameterName},
        }
      );
      push @records, $record;
    }
  }
  return @records;
}

1;
