package LISM::Storage::GoogleApps;

use strict;
use base qw(LISM::Storage);
use Net::LDAP::Constant qw(:all);
use Net::LDAP::Filter;
use VUser::Google::ProvisioningAPI;
use Encode;
use Data::Dumper;

our $RETRY = 3;

=head1 NAME

LISM::Storage::GoogleApps - Google Apps storage for LISM

=head1 DESCRIPTION

This class implements the L<LISM::Storage> interface for Google Apps data.

=head1 METHODS

=head2 init

Initialize the configuration data.

=cut

sub init
{
    my $self = shift;

    return $self->SUPER::init();
}

sub _getConnect
{
    my $self = shift;
    my $conf = $self->{_config};

    $self->_login();

    return 0;
}

sub _checkConfig
{
    my $self = shift;
    my $conf = $self->{_config};
    my $rc = 0;

    foreach my $oname (keys %{$conf->{object}}) {
        my $oconf = $conf->{object}{$oname};

        foreach my $attr (keys %{$oconf->{attr}}) {
            my $param = $oconf->{attr}{$attr}->{param}[0];

            if ($oname eq 'User') {
                if ($param =~ /^(isSuspended|isAdministrator)$/) {
                    if (!defined($oconf->{attr}{$attr}->{true})) {
                        $self->log(level => 'alert', message => "Set true value for parameter \"$param\"");
                        return 1;
                    }
                } elsif ($param !~ /^(User|FullName|FamilyName|GivenName|Password|Email|Quota|NickName)$/) {
                    $self->log(level => 'alert', message => "Invalid parameter name \"$param\" of $attr");
                    return 1;
                }
            } elsif ($oname eq 'EmailList') {
                if ($param !~ /^(EmailList|Recipient)$/) {
                    $self->log(level => 'alert', message => "Invalid parameter name \"$param\" of $attr");
                    return 1;
                }
            }
        }
    }

    if (defined($conf->{domain})) {
        if (!defined($conf->{admin}) || !defined($conf->{passwd})) {
            $self->log(level => 'alert', message => "Set google admin and passwd");
            return 1;
        }
        my $domain = encode('utf8', $conf->{domain}[0]);
        $self->{multilogin}->{$domain}->{admin} = encode('utf8', $conf->{admin}[0]);
        $self->{multilogin}->{$domain}->{passwd} = encode('utf8', $conf->{passwd}[0]);
        $self->{multilogin}->{current} = $domain;
    }

    if ($rc = $self->SUPER::_checkConfig()) {
        return $rc;
    }

    return $rc;
}

=pod

=head2 _objSearch($obj, $pkey, $suffix, $sizeLim, $timeLim, $filter)

Search the appropriate records in the object's file.

=cut

sub _objSearch
{
    my $self = shift;
    my ($obj, $pkeys, $suffix, $sizeLim, $filter) = @_;
    my $conf = $self->{_config};
    my $oconf = $obj->{conf};
    my $pkey = $self->_getPid($pkeys);
    my @match_entries = ();
    my @match_keys;
    my $rc = LDAP_SUCCESS;

    my $filterStr = $filter ? $filter->as_string : '(objectClass=*)';
    my ($rdn_val) = ($filterStr =~ /$oconf->{rdn}[0]=(.[^\)]*)/i);
    if ($oconf->{rdn}[0] eq 'mail') {
        $rdn_val =~ s/@.+$//;
    }

    # do plugin
    if (defined($oconf->{plugin})) {
        my $error;
        my $keys;
        my @entries;

        ($rc, $keys, @entries) = $self->_doPlugin('search', $obj, $pkeys, $suffix, $sizeLim, $filter);
        if ($rc) {
            return ($rc, \@match_keys, @match_entries);
        }

        push(@match_keys, @{$keys});
        push(@match_entries, @entries);
    }

    if (defined($oconf->{container})) {
        if (defined($oconf->{container}[0]->{login})) {
            $rc = $self->_relogin($pkey, $suffix, $oconf->{container}[0]->{login}[0]);
            if ($rc || !$self->{multilogin}->{current}) {
                return (LDAP_SUCCESS, \@match_keys, @match_entries);
            }
        }
    }

    for (my $i = 0; $i < $RETRY; $i++) {
        if ($obj->{name} eq 'User') {
            if ($rdn_val && $rdn_val !~ /\*/) {
                my $entry;
                eval "\$entry = \$self->{google}->RetrieveUser(\$rdn_val)";
                if ($@) {
                    $self->log(level => 'err', message => "Searching User $rdn_val failed: $@");
                    $rc = LDAP_OTHER;
                } elsif (!defined($entry)) {
                    my $reason = $self->{google}->{result}{reason};
                    $self->log(level => 'err', message => "Searching User $rdn_val failed: $reason");
                    if ($reason =~ / 1000 -/) {
                        next;
                    } elsif ($reason =~ / 1301 -/) {
                        $rc = LDAP_NO_SUCH_OBJECT;
                    } else {
                        $rc = LDAP_OPERATIONS_ERROR;
                    }
                } else {
                    my $entryStr = $self->_buildObjectEntry($obj, $suffix, $entry);
                    if (!Encode::is_utf8($entryStr)) {
                        $entryStr = decode('utf8', $entryStr);
                    }

                    if ($self->parseFilter($filter, $entryStr)) {
                        push(@match_entries, $entryStr);
                    }

                    $rc = LDAP_SUCCESS;
                }
            } else {
                my @entries;
                eval "\@entries = \$self->{google}->RetrieveAllUsers()";
                if ($@) {
                    $self->log(level => 'err', message => "Searching users failed: $@");
                } elsif (@entries && !defined($entries[0])) {
                    my $reason = $self->{google}->{result}{reason};
                    $self->log(level => 'err', message => "Searching users failed: $reason");
                    if ($reason =~ / 1000 -/) {
                        next;
                    } else {
                        $rc = LDAP_OPERATIONS_ERROR;
                    }
                } else {
                    foreach my $entry (@entries) {
                        my $entryStr = $self->_buildObjectEntry($obj, $suffix, $entry);
                        if (!Encode::is_utf8($entryStr)) {
                            $entryStr = decode('utf8', $entryStr);
                        }

                        if ($self->parseFilter($filter, $entryStr)) {
                            push(@match_entries, $entryStr);
                        }
                    }

                    $rc = LDAP_SUCCESS;
                }
            }
        } elsif ($obj->{name} eq 'EmailList') {
            my $recipient;
            foreach my $attr (keys %{$oconf->{attr}}) {
                if ($oconf->{attr}{$attr}->{param}[0] eq 'Recipient') {
                    ($recipient) = ($filterStr =~ /$attr=(.[^\)]*)/i);
                }
            }
            if ($recipient && $recipient !~ /\*/) {
                my $entry;
                eval "\$entry = \$self->{google}->RetrieveEmailLists(\$recipient)";
                if ($@) {
                    $self->log(level => 'err', message => "Searching mailing lists have $recipient failed: $@");
                    $rc = LDAP_OTHER;
                } elsif (!defined($entry)) {
                    my $reason = $self->{google}->{result}{reason};
                    if (!$reason) {
                        $rc = LDAP_SUCCESS;
                    } else {
                        $self->log(level => 'err', message => "Searching mailing lists have $recipient failed: $reason");
                        if ($reason =~ / 1000 -/) {
                            next;
                        } else {
                            $rc = LDAP_OPERATIONS_ERROR;
                        }
                    }
                } else {
                    my $entryStr = $self->_buildObjectEntry($obj, $suffix, $entry);
                    if (!Encode::is_utf8($entryStr)) {
                        $entryStr = decode('utf8', $entryStr);
                    }

                    if ($self->parseFilter($filter, $entryStr)) {
                        push(@match_entries, $entryStr);
                    }

                    $rc = LDAP_SUCCESS;
                }
            } else {
                my @entries;
                eval "\@entries = \$self->{google}->RetrieveAllEmailLists()";
                if ($@) {
                    $self->log(level => 'err', message => "Searching mailing lists failed: $@");
                } elsif (@entries && !defined($entries[0])) {
                    my $reason = $self->{google}->{result}{reason};
                    if (!$reason) {
                       $rc = LDAP_SUCCESS;
                    } else {
                        $self->log(level => 'err', message => "Searching mailing lists failed: $reason");
                        if ($reason =~ / 1000 -/) {
                            next;
                        } else {
                            $rc = LDAP_OPERATIONS_ERROR;
                        }
                    }
                } else {
                    foreach my $entry (@entries) {
                        my $entryStr = $self->_buildObjectEntry($obj, $suffix, $entry);
                        if (!Encode::is_utf8($entryStr)) {
                            $entryStr = decode('utf8', $entryStr);
                        }

                        if ($self->parseFilter($filter, $entryStr)) {
                            push(@match_entries, $entryStr);
                        }
                    }

                    $rc = LDAP_SUCCESS;
                }
            }
        }
        last;
    }

    return ($rc , \@match_keys, @match_entries);
}

=pod

=head2 _objModify($obj, $pkey, $dn, @list)

Write the modified record to the temporary file.

=cut

sub _objModify
{
    my $self = shift;
    my ($obj, $pkeys, $dn, @list) = @_;
    my $conf = $self->{_config};
    my $oconf = $obj->{conf};
    my $pkey = $self->_getPid($pkeys);
    my $rc = LDAP_SUCCESS;

    my ($rdn_val) = ($dn =~ /^[^=]+=([^,]+),/);
    if ($oconf->{rdn}[0] eq 'mail') {
        $rdn_val =~ s/@.+$//;
    }
    $rdn_val = encode('utf8', $rdn_val);

    if (defined($oconf->{container})) {
        if (defined($oconf->{container}[0]->{login})) {
            $rc = $self->_relogin($pkey, $dn, $oconf->{container}[0]->{login}[0]);
            if (!$self->{multilogin}->{current}) {
                $rc = LDAP_NO_SUCH_OBJECT;
            }

            if ($rc) {
                return $rc;
            }
        }
    }

    my $update_entry;
    if ($obj->{name} eq 'User') {
        $update_entry = VUser::Google::ProvisioningAPI::V2_0::UserEntry->new();
        if (!defined($update_entry)) {
            return LDAP_OTHER;
        }
    }

    while ( @list > 0 && !$rc) {
        my $action = shift @list;
        my $attr    = lc(shift @list);
        my @values;

        while (@list > 0 && $list[0] ne "ADD" && $list[0] ne "DELETE" && $list[0] ne "REPLACE") {
            push(@values, shift @list);
        }

        if (!defined($oconf->{attr}{$attr})) {
            next;
        }

        # can't modify the attribute for rdn
        if ($attr eq $oconf->{rdn}[0]) {
            next;
        }

        my $param = $oconf->{attr}{$attr}->{param}[0];
        if ($param eq 'FullName') {
            next;
        }

        if ($obj->{name} eq 'User' && $param =~ /^(isSuspended|isAdministrator)$/) {
            my $method;
            if ($values[0] =~ /^$oconf->{attr}{$attr}->{true}[0]$/i) {
                if ($param eq 'isSuspended') {
                    $method = 'SuspendUser';
                } elsif ($param eq 'isAdministrator') {
                    $method = 'addAdministrator';
                }
            } elsif (!defined($oconf->{attr}{$attr}->{false}[0]) || $values[0] =~ /^$oconf->{attr}{$attr}->{false}[0]$/i) {
                if ($param eq 'isSuspended') {
                    $method = 'RestoreUser';
                } elsif ($param eq 'isAdministrator') {
                    $method = 'removeAdministrator';
                }
            } else {
                next;
            }

            for (my $i = 0; $i < $RETRY; $i++) {
                my $entry;
                eval "\$entry = \$self->{google}->\$method(\$rdn_val)";
                if ($@) {
                    $self->log(level => 'err', message => "$method $rdn_val failed: $@");
                    $rc = LDAP_OTHER;
                } elsif (!defined($entry)) {
                    my $reason = $self->{google}->{result}{reason};
                    $self->log(level => 'err', message => "$method $rdn_val failed: $reason");
                    if ($reason =~ / 1000 -/) {
                        next;
                    } elsif ($reason =~ / 1301 -/) {
                        $rc = LDAP_NO_SUCH_OBJECT;
                    } else {
                        $rc = LDAP_OPERATIONS_ERROR;
                    }
                } else {
                    $rc = LDAP_SUCCESS;
                }
                last;
            }

            if ($rc) {
                return $rc;
            }
            next;
        }

        if (($obj->{name} eq 'User' && $param eq 'NickName') ||
            ($obj->{name} eq 'EmailList' && $param eq 'Recipient')) {
            for (my $i = 0; $i < $RETRY; $i++) {
                my @add_vals = ();
                my @delete_vals = ();

                if ($action eq "ADD") {
                    push(@add_vals, @values);
                } elsif ($action eq "DELETE" && @values && $values[0]) {
                    push(@delete_vals, @values);
                } else {
                    my @old_vals = ();
                    ($rc, @old_vals) = $self->_getAttrValues($param, $rdn_val);
                    if ($rc) {
                        last;
                    }

                    if ($action eq "DELETE") {
                        push(@delete_vals, @old_vals);
                    } else {
                        foreach my $value (@values) {
                            my $tmpval = $value;
                            $tmpval =~ s/([.*+?\[\]()|\^\$\\])/\\$1/g;

                            my $valmatch = 0;
                            my $i = 0;
                            for ($i = 0; $i < @old_vals; $i++) {
                                if ($old_vals[$i] =~ /^$tmpval$/i) {
                                    $valmatch = 1;
                                    last;
                                }
                            }
                            if ($valmatch) {
                                splice(@old_vals, $i, 1);
                            } else {
                                push(@add_vals, $value);
                            }
                        }
                        @delete_vals = @old_vals;
                    }
                }

                if (@add_vals) {
                    $rc = $self->_addAttrValues($param, $rdn_val, @add_vals);
                }

                if (!$rc && @delete_vals) {
                    $rc = $self->_delAttrValues($param, $rdn_val, @delete_vals);
                }

                if ($rc) {
                    return $rc;
                }
                last;
            }
            next;
        }

        if($action eq "ADD" || $action eq "REPLACE") {
            $update_entry->$param($values[0]);
        }
    }

    if ($rc) {
        return $rc;
    }

    for (my $i = 0; $i < $RETRY; $i++) {
        if ($obj->{name} eq 'User') {
            my $entry;
            eval "\$entry = \$self->{google}->UpdateUser(\$rdn_val, \$update_entry)";
            if ($@) {
                $self->log(level => 'err', message => "Modifying User $rdn_val failed: $@");
                $rc = LDAP_OTHER;
            } elsif (!defined($entry)) {
                my $reason = $self->{google}->{result}{reason};
                $self->log(level => 'err', message => "Modifying User $rdn_val failed: $reason");
                if ($reason =~ / 1000 -/) {
                    next;
                } elsif ($reason =~ / 1301 -/) {
                    $rc = LDAP_NO_SUCH_OBJECT;
                } else {
                    $rc = LDAP_OPERATIONS_ERROR;
                }
            } else {
                $rc = LDAP_SUCCESS;
            }
        } elsif ($obj->{name} ne 'EmailList') {
            return LDAP_UNWILLING_TO_PERFORM;
        }
        last;
    }

    return $rc;
}

=pod

=head2 _objAdd($obj, $pkey, $dn, $entryStr)

Copy the object's file to the temporary file and add the record to it.

=cut

sub _objAdd
{
    my $self = shift;
    my ($obj, $pkeys, $dn,  $entryStr) = @_;
    my $conf = $self->{_config};
    my $oconf = $obj->{conf};
    my $pkey = $self->_getPid($pkeys);
    my $rc = LDAP_SUCCESS;

    if (defined($oconf->{container})) {
        if (defined($oconf->{container}[0]->{login})) {
            $rc = $self->_relogin($pkey, $dn, $oconf->{container}[0]->{login}[0]);
            if (!$self->{multilogin}->{current}) {
                $rc = LDAP_NO_SUCH_OBJECT;
            }
            if ($rc) {
                return $rc;
            }
        }
    }

    my $rdn_val;
    my %values;
    my @nicknames;
    my @recipients;
    my $suspend = 0;
    my $admin = 0;
    foreach my $attr (keys %{$oconf->{attr}}) {
        my $param = $oconf->{attr}{$attr}->{param}[0];
        if ($param eq 'NickName') {
            @nicknames = ($entryStr =~ /^$attr: (.*)$/gmi);
        } elsif ($param eq 'Recipient') {
            @recipients = ($entryStr =~ /^$attr: (.*)$/gmi);
        } else {
            ($values{$param}) = ($entryStr =~ /^$attr: (.*)$/mi);
            if ($param eq 'Email' || $param eq 'EmailList') {
                $values{$param} =~ s/@.+$//;
            } elsif ($param eq 'isSuspended' && $values{isSuspended} =~ /^$oconf->{attr}{$attr}->{true}[0]$/i) {
                $suspend = 1;
            } elsif ($param eq 'isAdministrator' && $values{isAdministrator} =~ /^$oconf->{attr}{$attr}->{true}[0]$/i) {
                $admin = 1;
            }
            if ($oconf->{rdn}[0] eq $attr) {
                $rdn_val = $values{$param};
            }
        }
    }

    for (my $i = 0; $i < $RETRY; $i++) {
        if ($obj->{name} eq 'User') {
            my $entry;
            eval "\$entry = \$self->{google}->CreateUser(\$rdn_val, \$values{GivenName}, \$values{FamilyName}, \$values{Password}, \$values{Quota})";
            if ($@) {
                $self->log(level => 'err', message => "Adding User $values{User} failed: $@");
                $rc = LDAP_OTHER;
            } elsif (!defined($entry)) {
                my $reason = $self->{google}->{result}{reason};
                $self->log(level => 'err', message => "Adding User $values{User} failed: $reason");
                if ($reason =~ / 1000 -/) {
                    next;
                } elsif ($reason =~ / 1300 -/) {
                    $rc = LDAP_ALREADY_EXISTS;
                } else {
                    $rc = LDAP_OPERATIONS_ERROR;
                }
            } else {
                $rc = LDAP_SUCCESS;
            }
        } elsif ($obj->{name} eq 'EmailList') {
            my $entry;
            eval "\$entry = \$self->{google}->CreateEmailList(\$values{EmailList})";
            if ($@) {
                $self->log(level => 'err', message => "Adding mailing list $values{EmailList} failed: $@");
                $rc = LDAP_OTHER;
            } elsif (!defined($entry)) {
                my $reason = $self->{google}->{result}{reason};
                $self->log(level => 'err', message => "Adding mailing list $values{EmailList} failed: $reason");
                if ($reason =~ / 1000 -/) {
                    next;
                } elsif ($reason =~ / 1300 -/) {
                    $rc = LDAP_ALREADY_EXISTS;
                } else {
                    $rc = LDAP_OPERATIONS_ERROR;
                }
            } else {
                $rc = LDAP_SUCCESS;
            }
        } else {
            return LDAP_UNWILLING_TO_PERFORM;
        }
        last;
    }

    if ($suspend && !$rc) {
        for (my $i = 0; $i < $RETRY; $i++) {
            if ($obj->{name} eq 'User') {
                my $entry;
                eval "\$entry = \$self->{google}->SuspendUser(\$values{User})";
                if ($@) {
                    $self->log(level => 'err', message => "Suspending User $values{User} failed: $@");
                    $rc = LDAP_OTHER;
                } elsif (!defined($entry)) {
                    my $reason = $self->{google}->{result}{reason};
                    $self->log(level => 'err', message => "Suspending User $values{User} failed: $reason");
                    if ($reason =~ / 1000 -/) {
                        next;
                    } elsif ($reason =~ / 1301 -/) {
                        $rc = LDAP_NO_SUCH_OBJECT;
                    } else {
                        $rc = LDAP_OPERATIONS_ERROR;
                    }
                } else {
                    $rc = LDAP_SUCCESS;
                }
            }
            last;
        }
    }

    if ($admin && !$rc) {
        for (my $i = 0; $i < $RETRY; $i++) {
            if ($obj->{name} eq 'User') {
                my $entry;
                eval "\$entry = \$self->{google}->addAdministrator(\$values{User})";
                if ($@) {
                    $self->log(level => 'err', message => "Adding administrator privileges to User $values{User} failed: $@");
                    $rc = LDAP_OTHER;
                } elsif (!defined($entry)) {
                    my $reason = $self->{google}->{result}{reason};
                    $self->log(level => 'err', message => "Adding administrator privileges to User $values{User} failed: $reason");
                    if ($reason =~ / 1000 -/) {
                        next;
                    } elsif ($reason =~ / 1301 -/) {
                        $rc = LDAP_NO_SUCH_OBJECT;
                    } else {
                        $rc = LDAP_OPERATIONS_ERROR;
                    }
                } else {
                    $rc = LDAP_SUCCESS;
                }
            }
            last;
        }
    }

    if (@nicknames && !$rc) {
        $rc = $self->_addAttrValues('NickName', $values{User}, @nicknames);
    }

    if (@recipients && !$rc) {
        $rc = $self->_addAttrValues('Recipient', $values{EmailList}, @recipients);
    }

    return $rc;
}

=pod

=head2 _objDelete($dn)

Copy the object's file from which the appropriate record is deleted to the temporary file.

=cut

sub _objDelete
{
    my $self = shift;
    my ($obj, $pkeys, $dn) = @_;
    my $conf = $self->{_config};
    my $oconf = $obj->{conf};
    my $pkey = $self->_getPid($pkeys);
    my $rc = LDAP_SUCCESS;

    my ($rdn_val) = ($dn =~ /^[^=]+=([^,]+),/);
    if ($oconf->{rdn}[0] eq 'mail') {
        $rdn_val =~ s/@.+$//;
    }
    $rdn_val = encode('utf8', $rdn_val);

    if (defined($oconf->{container})) {
        if (defined($oconf->{container}[0]->{login})) {
            $rc = $self->_relogin($pkey, $dn, $oconf->{container}[0]->{login}[0]);
            if (!$self->{multilogin}->{current}) {
                $rc = LDAP_NO_SUCH_OBJECT;
            }
            if ($rc) {
                return $rc;
            }
        }
    }

    if ($obj->{name} eq 'User') {
        if (defined($conf->{admin})) {
            if ($conf->{admin}[0] eq $rdn_val) {
                return LDAP_UNWILLING_TO_PERFORM;
            }
        } elsif (defined($self->{multilogin})) {
            if ($self->{multilogin}{$self->{multilogin}->{current}}->{admin} eq $rdn_val) {
                return LDAP_UNWILLING_TO_PERFORM;
            }
        }

        my @nicknames;
        ($rc, @nicknames) = $self->_getAttrValues('NickName', $rdn_val);
        if ($rc) {
            return $rc;
        }

        if (@nicknames) {
            $rc = $self->_delAttrValues('NickName', $rdn_val, @nicknames);
            if ($rc) {
                return $rc;
            }
        }
    } elsif ($obj->{name} eq 'EmailList') {
        $rdn_val =~ s/@.+$//;
        my @recipients;
        ($rc, @recipients) = $self->_getAttrValues('Recipient', $rdn_val);
        if ($rc) {
            return $rc;
        }

        if (@recipients) {
            $rc = $self->_delAttrValues('Recipient', $rdn_val, @recipients);
            if ($rc) {
                return $rc;
            }
        }
    }

    for (my $i = 0; $i < $RETRY; $i++) {
        if ($obj->{name} eq 'User') {
            eval "\$rc = \$self->{google}->DeleteUser(\$rdn_val)";
            if ($@) {
                $self->log(level => 'err', message => "Deleting User $rdn_val failed: $@");
                $rc = LDAP_OTHER;
            } elsif (!$rc) {
                my $reason = $self->{google}->{result}{reason};
                $self->log(level => 'err', message => "Deleting User $rdn_val failed: $reason");
                if ($reason =~ / 1000 -/) {
                    next;
                } elsif ($reason =~ / 1301 -/) {
                    $rc = LDAP_NO_SUCH_OBJECT;
                } else {
                    $rc = LDAP_OPERATIONS_ERROR;
                }
            } else {
                $rc = LDAP_SUCCESS;
            }
        } elsif ($obj->{name} eq 'EmailList') {
            eval "\$rc = \$self->{google}->DeleteEmailList(\$rdn_val)";
            if ($@) {
                $self->log(level => 'err', message => "Deleting mailing list $rdn_val failed: $@");
                $rc = LDAP_OTHER;
            } elsif (!$rc) {
                my $reason = $self->{google}->{result}{reason};
                $self->log(level => 'err', message => "Deleting mailing list $rdn_val failed: $reason");
                if ($reason =~ / 1000 -/) {
                    next;
                } elsif ($reason =~ / 1301 -/) {
                    $rc = LDAP_NO_SUCH_OBJECT;
                } else {
                    $rc = LDAP_OPERATIONS_ERROR;
                }
            } else {
                $rc = LDAP_SUCCESS;
            }
        } else {
            return LDAP_UNWILLING_TO_PERFORM;
        }
        last;
    }

    return $rc;
}

sub _getAttrValues
{
    my $self = shift;
    my ($oname, $key) = @_;
    my @values = ();
    my $rc;

    for (my $i = 0; $i < $RETRY; $i++) {
        if ($oname eq 'NickName') {
            my @nicknames;
            eval "\@nicknames = \$self->{google}->RetrieveNicknames(\$key)";
            if ($@) {
                $self->log(level => 'err', message => "Searching nickname of $key failed: $@");
                $rc = LDAP_OTHER;
            } elsif (!defined(@nicknames)) {
                my $reason = $self->{google}->{result}{reason};
                if (!$reason) {
                    $rc = LDAP_SUCCESS;
                } else {
                    $self->log(level => 'err', message => "Searching nickname of $key failed: $reason");
                    if ($reason =~ / 1000 -/) {
                        next;
                    } elsif ($reason =~ / 1301 -/) {
                        $rc = LDAP_NO_SUCH_OBJECT;
                    } else {
                        $rc = LDAP_OPERATIONS_ERROR;
                    }
                }
            } else {
                foreach my $nickname (@nicknames) {
                    my $value = $nickname->Nickname();
                    if (!Encode::is_utf8($value)) {
                        $value = decode('utf8', $value);
                    }
                    push(@values, $value.'@'.$self->{multilogin}->{current});
                }
                $rc = LDAP_SUCCESS;
            }
        } elsif ($oname eq 'Recipient') {
            $key =~ s/@.+$//;
            my @recipients;
            eval "\@recipients = \$self->{google}->RetrieveAllRecipients(\$key)";
            if ($@) {
                $self->log(level => 'err', message => "Searching receipients of $key failed: $@");
                $rc = LDAP_OTHER;
            } elsif (!defined(@recipients)) {
                my $reason = $self->{google}->{result}{reason};
                if (!$reason) {
                    $rc = LDAP_SUCCESS;
                } else {
                    $self->log(level => 'err', message => "Searching recipients of $key failed: $reason");
                    if ($reason =~ / 1000 -/) {
                        next;
                    } elsif ($reason =~ / 1301 -/) {
                        $rc = LDAP_NO_SUCH_OBJECT;
                    } else {
                        $rc = LDAP_OPERATIONS_ERROR;
                    }
                }
            } else {
                foreach my $recipient (@recipients) {
                    my $value = $recipient->Who();
                    if (!Encode::is_utf8($value)) {
                        $value = decode('utf8', $value);
                    }
                    push(@values, $value);
                }
                $rc = LDAP_SUCCESS;
            }
        }
        last;
    }

    return ($rc, @values);
}

sub _addAttrValues
{
    my $self = shift;
    my ($oname, $key, @values) = @_;
    my $rc = LDAP_SUCCESS;

    foreach my $value (@values) {
        for (my $i = 0; $i < $RETRY; $i++) {
            if ($oname eq 'NickName') {
                $value =~ s/@.+$//;
                my $nickname;
                eval "\$nickname = \$self->{google}->CreateNickname(\$key, \$value)";
                if ($@) {
                    $self->log(level => 'err', message => "Adding nickname $value to user $key failed: $@");
                    $rc = LDAP_OTHER;
                } elsif (!defined($nickname)) {
                    my $reason = $self->{google}->{result}{reason};
                    $self->log(level => 'err', message => "Adding nickname $value to user $key failed: $reason");
                    if ($reason =~ / 1000 -/) {
                        next;
                    } elsif ($reason =~ / 1300 -/) {
                        $rc = LDAP_TYPE_OR_VALUE_EXISTS;
                    } elsif ($reason =~ / 1301 -/) {
                        $rc = LDAP_NO_SUCH_OBJECT;
                    } else {
                        $rc = LDAP_OPERATIONS_ERROR;
                    }
                } else {
                    $rc = LDAP_SUCCESS;
                }
            } elsif ($oname eq 'Recipient') {
                my $recipient;
                eval "\$recipient = \$self->{google}->AddRecipientToEmailList(\$value, \$key)";
                if ($@) {
                    $self->log(level => 'err', message => "Adding recipient $value to mailing list $key failed: $@");
                    $rc = LDAP_OTHER;
                } elsif (!defined($recipient)) {
                    my $reason = $self->{google}->{result}{reason};
                    $self->log(level => 'err', message => "Adding recipient $value to mailing list $key failed: $reason");
                    if ($reason =~ / 1000 -/) {
                        next;
                    } else {
                        $rc = LDAP_OPERATIONS_ERROR;
                    }
                } else {
                    $rc = LDAP_SUCCESS;
                }
            }
            last;
        }
    }

    return $rc;
}

sub _delAttrValues
{
    my $self = shift;
    my ($oname, $key, @values) = @_;
    my $rc = LDAP_SUCCESS;

    foreach my $value (@values) {
        for (my $i = 0; $i < $RETRY; $i++) {
            if ($oname eq 'NickName') {
                $value =~ s/@.+$//;
                eval "\$rc = \$self->{google}->DeleteNickname(\$value)";
                if ($@) {
                    $self->log(level => 'err', message => "Deleting nickname $value failed: $@");
                    $rc = LDAP_OTHER;
                } elsif ($rc != 1) {
                    my $reason = $self->{google}->{result}{reason};
                    $self->log(level => 'err', message => "Deleting nickname $value failed: $reason");
                    if ($reason =~ / 1000 -/) {
                        next;
                    } elsif ($reason =~ / 1301 -/) {
                        $rc = LDAP_NO_SUCH_ATTRIBUTE;
                    } else {
                        $rc = LDAP_OPERATIONS_ERROR;
                    }
                } else {
                    $rc = LDAP_SUCCESS;
                }
            } elsif ($oname eq 'Recipient') {
                eval "\$rc = \$self->{google}->RemoveRecipientFromEmailList(\$value, \$key)";
                if ($@) {
                    $self->log(level => 'err', message => "Deleting recipient $value from mailing list $key failed: $@");
                    $rc = LDAP_OTHER;
                } elsif ($rc != 1) {
                    my $reason = $self->{google}->{result}{reason};
                    $self->log(level => 'err', message => "Deleting recipient $value from mailing list $key failed: $reason");
                    if ($reason =~ / 1000 -/) {
                        next;
                    } elsif ($reason =~ / 1301 -/) {
                        $rc = LDAP_NO_SUCH_ATTRIBUTE;
                    } else {
                        $rc = LDAP_OPERATIONS_ERROR;
                    }
                } else {
                    $rc = LDAP_SUCCESS;
                }
            }
            last;
        }
    }

    return $rc;
}

sub _buildObjectEntry
{
    my $self = shift;
    my ($obj, $suffix, $entry) = @_;
    my $oconf = $obj->{conf};
    my $rdn_val;

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

    if ($obj->{name} eq 'User') {
        $rdn_val = $entry->User();
        if ($oconf->{rdn}[0] eq 'mail') {
            $rdn_val .= '@'.$self->{multilogin}->{current};
        }
    } elsif ($obj->{name} eq 'EmailList') {
        $rdn_val = $entry->EmailList().'@'.$self->{multilogin}->{current};
    }

    my $entryStr = "dn: ".$oconf->{rdn}[0]."=$rdn_val,$suffix\n";

    foreach my $oc (@{$oconf->{oc}}) {
        $entryStr = $entryStr."objectclass: $oc\n";
    }

    foreach my $attr (keys %{$oconf->{attr}}) {
        my $param = $oconf->{attr}{$attr}->{param}[0];
        my @values = ();

        if ($param eq 'rdn') {
            next;
        } elsif ($param eq 'NickName') {
            my $rc;
            ($rc, @values) = $self->_getAttrValues('NickName', $rdn_val);
        } elsif ($param eq 'FullName') {
            $values[0] = $entry->FamilyName()." ".$entry->GivenName();
        } elsif ($param eq 'Email') {
            $values[0] = $entry->User().'@'.$self->{multilogin}->{current};
        } elsif ($param eq 'EmailList') {
            $values[0] = $entry->EmailList().'@'.$self->{multilogin}->{current};
        } elsif ($param eq 'Recipient') {
            my $rc;
            ($rc, @values) = $self->_getAttrValues('Recipient', $rdn_val);
        } else {
            $values[0] = $entry->$param;
        }

        if ($param =~ /^(isSuspended|isAdministrator)$/) {
            my $value;
            if ($values[0]) {
                $value = $oconf->{attr}{$attr}->{true}[0];
            } elsif (defined($oconf->{attr}{$attr}->{false})) {
                $value = $oconf->{attr}{$attr}->{false}[0];
            }
            if ($value) {
                $entryStr = "$entryStr$attr: $value\n";
            }
        } elsif (@values) {
            foreach my $value (@values) {
                if ($value) {
                    $entryStr = "$entryStr$attr: $value\n";
                }
            }
        }
    }

    return $entryStr;
}

sub _relogin
{
    my $self = shift;
    my ($pkey, $dn, $login) = @_;
    my $domain;
    my $admin;
    my $passwd;

    if (!$pkey || !defined($login->{search})) {
        return LDAP_OTHER;
    }

    my ($pdn) = ($dn =~ /([^,=]=$pkey.*),$self->{suffix}$/i);

    my $search = $login->{search}[0];
    if ($search->{type} eq 'lism') {
        my $lism = $self->{lism};
        my $base = $search->{base};
        my $scope = 2;
        my $filter = '(objectClass=*)';

        if (defined($search->{scope})) {
            if ($search->{scope} eq 'base') {
                $scope = 0;
            } elsif ($login->{scope} eq 'one') {
                $scope = 1;
            }
        }
        if (defined($search->{filter})) {
            $filter = $search->{filter};
        }
        $base =~ s/\%c/$pkey/g;
        $base =~ s/\%p/$pdn/g;
        $filter =~ s/\%c/$pkey/g;

        my ($rc, @entries) = $lism->search($base, $scope, 1, 0, 0, $filter);
        if ($rc && $rc != LDAP_NO_SUCH_OBJECT) {
            $self->log(level => 'err', message => "Searching administrator account in LDAP failed: base=\"$base\" filter=\"$filter\" rc=\"$rc\"");
            return $rc;
        } elsif (!@entries) {
            $self->{multilogin}->{current} = undef;

            return LDAP_SUCCESS;
        }

        ($domain) = ($entries[0] =~ /^$search->{domain}: (.+)$/mi);
        ($admin) = ($entries[0] =~ /^$search->{admin}: (.+)$/mi);
        ($passwd) = ($entries[0] =~ /^$search->{passwd}: (.+)$/mi);
        if (defined($search->{decrypt})) {
            my $decrypt = $search->{decrypt};
            $decrypt =~ s/\%c/$pkey/;
            $decrypt =~ s/\%u/$admin/;
            $decrypt =~ s/\%s/$passwd/;
            $passwd = $self->_doFunction($decrypt);
            if (!defined($passwd)) {
                $self->log(level => 'err', message => "Decrypt of $domain password failed");
                return LDAP_OTHER;
            }
        }
    }

    if (!$domain) {
        $self->{multilogin}->{current} = undef;
    } else {
        $self->{multilogin}->{current} = encode('utf8', $domain);
        $self->{multilogin}{$domain}->{admin} = encode('utf8', $admin);
        $self->{multilogin}{$domain}->{passwd} = encode('utf8', $passwd);
        $self->{google} = $self->{multilogin}{$domain}->{google};

        if ($self->_login()) {
            return LDAP_OTHER;
        }
    }

    return LDAP_SUCCESS;
}

sub _login
{
    my $self = shift;
    my $domain = $self->{multilogin}->{current};
    my $rc = 0;

    if (!$domain) {
        return 0;
    }

    my $login = $self->{multilogin}{$domain};
    if (defined($self->{google})) {
        eval "\$rc = \$self->{google}->IsAuthenticated()";
        if ($@) {
            $self->log(level => 'err', message => "Authentication to $domain failed for ".$login->{admin}.": $@");
            undef($self->{google});
        } elsif ($rc) {
            return 0;
        } else {
            $self->log(level => 'err', message => "Authentication to $domain failed for ".$login->{admin}.": ".$self->{google}->{result}{reason});
            undef($self->{google});
        }
    }

    $login->{google} = VUser::Google::ProvisioningAPI->new($domain, $login->{admin}, $login->{passwd}, '2.0');
    eval "\$rc = \$login->{google}->IsAuthenticated";
    if ($@) {
        $self->log(level => 'alert', message => "Authentication to $domain failed for ".$login->{admin}.": $@");
        return -1;
    } elsif (!$rc) {
        $self->log(level => 'alert', message => "Authentication to $domain failed for ".$login->{admin}.": ".$login->{google}->{result}{reason});
        return -1;
    }
    $self->{google} = $login->{google};

    return 0;
}

=head1 SEE ALSO

L<LISM>,
L<LISM::Storage>

=head1 AUTHOR

Kaoru Sekiguchi, <sekiguchi.kaoru@secioss.co.jp>

=head1 COPYRIGHT AND LICENSE

Copyright (C) 2008 by SECIOSS Corporation

=cut

1;
