aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorGuilhem Moulin <guilhem.moulin@fripost.org>2012-04-17 01:25:28 +0200
committerGuilhem Moulin <guilhem.moulin@fripost.org>2012-04-17 01:25:56 +0200
commit8663144f1f5a3d163119f17a7f9c06655e32727a (patch)
treed81953df51b5b583d85caa40008e0ee9243e6906
parent033af5c7de65c2ba38c45ba649ad29823bfb7141 (diff)
OO Perl library for our LDAP schema.
-rwxr-xr-xfripost-adduser46
-rwxr-xr-xlib/Fripost/Schema.pm241
-rw-r--r--lib/Fripost/Schema/Result/Alias.pm52
-rw-r--r--lib/Fripost/Schema/Result/Domain.pm51
-rw-r--r--lib/Fripost/Schema/Result/Mailbox.pm72
-rw-r--r--lib/Fripost/Schema/Search.pm111
-rw-r--r--lib/Fripost/Schema/Type.pm48
7 files changed, 419 insertions, 202 deletions
diff --git a/fripost-adduser b/fripost-adduser
index f13868d..bd73aea 100755
--- a/fripost-adduser
+++ b/fripost-adduser
@@ -16,19 +16,21 @@ B<fripost-adduser> [B<--verbose>] [B<--debug>] [B<--pretend>] [I<username>]
=head1 DESCRIPTION
-B<fripost-adduser> adds a new mailbox to the system, unless B<--pretend>
-is set.
+B<fripost-adduser> adds a new virtual mailbox to the system, unless
+B<--pretend> is set.
If no I<username> or I<password> are given, the user is prompted for them.
If I<username> is not fully qualified, C<fripost.org> is appended.
+If I<username> is already an existing username or alias,
+B<fripost-adduser> raises an error.
=head1 OPTIONS
=over 8
-=item B<--prentend>
+=item B<--pretend>
-Only simulates the insertion. (But still query the LDAP server to check
-if I<username> is already in the database.)
+Only simulates the insertion. (But still query the LDAP server to ensure
+that I<username> is not already in the database.)
=item B<--password=>I<password>
@@ -134,8 +136,8 @@ GetOptions(
'base_dn=s' => \$conf->{base_dn},
'bind_dn=s' => \$conf->{bind_dn},
'bind_pw=s' => \$conf->{bind_pw},
- 'debug' => \$conf->{debug},
'pretend' => \$conf->{pretend},
+ 'debug' => \$conf->{debug},
'v|verbose' => \$conf->{verbose},
'password=s' => \$conf->{password},
'man' => sub { pod2usage(-exitstatus => 0,
@@ -152,12 +154,15 @@ my $ldap = Fripost::Schema->new( $conf );
# Define the new user
my $user;
{
- my $username = $ARGV[0];
- $username //= prompt_email("New username: ", 'is_user');
-
- # Default domain
- $username .= '@fripost.org' unless $username =~ /\@.+$/;
-
+ my $username;
+ if (defined $ARGV[0]) {
+ $username = fix_username ($ARGV[0]);
+ Email::Valid->address($username)
+ or die "Error: `" .$username. "' is not a valid e-mail.\n";
+ }
+ else {
+ $username = prompt_email("New username: ", 'is_user');
+ }
my ($domain, $login) = split /\@/, $username, 2;
my $maildir = "$domain/$login/Maildir/"; # Trailing slash important
my $isActive = 'TRUE';
@@ -184,9 +189,22 @@ my $user;
confirm_or_abort();
}
-die "Error: User already exists.\n"
- if $ldap->searchUser($user->{username})->count;
+# Check if the username already exists, or is an existing alias.
+{
+ die "Error: User `" .$user->{username}. "' already exists.\n"
+ if $ldap->user->search($user->{username})->count;
+
+ my $res = $ldap->alias->search({ address => $user->{username} });
+ if ($res->count) {
+ print STDERR "Error: Alias `" .$user->{username}. "' already exists. ";
+ print STDERR "(Targetting to ";
+ print STDERR (join ', ', map { '`' .$_->{goto}. "'"} ($res->entries));
+ say STDERR ".)";
+ exit 1;
+ }
+ exit 1;
+}
## Insert the new user
if ($conf->{pretend}) {
diff --git a/lib/Fripost/Schema.pm b/lib/Fripost/Schema.pm
index 8124a54..5b57cd3 100755
--- a/lib/Fripost/Schema.pm
+++ b/lib/Fripost/Schema.pm
@@ -1,17 +1,33 @@
package Fripost::Schema;
use 5.010_000;
+use warnings;
use strict;
+use Fripost::Schema::Search;
+use Fripost::Schema::Type;
+
use base qw/Net::LDAP/;
our $VERSION = '0.01';
-# Initialize a connection to the LDAP host.
+
+#######################################################################
+# Public methods
+
+
+# Change the context of the object.
+sub user { $_[0]->Fripost::Schema::Type::_set_type(MAILBOX); }
+sub domain { $_[0]->Fripost::Schema::Type::_set_type(DOMAIN); }
+sub alias { $_[0]->Fripost::Schema::Type::_set_type(ALIAS); }
+
+
+# Create the oject, and initialize a connection to the LDAP
+# host.
sub new {
my $class = shift;
my $h = shift;
- my $self = {options => $h};
+ my $self = {_options => $h};
bless $self, $class;
my $ldap = Net::LDAP->new ( $h->{server_host} )
@@ -29,23 +45,72 @@ sub new {
}
die "Error: " .$mesg->error. "\n" if $mesg->code;
- $self->{ldap} = $ldap;
+ $self->{_ldap} = $ldap;
return $self;
}
+# Search. Returns a `Fripost::Schema::Search'.
+sub search {
+ my $self = shift;
+ my $res;
+ if ( $self->{_type} == MAILBOX ) {
+ $res = $self->_searchUser(@_)
+ }
+ elsif ( $self->{_type} == DOMAIN ) {
+ $res = $self->_searchDomain(@_)
+ }
+ elsif ( $self->{_type} == ALIAS ) {
+ $res = $self->_searchAlias(@_)
+ }
+ else {
+ die "Something weird happened. Please report."
+ }
+ my $result = {_res => $res, _type => $self->{_type}};
+ bless $result, 'Fripost::Schema::Search';
+ return $result;
+}
+
+
+# Add.
+sub add {
+ my $self = shift;
+ if ( $self->{_type} == MAILBOX ) {
+ $self->_addUser(@_)
+ }
+ elsif ( $self->{_type} == DOMAIN ) {
+ $self->_addDomain(@_)
+ }
+ elsif ( $self->{_type} == ALIAS ) {
+ $self->_addAlias(@_)
+ }
+ else {
+ die "Something weird happened. Please report."
+ }
+}
+
+
+# Disconnect to the LDAP server.
+sub unbind {
+ $_[0]->{_ldap}->unbind();
+}
+
+
+#######################################################################
+# Search
+
# Search a user, and return the corresponding entries if found. If no
# user is given, returns all users.
-sub searchUser {
+sub _searchUser {
my $self = shift;
- my $base = join ',', ('ou=mailboxes',$self->{options}->{base_dn});
+ my $base = join ',', ('ou=mailboxes',$self->{_options}->{base_dn});
my $filter = "(ObjectClass=virtualMailbox)";
$filter = "(&" .$filter. "(uid=" .$_[0]. ")" .")"
if defined $_[0];
- my $res = $self->{ldap}->search(
+ my $res = $self->{_ldap}->search(
base => $base,
scope => 'one',
attrs => [ 'uid', 'gn' , 'sn', 'maildir', 'isActive' ],
@@ -56,14 +121,99 @@ sub searchUser {
return $res;
}
+
+# Search a domain, and return the corresponding entries if found. If no
+# domain is given, returns all domains. If the given domain has a
+# defined owner, scope the search for this very owner only. If the
+# owner is undefined, but still exists, scope the search to the "global"
+# domains only.
+sub _searchDomain {
+ my $self = shift;
+
+ my $base = $self->{_options}->{base_dn};
+ if (exists $_[0]->{owner}) {
+ if (defined $_[0]->{owner}) {
+ $base = join ',', ('uid='.$_[0]->{owner},'ou=mailboxes',$base);
+ }
+ else {
+ $base = join ',', ('ou=domains',$base);
+ }
+ }
+
+ my $filter = '(ObjectClass=virtualDomain)';
+ $filter = "(&" .$filter. "(dc=" .$_[0]->{domain}. ")" .")"
+ if defined $_[0]->{domain};
+
+ my $res = $self->{_ldap}->search(
+ base => $base,
+ scope => 'subtree',
+ attrs => [ 'dc', 'isActive' ],
+ filter => $filter
+ );
+ die "Error: " .$res->error. "\n" if $res->code;
+
+ return $res;
+}
+
+
+# Search an alias, and return the corresponding entries if found. If no
+# alias is given, returns all aliases. If the given alias has a
+# defined owner, scope the search for this very owner only. If the
+# owner is undefined, but still exists, scope the search to the "global"
+# domains only.
+sub _searchAlias {
+ my $self = shift;
+
+ my $base = $self->{_options}->{base_dn};
+ if (exists $_[0]->{owner}) {
+ if (defined $_[0]->{owner}) {
+ $base = join ',', ('uid='.$_[0]->{owner},'ou=mailboxes',$base);
+ }
+ else {
+ $base = join ',', ('ou=domains',$base);
+ }
+ }
+ $base = 'dc=' .$_[0]->{domain}. ',' .$base
+ if (exists $_[0]->{owner}) and (defined $_[0]->{domain});
+
+ my @filters = '(ObjectClass=virtualAliases)';
+ push @filters, '(mailLocalAddress=' .$_[0]->{address}. ')'
+ if defined $_[0]->{address};
+ push @filters, '(mailTarget=' .$_[0]->{goto}. ')'
+ if defined $_[0]->{goto};
+
+ my $filter;
+ if ($#filters == 0 ) {
+ $filter = $filters[0];
+ }
+ elsif ($#filters > 0) {
+ $filter = '(&' . (join '', @filters) . ')';
+ }
+
+ my $res = $self->{_ldap}->search(
+ base => $base,
+ scope => 'subtree',
+ attrs => [ 'mailLocalAddress', 'mailTarget', 'isActive' ],
+ filter => $filter
+ );
+ die "Error: " .$res->error. "\n" if $res->code;
+
+ return $res;
+}
+
+
+
+#######################################################################
+# Add
+
# Add a user
-sub addUser {
+sub _addUser {
my $self = shift;
my $user = shift;
- my $base = join ',', ('ou=mailboxes',$self->{options}->{base_dn});
+ my $base = join ',', ('ou=mailboxes',$self->{_options}->{base_dn});
- my $res = $self->{ldap}->add( 'uid=' .$user->{username}. ',' .$base,
+ my $res = $self->{_ldap}->add( 'uid=' .$user->{username}. ',' .$base,
attrs => [ uid => $user->{username},
objectClass => [ 'top', 'virtualMailbox' ],
userPassword => $user->{userPassword},
@@ -76,16 +226,81 @@ sub addUser {
}
-# Disconnect to the LDAP server.
-sub unbind {
- $_[0]->{ldap}->unbind();
+# Add a domain.
+sub _addDomain {
+ my $self = shift;
+ my $domain = shift;
+
+ my $base = $self->{_options}->{base_dn};
+ if (defined $domain->{owner}) {
+ $base = join ',', ('uid=' .$domain->{owner},'ou=mailboxes',$base)
+ }
+ else {
+ $base = join ',', ('ou=domains',$base);
+ }
+
+ my $res = $self->{_ldap}->add( 'dc=' .$domain->{domain}. ',' .$base,
+ attrs => [ dc => $domain->{domain},
+ objectClass => [ 'top', 'virtualDomain' ],
+ isActive => $domain->{isActive}
+ ]
+ );
+ die "Error: " .$res->error. "\n" if $res->code;
+ return $res;
}
+# Add an alias
+sub _addAlias {
+ my $self = shift;
+ my $alias = shift;
+
+ # TODO: detect cycles
+ die "Error: Cannot create alias `" .$alias->{address}.
+ "' targetting to itself.\n"
+ if $alias->{address} eq $alias->{goto};
+
+ my $base = $self->{_options}->{base_dn};
+ if (defined $alias->{owner}) {
+ $base = join ',', ('uid=' .$alias->{owner},'ou=mailboxes',$base)
+ }
+ else {
+ $base = join ',', ('ou=domains',$base);
+ }
+ $base = 'mailTarget='.$alias->{goto}.
+ ','.'dc='. (split /\@/, $alias->{address}, 2)[1].
+ ','.$base;
+
+ my @attrs = ( mailLocalAddress => $alias->{address}
+ , isActive => $alias->{isActive} );
+ my $res;
+ if ($self->_searchAlias($alias)->count) {
+ $res = $self->{_ldap}->modify( $base, attrs => [ @attrs ] );
+ }
+ else {
+ $res = $self->{_ldap}->add( $base,
+ attrs => [ mailTarget => $alias->{goto}
+ , objectClass => [ 'top',
+ 'inetLocalMailRecipient',
+ 'virtualAliases' ],
+ @attrs
+ ]
+ );
+ }
+ die "Error: " .$res->error. "\n" if $res->code;
+ return $res;
+}
+
+
+
+#######################################################################
+# Miscellaneous
+
# Debug print.
sub _dsay {
my $self = shift;
- return unless (exists $self->{options}->{debug}) and $self->{options}->{debug};
+ return unless (exists $self->{_options}->{debug})
+ and $self->{_options}->{debug};
print STDERR "Debug: ";
say STDERR @_;
}
diff --git a/lib/Fripost/Schema/Result/Alias.pm b/lib/Fripost/Schema/Result/Alias.pm
deleted file mode 100644
index 4d9306c..0000000
--- a/lib/Fripost/Schema/Result/Alias.pm
+++ /dev/null
@@ -1,52 +0,0 @@
-package Fripost::Schema::Result::Alias;
-
-use 5.010_000;
-use warnings;
-use strict;
-
-use base qw/DBIx::Class::Core/;
-
-# mysql> describe alias;
-# +-------------+--------------+------+-----+---------------------+-------+
-# | Field | Type | Null | Key | Default | Extra |
-# +-------------+--------------+------+-----+---------------------+-------+
-# | address | varchar(255) | NO | PRI | | |
-# | goto | text | NO | | NULL | |
-# | domain | varchar(255) | NO | | | |
-# | create_date | datetime | NO | | 0000-00-00 00:00:00 | |
-# | change_date | timestamp | NO | | CURRENT_TIMESTAMP | |
-# | active | tinyint(4) | NO | | 1 | |
-# +-------------+--------------+------+-----+---------------------+-------+
-# 6 rows in set (0.00 sec)
-
-__PACKAGE__->load_components(qw/InflateColumn::DateTime/);
-
-__PACKAGE__->table('alias');
-__PACKAGE__->add_columns(qw/ address goto domain create_date change_date active /);
-__PACKAGE__->add_columns(
- create_date => { data_type => 'datetime', timezone => "Europe/Stockholm", locale => "se_SV" },
- change_date => { data_type => 'datetime', timezone => "Europe/Stockholm", locale => "se_SV" },
-);
-
-__PACKAGE__->set_primary_key('address');
-
-=head1 NAME
-
-Fripost::Schema::Result::Alias -
-
-=head1 AUTHOR
-
-Stefan Kangas C<< <skangas at skangas.se> >>
-
-=head1 COPYRIGHT
-
-Copyright 2010,2011 Stefan Kangas, all rights reserved.
-
-=head1 LICENSE
-
-This program is free software; you can redistribute it and/or modify it
-under the same terms as perl itself.
-
-=cut
-
-1; # End of Alias.pm
diff --git a/lib/Fripost/Schema/Result/Domain.pm b/lib/Fripost/Schema/Result/Domain.pm
deleted file mode 100644
index 36649de..0000000
--- a/lib/Fripost/Schema/Result/Domain.pm
+++ /dev/null
@@ -1,51 +0,0 @@
-package Fripost::Schema::Result::Domain;
-
-use 5.010_000;
-use warnings;
-use strict;
-
-use base qw/DBIx::Class::Core/;
-
-# mysql> describe domain;
-# +-------------+--------------+------+-----+---------------------+-------+
-# | Field | Type | Null | Key | Default | Extra |
-# +-------------+--------------+------+-----+---------------------+-------+
-# | domain | varchar(255) | NO | PRI | | |
-# | description | varchar(255) | NO | | | |
-# | create_date | datetime | NO | | 0000-00-00 00:00:00 | |
-# | change_date | timestamp | NO | | CURRENT_TIMESTAMP | |
-# | active | tinyint(4) | NO | | 1 | |
-# +-------------+--------------+------+-----+---------------------+-------+
-# 5 rows in set (0.00 sec)
-
-__PACKAGE__->load_components(qw/InflateColumn::DateTime/);
-
-__PACKAGE__->table('domain');
-__PACKAGE__->add_columns(qw/ domain description create_date change_date active /);
-__PACKAGE__->add_columns(
- create_date => { data_type => 'datetime', timezone => "Europe/Stockholm", locale => "se_SV" },
- change_date => { data_type => 'datetime', timezone => "Europe/Stockholm", locale => "se_SV" },
-);
-
-__PACKAGE__->set_primary_key('domain');
-
-=head1 NAME
-
-Fripost::Schema::Result::Domain -
-
-=head1 AUTHOR
-
-Stefan Kangas C<< <skangas at skangas.se> >>
-
-=head1 COPYRIGHT
-
-Copyright 2010, 2011 Stefan Kangas, all rights reserved.
-
-=head1 LICENSE
-
-This program is free software; you can redistribute it and/or modify it
-under the same terms as perl itself.
-
-=cut
-
-1; # End of Domain.pm
diff --git a/lib/Fripost/Schema/Result/Mailbox.pm b/lib/Fripost/Schema/Result/Mailbox.pm
deleted file mode 100644
index 48d81fb..0000000
--- a/lib/Fripost/Schema/Result/Mailbox.pm
+++ /dev/null
@@ -1,72 +0,0 @@
-package Fripost::Schema::Result::Mailbox;
-
-use 5.010_000;
-use warnings;
-use strict;
-
-use base qw/DBIx::Class::Core/;
-
-use Fripost::Password;
-
-# mysql> describe mailbox;
-# +-------------+--------------+------+-----+---------------------+-------+
-# | Field | Type | Null | Key | Default | Extra |
-# +-------------+--------------+------+-----+---------------------+-------+
-# | username | varchar(255) | NO | PRI | | |
-# | password | varchar(255) | NO | | | |
-# | name | varchar(255) | NO | | | |
-# | maildir | varchar(255) | NO | | | |
-# | domain | varchar(255) | NO | | | |
-# | create_date | datetime | NO | | 0000-00-00 00:00:00 | |
-# | change_date | timestamp | NO | | CURRENT_TIMESTAMP | |
-# | active | tinyint(4) | NO | | 1 | |
-# +-------------+--------------+------+-----+---------------------+-------+
-# 8 rows in set (0.00 sec)
-
-__PACKAGE__->load_components(qw/InflateColumn::DateTime/);
-
-__PACKAGE__->table('mailbox');
-__PACKAGE__->add_columns(qw/ username password name maildir domain active /);
-__PACKAGE__->add_columns(
- create_date => { data_type => 'datetime', timezone => "Europe/Stockholm", locale => 'sv_SE' },
- change_date => { data_type => 'datetime', timezone => "Europe/Stockholm", locale => 'sv_SE' }
-);
-
-__PACKAGE__->set_primary_key('username');
-
-=head2 store_column
-
-override store_column to encrypt the password when stored
-
-=cut
-
-sub store_column {
- my ($self, $col, $val) = @_;
-
- if ($col eq 'password') {
- $val = smd5($val);
- }
-
- return $self->next::method($col,$val);
-}
-
-=head1 NAME
-
-Fripost::Schema::Result::Mailbox -
-
-=head1 AUTHOR
-
-Stefan Kangas C<< <skangas at skangas.se> >>
-
-=head1 COPYRIGHT
-
-Copyright 2010,2011 Stefan Kangas, all rights reserved.
-
-=head1 LICENSE
-
-This program is free software; you can redistribute it and/or modify it
-under the same terms as perl itself.
-
-=cut
-
-1; # End of Mailbox.pm
diff --git a/lib/Fripost/Schema/Search.pm b/lib/Fripost/Schema/Search.pm
new file mode 100644
index 0000000..30d92d6
--- /dev/null
+++ b/lib/Fripost/Schema/Search.pm
@@ -0,0 +1,111 @@
+package Fripost::Schema::Search;
+
+use 5.010_000;
+use warnings;
+use strict;
+
+use Fripost::Schema::Type;
+
+use base qw/Net::LDAP::Search/;
+our $VERSION = '0.01';
+
+# Count the entries got out from the query.
+sub count { $_[0]->{_res}->count }
+
+# Create a hash out of the LDAP entry. Keys depend on the context
+# of the object.
+# The value can be an array reference (multi-valued attributes) or
+# a scalar (otherwise).
+sub entries {
+ my $self = shift;
+
+ my $dumpEntry;
+ if ( $self->{_type} == MAILBOX ) {
+ $dumpEntry = "_userEntry";
+ }
+ elsif ( $self->{_type} == DOMAIN ) {
+ $dumpEntry = "_domainEntry";
+ }
+ elsif ( $self->{_type} == ALIAS ) {
+ $dumpEntry = "_aliasEntry";
+ }
+ else {
+ die "Something weird happened. Please report."
+ }
+
+ no strict "refs";
+ return (map {&$dumpEntry($_)} $self->{_res}->entries);
+}
+
+
+sub _userEntry {
+ my $entry = shift;
+ my %user;
+ &_get_values( $entry, \%user, 'username', 'uid');
+ map { &_get_values($entry, \%user, $_) }
+ qw /maildir isActive userPassword/;
+ return \%user;
+}
+
+sub _domainEntry {
+ my $entry = shift;
+ my %domain;
+ &_get_values( $entry, \%domain, 'domain', 'dc');
+ &_get_values( $entry, \%domain, 'isActive');
+ my $parent = &_get_dn($entry)->[1];
+ unless ($parent eq 'ou=domains') {
+ $domain{owner} = (split /=/, $parent, 2)[1];
+ }
+ return \%domain;
+}
+
+sub _aliasEntry {
+ my $entry = shift;
+ my %alias;
+ &_get_values( $entry, \%alias, 'address', 'mailLocalAddress');
+ &_get_values( $entry, \%alias, 'goto', 'mailTarget');
+ &_get_values( $entry, \%alias, 'isActive');
+ return \%alias;
+}
+
+sub _get_values {
+ my ($entry, $h, $attr, $attr2) = @_;
+ $attr2 //= $attr;
+ my $values = $entry->get_value ( $attr2, asref => 1 );
+
+ return unless defined $values;
+ if ($#$values == 0) {
+ $h->{$attr} = $values->[0];
+ }
+ else {
+ $h->{$attr} = $values;
+ }
+}
+
+sub _get_dn {
+ return [split ',', $_[0]->dn()];
+}
+
+
+=head1 NAME
+
+Fripost::Schema::Search - Class for the result of LDAP queries.
+
+=head1 AUTHOR
+
+Guilhem Moulin C<< <guilhem at fripost.org> >>
+
+=head1 COPYRIGHT
+
+Copyright 2012 Guilhem Moulin, all rights reserved.
+
+=head1 LICENSE
+
+This program is free software; you can redistribute it and/or modify it
+under the same terms as perl itself.
+
+=cut
+
+1; # End of Search.pm
+
+__END__
diff --git a/lib/Fripost/Schema/Type.pm b/lib/Fripost/Schema/Type.pm
new file mode 100644
index 0000000..bfa8f73
--- /dev/null
+++ b/lib/Fripost/Schema/Type.pm
@@ -0,0 +1,48 @@
+package Fripost::Schema::Type;
+
+use 5.010_000;
+use warnings;
+use strict;
+
+use Exporter;
+
+our $VERSION = '0.01';
+
+our @EXPORT = qw/MAILBOX DOMAIN ALIAS/;
+our @ISA = qw(Exporter);
+
+use constant {
+ MAILBOX => 0,
+ DOMAIN => 1,
+ ALIAS => 2
+};
+
+# Change the context of the object.
+sub _set_type {
+ $_[0]->{_type} = $_[1];
+ return $_[0];
+}
+
+
+=head1 NAME
+
+Fripost::Schema::Type - Context of Fripost::Schema objects.
+
+=head1 AUTHOR
+
+Guilhem Moulin C<< <guilhem at fripost.org> >>
+
+=head1 COPYRIGHT
+
+Copyright 2012 Guilhem Moulin, all rights reserved.
+
+=head1 LICENSE
+
+This program is free software; you can redistribute it and/or modify it
+under the same terms as perl itself.
+
+=cut
+
+1; # End of Type.pm
+
+__END__