# --
# Copyright (C) 2001-2021 OTRS AG, https://otrs.com/
# Copyright (C) 2021 Znuny GmbH, https://znuny.org/
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (GPL). If you
# did not receive this file, see https://www.gnu.org/licenses/gpl-3.0.txt.
# --

package Kernel::System::CustomerUser::LDAP;

# Throws errors when using LDAP_SIZELIMIT_EXCEEDED constant so disabled for now.
## nofilter(TidyAll::Plugin::Znuny::Perl::SyntaxCheck)

use strict;
use warnings;

use Net::LDAP;
use Net::LDAP::Constant qw(LDAP_SIZELIMIT_EXCEEDED);
use Net::LDAP::Util qw(escape_filter_value);

use Kernel::System::VariableCheck qw(:all);

our @ObjectDependencies = (
    'Kernel::Config',
    'Kernel::System::Cache',
    'Kernel::System::DateTime',
    'Kernel::System::DB',
    'Kernel::System::DynamicField',
    'Kernel::System::DynamicField::Backend',
    'Kernel::System::Encode',
    'Kernel::System::Log',
    'Kernel::System::Main',
);

sub new {
    my ( $Type, %Param ) = @_;

    # allocate new hash for object
    my $Self = {};
    bless( $Self, $Type );

    # check needed data
    for my $Needed (qw( PreferencesObject CustomerUserMap )) {
        $Self->{$Needed} = $Param{$Needed} || die "Got no $Needed!";
    }

    # max shown user a search list
    $Self->{UserSearchListLimit} = $Self->{CustomerUserMap}->{CustomerUserSearchListLimit} || 200;

    # get ldap preferences
    $Self->{Die} = 0;
    if ( defined $Self->{CustomerUserMap}->{Params}->{Die} ) {
        $Self->{Die} = $Self->{CustomerUserMap}->{Params}->{Die};
    }

    # get config object
    my $ConfigObject = $Kernel::OM->Get('Kernel::Config');

    # params
    if ( $Self->{CustomerUserMap}->{Params}->{Params} ) {
        $Self->{Params} = $Self->{CustomerUserMap}->{Params}->{Params};
    }

    # Net::LDAP new params
    elsif ( $ConfigObject->Get( 'AuthModule::LDAP::Params' . $Param{Count} ) ) {
        $Self->{Params} = $ConfigObject->Get( 'AuthModule::LDAP::Params' . $Param{Count} );
    }
    else {
        $Self->{Params} = {};
    }

    # host
    if ( $Self->{CustomerUserMap}->{Params}->{Host} ) {
        $Self->{Host} = $Self->{CustomerUserMap}->{Params}->{Host};
    }
    else {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => 'Need CustomerUser->Params->Host in Kernel/Config.pm',
        );
        return;
    }

    # base dn
    if ( defined $Self->{CustomerUserMap}->{Params}->{BaseDN} ) {
        $Self->{BaseDN} = $Self->{CustomerUserMap}->{Params}->{BaseDN};
    }
    else {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => 'Need CustomerUser->Params->BaseDN in Kernel/Config.pm',
        );
        return;
    }

    # scope
    if ( $Self->{CustomerUserMap}->{Params}->{SSCOPE} ) {
        $Self->{SScope} = $Self->{CustomerUserMap}->{Params}->{SSCOPE};
    }
    else {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => 'Need CustomerUser->Params->SSCOPE in Kernel/Config.pm',
        );
        return;
    }

    # search user
    $Self->{SearchUserDN} = $Self->{CustomerUserMap}->{Params}->{UserDN} || '';
    $Self->{SearchUserPw} = $Self->{CustomerUserMap}->{Params}->{UserPw} || '';

    # group dn
    $Self->{GroupDN} = $Self->{CustomerUserMap}->{Params}->{GroupDN} || '';

    # customer key
    if ( $Self->{CustomerUserMap}->{CustomerKey} ) {
        $Self->{CustomerKey} = $Self->{CustomerUserMap}->{CustomerKey};
    }
    else {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => 'Need CustomerUser->CustomerKey in Kernel/Config.pm',
        );
        return;
    }

    # customer id
    if ( $Self->{CustomerUserMap}->{CustomerID} ) {
        $Self->{CustomerID} = $Self->{CustomerUserMap}->{CustomerID};
    }
    else {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => 'Need CustomerUser->CustomerID in Kernel/Config.pm',
        );
        return;
    }

    # ldap filter always used
    $Self->{AlwaysFilter} = $Self->{CustomerUserMap}->{Params}->{AlwaysFilter} || '';

    $Self->{ExcludePrimaryCustomerID} = $Self->{CustomerUserMap}->{CustomerUserExcludePrimaryCustomerID} || 0;
    $Self->{SearchPrefix}             = $Self->{CustomerUserMap}->{CustomerUserSearchPrefix};
    if ( !defined $Self->{SearchPrefix} ) {
        $Self->{SearchPrefix} = '';
    }
    $Self->{SearchSuffix} = $Self->{CustomerUserMap}->{CustomerUserSearchSuffix};
    if ( !defined $Self->{SearchSuffix} ) {
        $Self->{SearchSuffix} = '*';
    }

    # charset settings
    $Self->{SourceCharset} = $Self->{CustomerUserMap}->{Params}->{SourceCharset} || '';

    # set cache type
    $Self->{CacheType} = 'CustomerUser' . $Param{Count};

    # create cache object, but only if CacheTTL is set in customer config
    if ( $Self->{CustomerUserMap}->{CacheTTL} ) {
        $Self->{CacheObject} = $Kernel::OM->Get('Kernel::System::Cache');
    }

    # get valid filter if used
    $Self->{ValidFilter} = $Self->{CustomerUserMap}->{CustomerUserValidFilter} || '';

    # connect first if Die is enabled, make sure that connection is possible, else die
    if ( $Self->{Die} ) {
        return if !$Self->_Connect();
    }

    # fetch names of configured dynamic fields
    my @DynamicFieldMapEntries = grep { $_->[5] eq 'dynamic_field' } @{ $Self->{CustomerUserMap}->{Map} };
    $Self->{ConfiguredDynamicFieldNames} = { map { $_->[2] => 1 } @DynamicFieldMapEntries };

    return $Self;
}

sub _Connect {
    my ( $Self, %Param ) = @_;

    # return if connection is already open
    return 1 if $Self->{LDAP};

    # ldap connect and bind (maybe with SearchUserDN and SearchUserPw)
    $Self->{LDAP} = Net::LDAP->new( $Self->{Host}, %{ $Self->{Params} } );

    if ( !$Self->{LDAP} ) {
        if ( $Self->{Die} ) {
            die "Can't connect to $Self->{Host}: $@";
        }
        else {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'error',
                Message  => "Can't connect to $Self->{Host}: $@",
            );
            return;
        }
    }

    my $Result;
    if ( $Self->{SearchUserDN} && $Self->{SearchUserPw} ) {
        $Result = $Self->{LDAP}->bind(
            dn       => $Self->{SearchUserDN},
            password => $Self->{SearchUserPw},
        );
    }
    else {
        $Result = $Self->{LDAP}->bind();
    }

    if ( $Result->code() ) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => 'First bind failed! ' . $Result->error(),
        );
        $Self->{LDAP}->disconnect();
        return;
    }

    return 1;
}

sub CustomerName {
    my ( $Self, %Param ) = @_;

    # check needed stuff
    if ( !$Param{UserLogin} ) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => 'Need UserLogin!'
        );
        return;
    }

    # build filter
    my $Filter = "($Self->{CustomerKey}=" . escape_filter_value( $Param{UserLogin} ) . ')';

    # prepare filter
    if ( $Self->{AlwaysFilter} ) {
        $Filter = "(&$Filter$Self->{AlwaysFilter})";
    }

    # check cache
    if ( $Self->{CacheObject} ) {
        my $Name = $Self->{CacheObject}->Get(
            Type => $Self->{CacheType},
            Key  => 'CustomerName::' . $Param{UserLogin},
        );
        return $Name if defined $Name;
    }

    # create ldap connect
    return if !$Self->_Connect();

    # perform user search
    my $Result = $Self->{LDAP}->search(
        base      => $Self->{BaseDN},
        scope     => $Self->{SScope},
        filter    => $Filter,
        sizelimit => $Self->{UserSearchListLimit},
        attrs     => $Self->{CustomerUserMap}->{CustomerUserNameFields},
    );

    if ( $Result->code() ) {
        if ( $Result->code() == LDAP_SIZELIMIT_EXCEEDED ) {

            # LDAP_SIZELIMIT_EXCEEDED result is ok if there
            # are more items in LDAP than search limit defined in Znuny or
            # in LDAP server. Avoid spamming logs with such errors.
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'debug',
                Message  => 'LDAP size limit exceeded (' . $Result->error() . ').',
            );
        }
        else {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'error',
                Message  => 'Search failed! ' . $Result->error(),
            );
        }
        return;
    }

    my %NameParts;

    for my $Entry ( $Result->all_entries() ) {

        for my $Field ( @{ $Self->{CustomerUserMap}->{CustomerUserNameFields} } ) {

            if ( defined $Entry->get_value($Field) ) {
                $NameParts{$Field} = $Self->_ConvertFrom( $Entry->get_value($Field) );
            }
        }
    }

    # fetch dynamic field values, if configured
    my @DynamicFieldCustomerUserNameFields = grep { exists $Self->{ConfiguredDynamicFieldNames}->{$_} }
        @{ $Self->{CustomerUserMap}->{CustomerUserNameFields} };
    if (@DynamicFieldCustomerUserNameFields) {
        my $DynamicFieldBackendObject = $Kernel::OM->Get('Kernel::System::DynamicField::Backend');

        DYNAMICFIELDNAME:
        for my $DynamicFieldName (@DynamicFieldCustomerUserNameFields) {
            my $DynamicFieldConfig = $Kernel::OM->Get('Kernel::System::DynamicField')->DynamicFieldGet(
                Name => $DynamicFieldName,
            );
            next DYNAMICFIELDNAME if !IsHashRefWithData($DynamicFieldConfig);

            my $Value = $DynamicFieldBackendObject->ValueGet(
                DynamicFieldConfig => $DynamicFieldConfig,
                ObjectName         => $Param{UserLogin},
            );

            next DYNAMICFIELDNAME if !defined $Value;

            if ( !IsArrayRefWithData($Value) ) {
                $Value = [$Value];
            }

            my @RenderedValues;

            VALUE:
            for my $CurrentValue ( @{$Value} ) {
                next VALUE if !defined $CurrentValue || !length $CurrentValue;

                my $RenderedValue = $DynamicFieldBackendObject->ReadableValueRender(
                    DynamicFieldConfig => $DynamicFieldConfig,
                    Value              => $CurrentValue,
                );

                next VALUE if !IsHashRefWithData($RenderedValue) || !defined $RenderedValue->{Value};

                push @RenderedValues, $RenderedValue->{Value};
            }

            $NameParts{$DynamicFieldName} = join ' ', @RenderedValues;
        }
    }

    # assemble name
    my @NameParts;
    CUSTOMERUSERNAMEFIELD:
    for my $CustomerUserNameField ( @{ $Self->{CustomerUserMap}->{CustomerUserNameFields} } ) {
        next CUSTOMERUSERNAMEFIELD
            if !exists $NameParts{$CustomerUserNameField}
            || !defined $NameParts{$CustomerUserNameField}
            || !length $NameParts{$CustomerUserNameField};
        push @NameParts, $NameParts{$CustomerUserNameField};
    }

    my $JoinCharacter = $Self->{CustomerUserMap}->{CustomerUserNameFieldsJoin} // ' ';
    my $Name          = join $JoinCharacter, @NameParts;

    # cache request
    if ( $Self->{CacheObject} ) {
        $Self->{CacheObject}->Set(
            Type  => $Self->{CacheType},
            Key   => 'CustomerName::' . $Param{UserLogin},
            Value => $Name,
            TTL   => $Self->{CustomerUserMap}->{CacheTTL},
        );
    }

    return $Name;
}

sub CustomerSearch {
    my ( $Self, %Param ) = @_;

    if ( $Param{CustomerIDRaw} ) {
        $Param{CustomerID} = $Param{CustomerIDRaw};
    }

    # check needed stuff
    if ( !$Param{Search} && !$Param{UserLogin} && !$Param{PostMasterSearch} && !$Param{CustomerID} )
    {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => 'Need Search, UserLogin, PostMasterSearch or CustomerID!'
        );
        return;
    }

    # build filter
    my $Filter = '';
    if ( $Param{Search} ) {

        my $Count = 0;
        my @Parts = split( /\+/, $Param{Search}, 6 );
        for my $Part (@Parts) {

            $Part = $Self->{SearchPrefix} . $Part . $Self->{SearchSuffix};
            $Part =~ s/(\%+)/\%/g;
            $Part =~ s/(\*+)\*/*/g;
            $Count++;

            # remove dynamic field names that are configured in CustomerUserSearchFields
            # as they cannot be retrieved here
            my @CustomerUserSearchFields = grep { !exists $Self->{ConfiguredDynamicFieldNames}->{$_} }
                @{ $Self->{CustomerUserMap}->{CustomerUserSearchFields} };

            if (@CustomerUserSearchFields) {

                # quote LDAP filter value but keep asterisks unescaped (wildcard)
                $Part =~ s/\*/encodedasterisk20160930/g;
                $Part = escape_filter_value( $Self->_ConvertTo($Part) );
                $Part =~ s/encodedasterisk20160930/*/g;

                $Filter .= '(|';
                for my $Field (@CustomerUserSearchFields) {
                    $Filter .= "($Field=" . $Part . ')';
                }
                $Filter .= ')';
            }
            else {

                # quote LDAP filter value but keep asterisks unescaped (wildcard)
                $Part =~ s/\*/encodedasterisk20160930/g;
                $Part = escape_filter_value($Part);
                $Part =~ s/encodedasterisk20160930/*/g;

                $Filter .= "($Self->{CustomerKey}=" . $Part . ')';
            }
        }

        if ( $Count > 1 ) {
            $Filter = "(&$Filter)";
        }
    }
    elsif ( $Param{PostMasterSearch} ) {

        # remove dynamic field names that are configured in CustomerUserPostMasterSearchFields
        # as they cannot be retrieved here
        my @CustomerUserPostMasterSearchFields = grep { !exists $Self->{ConfiguredDynamicFieldNames}->{$_} }
            @{ $Self->{CustomerUserMap}->{CustomerUserPostMasterSearchFields} };

        if (@CustomerUserPostMasterSearchFields) {

            # quote LDAP filter value but keep asterisks unescaped (wildcard)
            $Param{PostMasterSearch} =~ s/\*/encodedasterisk20160930/g;
            $Param{PostMasterSearch} = escape_filter_value( $Param{PostMasterSearch} );
            $Param{PostMasterSearch} =~ s/encodedasterisk20160930/*/g;

            $Filter = '(|';
            for my $Field (@CustomerUserPostMasterSearchFields) {
                $Filter .= "($Field=$Param{PostMasterSearch})";
            }
            $Filter .= ')';
        }
    }
    elsif ( $Param{UserLogin} ) {
        $Filter = "($Self->{CustomerKey}=" . escape_filter_value( $Param{UserLogin} ) . ')';
    }
    elsif ( $Param{CustomerID} ) {
        $Filter = "($Self->{CustomerID}=" . escape_filter_value( $Param{CustomerID} ) . ')';
    }

    # prepare filter
    if ( $Self->{AlwaysFilter} ) {
        $Filter = "(&$Filter$Self->{AlwaysFilter})";
    }

    # add valid filter
    if ( $Self->{ValidFilter} ) {
        $Filter = "(&$Filter$Self->{ValidFilter})";
    }

    # check cache
    my $CacheKey = join '::', map { $_ . '=' . $Param{$_} } sort keys %Param;

    if ( $Self->{CacheObject} ) {
        my $Users = $Self->{CacheObject}->Get(
            Type => $Self->{CacheType} . '_CustomerSearch',
            Key  => $CacheKey,
        );
        return %{$Users} if ref $Users eq 'HASH';
    }

    # create ldap connect
    return if !$Self->_Connect();

    my $CustomerUserListFields = $Self->{CustomerUserMap}->{CustomerUserListFields};

    # remove dynamic field names that are configured in CustomerUserListFields
    # as they cannot be handled here
    my @CustomerUserListFieldsWithoutDynamicFields
        = grep { !exists $Self->{ConfiguredDynamicFieldNames}->{$_} } @{$CustomerUserListFields};

    # combine needed attrs
    my @Attributes = ( @CustomerUserListFieldsWithoutDynamicFields, $Self->{CustomerKey} );

    # perform user search
    my $Result = $Self->{LDAP}->search(
        base      => $Self->{BaseDN},
        scope     => $Self->{SScope},
        filter    => $Filter,
        sizelimit => $Param{Limit} || $Self->{UserSearchListLimit},
        attrs     => \@Attributes,
    );

    # log ldap errors
    if ( $Result->code() ) {
        if ( $Result->code() == LDAP_SIZELIMIT_EXCEEDED ) {

            # LDAP_SIZELIMIT_EXCEEDED result is ok if there
            # are more items in LDAP than search limit defined in Znuny or
            # in LDAP server. Avoid spamming logs with such errors.
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'debug',
                Message  => 'LDAP size limit exceeded (' . $Result->error() . ').',
            );
        }
        else {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'error',
                Message  => 'Search failed! ' . $Result->error(),
            );
        }
    }

    # dynamic field handling
    my @CustomerUserListFieldsDynamicFields
        = grep { exists $Self->{ConfiguredDynamicFieldNames}->{$_} } @{$CustomerUserListFields};
    my %CustomerUserListFieldsDynamicFields = map { $_ => 1 } @CustomerUserListFieldsDynamicFields;

    my $DynamicFieldBackendObject = $Kernel::OM->Get('Kernel::System::DynamicField::Backend');

    my $DynamicFieldConfigs = $Kernel::OM->Get('Kernel::System::DynamicField')->DynamicFieldListGet(
        ObjectType => 'CustomerUser',
        Valid      => 1,
    );
    my %DynamicFieldConfigsByName = map { $_->{Name} => $_ } @{$DynamicFieldConfigs};

    my %Users;
    for my $Entry ( $Result->all_entries() ) {

        my $CustomerString = '';

        my $CustomerKey;
        if ( defined $Entry->get_value( $Self->{CustomerKey} ) ) {
            $CustomerKey = $Self->_ConvertFrom( $Entry->get_value( $Self->{CustomerKey} ) );
        }

        FIELD:
        for my $Field ( @{$CustomerUserListFields} ) {

            # dynamic field value
            if ( $CustomerUserListFieldsDynamicFields{$Field} ) {
                next FIELD if !defined $CustomerKey;
                next FIELD if !exists $DynamicFieldConfigsByName{$Field};

                my $Value = $DynamicFieldBackendObject->ValueGet(
                    DynamicFieldConfig => $DynamicFieldConfigsByName{$Field},
                    ObjectName         => $CustomerKey,
                );

                next FIELD if !defined $Value;

                if ( !IsArrayRefWithData($Value) ) {
                    $Value = [$Value];
                }

                my @Values;

                VALUE:
                for my $CurrentValue ( @{$Value} ) {
                    next VALUE if !defined $CurrentValue || !length $CurrentValue;

                    my $ReadableValue = $DynamicFieldBackendObject->ReadableValueRender(
                        DynamicFieldConfig => $DynamicFieldConfigsByName{$Field},
                        Value              => $CurrentValue,
                    );

                    next VALUE if !IsHashRefWithData($ReadableValue) || !defined $ReadableValue->{Value};

                    my $IsACLReducible = $DynamicFieldBackendObject->HasBehavior(
                        DynamicFieldConfig => $DynamicFieldConfigsByName{$Field},
                        Behavior           => 'IsACLReducible',
                    );
                    if ($IsACLReducible) {
                        my $PossibleValues = $DynamicFieldBackendObject->PossibleValuesGet(
                            DynamicFieldConfig => $DynamicFieldConfigsByName{$Field},
                        );

                        if (
                            IsHashRefWithData($PossibleValues)
                            && defined $PossibleValues->{ $ReadableValue->{Value} }
                            )
                        {
                            $ReadableValue->{Value} = $PossibleValues->{ $ReadableValue->{Value} };
                        }
                    }

                    push @Values, $ReadableValue->{Value};
                }

                $CustomerString .= ( join ' ', @Values ) . ' ';

                next FIELD;
            }

            my $Value = $Self->_ConvertFrom( $Entry->get_value($Field) );

            if ($Value) {
                if ( $Field =~ /^targetaddress$/i ) {
                    $Value =~ s/SMTP:(.*)/$1/;
                }
                $CustomerString .= $Value . ' ';
            }
        }

        $CustomerString =~ s/^(.*)\s(.+?\@.+?\..+?)(\s|)$/"$1" <$2>/;

        if ( defined $CustomerKey ) {
            $Users{$CustomerKey} = $CustomerString;
        }
    }

    # check if user need to be in a group!
    if ( $Self->{GroupDN} ) {

        for my $Filter2 ( sort keys %Users ) {

            my $Result2 = $Self->{LDAP}->search(
                base      => $Self->{GroupDN},
                scope     => $Self->{SScope},
                filter    => 'memberUid=' . escape_filter_value($Filter2),
                sizelimit => $Param{Limit} || $Self->{UserSearchListLimit},
                attrs     => ['1.1'],
            );

            if ( !$Result2->all_entries() ) {
                delete $Users{$Filter2};
            }
        }
    }

    # cache request
    if ( $Self->{CacheObject} ) {
        $Self->{CacheObject}->Set(
            Type  => $Self->{CacheType} . '_CustomerSearch',
            Key   => $CacheKey,
            Value => \%Users,
            TTL   => $Self->{CustomerUserMap}->{CacheTTL},
        );
    }

    return %Users;
}

sub CustomerSearchDetail {
    my ( $Self, %Param ) = @_;

    if ( ref $Param{SearchFields} ne 'ARRAY' ) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => "SearchFields must be an array reference!",
        );
        return;
    }

    my $Valid = defined $Param{Valid} ? $Param{Valid} : 1;

    $Param{Limit} //= '';

    # Split the search fields in scalar and array fields, before the diffrent handling.
    my @ScalarSearchFields = grep { 'Input' eq $_->{Type} } @{ $Param{SearchFields} };
    my @ArraySearchFields  = grep { 'Selection' eq $_->{Type} } @{ $Param{SearchFields} };

    # Verify that all passed array parameters contain an arrayref.
    ARGUMENT:
    for my $Argument (@ArraySearchFields) {
        if ( !defined $Param{ $Argument->{Name} } ) {
            $Param{ $Argument->{Name} } ||= [];

            next ARGUMENT;
        }

        if ( ref $Param{ $Argument->{Name} } ne 'ARRAY' ) {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'error',
                Message  => "$Argument->{Name} must be an array reference!",
            );
            return;
        }
    }

    # Set the default behaviour for the return type.
    my $Result = $Param{Result} || 'ARRAY';

    # Special handling if the result type is 'COUNT'.
    if ( $Result eq 'COUNT' ) {

        # Ignore the parameter 'Limit' when result type is 'COUNT'.
        $Param{Limit} = '';

        # Delete the OrderBy parameter when the result type is 'COUNT'.
        $Param{OrderBy} = [];
    }

    # Define order table from the search fields.
    my %OrderByTable = map { $_->{Name} => $_->{DatabaseField} } @{ $Param{SearchFields} };

    for my $Field (@ArraySearchFields) {

        my $SelectionsData = $Field->{SelectionsData};

        for my $SelectedValue ( @{ $Param{ $Field->{Name} } } ) {

            # Check if the selected value for the current field is valid.
            if ( !$SelectionsData->{$SelectedValue} ) {
                $Kernel::OM->Get('Kernel::System::Log')->Log(
                    Priority => 'error',
                    Message  => "The selected value $Field->{Name} is not valid!",
                );
                return;
            }
        }
    }

    # Build the ldap filter for the diffrent search params.
    my $Filter = '';

    for my $Field (@ScalarSearchFields) {

        # Search for scalar fields (wildcards are allowed).
        if ( $Param{ $Field->{Name} } ) {

            $Param{ $Field->{Name} } =~ s/(\%+)/\%/g;
            $Param{ $Field->{Name} } =~ s/(\*+)\*/*/g;

            $Filter .= "($Field->{DatabaseField}=" . $Self->_ConvertTo( $Param{ $Field->{Name} } ) . ")";
        }
    }

    if ($Filter) {
        $Filter = "(&$Filter)";
    }

    my $ArrayFilter = '';

    # Special parameter for CustomerIDs from a customer company search result.
    if ( IsArrayRefWithData( $Param{CustomerCompanySearchCustomerIDs} ) ) {
        $ArrayFilter .= '(|';
        for my $OneParam ( @{ $Param{CustomerCompanySearchCustomerIDs} } ) {
            $ArrayFilter .= "($Self->{CustomerID}=" . $Self->_ConvertTo($OneParam) . ")";
        }
        $ArrayFilter .= ')';
    }

    FIELD:
    for my $Field (@ArraySearchFields) {

        # Ignore empty lists.
        next FIELD if !@{ $Param{ $Field->{Name} } };

        $ArrayFilter .= '(|';
        for my $OneParam ( @{ $Param{ $Field->{Name} } } ) {
            $ArrayFilter .= "($Field->{DatabaseField}=" . $Self->_ConvertTo($OneParam) . ")";
        }
        $ArrayFilter .= ')';
    }

    # Add the array filter fields to the ldap filter.
    if ($ArrayFilter) {
        $Filter = "(&$Filter$ArrayFilter)";
    }

    my $DBObject                  = $Kernel::OM->Get('Kernel::System::DB');
    my $DynamicFieldObject        = $Kernel::OM->Get('Kernel::System::DynamicField');
    my $DynamicFieldBackendObject = $Kernel::OM->Get('Kernel::System::DynamicField::Backend');

    # Check all configured change dynamic fields, build lookup hash by name.
    my %CustomerUserDynamicFieldName2Config;
    my $CustomerUserDynamicFields = $DynamicFieldObject->DynamicFieldListGet(
        ObjectType => 'CustomerUser',
    );
    for my $DynamicField ( @{$CustomerUserDynamicFields} ) {
        $CustomerUserDynamicFieldName2Config{ $DynamicField->{Name} } = $DynamicField;
    }

    my $SQLDynamicFieldFrom     = '';
    my $SQLDynamicFieldWhere    = '';
    my $DynamicFieldJoinCounter = 1;

    DYNAMICFIELD:
    for my $DynamicField ( @{$CustomerUserDynamicFields} ) {

        my $SearchParam = $Param{ "DynamicField_" . $DynamicField->{Name} };

        next DYNAMICFIELD if ( !$SearchParam );
        next DYNAMICFIELD if ( ref $SearchParam ne 'HASH' );

        my $NeedJoin;

        for my $Operator ( sort keys %{$SearchParam} ) {

            my @SearchParams = ( ref $SearchParam->{$Operator} eq 'ARRAY' )
                ? @{ $SearchParam->{$Operator} }
                : ( $SearchParam->{$Operator} );

            my $SQLDynamicFieldWhereSub = '';
            if ($SQLDynamicFieldWhere) {
                $SQLDynamicFieldWhereSub = ' AND (';
            }
            else {
                $SQLDynamicFieldWhereSub = ' (';
            }

            my $Counter = 0;
            TEXT:
            for my $Text (@SearchParams) {
                next TEXT if ( !defined $Text || $Text eq '' );

                $Text =~ s/\*/%/gi;

                # Check search attribute, we do not need to search for '*'.
                next TEXT if $Text =~ /^\%{1,3}$/;

                my $ValidateSuccess = $DynamicFieldBackendObject->ValueValidate(
                    DynamicFieldConfig => $DynamicField,
                    Value              => $Text,
                    UserID             => $Param{UserID} || 1,
                );
                if ( !$ValidateSuccess ) {
                    $Kernel::OM->Get('Kernel::System::Log')->Log(
                        Priority => 'error',
                        Message  => "Search not executed due to invalid value '"
                            . $Text
                            . "' on field '"
                            . $DynamicField->{Name} . "'!",
                    );
                    return;
                }

                if ($Counter) {
                    $SQLDynamicFieldWhereSub .= ' OR ';
                }
                $SQLDynamicFieldWhereSub .= $DynamicFieldBackendObject->SearchSQLGet(
                    DynamicFieldConfig => $DynamicField,
                    TableAlias         => "dfv$DynamicFieldJoinCounter",
                    Operator           => $Operator,
                    SearchTerm         => $Text,
                );

                $Counter++;
            }
            $SQLDynamicFieldWhereSub .= ') ';

            if ($Counter) {
                $SQLDynamicFieldWhere .= $SQLDynamicFieldWhereSub;
                $NeedJoin = 1;
            }
        }

        if ($NeedJoin) {
            $SQLDynamicFieldFrom .= "
                INNER JOIN dynamic_field_value dfv$DynamicFieldJoinCounter
                    ON (df_obj_id_name.object_id = dfv$DynamicFieldJoinCounter.object_id
                        AND dfv$DynamicFieldJoinCounter.field_id = "
                . $DBObject->Quote( $DynamicField->{ID}, 'Integer' ) . ")
            ";

            $DynamicFieldJoinCounter++;
        }
    }

    # Execute a dynamic field search, if a dynamic field where statement exists.
    if ($SQLDynamicFieldWhere) {

        my @DynamicFieldUserLogins;

        # sql uery for the dynamic fields
        my $SQLDynamicField
            = "SELECT DISTINCT(df_obj_id_name.object_name) FROM dynamic_field_obj_id_name df_obj_id_name "
            . $SQLDynamicFieldFrom
            . " WHERE "
            . $SQLDynamicFieldWhere;

        my $UsedCache;

        if ( $Self->{CacheObject} ) {

            my $DynamicFieldSearchCacheData = $Self->{CacheObject}->Get(
                Type => $Self->{CacheType} . '_CustomerSearchDetailDynamicFields',
                Key  => $SQLDynamicField,
            );

            if ( defined $DynamicFieldSearchCacheData ) {
                if ( ref $DynamicFieldSearchCacheData eq 'ARRAY' ) {
                    @DynamicFieldUserLogins = @{$DynamicFieldSearchCacheData};

                    # set the used cache flag
                    $UsedCache = 1;
                }
                else {
                    $Kernel::OM->Get('Kernel::System::Log')->Log(
                        Priority => 'error',
                        Message  => 'Invalid ref ' . ref($DynamicFieldSearchCacheData) . '!'
                    );
                    return;
                }
            }
        }

        # Get the data only from database, if no cache entry exists.
        if ( !$UsedCache ) {

            return if !$DBObject->Prepare(
                SQL => $SQLDynamicField,
            );

            while ( my @Row = $DBObject->FetchrowArray() ) {
                push @DynamicFieldUserLogins, $Row[0];
            }

            if ( $Self->{CacheObject} ) {
                $Self->{CacheObject}->Set(
                    Type  => $Self->{CacheType} . '_CustomerSearchDetailDynamicFields',
                    Key   => $SQLDynamicField,
                    Value => \@DynamicFieldUserLogins,
                    TTL   => $Self->{CustomerUserMap}->{CacheTTL},
                );
            }
        }

        # Add the user logins from the dynamic fields, if a search result exists from the dynamic field search
        #   or skip the search and return a emptry array ref, if no user logins exists from the dynamic field search.
        if (@DynamicFieldUserLogins) {

            my $DynamicFieldUserLoginsFilter = '(|';
            for my $OneParam (@DynamicFieldUserLogins) {
                $DynamicFieldUserLoginsFilter .= "($Self->{CustomerKey}=" . $Self->_ConvertTo($OneParam) . ")";
            }
            $DynamicFieldUserLoginsFilter .= ')';

            # Add the dynamic field user logins filter to the ldap filter.
            $Filter = "(&$Filter$DynamicFieldUserLoginsFilter)";
        }
        else {
            return $Result eq 'COUNT' ? 0 : [];
        }
    }

    # Special parameter to exclude some user logins from the search result.
    if ( IsArrayRefWithData( $Param{ExcludeUserLogins} ) ) {
        my $ExcludeUserLoginsFilter = '(&';
        for my $OneParam ( @{ $Param{ExcludeUserLogins} } ) {
            $ExcludeUserLoginsFilter .= "(!($Self->{CustomerKey}=" . $Self->_ConvertTo($OneParam) . "))";
        }
        $ExcludeUserLoginsFilter .= ')';

        # Add the exclude user logins filter to the ldap filter.
        $Filter = "(&$Filter$ExcludeUserLoginsFilter)";
    }

    if ( $Self->{AlwaysFilter} ) {
        $Filter = "(&$Filter$Self->{AlwaysFilter})";
    }

    if ( $Self->{ValidFilter} && $Valid ) {
        $Filter = "(&$Filter$Self->{ValidFilter})";
    }

    # Default filter for the search, if no filter exists.
    if ( !$Filter ) {
        $Filter = "($Self->{CustomerKey}=*)";
    }

    # Check if OrderBy contains only unique valid values.
    my %OrderBySeen;
    for my $OrderBy ( @{ $Param{OrderBy} } ) {

        if ( !$OrderBy || $OrderBySeen{$OrderBy} ) {

            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'error',
                Message  => "OrderBy contains invalid value '$OrderBy' or the value is used more than once!",
            );
            return;
        }

        # Remember the value to check if it appears more than once.
        $OrderBySeen{$OrderBy} = 1;
    }

    # Check if OrderByDirection array contains only 'Up' or 'Down'.
    DIRECTION:
    for my $Direction ( @{ $Param{OrderByDirection} } ) {

        # Only 'Up' or 'Down' allowed.
        next DIRECTION if $Direction eq 'Up';
        next DIRECTION if $Direction eq 'Down';

        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => "OrderByDirection can only contain 'Up' or 'Down'!",
        );
        return;
    }

    # Assemble the ORDER BY clause.
    my @OrderByFields;
    my $OrderByString = '';
    my $Count         = 0;

    ORDERBY:
    for my $OrderBy ( @{ $Param{OrderBy} } ) {
        next ORDERBY if !$OrderByTable{$OrderBy};

        my $Direction = $Param{OrderByDirection}->[$Count] || 'Down';

        $OrderByString .= $OrderByTable{$OrderBy} . $Direction;

        push @OrderByFields, {
            Name          => $OrderBy,
            DatabaseField => $OrderByTable{$OrderBy},
            Direction     => $Direction,
        };
    }
    continue {
        $Count++;
    }

    # If there is a possibility that the ordering is not determined
    #   we add an descending ordering by id.
    if ( !grep { $_ eq 'UserLogin' } ( @{ $Param{OrderBy} } ) ) {
        push @OrderByFields, {
            Name          => 'UserLogin',
            DatabaseField => "$Self->{CustomerKey}",
            Direction     => 'Down',
        };
    }

    my $CacheKey = 'CustomerSearchDetail::' . $Result . $Filter . $Param{Limit} . $OrderByString;

    if ( $Self->{CacheObject} ) {
        my $CacheData = $Self->{CacheObject}->Get(
            Type => $Self->{CacheType},
            Key  => $CacheKey,
        );

        if ( defined $CacheData ) {
            if ( ref $CacheData eq 'ARRAY' ) {
                return $CacheData;
            }
            elsif ( ref $CacheData eq '' ) {
                return $CacheData;
            }
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'error',
                Message  => 'Invalid ref ' . ref($CacheData) . '!'
            );
            return;
        }
    }

    return if !$Self->_Connect();

    # cCmbine needed attributes.
    my @Attributes = map { $_->{DatabaseField} } @OrderByFields;

    # Perform the ldap user search.
    my $ResultSearch = $Self->{LDAP}->search(
        base      => $Self->{BaseDN},
        scope     => $Self->{SScope},
        filter    => $Filter,
        sizelimit => $Self->{UserSearchListLimit},
        attrs     => \@Attributes,
    );

    if ( $ResultSearch->code() ) {
        if ( $ResultSearch->code() == LDAP_SIZELIMIT_EXCEEDED ) {

            # LDAP_SIZELIMIT_EXCEEDED result is ok if there
            # are more items in LDAP than search limit defined in Znuny or
            # in LDAP server. Avoid spamming logs with such errors.
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'debug',
                Message  => 'LDAP size limit exceeded (' . $ResultSearch->error() . ').',
            );
        }
        else {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'error',
                Message  => 'Search failed! ' . $ResultSearch->error(),
            );
        }
    }

    my @TmpCustomerUsers;
    for my $Entry ( $ResultSearch->all_entries() ) {

        my %Data;
        for my $OrderBy (@OrderByFields) {

            my $FieldValue = $Entry->get_value( $OrderBy->{DatabaseField} );
            $FieldValue = $Self->_ConvertFrom($FieldValue);

            $Data{ $OrderBy->{Name} } = $FieldValue;
        }

        push @TmpCustomerUsers, \%Data;
    }

    # Sort the customer users.
    @TmpCustomerUsers = sort { $Self->_SearchResultSort(@OrderByFields) } @TmpCustomerUsers;

    my @IDs;

    # Check if user need to be in a group!
    if ( $Self->{GroupDN} ) {

        FILTERID:
        for my $Data (@TmpCustomerUsers) {

            my $ResultGroupDN = $Self->{LDAP}->search(
                base      => $Self->{GroupDN},
                scope     => $Self->{SScope},
                filter    => 'memberUid=' . $Data->{UserLogin},
                sizelimit => $Self->{UserSearchListLimit},
                attrs     => ['1.1'],
            );

            next FILTERID if !$ResultGroupDN->all_entries();

            push @IDs, $Data->{UserLogin};
        }
    }
    else {
        @IDs = map { $_->{UserLogin} } @TmpCustomerUsers;
    }

    if ( $Param{Limit} ) {
        splice @IDs, $Param{Limit};
    }

    if ( $Result eq 'COUNT' ) {

        if ( $Self->{CacheObject} ) {
            $Self->{CacheObject}->Set(
                Type  => $Self->{CacheType},
                Key   => $CacheKey,
                Value => scalar @IDs,
                TTL   => $Self->{CustomerUserMap}->{CacheTTL},
            );
        }
        return scalar @IDs;
    }
    else {

        if ( $Self->{CacheObject} ) {
            $Self->{CacheObject}->Set(
                Type  => $Self->{CacheType},
                Key   => $CacheKey,
                Value => \@IDs,
                TTL   => $Self->{CustomerUserMap}->{CacheTTL},
            );
        }
        return \@IDs;
    }
}

sub CustomerIDList {
    my ( $Self, %Param ) = @_;

    my $Valid      = defined $Param{Valid} ? $Param{Valid} : 1;
    my $SearchTerm = $Param{SearchTerm} || '';

    my $CacheKey = "CustomerIDList::${Valid}::$SearchTerm";

    # check cache
    if ( $Self->{CacheObject} ) {
        my $Result = $Self->{CacheObject}->Get(
            Type => $Self->{CacheType},
            Key  => $CacheKey,
        );
        return @{$Result} if ref $Result eq 'ARRAY';
    }

    # prepare filter
    my $Filter = "($Self->{CustomerID}=*)";
    if ($SearchTerm) {

        my $SearchFilter = $Self->{SearchPrefix} . $SearchTerm . $Self->{SearchSuffix};
        $SearchFilter =~ s/(\%+)/\%/g;
        $SearchFilter =~ s/(\*+)\*/*/g;

        # quote LDAP filter value but keep asterisks unescaped (wildcard)
        $SearchFilter =~ s/\*/encodedasterisk20160930/g;
        $SearchFilter = escape_filter_value($SearchFilter);
        $SearchFilter =~ s/encodedasterisk20160930/*/g;

        $Filter = "($Self->{CustomerID}=$SearchFilter)";

    }

    if ( $Self->{AlwaysFilter} ) {
        $Filter = "(&$Filter$Self->{AlwaysFilter})";
    }

    # add valid filter
    if ( $Self->{ValidFilter} && $Valid ) {
        $Filter = "(&$Filter$Self->{ValidFilter})";
    }

    # create ldap connect
    return if !$Self->_Connect();

    # combine needed attrs
    my @Attributes = ( $Self->{CustomerKey}, $Self->{CustomerID} );

    # perform user search
    my $Result = $Self->{LDAP}->search(
        base      => $Self->{BaseDN},
        scope     => $Self->{SScope},
        filter    => $Filter,
        sizelimit => $Self->{UserSearchListLimit},
        attrs     => \@Attributes,
    );

    # log ldap errors
    if ( $Result->code() ) {

        if ( $Result->code() == LDAP_SIZELIMIT_EXCEEDED ) {

            # LDAP_SIZELIMIT_EXCEEDED result is ok if there
            # are more items in LDAP than search limit defined in Znuny or
            # in LDAP server. Avoid spamming logs with such errors.
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'debug',
                Message  => 'LDAP size limit exceeded (' . $Result->error() . ').',
            );
        }
        else {
            $Kernel::OM->Get('Kernel::System::Log')->Log(
                Priority => 'error',
                Message  => 'Search failed! ' . $Result->error(),
            );
        }
    }

    my %Users;
    for my $Entry ( $Result->all_entries() ) {

        my $FieldValue = $Entry->get_value( $Self->{CustomerID} );
        $FieldValue = defined $FieldValue ? $FieldValue : '';

        my $KeyValue = $Entry->get_value( $Self->{CustomerKey} );
        $KeyValue = defined $KeyValue ? $KeyValue : '';
        $Users{ $Self->_ConvertFrom($KeyValue) } = $Self->_ConvertFrom($FieldValue);
    }

    # check if user need to be in a group!
    if ( $Self->{GroupDN} ) {
        for my $Filter2 ( sort keys %Users ) {
            my $Result2 = $Self->{LDAP}->search(
                base      => $Self->{GroupDN},
                scope     => $Self->{SScope},
                filter    => 'memberUid=' . escape_filter_value($Filter2),
                sizelimit => $Self->{UserSearchListLimit},
                attrs     => ['1.1'],
            );
            if ( !$Result2->all_entries() ) {
                delete $Users{$Filter2};
            }
        }
    }

    # make CustomerIDs unique
    my %Tmp;
    @Tmp{ values %Users } = undef;
    my @Result = keys %Tmp;

    # cache request
    if ( $Self->{CacheObject} ) {
        $Self->{CacheObject}->Set(
            Type  => $Self->{CacheType},
            Key   => $CacheKey,
            Value => \@Result,
            TTL   => $Self->{CustomerUserMap}->{CacheTTL},
        );
    }

    return @Result;
}

sub CustomerIDs {
    my ( $Self, %Param ) = @_;

    # check needed stuff
    if ( !$Param{User} ) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => 'Need User!'
        );
        return;
    }

    # check cache
    if ( $Self->{CacheObject} ) {
        my $CustomerIDs = $Self->{CacheObject}->Get(
            Type => $Self->{CacheType},
            Key  => "CustomerIDs::$Param{User}",
        );
        return @{$CustomerIDs} if ref $CustomerIDs eq 'ARRAY';
    }

    # get customer data
    my %Data = $Self->CustomerUserDataGet(
        User => $Param{User},
    );

    # there are multi customer ids
    my @CustomerIDs;
    if ( $Data{UserCustomerIDs} ) {

        # used separators
        SEPARATOR:
        for my $Separator ( ';', ',', '|' ) {

            next SEPARATOR if $Data{UserCustomerIDs} !~ /\Q$Separator\E/;

            # split it
            my @IDs = split /\Q$Separator\E/, $Data{UserCustomerIDs};

            for my $ID (@IDs) {
                $ID =~ s/^\s+//g;
                $ID =~ s/\s+$//g;
                push @CustomerIDs, $ID;
            }

            last SEPARATOR;
        }

        # fallback if no separator got found
        if ( !@CustomerIDs ) {
            $Data{UserCustomerIDs} =~ s/^\s+//g;
            $Data{UserCustomerIDs} =~ s/\s+$//g;
            push @CustomerIDs, $Data{UserCustomerIDs};
        }
    }

    # use also the primary customer id
    if ( $Data{UserCustomerID} && !$Self->{ExcludePrimaryCustomerID} ) {
        push @CustomerIDs, $Data{UserCustomerID};
    }

    # cache request
    if ( $Self->{CacheObject} ) {
        $Self->{CacheObject}->Set(
            Type  => $Self->{CacheType},
            Key   => 'CustomerIDs::' . $Param{User},
            Value => \@CustomerIDs,
            TTL   => $Self->{CustomerUserMap}->{CacheTTL},
        );
    }

    return @CustomerIDs;
}

sub CustomerUserDataGet {
    my ( $Self, %Param ) = @_;

    # check needed stuff
    if ( !$Param{User} ) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => 'Need User!'
        );
        return;
    }

    # perform user search
    my @Attributes;
    ENTRY:
    for my $Entry ( @{ $Self->{CustomerUserMap}->{Map} } ) {
        next ENTRY if $Entry->[5] eq 'dynamic_field';
        push( @Attributes, $Entry->[2] );
    }
    my $Filter = "($Self->{CustomerKey}=" . escape_filter_value( $Param{User} ) . ')';

    # prepare filter
    if ( $Self->{AlwaysFilter} ) {
        $Filter = "(&$Filter$Self->{AlwaysFilter})";
    }

    # check cache
    if ( $Self->{CacheObject} ) {
        my $Data = $Self->{CacheObject}->Get(
            Type => $Self->{CacheType},
            Key  => 'CustomerUserDataGet::' . $Param{User},
        );
        return %{$Data} if ref $Data eq 'HASH';
    }

    # create ldap connect
    return if !$Self->_Connect();

    # perform search
    my $Result = $Self->{LDAP}->search(
        base   => $Self->{BaseDN},
        scope  => $Self->{SScope},
        filter => $Filter,
        attrs  => \@Attributes,
    );

    # log ldap errors
    if ( $Result->code() ) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => $Result->error(),
        );
        return;
    }

    # get first entry
    my $Result2 = $Result->entry(0);
    if ( !$Result2 ) {
        return;
    }

    # get customer user info
    my %Data;
    ENTRY:
    for my $Entry ( @{ $Self->{CustomerUserMap}->{Map} } ) {
        next ENTRY if $Entry->[5] eq 'dynamic_field';

        my $Value = $Self->_ConvertFrom( $Result2->get_value( $Entry->[2] ) ) || '';

        if ( $Value && $Entry->[2] =~ /^targetaddress$/i ) {
            $Value =~ s/SMTP:(.*)/$1/;
        }

        $Data{ $Entry->[0] } = $Value;
    }

    return if !$Data{UserLogin};

    # to build the UserMailString
    my $UserMailString = '';
    my @UserMailStringParts;

    my $CustomerUserListFieldsMap = $Self->{CustomerUserMap}->{CustomerUserListFields};
    if ( !IsArrayRefWithData($CustomerUserListFieldsMap) ) {
        $CustomerUserListFieldsMap = [ 'first_name', 'last_name', 'email', ];
    }

    for my $Field ( @{$CustomerUserListFieldsMap} ) {

        my $Value = $Self->_ConvertFrom( $Result2->get_value($Field) ) || '';

        if ($Value) {
            if ( $Field =~ /^targetaddress$/i ) {
                $Value =~ s/SMTP:(.*)/$1/;
            }
            push @UserMailStringParts, $Value;
        }
    }
    $UserMailString = join ' ', @UserMailStringParts;
    $UserMailString =~ s/^(.*)\s(.+?\@.+?\..+?)(\s|)$/"$1" <$2>/;

    # add the UserMailString to the data hash
    $Data{UserMailString} = $UserMailString;

    # compat!
    $Data{UserID} = $Data{UserLogin};

    # get preferences
    my %Preferences = $Self->GetPreferences( UserID => $Data{UserLogin} );

    # add last login timestamp
    if ( $Preferences{UserLastLogin} ) {

        my $DateTimeObject = $Kernel::OM->Create(
            'Kernel::System::DateTime',
            ObjectParams => {
                Epoch => $Preferences{UserLastLogin},
            },
        );

        $Preferences{UserLastLoginTimestamp} = $DateTimeObject->ToString();

    }

    # cache request
    if ( $Self->{CacheObject} ) {
        $Self->{CacheObject}->Set(
            Type  => $Self->{CacheType},
            Key   => 'CustomerUserDataGet::' . $Param{User},
            Value => { %Data, %Preferences },
            TTL   => $Self->{CustomerUserMap}->{CacheTTL},
        );
    }

    return ( %Data, %Preferences );
}

sub CustomerUserAdd {
    my ( $Self, %Param ) = @_;

    # check ro/rw
    if ( $Self->{ReadOnly} ) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => 'Customer backend is read only!'
        );
        return;
    }

    $Kernel::OM->Get('Kernel::System::Log')->Log(
        Priority => 'error',
        Message  => 'Not supported for this module!'
    );

    return;
}

sub CustomerUserUpdate {
    my ( $Self, %Param ) = @_;

    # check ro/rw
    if ( $Self->{ReadOnly} ) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => 'Customer backend is read only!'
        );
        return;
    }

    $Kernel::OM->Get('Kernel::System::Log')->Log(
        Priority => 'error',
        Message  => 'Not supported for this module!'
    );

    return;
}

sub SetPassword {
    my ( $Self, %Param ) = @_;

    my $Pw = $Param{PW} || '';

    # check ro/rw
    if ( $Self->{ReadOnly} ) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => 'Customer backend is read only!'
        );
        return;
    }

    $Kernel::OM->Get('Kernel::System::Log')->Log(
        Priority => 'error',
        Message  => 'Not supported for this module!'
    );

    return;
}

sub GenerateRandomPassword {
    my ( $Self, %Param ) = @_;

    return $Kernel::OM->Get('Kernel::System::Main')->GenerateRandomString(
        Length     => $Param{Size} || 8,
        Dictionary => [ 0 .. 9, 'A' .. 'Z', 'a' .. 'z', '-', '_', '!', '@', '#', '$', '%', '^', '&', '*' ],
    );
}

sub SetPreferences {
    my ( $Self, %Param ) = @_;

    # check needed params
    if ( !$Param{UserID} ) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => 'Need UserID!'
        );
        return;
    }

    # cache reset
    if ( $Self->{CacheObject} ) {
        $Self->{CacheObject}->Delete(
            Type => $Self->{CacheType},
            Key  => "CustomerUserDataGet::$Param{UserID}",
        );
    }
    return $Self->{PreferencesObject}->SetPreferences(%Param);
}

sub GetPreferences {
    my ( $Self, %Param ) = @_;

    # check needed params
    if ( !$Param{UserID} ) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => 'Need UserID!'
        );
        return;
    }

    return $Self->{PreferencesObject}->GetPreferences(%Param);
}

sub SearchPreferences {
    my ( $Self, %Param ) = @_;

    return $Self->{PreferencesObject}->SearchPreferences(%Param);
}

sub _ConvertFrom {
    my ( $Self, $Text ) = @_;

    return if !defined $Text;

    if ( !$Self->{SourceCharset} ) {
        return $Text;
    }

    return $Kernel::OM->Get('Kernel::System::Encode')->Convert(
        Text => $Text,
        From => $Self->{SourceCharset},
        To   => 'utf-8',
    );
}

sub _ConvertTo {
    my ( $Self, $Text ) = @_;

    return if !defined $Text;

    # get encode object
    my $EncodeObject = $Kernel::OM->Get('Kernel::System::Encode');

    if ( !$Self->{SourceCharset} ) {
        $EncodeObject->EncodeInput( \$Text );
        return $Text;
    }

    return $EncodeObject->Convert(
        Text => $Text,
        To   => $Self->{SourceCharset},
        From => 'utf-8',
    );
}

sub _CustomerUserCacheClear {
    my ( $Self, %Param ) = @_;

    return if !$Self->{CacheObject};

    if ( !$Param{UserLogin} ) {
        $Kernel::OM->Get('Kernel::System::Log')->Log(
            Priority => 'error',
            Message  => 'Need UserLogin!',
        );
        return;
    }

    $Self->{CacheObject}->Delete(
        Type => $Self->{CacheType},
        Key  => "CustomerUserDataGet::$Param{UserLogin}",
    );
    $Self->{CacheObject}->Delete(
        Type => $Self->{CacheType},
        Key  => "CustomerName::$Param{UserLogin}",
    );

    $Self->{CacheObject}->CleanUp(
        Type => $Self->{CacheType} . '_CustomerSearch',
    );

    $Self->{CacheObject}->CleanUp(
        Type => 'CustomerGroup',
    );

    return 1;
}

sub _SearchResultSort {
    my ( $Self, @OrderByFields ) = @_;

    for my $OrderBy (@OrderByFields) {
        my $Compare;

        if ( $OrderBy->{Direction} && $OrderBy->{Direction} eq 'Up' ) {
            $Compare = lc( $a->{ $OrderBy->{Name} } ) cmp lc( $b->{ $OrderBy->{Name} } );
        }
        else {
            $Compare = lc( $b->{ $OrderBy->{Name} } ) cmp lc( $a->{ $OrderBy->{Name} } );
        }
        return $Compare if $Compare;
    }
    return 0;
}

sub DESTROY {
    my ( $Self, %Param ) = @_;

    # take down session
    if ( $Self->{LDAP} ) {
        $Self->{LDAP}->unbind();
    }

    return 1;
}

1;
