richard: server/perl-kolab/Kolab-LDAP LDAP.pm.in, NONE, 1.1 Makefile.PL, 1.1, NONE

cvs at intevation.de cvs at intevation.de
Sat Dec 17 22:45:34 CET 2005


Author: richard

Update of /kolabrepository/server/perl-kolab/Kolab-LDAP
In directory doto:/tmp/cvs-serv16585/Kolab-LDAP

Added Files:
	LDAP.pm.in 
Removed Files:
	Makefile.PL 
Log Message:
perl-kolab is now autoconfiscated using the regular autoconf
and automake machinery.  Discarded autoperl machinery.

Much work has been done by Marcus Huwe.



--- NEW FILE: LDAP.pm.in ---
package Kolab::LDAP;

##
##  Copyright (c) 2005  Klaralvdalens Datakonsult AB
##  Copyright (c) 2003  Code Fusion cc
##
##    Writen by Stuart Bingë <s.binge at codefusion.co.za>
##              Steffen Hansen <steffen at klaralvdalens-datakonsult.se>
##
##  This  program is free  software; you can redistribute  it and/or
##  modify it  under the terms of the GNU  General Public License as
##  published by the  Free Software Foundation; either version 2, or
##  (at your option) any later version.
##
##  This program is  distributed in the hope that it will be useful,
##  but WITHOUT  ANY WARRANTY; without even the  implied warranty of
##  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
##  General Public License for more details.
##
##  You can view the  GNU General Public License, online, at the GNU
##  Project's homepage; see <http://www.gnu.org/licenses/gpl.html>.
##

use 5.008;
use strict;
use warnings;
use UNIVERSAL;
use Net::LDAP qw( LDAP_SUCCESS LDAP_PROTOCOL_ERROR LDAP_REFERRAL );
use Net::LDAPS;
use Net::LDAP::Util;
use DB_File;
use Kolab;
use Kolab::Util;
use Kolab::Cyrus;
use Kolab::DirServ;
use vars qw(%uid_db %gyard_db %newuid_db %gyard_ts_db);

require Exporter;

our @ISA = qw(Exporter);

our %EXPORT_TAGS = (
    'all' => [ qw(
        &startup
        &shutdown
        &create
        &destroy
        &ensureAsync
        &isObject
        &isDeleted
        &createObject
        &deleteObject
        &sync
    ) ]
);

our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );

our @EXPORT = qw(

);

our $VERSION = '0.9';

sub startup
{
    Kolab::log('L', 'Starting up');

    Kolab::log('L', 'Opening mailbox uid cache DB');

    if (!dbmopen(%uid_db, "@kolab_statedir@/mailbox-uidcache.db", 0666)) {
        Kolab::log('L', 'Unable to open mailbox uid cache DB', KOLAB_ERROR);
        exit(1);
    }

    Kolab::log('L', 'Opening graveyard uid/timestamp cache DB');

    if (!dbmopen(%gyard_db, "@kolab_statedir@/graveyard-uidcache.db", 0666)) {
        Kolab::log('L', 'Unable to open graveyard uid cache DB', KOLAB_ERROR);
        exit(1);
    }

    if (!dbmopen(%gyard_ts_db, "@kolab_statedir@/graveyard-tscache.db", 0666)) {
        Kolab::log('L', 'Unable to open graveyard timestamp cache DB', KOLAB_ERROR);
        exit(1);
    }
}

sub shutdown
{
    Kolab::log('L', 'Shutting down');

    dbmclose(%uid_db);
    dbmclose(%gyard_db);
}

sub create
{
    my $ip = shift;
    my $pt = shift;
    my $dn = shift;
    my $pw = shift;
    my $as = shift || 0;

    Kolab::log('L', "Connecting to LDAP server `$ip:$pt'");

    my $ldap;
    if( $pt == 636 ) {
      # Use SSL
      $ldap = Net::LDAPS->new(
        $ip,
        port    => $pt,
        version => 3,
        timeout => 20,
        async   => $as,
	verify => 'none',
	onerror => 'undef'
      );
    } else {
      $ldap = Net::LDAP->new(
        $ip,
        port    => $pt,
        version => 3,
        timeout => 20,
        async   => $as,
	onerror => 'undef'
      );
    }
    if (!$ldap) {
        Kolab::log('L', "Unable to connect to LDAP server `$ip:$pt'", KOLAB_ERROR);
        if ($as) { return 0; } else { exit(1); }
    }

    Kolab::log('L', "Binding to `$dn'");
    my $ldapmesg = $ldap->bind(
        $dn,
        password    => $pw
    );
    if ($ldapmesg->code) {
        Kolab::log('L', "Unable to bind to `$dn', LDAP Error = `" . $ldapmesg->error . "'", KOLAB_ERROR);
        if ($as) { return 0; } else { exit(1); }
    }

    return $ldap;
}

sub destroy
{
    my $ldap = shift;

    if (defined($ldap) && ($ldap->isa('Net::LDAP') || $ldap->isa('Net::LDAPS'))) {
        $ldap->abandon;
        $ldap->unbind;
        $ldap->disconnect;
    }
}

sub ensureAsync
{
    my $ldap = shift || 0;

    if ($ldap && !$ldap->async) {
        Kolab::log('L', 'LDAP operations are not asynchronous', KOLAB_ERROR);
        exit(1);
    }

    Kolab::log('L', 'LDAP operations are asynchronous', KOLAB_DEBUG);
}

sub isObject
{
    my $object = shift;
    my $class = shift;

    my $classes = $object->get_value('objectClass', asref => 1);
    return 0 if !defined($classes);
    foreach my $oc (@$classes) {
        if ($oc =~ /$class/i) {
            return 1;
        }
    }
    return 0;
}

sub isDeleted
{
    my $object = shift;
    my $p = shift || 'user';
    my $del = $object->get_value($Kolab::config{$p . '_field_deleted'}, asref => 1 );
    #foreach (@$del) {
    #  return 1 if lc($_) eq lc($Kolab::config{'fqdnhostname'});
    #}
    #return 0;
    return $#$del > 0;
}

# Map from Kolab ACLs to Cyrus ACLs
sub mapAcls {
  my $acls = shift;
  my $sf = shift || 0;
  my @acls = map {
    my ($uid,$perm) = split(/\s+/,$_,2);
    Kolab::log('L', "Kolab::LDAP::mapAcls() uid=$uid perm=$perm", KOLAB_DEBUG);
    my $post = 0;
    if( $perm =~ /(.*)\/post/ ) {
      $perm = $1;
      $post = 1;
    }
    Kolab::log('L', "Kolab::LDAP::mapAcls() uid=$uid perm=$perm post=$post", KOLAB_DEBUG);
    if( lc $perm eq 'none' ) { $_ = "$uid none"; }
    elsif( lc $perm eq 'post' ) { $_ = "$uid p"; }
    elsif( lc $perm eq 'read' ) { $_ = "$uid lrs"; }
    elsif( lc $perm eq 'read anon' ) { $_ = "$uid lr"; }
    elsif( lc $perm eq 'read hidden' ) { $_ = "$uid rs"; }
    elsif( lc $perm eq 'append' ) { $_ = "$uid lrsip"; }
    elsif( lc $perm eq 'write' ) { if( $sf ) { $_ = "$uid lrsiwdp"; } else { $_ = "$uid lrsiwcdp"; } }
    elsif( lc $perm eq 'all' ) { if( $sf ) { $_ = "$uid lrsiwdap"; } else { $_ = "$uid lrsiwcdap"; } }
    else { $_ = "$uid $perm"; } # passthrough
    if( $post ) { $_ .= 'p'; }
    Kolab::log('L', "Kolab::LDAP::mapAcls() acl=$_", KOLAB_DEBUG);
  } @$acls;
  if( $sf ) {
    # Do we need to push admin rights for manager?
  }
  Kolab::log('L', "Kolab::LDAP::mapAcls() acls=".join(", ", @$acls), KOLAB_DEBUG);
  return $acls;
}

sub createObject
{
    my $ldap = shift;
    my $cyrus = shift;
    my $object = shift;
    my $sync = shift || 0;
    my $p = shift || 'user';
    my $doacls = shift || 0;
    my $objuidfield = shift || ($p eq 'user' ? 'mail' : ($p eq 'sf' ? 'cn' : ''));

    Kolab::log('L', "Kolab::LDAP::createObject() called with obj uid field `$objuidfield' for obj type `$p'", KOLAB_DEBUG);

    # No action for groups or external
    return if( $objuidfield eq '' );
    my $uid = lc(trim($object->get_value($objuidfield))) || 0;
    return unless $uid;
    return if( $objuidfield eq 'mail' && !$object->get_value('uid') );

    my $kolabhomeserver = lc($object->get_value('kolabhomeserver'));
    my $islocal = 1;
    my $del = $object->get_value($Kolab::config{$p . '_field_deleted'}, asref => 1);
    if( ref($del) eq 'ARRAY' && @$del > 0 ) {
      Kolab::log('L', "Kolab::LDAP::createObject() skipping object ".lc($object->get_value($objuidfield))
		 ." because it is deleted");
      return;
    }
    if( $kolabhomeserver && $kolabhomeserver ne lc($Kolab::config{'fqdnhostname'}) ) {
      if( $p eq 'sf' ) {
	# Dont create shared folders on other hosts than it's kolabhomeserver
	return;
      }
      Kolab::log('L', "Kolab::LDAP::createObject() for other server $kolabhomeserver. TODO: Create referral or something, for now we just create ", KOLAB_DEBUG);
      $islocal = 0;
    }

    # Intermediate multidomain support:
    # We accept domain encoded in CN...
    if( $p eq 'sf' && index( $uid, '@' ) < 0 ) {
      # We have to create shared folders
      # with names shared.<fldrname>@<domain>
      my @dcs = split(/,/,$object->dn());
      my @dn;
      while( pop( @dcs ) =~ /dc=(.*)/ ) {
	push(@dn, $1);
      }
      if( $#dn > 0 ) { $uid .= '@'.join('.',reverse(@dn)); }
    }
    if (!$uid) {
        Kolab::log('L', "Kolab::LDAP::createObject() called with null id attribute `$objuidfield', returning", KOLAB_DEBUG);
        return;
    }

    Kolab::log('L', "Synchronising object `$uid'", KOLAB_DEBUG);

    my $guid = $object->get_value($Kolab::config{$p . '_field_guid'});
    Kolab::log('L', "GUID attribute `" . $Kolab::config{$p . '_field_guid'} . "' is `$guid'", KOLAB_DEBUG);
    my $olduid = $uid_db{$guid} || '';
    if ($olduid) {
        # We have records of the object
        $newuid_db{$guid} = $olduid if ($sync);
        if ($olduid ne $uid) {
            # The mailbox changed; bitch
            Kolab::log('L', "Object `$uid' already exists as `$olduid'; refusing to create", KOLAB_WARN);
        }
        # Nothing changed; nothing to do
        #Kolab::DirServ::genericRequest($object, "modify alias");
    } else {
        # No official records - check the graveyard
        my $oldgyarduid = $gyard_db{$guid} || '';
        if ($oldgyarduid) {
            # The object needs to be resurrected!
            if ($oldgyarduid ne $uid) {
                Kolab::log('L', "Resurrected object `$uid' already exists as `$oldgyarduid'; refusing to create", KOLAB_WARN);
            } else {
                Kolab::log('L', "Object `$uid' has been resurrected", KOLAB_DEBUG);
            }

            # Remove the object from the graveyard
            if ($sync) { $newuid_db{$guid} = $oldgyarduid; } else { $uid_db{$guid} = $oldgyarduid; }
            delete $gyard_db{$guid};
            delete $gyard_ts_db{$guid};
        } else {
            Kolab::log('L', "Creating user `$uid' corresponding to GUID `$guid'", KOLAB_DEBUG);
            # We have a object that we have no previous record of, so create everything
            if ($sync) { $newuid_db{$guid} = $uid; } else { $uid_db{$guid} = $uid; }
            Kolab::Cyrus::createMailbox($cyrus, $uid, ($p eq 'sf' ? 1 : 0));
	    if( $p ne 'sf' && !$islocal ) {
	      # Hide user mailboxes on other servers
	      Kolab::Cyrus::setACL($cyrus,$uid,0, ["$uid rswipcda"]);
	    } elsif( $p ne 'sf' ) {
	      # Deal with group and resource accounts
	      my $edn = Net::LDAP::Util::ldap_explode_dn($object->dn(), casefold=>'lower' );
	      my $gcn = $edn->[1]->{'cn'};
	      if( $gcn eq 'groups' || $gcn eq 'resources' ) {
		# We need to give the calendar user access to 
		# the groups/resources folder.
		# TODO: Don't hardcode username
		Kolab::log('L', "Detected group or resource account, adding ACL for calendar", KOLAB_ERROR );
		Kolab::Cyrus::setACL($cyrus,$uid,0, ["$uid all", 
						     'calendar@'.$Kolab::config{'postfix-mydomain'}
						     .' all']);		
	      }
	    }
#	    if( $p ne 'sf' ) {
#	      # Create FB dir for user
#	      my $fbdir = '@webserver_document_root@/freebusy/'.$uid;
#	      mkdir( $fbdir, 0750);
#	      chown( $Kolab::config{'kolab_n_uid'},$Kolab::config{'kolab_n_gid'},$fbdir );
#	    }
            Kolab::DirServ::genericRequest($object, "new alias") if $p eq 'user';
        }
    }

    if ($doacls) {
        my $acls = $object->get_value('acl', 'asref' => 1);
        Kolab::Cyrus::setACL($cyrus, $uid, ($p eq 'sf' ? 1 : 0), mapAcls( $acls, ($p eq 'sf' ? 1:0)));
    }

    my $quota = $object->get_value($Kolab::config{$p . '_field_quota'});
    defined($quota) or ($quota = 0);
    Kolab::Cyrus::setQuota($cyrus, $uid, $quota*1024, ($p eq 'sf' ? 1 : 0));
}

sub createMasterLDAP {
  my $uri = $Kolab::config{'ldap_master_uri'};

  my $masterldap = Net::LDAP->new(
	 $uri,
	 version => 3,
	 timeout => 20,
	 verify => 'none',
	 onerror => 'undef' );
  if( defined( $masterldap ) ) {
    my $mesg = $masterldap->bind(
				 $Kolab::config{'bind_dn'},
				 password    => $Kolab::config{'bind_pw'});
    if ($mesg->code) {
      Kolab::log('L', "Unable to bind to `$uri', LDAP Error = `"
		 .$mesg->error."'", KOLAB_ERROR);
      undef( $masterldap );
    }
  } else {
    Kolab::log('L', "Unable to connect to `$uri'"
	       , KOLAB_ERROR);
  }
  return $masterldap;
}

sub deleteObject
{
    # This should only ever be called if the object is specifically flagged for
    # deletion, as we nuke the mailbox
    #
    # The graveyard code will handle the case of an object `going missing'.
    
    my $ldap = shift;
    my $cyrus = shift;
    my $object = shift;
    my $remfromldap = shift || 0;
    my $p = shift || 'user';

    if ($remfromldap) {
        my $dn = $object->dn;
	my $del = $object->get_value($Kolab::config{$p . '_field_deleted'}, asref => 1);
	my $masterldap;
	if( lc($Kolab::config{'is_master'}) eq 'true' ) {
	  # We are the master, just go ahead
	  $masterldap = $ldap;
	} else {
	  $masterldap = createMasterLDAP;
	}
	if( !defined( $masterldap ) ) {
	  # Problem here, could not connect to master!
	  Kolab::log('L', "Unable to remove DN `$dn', master LDAP server not available", KOLAB_WARN);
	  return 0;
	}
	if( lc ($Kolab::config{'is_master'}) eq 'true' && ref($del) eq 'ARRAY' && scalar(@$del) == 1 ) {
	    # Ok we are the last one and the master
	    if( $Kolab::config{'kolab_remove_objectclass'} ) {
		# Remove the kolab-related objectClasses
		# Some people find it useful to integrate Kolab 
		# with an existing LDAP database and when a Kolab
		# object is to be deleted, it should just remove
		# the Kolab stuff and leave the rest of the object
		# in the database.
		#
		# This is what we do here. 
		# Warning: All attributes in the kolab-related 
		# objectclasses will be deleted!
		#
		# PENDING(steffen): Only remove attributes that _have_ to
		# be removed.
		Kolab::log('L', "Removing Kolab objectClasses from DN `$dn'");
		my $schema = $masterldap->schema( $dn );
                # PENDING(steffen): Dont hardcode objectClasses
		foreach my $c qw(kolabInetOrgPerson kolabGroupOfNames) {
		    my @may = map $_->{name}, $schema->may($c);
		    my @must = map $_->{name}, $schema->must($c);
		    foreach my $attr (@must, at may,split(' ',$Kolab::config{'kolab_remove_attributes'})) {
			# Remove attributes
			Kolab::log('L', "Removing attribute $attr", KOLAB_WARN);
			my $mesg = $masterldap->modify( $dn,
							delete => $attr );
			if ($mesg && $mesg->code ) {
			    Kolab::log('L', "Unable to remove attribute $attr from DN `$dn': ".$mesg->error, KOLAB_WARN);
			}
		    }
		    # Remove objectClass
		    my $mesg = $masterldap->modify( $dn,
						    delete => { 'objectClass' => $c } );
		    if ($mesg && $mesg->code ) {
			Kolab::log('L', "Unable to remove Kolab objectClas $_ from DN `$dn': ".$mesg->error, KOLAB_WARN);
		    }
		}
	    } else {
		# Default behaviour, delete the object
		Kolab::log('L', "Removing DN `$dn'");
		my $mesg = $masterldap->delete($dn);
		if ($mesg && $mesg->code ) {
		    Kolab::log('L', "Unable to remove DN `$dn': ".$mesg->error, KOLAB_WARN);
		}
	    }
	} elsif( lc ($Kolab::config{'is_master'}) eq 'false' ) {
	  # Just remove us from the kolabdeleteflag
	  # master does not perform this step as it should 
	  # be the last to delete and remove the object
	  Kolab::log('L', "Removing ".$Kolab::config{'fqdnhostname'}." from ".
		     $Kolab::config{$p . '_field_deleted'}." in `$dn'");
	  my $mesg = $masterldap->modify( $dn, delete =>
					  { $Kolab::config{$p . '_field_deleted'} =>
					    $Kolab::config{'fqdnhostname'} } );
	  if ($mesg && $mesg->code) {
	    Kolab::log('L', "Unable to remove ".$Kolab::config{'fqdnhostname'}
		       ." from kolabdeleteflag in `$dn': ".$mesg->error, KOLAB_WARN);
	  }
	}
	if( $ldap != $masterldap ) {
	  # Disconnect from master if we are the slave
	  $masterldap->disconnect;
	}
    }

    my $guid = $object->get_value($Kolab::config{$p . '_field_guid'});
    my $uid = $uid_db{$guid} || 0;
    if (!$uid) {
        Kolab::log('L', 'Deleted object not found in mboxcache, returning', KOLAB_DEBUG);
        return;
    }

    Kolab::DirServ::genericRequest($object, "remove alias") if $p eq 'user';

    Kolab::Cyrus::deleteMailbox($cyrus, $uid, ($p eq 'sf' ? 1 : 0));
#    if( $p ne 'sf' ) {
#      # Create FB dir for user
#      my $fbdir = '@webserver_document_root@/freebusy/'.$uid;
#      system("rm -rf \"$fbdir\"" );
#    }
    delete $uid_db{$guid};
    return 1;
}

sub sync
{
    Kolab::log('L', 'Synchronising');

    my $cyrus = Kolab::Cyrus::create;
    if( !$cyrus ) {
      # We could not connect, bail out for now
      return 0;
    }
    %newuid_db = ();

    syncBasic($cyrus, 'user', '', 0);
    syncBasic($cyrus, 'sf', '', 1);
    syncBasic($cyrus, 'group', '', 0);

    # Check that all mailboxes correspond to LDAP objects
    Kolab::log('L', 'Synchronising mailboxes');

    my @mailboxes = $cyrus->list('*');
    my %objects;
    my $mailbox;
    foreach $mailbox (@mailboxes) {
        my $u = ${@{$mailbox}}[0];
        $u =~ /user[\/\.]([^\/]*)\/?.*/;
        $objects{$1} = 1 if ($1);
    }

    my $guid;
    foreach $guid (keys %newuid_db) {
        delete $objects{$newuid_db{$guid}} if (exists $objects{$newuid_db{$guid}});
    }

    # Any mailboxes left should be sent to the graveyard; these are mailboxes
    # without a corresponding LDAP object, yet we were never informed of their
    # deletion, i.e. either we missed the deletion notification or there was
    # an error when iterating through the objects (Lost connection, invalid DNs)
    foreach $guid (keys %uid_db) {
        if (exists $objects{$uid_db{$guid}}) {
            $gyard_db{$guid} = $uid_db{$guid};
            $gyard_ts_db{$guid} = time;
        }
    }

    my $now = time;
    my $period = $Kolab::config{'gyard_deletion_period'} * 60;
    Kolab::log('L', 'Gravekeeping (period = ' . $Kolab::config{'gyard_deletion_period'} . ' minutes)');
    foreach $guid (keys %gyard_ts_db) {
        if ($now - $gyard_ts_db{$guid} > $period) {
            Kolab::log('L', "Clearing graveyard database entry `" . $gyard_db{$guid} . "'");
            #Kolab::Cyrus::deleteMailbox($cyrus, $gyard_db{$guid}, 0);
            delete $gyard_ts_db{$guid};
            delete $gyard_db{$guid};
        }
    }

    %uid_db = %newuid_db;

    Kolab::log('L', 'Finished synchronisation');
}

sub syncBasic
{
    my $cyrus = shift;
    my $p = shift || 'user';
    my $add = shift || ($p eq 'user' ? '' : '');
    my $doacls = shift || 0;

    Kolab::log('L', "Synchronising `$p' objects");

    my $ldap = &create(
        $Kolab::config{$p . '_ldap_ip'},
        $Kolab::config{$p . '_ldap_port'},
        $Kolab::config{$p . '_bind_dn'},
        $Kolab::config{$p . '_bind_pw'}
    );

    my $ldapmesg;
    my $ldapobject;

    my @dnlist = split(/;/, $Kolab::config{$p . '_dn_list'});
    my $dn;

    foreach $dn (@dnlist) {
        Kolab::log('L', "Synchronising `$p' DN `$dn'");

        # First of all, remove any objects explicitly marked for deletion
        $ldapmesg = $ldap->search(
            base    => $dn,
            scope   => 'sub',
            filter  => '(&(objectClass=' . $Kolab::config{$p . '_object_class'} . ")$add(" . $Kolab::config{$p . '_field_deleted'} . '='.$Kolab::config{'fqdnhostname'}.'))',
            attrs   => [
                '*',
                $Kolab::config{$p . '_field_guid'},
                $Kolab::config{$p . '_field_modified'},
                $Kolab::config{$p . '_field_quota'},
                $Kolab::config{$p . '_field_deleted'},
            ],
        );

        if ( UNIVERSAL::isa( $ldapmesg, 'Net::LDAP::Search') && $ldapmesg->code() <= 0) {
            foreach $ldapobject ($ldapmesg->entries) {
                deleteObject($ldap, $cyrus, $ldapobject, 1, $p);
            }
        } else {
            Kolab::log('L', "Unable to locate deleted `$p' objects in DN `$dn'", KOLAB_WARN);
        }

        # Now check that all objects in LDAP have corresponding mailboxes
        # This also resurrects any missing users, if neccessary
        $ldapmesg = $ldap->search(
            base    => $dn,
            scope   => 'sub',
            filter  => '(&(objectClass=' . $Kolab::config{$p . '_object_class'} . ")$add)",
            attrs   => [
                '*',
                $Kolab::config{$p . '_field_guid'},
                $Kolab::config{$p . '_field_modified'},
                $Kolab::config{$p . '_field_quota'},
                $Kolab::config{$p . '_field_deleted'},
            ],
        );

        if ( UNIVERSAL::isa( $ldapmesg, 'Net::LDAP::Search') && $ldapmesg->code() <= 0) {
            foreach $ldapobject ($ldapmesg->entries) {
                createObject($ldap, $cyrus, $ldapobject, 1, $p, $doacls);
            }
        } else {
            Kolab::log('L', "Unable to locate `$p' objects in DN `$dn'", KOLAB_WARN);
        }

        Kolab::log('L', "Finished synchronising `$p' DN `$dn'");
    }

    &destroy($ldap);

    Kolab::log('L', "Finished `$p' object synchronisation");
}

1;
__END__
# Below is stub documentation for your module. You'd better edit it!

=head1 NAME

Kolab::LDAP - Perl extension for generic LDAP code

=head1 ABSTRACT

  Kolab::LDAP contains functions used to create/delete objects,
  as well as synchronise LDAP and Cyrus.

=head1 AUTHOR

Stuart Bing묠E<lt>s.binge at codefusion.co.zaE<gt>

=head1 COPYRIGHT AND LICENSE

Copyright (c) 2003  Code Fusion cc

This  program is free  software; you can redistribute  it and/or
modify it  under the terms of the GNU  General Public License as
published by the  Free Software Foundation; either version 2, or
(at your option) any later version.

This program is  distributed in the hope that it will be useful,
but WITHOUT  ANY WARRANTY; without even the  implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
General Public License for more details.

You can view the  GNU General Public License, online, at the GNU
Project's homepage; see <http://www.gnu.org/licenses/gpl.html>.

=cut

--- Makefile.PL DELETED ---





More information about the commits mailing list