# vim: set cindent expandtab ts=4 sw=4:
#
# Copyright (c) 1998-2005 Chi-Keung Ho. All rights reserved.
#
# This programe 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
# of the License, or (at your option) any later version.
#
# ExtMan - web interface to manage virtual accounts
# $Id$
use strict;
use Net::LDAP;

package Ext::Mgr::LDAP;
use Exporter;
use Ext::Mgr;
use vars qw(@ISA @EXPORT $BASE);
@ISA = qw(Exporter Ext::Mgr);
@EXPORT = qw(auth);
$BASE = 'extmail.org'; # default base

sub new {
    my $this = shift;
    my $self = bless {@_}, ref $this || $this;
    $self->init(@_);
    $self;
}

sub init {
    my $self = shift;
    my %opt = @_;

    $opt{host} = '127.0.0.1' if not defined $opt{host};
    $opt{base} = 'dc=extmail.org' if not defined $opt{base};
    $opt{rootdn} = 'cn=Manager,dc=extmail.org'
        if not defined $opt{rootdn};
    $opt{rootpw} = 'rootpw' if not defined $opt{rootpw};
    $opt{bind} = 0 if not defined $opt{bind};
    $opt{filter} = 'mail=*' if not defined $opt{filter};

    $self->{opt}=\%opt;

    $BASE = $opt{base}; # initialize global BASE varible

    my ($ldap, $msg);
    $ldap = Net::LDAP->new($opt{host}) or die "LDAP operation fail, $!\n";
    if($opt{bind}) {
        $msg = $ldap->bind(
            $opt{rootdn},
            password=>$opt{rootpw},
            version => 3
        );
        $self->{msg} = $msg;
    }

    $self->{psize} = $opt{psize} || 10;
    $self->{crypt_type} = $opt{crypt_type} || 'crypt';
    $self->{ldap} = $ldap;
}

sub encrypt {
    my $self = shift;
    my $type = uc shift;
    my $pass = shift;

    my $password = $self->SUPER::encrypt($type, $pass);
    if ($type eq 'CRYPT') {
        return '{CRYPT}'.$password;
    }
    $password;
}

# search($filter, $base, $attrs)
sub search {
    my $self = shift;
    my $result = $self->{ldap}->search(
        base => $_[1] || $self->{opt}->{base},
        scope => "sub",
        filter => "$_[0]" || $self->{opt}->{filter},
        attrs => $_[2]
    );
    $result;
}

sub auth {
    my $self = shift;
    my ($username, $password) = (@_);

    # here we don't use $self, for it init LDAP without bind, if the
    # auth operation can receive userPassword field without bind, then
    # we can simplly use $self->search not create a new obj.
    #
    # Caution: filter should advoid special quoted chars. if you must
    # do it, prepend \\\, eg: \\\@domain.tld
    my $res = $self->search("(&(mail=$username)(objectclass=extmailManager))", undef, undef);

    if($res->entry(0)) {
        my $attr_pwd = $self->{opt}->{'ldif_attr_passwd'};
        my $pwd = $res->entry(0)->get_value($attr_pwd);

        # this step is a must, or null userpassword record will cause hole
        # that anonymous can step in the system
        return 0 unless($password && $pwd);

        if($self->verify($password, $pwd)) {
            $self->{INFO} = $self->_fill_user_info($res->entry(0));
            return 1;
        }else {
            return 0;
        }
    }

    0; # default ?:)
}

sub change_passwd {
    my $self = shift;
    my ($username, $old, $new) = @_;
    my $ctype = $self->{crypt_type} || 'crypt';

    if($self->auth($username, $old)) {
        my $new = $self->encrypt($ctype, $new);
        my $res = $self->search("mail=$username", undef, undef);

        my $mesg = $self->{ldap}->modify(
            $res->entry(0)->dn,
            replace => {
                $self->{opt}->{'ldif_attr_passwd'} => $new,
            },
        );
        return 0 if($mesg->code); # error while modifying
        return 1;
    }else {
        return 0;
    }
}

sub _fill_user_info {
    my $self = shift;
    my $opt = $self->{opt};
    my $entry = $_[0];
    my %info = ();

    foreach my $attr ($entry->attributes) {
        $info{$attr} = join(",", $entry->get_value($attr));
    }

    $info{TYPE} = $info{'managerType'};

    \%info;
}

sub get_entry {
    my $self = shift;
    my ($filter, $base) = @_;
    my $entry;

    my $res = $self->search($filter, $base, undef);
    if ($res->entries) {
        my %hash = ();
        my $e = $res->entry(0);
        for my $attr ($e->attributes) {
            my $val = [$e->get_value($attr)];
            if (scalar @$val > 1) {
                $hash{$attr} = $val;
            }else {
                $hash{$attr} = $val->[0];
            }
        }
        return \%hash;
    }else {
        return undef;
    }
}

sub get_entries {
    my $self = shift;
    my ($filter, $base) = @_;
    my @entries;

    my $res = $self->search($filter, $base, undef);
    while (my $e = $res->shift_entry()) {
        my %hash = ();
        foreach my $attr ($e->attributes) {
            my $val = [$e->get_value($attr)];
            if (scalar @$val >1) {
                # save ARRAY ref
                $hash{$attr} = $val;
            }else {
                $hash{$attr} = $val->[0];
            }
        }
        push @entries, \%hash;
    }
    \@entries;
}

#==========================#
# extmailUser land handler #
#==========================#

sub by_domain {
    lc $a->{virtualDomain} cmp lc $b->{virtualDomain};
}

sub by_username {
    lc $a->{mail} cmp lc $b->{mail};
}

sub by_alias {
    lc $a->{mailLocalAddress} cmp lc $b->{mailLocalAddress};
}

sub by_manager {
    lc $a->{mail} cmp lc $b->{mail};
}

sub get_users_list {
    my $self = shift;
    my $filter = "(&(objectclass=extmailUser)(virtualdomain=$_[0]))";
    my $base = "o=extmailAccount,$BASE";

    my $rs = $self->get_entries($filter, $base);
    my $arr = []; # null ARRAY ref
    foreach my $ref ( sort by_username @$rs ) {
        push @$arr, {
            mail => $ref->{mail},
            cn => $ref->{cn},
            domain => $ref->{virtualDomain},
            uidnumber => $ref->{uidNumber},
            gidnumber => $ref->{gidNumber},
            uid => $ref->{uid},
            netdiskquota => $ref->{netdiskQuota},
            active => $ref->{active} ? 1 : 0,
            quota => $ref->{mailQuota},
            passwd => $ref->{userPassword},
            clearpw => $ref->{clearPassword},
            mailhost => $ref->{mailHost},
            maildir => $ref->{mailMessageStore},
            homedir => $ref->{homeDirectory},
            expire => $ref->{expireDate},
            create => $ref->{createDate},
            disablepwdchange => $ref->{disablePasswdChange},
            disablesmtpd => $ref->{disablesmtpd},
            disablesmtp => $ref->{disablesmtp},
            disablewebmail => $ref->{disablewebmail},
            disablenetdisk => $ref->{disablenetdisk},
            disableimap => $ref->{disableimap},
            disablepop3 => $ref->{disablepop3},
        }
    }
    scalar @$arr ? $arr : undef;
}

sub get_domains_list {
    my $self = shift;
    my $filter = '(objectclass=extmailDomain)';
    my $base = "o=extmailAccount,$BASE";

    my $rs = $self->get_entries($filter, $base);
    my $arr = []; # null ARRAY ref
    foreach my $ref ( sort by_domain @$rs ) {
        push @$arr, {
            domain => $ref->{virtualDomain},
            create => $ref->{createDate},
            expire => $ref->{expireDate},
            hashdirpath => $ref->{hashDirPath},
            description => $ref->{description},
            maxalias => $ref->{domainMaxAlias},
            maxusers => $ref->{domainMaxUsers},
            maxquota => $ref->{domainMaxQuota},
            maxndquota => $ref->{domainMaxNetStore},
            transport => $ref->{Transport},
            can_signup => $ref->{canSignup},
            default_quota => $ref->{defaultQuota},
            default_ndquota => $ref->{defaultNetStore},
            default_expire => $ref->{defaultExpire},
            disablesmtpd => $ref->{disablesmtpd},
            disablesmtp => $ref->{disablesmtp},
            disablewebmail => $ref->{disablewebmail},
            disablenetdisk => $ref->{disablenetdisk},
            disableimap => $ref->{disableimap},
            disablepop3 => $ref->{disablepop3},
            active => $ref->{active} ? 1 : 0,
        }
    }
    scalar @$arr ? $arr : undef;
}

sub get_aliases_list {
    my $self = shift;
    my $filter = "(&(objectclass=extmailAlias)(virtualdomain=$_[0]))";
    my $base = "o=extmailAlias,$BASE";

    my $rs = $self->get_entries($filter, $base);
    my $arr = []; # null ARRAY ref
    foreach my $ref ( sort by_alias @$rs ) {
        push @$arr, {
            alias => $ref->{mailLocalAddress},
            domain => $ref->{virtualDomain},
            goto => $ref->{mail},
            active => $ref->{active} ? 1 : 0,
            create => $ref->{createDate},
            expire => $ref->{expireDate},
        }
    }
    scalar @$arr ? $arr : undef;
}

sub get_managers_list {
    my $self = shift;
    my $filter = "(objectclass=extmailManager)";
    my $base = "o=extmailManager,$BASE";

    my $rs = $self->get_entries($filter, $base);
    my $arr = [];
    foreach my $ref ( sort by_manager @$rs ) {
        push @$arr, {
            manager => $ref->{mail},
            cn => $ref->{cn},
            question => $ref->{question},
            answer => $ref->{answer},
            disablepwdchange => $ref->{disablePasswdChange},
            create => $ref->{createDate},
            expire => $ref->{expireDate},
            type => $ref->{managerType},
            passwd => $ref->{userPassword},
            active => $ref->{active} ? 1 : 0,
            domain => (ref $ref->{virtualDomain} ? $ref->{virtualDomain} : [$ref->{virtualDomain}]),
        }
    }
    scalar @$arr ? $arr : undef;
}

sub add_user {
    my $self = shift;
    my %opt = @_;

    my $ctype = $self->{crypt_type};
    my $base = "o=extmailAccount,$BASE";
    my $dn = "mail=$opt{mail},virtualDomain=$opt{domain},$base";

    my $attr = [
        mail => $opt{mail},
        cn => $opt{cn},
        virtualDomain => $opt{domain},
        uidNumber => $opt{uidnumber} || '1000',
        gidNumber => $opt{gidnumber} || '1000',
        uid => $opt{uid},
        objectClass => ['top', 'uidObject', 'extmailUser'],
        netdiskQuota => $opt{netdiskquota},
        active => $opt{active} ? 1 : 0,
        mailQuota => $opt{quota},
        userPassword => $self->encrypt($ctype, $opt{passwd}),
        ];

    push @$attr, (clearPassword => $opt{passwd}) if $self->{opt}->{'ldif_attr_clearpw'};
    push @$attr, (mailHost => $opt{mailhost}) if ($opt{mailhost});

    push @$attr, (
        mailMessageStore => $opt{maildir},
        homeDirectory => $opt{homedir},
        expireDate => $opt{expire},
        createDate => $opt{create},
        disablePasswdChange => $opt{disablepwdchange},
        disablesmtpd => $opt{disablesmtpd},
        disablesmtp => $opt{disablesmtp},
        disablewebmail => $opt{disablewebmail},
        disablenetdisk => $opt{disablenetdisk},
        disableimap => $opt{disableimap},
        disablepop3 => $opt{disablepop3},
    );

    my $mesg = $self->{ldap}->add($dn, attr => $attr);
    if ($mesg->code) {
        return $mesg->error;
    }else {
        return 0;
    }
}

sub add_domain {
    my $self = shift;
    my %opt = @_;

    my $base = "o=extmailAccount,$BASE";
    my $dn = "virtualDomain=$opt{domain},$base";

    my $attr = [
        virtualDomain => $opt{domain},
        createDate => $opt{create},
        expireDate => $opt{expire},
        description => $opt{description},
        domainMaxAlias => $opt{maxalias},
        domainMaxUsers => $opt{maxusers},
        domainMaxQuota => $opt{maxquota},
        domainMaxNetStore => $opt{maxndquota},
        Transport => $opt{transport},
        canSignup => $opt{can_signup} ? 1 : 0,
        defaultQuota => $opt{default_quota},
        defaultNetStore => $opt{default_ndquota},
        defaultExpire => $opt{default_expire},
        disablesmtpd => $opt{disablesmtpd},
        disablesmtp => $opt{disablesmtp},
        disablewebmail => $opt{disablewebmail},
        disablenetdisk => $opt{disablenetdisk},
        disableimap => $opt{disableimap},
        disablepop3 => $opt{disablepop3},
        active => $opt{active} ? 1 : 0,
        objectClass => ['top', 'extmailDomain'],
        ];

    # XXX FIXME
    unshift @$attr, (hashDirPath => $opt{hashdirpath}) if ($opt{hashdirpath});

    my $mesg = $self->{ldap}->add($dn, attr => $attr);
    if ($mesg->code) {
        return $mesg->error;
    } else {
        return 0;
    }
}

sub add_alias {
    my $self = shift;
    my %opt = @_;

    my $base = "o=extmailAlias,$BASE";
    my $dn = "mailLocalAddress=$opt{alias},$base";

    my $attr = [
        mailLocalAddress => $opt{alias},
        virtualDomain => $opt{domain},
        objectClass => ['top', 'extmailAlias'],
        mail => [split(/\n/, $opt{goto})],
        active => $opt{active} ? 1 : 0,
        createDate => $opt{create},
        expireDate => $opt{expire},
        ];

    my $mesg = $self->{ldap}->add($dn, attr => $attr);
    if ($mesg->code) {
        return $mesg->error;
    } else {
        return 0;
    }
}

sub add_manager {
    my $self = shift;
    my %opt = @_;

    my $base = "o=extmailManager,$BASE";
    my $dn = "mail=$opt{manager},$base";
    my $ctype = $self->{crypt_type};

    my $attr = [
        mail => $opt{manager},
        cn => $opt{cn},
        objectClass => ['top', 'extmailManager'],
        active => $opt{active} ? 1 : 0,
        disablePasswdChange => $opt{disablepwdchange},
        createDate => $opt{create},
        expireDate => $opt{expire},
        managerType => $opt{type},
        userPassword => $self->encrypt($ctype, $opt{passwd}),
        virtualDomain => [split(/\s+/, $self->mshack($opt{domain}))],
        ];

    if ($opt{question} && $opt{answer}) {
        push @$attr, question => $opt{question};
        push @$attr, answer => $opt{answer};
    }

    my $mesg = $self->{ldap}->add($dn, attr => $attr);
    if ($mesg->code) {
        return $mesg->error;
    } else {
        return 0;
    }
}

sub delete_user {
    my $self = shift;
    my $user = $_[0];

    my ($domain) = ($user =~ m!.*@(.*)!);
    my $mesg = $self->{ldap}->delete("mail=$user,virtualDomain=$domain,o=extmailAccount,$BASE");
    if ($mesg->code) {
        return $mesg->error;
    } else {
        return 0;
    }
}

sub delete_alias {
    my $self = shift;
    my $mesg = $self->{ldap}->delete("mailLocalAddress=$_[0],o=extmailAlias,$BASE");
    if ($mesg->code) {
        return $mesg->error;
    } else {
        return 0;
    }
}

sub delete_domain {
    my $self = shift;
    my $mesg = $self->{ldap}->delete("virtualDomain=$_[0],o=extmailAccount,$BASE");
    if ($mesg->code) {
        return $mesg->error;
    } else {
        return 0;
    }
}

sub delete_manager {
    my $self = shift;
    my $mesg = $self->{ldap}->delete("mail=$_[0],o=extmailManager,$BASE");
    if ($mesg->code) {
        return $mesg->error;
    } else {
        return 0;
    }
}

sub modify_user {
    my $self = shift;
    my %opt = @_;

    my $ctype = $self->{crypt_type};
    my $base = "o=extmailAccount,$BASE";
    my $dn = "mail=$opt{user},virtualDomain=$opt{domain},$base";
    my $attr = [
        cn => $opt{cn},
        uidNumber => $opt{uidnumber} || '1000',
        gidNumber => $opt{gidnumber} || '1000',
        netdiskQuota => $opt{netdiskquota},
        active => $opt{active} ? 1 : 0,
        mailQuota => $opt{quota},
        expireDate => $opt{expire},
        disablePasswdChange => $opt{disablepwdchange},
        disablesmtpd => $opt{disablesmtpd},
        disablesmtp => $opt{disablesmtp},
        disablewebmail => $opt{disablewebmail},
        disablenetdisk => $opt{disablenetdisk},
        disableimap => $opt{disableimap},
        disablepop3 => $opt{disablepop3},
    ];

    my $mesg = $self->{ldap}->modify($dn,  replace => $attr);
    return $mesg->error if ($mesg->code);

    if ($opt{passwd}) {
        my $pwa = [ userPassword => $self->encrypt($ctype, $opt{passwd}) ];
        if ($self->{opt}->{'ldif_attr_clearpw'}){
            push @$pwa, (clearPassword => $opt{passwd});
        }

        $mesg = $self->{ldap}->modify(
            $dn,
            replace => $pwa,
        );
    }

    if ($mesg->code) {
        return $mesg->error;
    } else {
        return 0;
    }
}

sub modify_alias {
    my $self = shift;
    my %opt = @_;
    my $mgr = $self->{ldap}; # ldap obj

    my $base = "o=extmailAlias,$BASE";
    my $dn = "mailLocalAddress=$opt{alias},$base";

    $mgr->modify($dn, delete => [qw(mail)]); # ignore error!
    my $mesg = $mgr->modify($dn,
        add => {
            mail => [split(/\n/, $opt{goto})]
        }
    );
    return $mesg->error if ($mesg->code);

    $mesg = $mgr->modify($dn,
        replace => [
            active => $opt{active} ? 1:0,
            expireDate => $opt{expire},
        ]
    );
    return $mesg->error if ($mesg->code);
    0;
}

sub modify_domain {
    my $self = shift;
    my %opt = @_;

    my $base = "o=extmailAccount,$BASE";
    my $dn = "virtualDomain=$opt{domain},$base";

    my $attr = {
        virtualDomain => $opt{domain},
        expireDate => $opt{expire},
        description => $opt{description},
        domainMaxAlias => $opt{maxalias},
        domainMaxUsers => $opt{maxusers},
        domainMaxQuota => $opt{maxquota},
        domainMaxNetStore => $opt{maxndquota},
        Transport => $opt{transport},
        canSignup => $opt{can_signup} ? 1 : 0,
        defaultQuota => $opt{default_quota},
        defaultNetStore => $opt{default_ndquota},
        defaultExpire => $opt{default_expire},
        disablesmtpd => $opt{disablesmtpd},
        disablesmtp => $opt{disablesmtp},
        disablewebmail => $opt{disablewebmail},
        disablenetdisk => $opt{disablenetdisk},
        disableimap => $opt{disableimap},
        disablepop3 => $opt{disablepop3},
        active => $opt{active} ? 1 : 0,
    };

    my $mesg = $self->{ldap}->modify($dn, replace => $attr);
    if ($mesg->code) {
        return $mesg->error;
    } else {
        return 0;
    }
}

sub modify_manager {
    my $self = shift;
    my %opt = @_;

    my $base = "o=extmailManager,$BASE";
    my $dn = "mail=$opt{manager},$base";
    my $mgr = $self->{ldap};
    my $ctype = $self->{crypt_type};

    $mgr->modify($dn, delete => [qw(virtualDomain)]); # ignore error
    my $mesg = $mgr->modify($dn,
        add => {
            virtualDomain => [split(/\s+/, $self->mshack($opt{domain}))]
        }
    );
    return $mesg->error if ($mesg->code);

    $mesg = $mgr->modify($dn,
        replace => [
            active => $opt{active} ? 1:0,
            expireDate => $opt{expire},
            cn => $opt{cn},
            disablePasswdChange => $opt{disablepwdchange},
        ]
    );

    if ($opt{question} and $opt{answer}) {
        $mesg = $mgr->modify($dn,
            replace => [
                question => $opt{question},
                answer => $opt{answer},
            ]
        );
    }

    if ($opt{passwd}) {
        $mgr->modify($dn,
            replace => [
            userPassword => $self->encrypt($ctype, $opt{passwd}),
            ]
        );
    }

    return $mesg->error if ($mesg->code);
    0;
}

sub get_user_info {
    my $self = shift;
    my $user = $_[0];
    my $domain = $user;

    $domain =~ s#^([^\@]+)@##;
    my $filter = "(&(objectclass=extmailUser)(mail=$user)(virtualdomain=$domain))";
    my $base = "o=extmailAccount,$BASE";

    my $ref = $self->get_entry($filter, $base);
    return undef unless ($ref);
    return {
        mail => $ref->{mail},
        cn => $ref->{cn},
        domain => $ref->{virtualDomain},
        uidnumber => $ref->{uidNumber},
        gidnumber => $ref->{gidNumber},
        uid => $ref->{uid},
        netdiskquota => $ref->{netdiskQuota},
        active => $ref->{active} ? 1 : 0,
        quota => $ref->{mailQuota},
        passwd => $ref->{userPassword},
        clearpw => $ref->{clearPassword},
        mailhost => $ref->{mailHost},
        maildir => $ref->{mailMessageStore},
        homedir => $ref->{homeDirectory},
        expire => $ref->{expireDate},
        create => $ref->{createDate},
        disablepwdchange => $ref->{disablePasswdChange},
        disablesmtpd => $ref->{disablesmtpd},
        disablesmtp => $ref->{disablesmtp},
        disablewebmail => $ref->{disablewebmail},
        disablenetdisk => $ref->{disablenetdisk},
        disableimap => $ref->{disableimap},
        disablepop3 => $ref->{disablepop3},
    }
}

sub get_domain_info {
    my $self = shift;
    my $filter = "(&(objectclass=extmailDomain)(virtualDomain=$_[0]))";
    my $base = "o=extmailAccount,$BASE";

    my $ref = $self->get_entry($filter, $base);
    return undef unless ($ref);
    return {
        domain => $ref->{virtualDomain},
        create => $ref->{createDate},
        expire => $ref->{expireDate},
        hashdirpath => $ref->{hashDirPath},
        description => $ref->{description},
        maxalias => $ref->{domainMaxAlias},
        maxusers => $ref->{domainMaxUsers},
        maxquota => $ref->{domainMaxQuota},
        maxndquota => $ref->{domainMaxNetStore},
        transport => $ref->{Transport},
        can_signup => $ref->{canSignup},
        default_quota => $ref->{defaultQuota},
        default_ndquota => $ref->{defaultNetStore},
        default_expire => $ref->{defaultExpire},
        disablesmtpd => $ref->{disablesmtpd},
        disablesmtp => $ref->{disablesmtp},
        disablewebmail => $ref->{disablewebmail},
        disablenetdisk => $ref->{disablenetdisk},
        disableimap => $ref->{disableimap},
        disablepop3 => $ref->{disablepop3},
        active => $ref->{active} ? 1 : 0,
    }
}

sub get_alias_info {
    my $self = shift;
    my $filter = "(&(objectclass=extmailAlias)(mailLocalAddress=$_[0]))";
    my $base = "o=extmailAlias,$BASE";

    my $ref = $self->get_entry($filter, $base);
    return undef unless ($ref);
    return {
        alias => $ref->{mailLocalAddress},
        domain => $ref->{virtualDomain},
        goto => $ref->{mail},
        active => $ref->{active} ? 1 : 0,
        create => $ref->{createDate},
        expire => $ref->{expireDate},
    }
}

sub get_manager_info {
    my $self = shift;
    my $filter = "(&(objectclass=extmailManager)(mail=$_[0]))";
    my $base = "o=extmailManager,$BASE";

    my $ref = $self->get_entry($filter, $base);
    return undef unless ($ref);
    return {
        manager => $ref->{mail},
        cn => $ref->{cn},
        question => $ref->{question},
        answer => $ref->{answer},
        disablepwdchange => $ref->{disablePasswdChange},
        create => $ref->{createDate},
        expire => $ref->{expireDate},
        type => $ref->{managerType},
        passwd => $ref->{userPassword},
        active => $ref->{active} ? 1 : 0,
        domain => (ref $ref->{virtualDomain} ? $ref->{virtualDomain} : [$ref->{virtualDomain}]),
    }
}

#---------------------------------#
# search and sort, paging handler #
#---------------------------------#

# method and parameters
#
# $self, %opt => (
#   domain => $domain,
#   page => $page,
#   filter => $filter,        # NULL means retreive all
#   filter_type => $type      # mail or name(cn) is ok
#   );
sub user_paging {
    my $self = shift;
    my %opt = @_;

    my $domain = $opt{domain};
    my $page = $opt{page} || 0;
    my $filter = $opt{filter};
    my $filter_type = $opt{filter_type};

    my ($has_prev, $has_next) = (1, 0);
    my $psize = $self->{psize}; # page size
    my $begin = $page*$psize;

    # all un-filltered result
    my $all = $self->get_users_list($domain) || [];
    # array to contain filltered result
    my $arr = [];

    delete $self->{_ext_info};

    for(my $i=0; $i<scalar @$all; $i++) {
        my $e = $all->[$i];
        if ($filter) {
            next unless $e->{$filter_type} =~ /$filter/i;
        }
        push @$arr, $e;
    }

    # the result array
    my $res = [];
    for(my $i=$begin;$i<scalar @$arr;$i++) {
        push @$res, $arr->[$i];
        last if (scalar @$res>= $psize);
    }

    if (scalar @$res == $psize && $begin + $psize < scalar @$arr) {
        $has_next =1;
    }
    if ($page <= 0) { $has_prev = 0 };

    # XXX ext_info
    $self->{_ext_info} = { total => scalar @$all };

    return ($res, $has_prev, $has_next);
}

# 
# $self, %opt => (
#   domain => $domain,
#   page => $page,
#   filter => $filter,    # NULL means all
# )
sub alias_paging {
    my $self = shift;
    my %opt = @_;

    my $domain = $opt{domain};
    my $page = $opt{page} || 0;
    my $filter = $opt{filter};
    my ($has_prev, $has_next) = (1, 0);

    my $psize = $self->{psize}; # page size
    my $begin = $page*$psize;

    # all un-filltered result
    my $all = $self->get_aliases_list($domain) || [];
    # array to contain filltered result
    my $arr = [];

    delete $self->{_ext_info};

    for(my $i=0; $i<scalar @$all; $i++) {
        my $e = $all->[$i];
        if ($filter) {
            next unless ($e->{alias} =~ /$filter/i ||
               $e->{goto} =~ /$filter/i);
        }
        push @$arr, $e;
    }

    my $res = [];
    for(my $i=$begin; $i<scalar @$arr; $i++) {
        push @$res, $arr->[$i];
        last if (scalar @$res>= $psize);
    }

    if (scalar @$res == $psize && $begin + $psize < scalar @$arr) {
        $has_next =1;
    }
    if ($page <= 0) { $has_prev = 0 };

    # XXX ext_info
    $self->{_ext_info} = { total => scalar @$all };

    return ($res, $has_prev, $has_next);
}

#
# $self, %opt => (
#   filter => $filter,          # NULL means all
#   filter_type => $filter_type # admin or postmaster
# )
sub manager_paging {
    my $self = shift;
    my %opt = @_;

    my $page = $opt{page} || 0;
    my $filter = $opt{filter};
    my $filter_type = $opt{filter_type};
    my ($has_prev, $has_next) = (1, 0);

    my $psize = $self->{psize}; # page size
    my $begin = $page*$psize;

    # all un-filltered result
    my $all = $self->get_managers_list || [];
    # array to contain filltered result
    my $arr = [];

    delete $self->{_ext_info};

    for(my $i=0; $i<scalar @$all; $i++) {
        my $e = $all->[$i];
        if ($filter) {
            next unless ($e->{manager} =~ /$filter/i ||
                $e->{cn} =~ /$filter/i) and 
                $e->{type} eq $filter_type;
        }
        push @$arr, $e;
    }

    my $res = [];
    for(my $i=$begin; $i<scalar @$arr;$i++) {
        push @$res, $arr->[$i];
        last if (scalar @$res>= $psize);
    }

    if (scalar @$res == $psize && $begin + $psize < scalar @$arr) {
        $has_next =1;
    }

    # XXX ext_info
    $self->{_ext_info} = { total => scalar @$all };

    if ($page <= 0) { $has_prev = 0 };
    return ($res, $has_prev, $has_next);
}

sub ext_info {
    my $self = shift;
    return $self->{_ext_info};
}

sub domain_paging {
    die "use Ext::MgrApp::domain_paging() instead\n";
}

1;


syntax highlighted by Code2HTML, v. 0.9.1