From ae6b8a2905bfc7905030479e06f3490f2c901099 Mon Sep 17 00:00:00 2001 From: Guilhem Moulin Date: Sat, 26 Jan 2013 23:59:24 +0100 Subject: Factorized the code to list localparts. --- lib/Fripost/Schema/Local.pm | 312 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 308 insertions(+), 4 deletions(-) (limited to 'lib/Fripost/Schema/Local.pm') diff --git a/lib/Fripost/Schema/Local.pm b/lib/Fripost/Schema/Local.pm index 7a1ae22..1f09f66 100644 --- a/lib/Fripost/Schema/Local.pm +++ b/lib/Fripost/Schema/Local.pm @@ -2,12 +2,12 @@ package Fripost::Schema::Local; =head1 NAME -Local.pm - +Local.pm - Localpart related method for the Fripost Schema =head1 DESCRIPTION -Local.pm abstracts the LDAP schema definition and provides methods to -search for virtual users, aliases or lists alltogether. +This module abstracts the LDAP schema definition and provides methods to +add, list or delete virtual users, aliases or lists. =cut @@ -17,11 +17,315 @@ use warnings; use utf8; use parent 'Fripost::Schema'; -use Fripost::Schema::Util qw/concat split_addr canonical_dn/; +use Fripost::Schema::Util qw/concat split_addr canonical_dn + ldap_error dn2mail/; use Net::IDN::Encode qw/email_to_ascii email_to_unicode/; use Net::LDAP::Util 'escape_filter_value'; +=head1 REPRESENTATION + +Virtual users, aliases and lists are imported and exported as hash +references, with the following keys: + +=over 4 + +=item B + +A UTF-8 string representing the (internationalized) e-mail address for +the user, alias or list. + +=item B => user|alias|list + +The type of the entry. + +=item B => 0|1 + +Whether or not the entry is active. + +=item B + +An array reference containing UTF-8 strings describing the entry. + +=item B => 0|1 + +(List only) Whether or not the entry is pending. New lists are always +marked as pending, and it is up to the list manager's side to unlock +them. + +=item B + +(User only) A string e.g., C<100 MB> representing the current quota on +the user's mailboxes. + +=item B + +(Alias and list only) An optional array reference containing the +(internationalized) e-mails addresses of the entry owners. + +=item B + +(User only) An optional array reference containing a (internationalized) +e-mails addresses that will also receive every single message sent to +that user. + +=item B + +(Alias only) An array reference containing a (internationalized) e-mails +addresses that will receive messages sent to that alias. + +=item B mailman|schleuder + +(List only) The list manager associated with list entries. + + +=head1 METHODS + +=over 4 + +=item B (I, I) + +Search for I, or list all the known (and visible) users, aliases +and lists when I is not defined. If I does not contain a '@' +symbol, it is interpreted as a domain name, which limits the scope of +that search. In list context, return a list of entries represented as +hash references, as explained above. In scalar context, only the first +entry found is returned. In void context, no attributes are returned +from the LDAP server, but it may nevertheless be useful, to ensure that +the result set is not empty for instance. + +The following options are considered: + +=over 4 + +=item B<-no-escape> => 0|1 + +By default, the local and domain parts of I - when defined - are +safely escaped before insertion into the LDAP DN and filter. This flag +disables escaping. It is useful if I contains wildcards for +instance. Note that in case the domain part contains wildcard, this +method will query the LDAP server for every single matching domain. + +=item B<-filter> => locked|unlocked + +Limit the search scope to pending / non-pending entries only. + +=item B<-type> => user|alias|list + +Limit the search scope to the given entry type. + +=item B<-keys> + +An array reference containing the attributes that are to be retrived +from the LDAP server. Note that Access Control List may prevent the +current user to read or even search anything, though. The default is to +retrieve every visible attribute, unless in void context where B<-keys> +is set to [] that is, no attribute is sent back to the client. + +=item B<-count> => 0|1 + +Return the number of entries in the result set. When set, the B<-keys> +option is bypassed not to ask any attribute from the server. + +=item B<-sort> => 0|1 + +In list context, sort the results per localpart. + +=back + +Errors can be caught with options B<-die> and B<-error>, see +B for details. + +=cut + +sub search { + my $self = shift; + my $in = shift; + my %options = @_; + + # Nothing to do after an error. + return if $options{'-error'} && ${$options{'-error'}}; + + # If there is not '@', we interpret $in as a domain name. + $in =~ s/^([^\@]+)$/\@$1/; + my ($localname, $domainname) = split_addr($in); + + my @filters; + if (defined $options{'-type'}) { + # Limit the scope to the given type. + if ($options{'-type'} eq 'user') { + push @filters, 'objectClass=FripostVirtualUser'; + } + elsif ($options{'-type'} eq 'alias') { + push @filters, 'objectClass=FripostVirtualAlias'; + } + elsif ($options{'-type'} eq 'list') { + push @filters, 'objectClass=FripostVirtualList'; + } + } + else { + push @filters, '(|(objectClass=FripostVirtualUser)'. + '(objectClass=FripostVirtualAlias)'. + '(objectClass=FripostVirtualList))'; + } + + my @domainnames; + if ($domainname) { + if ($options{'-no-escape'} and $domainname =~ /\*/) { + # If the domain part contains a wildcard, we have to query + # the LDAP server to list the matching domains. + my %opts = ( '-no-escape' => 1, -keys => [ 'name' ]) ; + $opts{'-filter'} = 'unlocked'; + foreach (qw/-filter -error -die/) { + $opts{$_} = $options{$_} if $options{$_}; + } + push @domainnames, map {$_->{name}} + $self->domain->search($domainname, %opts); + } + else { + # Otherwise, a single query is enough. + $domainname = Net::LDAP::Util::escape_dn_value($domainname) + unless $options{'-no-escape'}; + push @domainnames, $domainname; + } + } + + if ($localname) { + $localname = Net::LDAP::Util::escape_filter_value($localname) + unless $options{'-no-escape'}; + push @filters, 'fvl='.$localname; + } + + if (defined $options{'-filter'}) { + push @filters, 'objectClass=FripostPendingEntry' + if $options{'-filter'} eq 'locked'; + push @filters, '!(objectClass=FripostPendingEntry)' + if $options{'-filter'} eq 'unlocked'; + } + + my $attrs = []; + if (not (defined wantarray)) { + # In void context, we are only interested in whether or not the + # result set is empty. + $attrs = [ '1.1' ]; + } + elsif (defined $options{'-keys'}) { + $attrs = @{$options{'-keys'}} ? [ &_keys_to_attrs(@{$options{'-keys'}}) ] + : [ '1.1' ]; + } + + my $filter = Fripost::Schema::Util::ldap_and_filter(@filters); + my $count = 0; + my @resultset; + foreach my $domainname (@domainnames) { + # For query the server for each matching domain. + my $locals = $self->ldap->search( base => $self->mail2dn('@'.$domainname) + , scope => 'one' + , deref => 'never' + , filter => $filter + , attrs => $attrs + ); + ldap_error($locals, %options) // return; + next unless defined wantarray; # We'll drop the result anyway + + if ($options{'-count'}) { + $count += $locals->count; + } + elsif (wantarray) { + push @resultset, + &_entries_to_locals( $domainname, $options{'-keys'}, + $locals->entries ); + } + else { + # In scalar context, we stop here if we got a match. + return &_entries_to_locals( $domainname, $options{'-keys'}, + $locals->pop_entry ) + if $locals->count; + } + } + + return $count if $options{'-count'}; + # In list context, we return the whole result set, maybe sorted. + $options{'-sort'} ? sort { $a->{name} cmp $b->{name} } @resultset + : @resultset; +} + + +# Map a list of LDAP::Entry object into our public representation of +# users, aliases and lists. +sub _entries_to_locals { + my $domainname = shift; + my $keys = shift // []; + + my @locals; + foreach my $entry (@_) { + + # Ignore bogus entries. + return unless defined $entry; + my %local; + + foreach my $attr ($entry->attributes) { + my $val = $entry->get_value($attr, asref => 1); + if ($attr eq 'fvl') { + $local{name} = email_to_unicode($val->[0].'@'.$domainname) + if not @$keys or grep { $_ eq 'name' } @$keys; + } + elsif ($attr eq 'fripostIsStatusActive') { + $local{isActive} = $val->[0] eq 'TRUE' + if not @$keys or grep { $_ eq 'isActive' } @$keys; + } + elsif ($attr eq 'objectClass') { + if (grep { lc $_ eq lc 'FripostVirtualUser' } @$val) { + $local{type} = 'user'; + } + elsif (grep { lc $_ eq lc 'FripostVirtualAlias' } @$val) { + $local{type} = 'alias'; + } + elsif (grep { lc $_ eq lc 'FripostVirtualList' } @$val) { + $local{type} = 'list'; + } + $local{isPending} = scalar (grep { lc $_ eq lc 'FripostPendingEntry' } + @$val ) + if not @$keys or grep { $_ eq 'isPending' } @$keys; + } + elsif ($attr eq 'description') { + $local{description} = [ map { Encode::_utf8_on($_); $_ } @$val ] + if not @$keys or grep { $_ eq 'description' } @$keys; + } + elsif ($attr eq 'fripostUserQuota') { + $local{quota} = $val->[0] + if not @$keys or grep { $_ eq 'quota' } @$keys; + } + elsif ($attr eq 'fripostOwner') { + $local{owner} = [ map { dn2mail($_) } @$val ] + if not @$keys or grep { $_ eq 'owner' } @$keys; + } + elsif ($attr eq 'fripostOptionalMaildrop') { + $local{forward} = [ map { email_to_unicode($_) } @$val ] + if not @$keys or grep { $_ eq 'forward' } @$keys; + } + elsif ($attr eq 'fripostMaildrop') { + $local{destination} = [ map { email_to_unicode($_) } @$val ] + if not @$keys or grep { $_ eq 'destination' } @$keys; + } + elsif ($attr eq 'fripostListManager') { + $local{transport} = $val->[0] + if not @$keys or grep { $_ eq 'transport' } @$keys; + } + else { + die "Missing translation for local attribute ‘".$attr."’."; + } + } + + # Stop after the first processed domain in scalar mode. + return \%local unless wantarray; + push @locals, \%local; + } + return @locals; +} + + + =head1 METHODS =over 4 -- cgit v1.2.3