diff --git a/postfix/config b/postfix/config
index 4fefca0a6..9d9cd1833 100644
--- a/postfix/config
+++ b/postfix/config
@@ -25,3 +25,4 @@ postfix_master=/etc/postfix/master.cf
columns=2
show_cmts=0
prefix_cmts=0
+ldap_doms=1
diff --git a/postfix/config-freebsd b/postfix/config-freebsd
index 9ccbaa200..dc2cff43c 100644
--- a/postfix/config-freebsd
+++ b/postfix/config-freebsd
@@ -25,3 +25,4 @@ postfix_master=/usr/local/etc/postfix/master.cf
columns=2
show_cmts=0
prefix_cmts=0
+ldap_doms=1
diff --git a/postfix/config-mandrake-linux b/postfix/config-mandrake-linux
index 4fefca0a6..9d9cd1833 100644
--- a/postfix/config-mandrake-linux
+++ b/postfix/config-mandrake-linux
@@ -25,3 +25,4 @@ postfix_master=/etc/postfix/master.cf
columns=2
show_cmts=0
prefix_cmts=0
+ldap_doms=1
diff --git a/postfix/config-msc-linux b/postfix/config-msc-linux
index 4fefca0a6..9d9cd1833 100644
--- a/postfix/config-msc-linux
+++ b/postfix/config-msc-linux
@@ -25,3 +25,4 @@ postfix_master=/etc/postfix/master.cf
columns=2
show_cmts=0
prefix_cmts=0
+ldap_doms=1
diff --git a/postfix/config-netbsd b/postfix/config-netbsd
index 4fefca0a6..9d9cd1833 100644
--- a/postfix/config-netbsd
+++ b/postfix/config-netbsd
@@ -25,3 +25,4 @@ postfix_master=/etc/postfix/master.cf
columns=2
show_cmts=0
prefix_cmts=0
+ldap_doms=1
diff --git a/postfix/config.info b/postfix/config.info
index 0d36f96f3..51feb59f0 100644
--- a/postfix/config.info
+++ b/postfix/config.info
@@ -26,3 +26,9 @@ postcat_cmd=Mail queue decoding command,0
start_cmd=Command to start Postfix,3,Use control command
stop_cmd=Command to stop Postfix,3,Use control command
reload_cmd=Command to apply Postfix configuration,3,Use control command
+
+line3=LDAP options,11
+ldap_class=Object classes for maps,3,Default (top)
+ldap_attrs=Other LDAP attributes for maps
(In fieldname: value format),9,40,3,\t
+ldap_id=Key attribute for map objects,3,Default (cn)
+ldap_doms=Create separate DN for each domain?,1,1-Yes,0-No
diff --git a/postfix/lang/en b/postfix/lang/en
index cd58cb206..2fa22f4e1 100644
--- a/postfix/lang/en
+++ b/postfix/lang/en
@@ -843,3 +843,14 @@ mysql_edelete=SQL delete failed : $1
mysql_eupdate=SQL update failed : $1
mysql_esource=No MySQL source named $1 was found
mysql_eneed=The MySQL configuration parameter $1 was not found. Virtualmin needs this to figure out which table and fields to query.
+
+ldap_ecfile=LDAP configuration file $1 was not found
+ldap_eldapmod=Perl module $1 needed to communicate with LDAP is not installed or not loadable
+ldap_eldap=Failed to connect to LDAP server $1 on port $2
+ldap_eldaplogin=Failed to login to LDAP server $1 as $2 : $3
+ldap_ebase=LDAP base DN $1 is not valid : $2
+ldap_eadd=LDAP add of $1 failed : $2
+ldap_edelete=LDAP delete of $1 failed : $2
+ldap_equery=LDAP search of $1 failed : $2
+ldap_erename=LDAP rename of $1 to $2 failed : $3
+ldap_emodify=LDAP modify of $1 failed : $2
diff --git a/postfix/postfix-lib.pl b/postfix/postfix-lib.pl
index d64ef1acb..debb8f910 100644
--- a/postfix/postfix-lib.pl
+++ b/postfix/postfix-lib.pl
@@ -782,7 +782,32 @@ sub get_maps
} elsif ($maps_type eq "ldap") {
# Get from an LDAP database
- # XXX
+ local $conf = &ldap_value_to_conf($maps_file);
+ local $ldap = &connect_ldap_db($conf);
+ ref($ldap) || &error($ldap);
+ local ($name_attr, $filter) = &get_ldap_key($conf);
+ local $scope = $conf->{'scope'} || 'sub';
+ local $rv = $ldap->search(base => $conf->{'search_base'},
+ scope => $scope,
+ filter => $filter);
+ if (!$rv || $rv->code) {
+ # Search failed!
+ &error(&text('ldap_equery',
+ "$conf->{'search_base'}",
+ "".&html_escape($rv->error).""));
+ }
+ foreach my $o ($rv->all_entries) {
+ $number++;
+ my %map;
+ $map{'name'} = $o->get_value($name_attr);
+ $map{'value'} = $o->get_value(
+ $conf->{'result_attribute'} || "maildrop");
+ $map{'dn'} = $o->dn();
+ $map{'map_file'} = $maps_file;
+ $map{'map_type'} = $maps_type;
+ $map{'number'} = $number;
+ push(@{$maps_cache{$_[0]}}, \%map);
+ }
}
}
}
@@ -925,9 +950,35 @@ elsif ($maps_type eq "mysql") {
}
$cmd->finish();
$dbh->disconnect();
+ $_[1]->{'key'} = $_[1]->{'name'};
}
elsif ($maps_type eq "ldap") {
# Adding to an LDAP database
+ local $conf = &ldap_value_to_conf($maps_file);
+ local $ldap = &connect_ldap_db($conf);
+ ref($ldap) || &error($ldap);
+ local @classes = split(/\s+/, $config{'ldap_class'} || "top");
+ local @attrs = ( "objectClass", \@classes );
+ local $name_attr = &get_ldap_key($conf);
+ push(@attrs, $name_attr, $_[1]->{'name'});
+ push(@attrs, $conf->{'result_attribute'} || "maildrop",
+ $_[1]->{'value'});
+ push(@attrs, &split_props($config{'ldap_attrs'}));
+ local $dn = &make_map_ldap_dn($_[1], $conf);
+ if ($dn =~ /^([^=]+)=([^, ]+)/) {
+ push(@attrs, $1, $2);
+ }
+
+ # Make sure the parent DN exists - for example, when adding a domain
+ &ensure_ldap_parent($ldap, $dn);
+
+ # Actually add
+ local $rv = $ldap->add($dn, attr => \@attrs);
+ if ($rv->code) {
+ &error(&text('ldap_eadd', "$dn",
+ "".&html_escape($rv->error).""));
+ }
+ $_[1]->{'dn'} = $dn;
}
# Update the in-memory cache
@@ -968,7 +1019,14 @@ elsif ($_[1]->{'map_type'} eq 'mysql') {
}
elsif ($_[1]->{'map_type'} eq 'ldap') {
# Deleting from LDAP
- # XXX
+ local $conf = &ldap_value_to_conf($maps_file);
+ local $ldap = &connect_ldap_db($conf);
+ ref($ldap) || &error($ldap);
+ local $rv = $ldap->delete($_[1]->{'dn'});
+ if ($rv->code) {
+ &error(&text('ldap_edelete', "$_[1]->{'dn'}",
+ "".&html_escape($rv->error).""));
+ }
}
# Delete from in-memory cache
@@ -1012,7 +1070,50 @@ elsif ($_[1]->{'map_type'} eq 'mysql') {
}
elsif ($_[1]->{'map_type'} eq 'ldap') {
# Updating in LDAP
- # XXX
+ local $conf = &ldap_value_to_conf($_[1]->{'map_file'});
+ local $ldap = &connect_ldap_db($conf);
+ ref($ldap) || &error($ldap);
+
+ # Work out attribute changes
+ local %replace;
+ local $name_attr = &get_ldap_key($conf);
+ $replace{$name_attr} = [ $_[2]->{'name'} ];
+ $replace{$conf->{'result_attribute'} || "maildrop"} =
+ [ $_[2]->{'value'} ];
+
+ # Work out new DN, if needed
+ # XXX fails and messes up DN!!!
+ local $newdn = &make_map_ldap_dn($_[2], $conf);
+ if ($_[1]->{'name'} ne $_[2]->{'name'} &&
+ $_[1]->{'dn'} ne $newdn) {
+ # Changed .. update the object in LDAP
+ &ensure_ldap_parent($ldap, $newdn);
+ local ($newprefix, $newrest) = split(/,/, $newdn, 2);
+ local $rv = $ldap->moddn($_[1]->{'dn'},
+ newrdn => $newprefix,
+ newsuperior => $newrest);
+ if ($rv->code) {
+ &error(&text('ldap_erename',
+ "$_[1]->{'dn'}",
+ "$newdn",
+ "".&html_escape($rv->error).""));
+ }
+ $_[2]->{'dn'} = $newdn;
+ if ($newdn =~ /^([^=]+)=([^, ]+)/) {
+ $replace{$1} = [ $2 ];
+ }
+ }
+ else {
+ $_[2]->{'dn'} = $_[1]->{'dn'};
+ }
+
+ # Modify attributes
+ local $rv = $ldap->modify($_[2]->{'dn'}, replace => \%replace);
+ if ($rv->code) {
+ &error(&text('ldap_emodify',
+ "$_[2]->{'dn'}",
+ "".&html_escape($rv->error).""));
+ }
}
# Update in-memory cache
@@ -1025,21 +1126,84 @@ $_[2]->{'eline'} = $_[2]->{'cmt'} ? $_[1]->{'line'}+1 : $_[1]->{'line'};
$maps_cache{$_[0]}->[$idx] = $_[2] if ($idx != -1);
}
+# make_map_ldap_dn(&map, &conf)
+# Work out an LDAP DN for a map
+sub make_map_ldap_dn
+{
+local ($map, $conf) = @_;
+local $dn;
+local $scope = $conf->{'scope'} || 'sub';
+$scope = 'base' if (!$config{'ldap_doms'}); # Never create sub-domains
+local $id = $config{'ldap_id'} || 'cn';
+if ($map->{'name'} =~ /^(\S+)\@(\S+)$/ && $scope ne 'base') {
+ # Within a domain
+ $dn = "$id=$1,cn=$2,$conf->{'search_base'}";
+ }
+elsif ($map->{'name'} =~ /^\@(\S+)$/ && $scope ne 'base') {
+ # Domain catchall
+ $dn = "$id=default,cn=$1,$conf->{'search_base'}";
+ }
+else {
+ # Some other string
+ $dn = "$id=$map->{'name'},$conf->{'search_base'}";
+ }
+return $dn;
+}
+
+# get_ldap_key(&config)
+# Returns the attribute name for the LDAP key. May call &error
+sub get_ldap_key
+{
+local ($conf) = @_;
+local ($filter, $name_attr) = @_;
+if ($conf->{'query_filter'}) {
+ $filter = $conf->{'query_filter'};
+ $conf->{'query_filter'} =~ /([a-z0-9]+)=\%s/i ||
+ &error("Could not get attribute from ".
+ $conf->{'query_filter'});
+ $name_attr = $1;
+ $filter = "($filter)" if ($filter !~ /^\(/);
+ $filter =~ s/\%s/\*/g;
+ }
+else {
+ $filter = "(mailacceptinggeneralid=*)";
+ $name_attr = "mailacceptinggeneralid";
+ }
+return wantarray ? ( $name_attr, $filter ) : $name_attr;
+}
+
+# ensure_ldap_parent(&ldap, dn)
+# Create the parent of some DN if needed
+sub ensure_ldap_parent
+{
+local ($ldap, $dn) = @_;
+local $pdn = $dn;
+$pdn =~ s/^([^,]+),//;
+local $rv = $ldap->search(base => $pdn, scope => 'base',
+ filter => "(objectClass=top)",
+ sizelimit => 1);
+if (!$rv || $rv->code || !$rv->all_entries) {
+ # Does not .. so add it
+ local @pclasses = ( "top" );
+ local @pattrs = ( "objectClass", \@pclasses );
+ local $rv = $ldap->add($pdn, attr => \@pattrs);
+ }
+}
# init_new_mapping($maps_parameter) : $number
# gives a new number of mapping
sub init_new_mapping
{
- $maps = &get_maps($_[0]);
+$maps = &get_maps($_[0]);
- my $max_number = 0;
+my $max_number = 0;
- foreach $trans (@{$maps})
- {
- if ($trans->{'number'} > $max_number) { $max_number = $trans->{'number'}; }
- }
-
- return $max_number+1;
+foreach $trans (@{$maps})
+{
+if ($trans->{'number'} > $max_number) { $max_number = $trans->{'number'}; }
+}
+
+return $max_number+1;
}
# postfix_mail_file(user)
@@ -1047,15 +1211,15 @@ sub postfix_mail_file
{
local @s = &postfix_mail_system();
if ($s[0] == 0) {
- return "$s[1]/$_[0]";
- }
+return "$s[1]/$_[0]";
+}
elsif (@_ > 1) {
- return "$_[7]/$s[1]";
- }
+return "$_[7]/$s[1]";
+}
else {
- local @u = getpwnam($_[0]);
- return "$u[7]/$s[1]";
- }
+local @u = getpwnam($_[0]);
+return "$u[7]/$s[1]";
+}
}
# postfix_mail_system()
@@ -1065,17 +1229,17 @@ else {
sub postfix_mail_system
{
if (!defined(@mail_system_cache)) {
- local $home_mailbox = &get_current_value("home_mailbox");
- if ($home_mailbox) {
- @mail_system_cache = $home_mailbox =~ /^(.*)\/$/ ?
- (2, $1) : (1, $home_mailbox);
- }
- else {
- local $mail_spool_directory =
- &get_current_value("mail_spool_directory");
- @mail_system_cache = (0, $mail_spool_directory);
- }
- }
+local $home_mailbox = &get_current_value("home_mailbox");
+if ($home_mailbox) {
+@mail_system_cache = $home_mailbox =~ /^(.*)\/$/ ?
+ (2, $1) : (1, $home_mailbox);
+}
+else {
+local $mail_spool_directory =
+ &get_current_value("mail_spool_directory");
+@mail_system_cache = (0, $mail_spool_directory);
+}
+}
return wantarray ? @mail_system_cache : $mail_system_cache[0];
}
@@ -1086,20 +1250,20 @@ sub list_queue
local @qfiles;
&open_execute_command(MAILQ, $config{'mailq_cmd'}, 1, 1);
while() {
- next if (/^(\S+)\s+is\s+empty/i || /^\s+Total\s+requests:/i);
- if (/^([^\s\*\!]+)[\*\!]?\s*(\d+)\s+(\S+\s+\S+\s+\d+\s+\d+:\d+:\d+)\s+(.*)/) {
- push(@qfiles, { 'id' => $1,
- 'size' => $2,
- 'date' => $3,
- 'from' => $4 });
- }
- elsif (/\((.*)\)/ && @qfiles) {
- $qfiles[$#qfiles]->{'status'} = $1;
- }
- elsif (/^\s+(\S+)/ && @qfiles) {
- $qfiles[$#qfiles]->{'to'} .= "$1 ";
- }
- }
+next if (/^(\S+)\s+is\s+empty/i || /^\s+Total\s+requests:/i);
+if (/^([^\s\*\!]+)[\*\!]?\s*(\d+)\s+(\S+\s+\S+\s+\d+\s+\d+:\d+:\d+)\s+(.*)/) {
+push(@qfiles, { 'id' => $1,
+ 'size' => $2,
+ 'date' => $3,
+ 'from' => $4 });
+}
+elsif (/\((.*)\)/ && @qfiles) {
+$qfiles[$#qfiles]->{'status'} = $1;
+}
+elsif (/^\s+(\S+)/ && @qfiles) {
+$qfiles[$#qfiles]->{'to'} .= "$1 ";
+}
+}
close(MAILQ);
return @qfiles;
}
@@ -1109,11 +1273,11 @@ return @qfiles;
sub parse_queue_file
{
local @qfiles = ( &recurse_files("$config{'mailq_dir'}/active"),
- &recurse_files("$config{'mailq_dir'}/incoming"),
- &recurse_files("$config{'mailq_dir'}/deferred"),
- &recurse_files("$config{'mailq_dir'}/corrupt"),
- &recurse_files("$config{'mailq_dir'}/hold"),
- );
+ &recurse_files("$config{'mailq_dir'}/incoming"),
+ &recurse_files("$config{'mailq_dir'}/deferred"),
+ &recurse_files("$config{'mailq_dir'}/corrupt"),
+ &recurse_files("$config{'mailq_dir'}/hold"),
+);
local $f = $_[0];
local ($file) = grep { $_ =~ /\/$f$/ } @qfiles;
return undef if (!$file);
@@ -1121,31 +1285,31 @@ local $mode = 0;
local ($mail, @headers);
&open_execute_command(QUEUE, "$config{'postcat_cmd'} ".quotemeta($file), 1, 1);
while() {
- if (/^\*\*\*\s+MESSAGE\s+CONTENTS/ && !$mode) { # Start of headers
- $mode = 1;
- }
- elsif (/^\*\*\*\s+HEADER\s+EXTRACTED/ && $mode) { # End of email
- last;
- }
- elsif ($mode == 1 && /^\s*$/) { # End of headers
- $mode = 2;
- }
- elsif ($mode == 1 && /^(\S+):\s*(.*)/) { # Found a header
- push(@headers, [ $1, $2 ]);
- }
- elsif ($mode == 1 && /^(\s+.*)/) { # Header continuation
- $headers[$#headers]->[1] .= $1 unless($#headers < 0);
- }
- elsif ($mode == 2) { # Part of body
- $mail->{'size'} += length($_);
- $mail->{'body'} .= $_;
- }
+if (/^\*\*\*\s+MESSAGE\s+CONTENTS/ && !$mode) { # Start of headers
+$mode = 1;
+}
+elsif (/^\*\*\*\s+HEADER\s+EXTRACTED/ && $mode) { # End of email
+ last;
}
+elsif ($mode == 1 && /^\s*$/) { # End of headers
+ $mode = 2;
+ }
+elsif ($mode == 1 && /^(\S+):\s*(.*)/) { # Found a header
+ push(@headers, [ $1, $2 ]);
+ }
+elsif ($mode == 1 && /^(\s+.*)/) { # Header continuation
+ $headers[$#headers]->[1] .= $1 unless($#headers < 0);
+ }
+elsif ($mode == 2) { # Part of body
+ $mail->{'size'} += length($_);
+ $mail->{'body'} .= $_;
+ }
+}
close(QUEUE);
$mail->{'headers'} = \@headers;
foreach $h (@headers) {
- $mail->{'header'}->{lc($h->[0])} = $h->[1];
- }
+$mail->{'header'}->{lc($h->[0])} = $h->[1];
+}
return $mail;
}
@@ -1656,7 +1820,25 @@ elsif ($type eq "mysql") {
return undef;
}
elsif ($type eq "ldap") {
- # XXX
+ # Parse config, connect to LDAP server
+ local $conf = &ldap_value_to_conf($value);
+ $conf->{'search_base'} || return &text('ldap_esource', $value);
+
+ # Try a connect and a search
+ local $ldap = &connect_ldap_db($conf);
+ if (!ref($ldap)) {
+ return $ldap;
+ }
+ local @classes = split(/\s+/, $config{'ldap_class'} || "top");
+ local $rv = $ldap->search(base => $conf->{'search_base'},
+ filter => "(objectClass=$classes[0])",
+ sizelimit => 1);
+ if (!$rv || $rv->code && !$rv->all_entries) {
+ return &text('ldap_ebase', "$conf->{'search_base'}",
+ $rv ? $rv->error : "Unknown search error");
+ }
+
+ return undef;
}
else {
return &text('map_unknown', "$type");
@@ -1688,6 +1870,43 @@ $dbh || return &text('mysql_elogin',
return $dbh;
}
+# connect_ldap_db(&config)
+# Attempts to connect to an LDAP server with Postfix maps. Returns
+# a driver handle on success, or an error message string on failure.
+# XXX try all hosts
+# XXX handle :port syntax and ldap: syntax
+sub connect_ldap_db
+{
+local ($conf) = @_;
+if (defined($connect_ldap_db_cache)) {
+ return $connect_ldap_db_cache;
+ }
+eval "use Net::LDAP";
+if ($@) {
+ return &text('ldap_eldapmod', "Net::LDAP");
+ }
+local $port = $conf->{'server_port'} || 389;
+local @servers = split(/\s+/, $conf->{'server_host'} || "localhost");
+local $ldap = Net::LDAP->new($servers[0], port => $port);
+if (!$ldap) {
+ return &text('ldap_eldap', "$servers[0]", $port);
+ }
+if ($conf->{'start_tls'} eq 'yes') {
+ $ldap->start_tls;
+ }
+if ($conf->{'bind'} eq 'yes') {
+ local $mesg = $ldap->bind(dn => $conf->{'bind_dn'},
+ password => $conf->{'bind_pw'});
+ if (!$mesg || $mesg->code) {
+ return &text('ldap_eldaplogin', "$servers[0]",
+ "$conf->{'bind_dn'}",
+ $mesg ? $mesg->error : "Unknown error");
+ }
+ }
+$connect_ldap_db_cache = $ldap;
+return $ldap;
+}
+
# mysql_value_to_conf(value)
# Converts a MySQL config file or source name to a config hash ref
sub mysql_value_to_conf
@@ -1716,6 +1935,20 @@ else {
return $conf;
}
+# ldap_value_to_conf(value)
+# Converts an LDAP config file name to a config hash ref
+sub ldap_value_to_conf
+{
+local ($value) = @_;
+local $conf;
+local $cfile = $value;
+if ($cfile !~ /^\//) {
+ $cfile = &guess_config_dir()."/".$cfile;
+ }
+-r $cfile || &error(&text('ldap_ecfile', "$cfile"));
+return &get_backend_config($cfile);
+}
+
# can_map_comments(name)
# Returns 1 if some map can have comments. Not allowed for MySQL and LDAP.
sub can_map_comments
@@ -1743,6 +1976,7 @@ return 1;
sub supports_map_type
{
local ($type) = @_;
+return 1 if ($type eq 'hash'); # Assume always supported
if (!defined(@supports_map_type_cache)) {
@supports_map_type = ( );
open(POSTCONF, "$config{'postfix_config_command'} -m |");
@@ -1755,5 +1989,30 @@ if (!defined(@supports_map_type_cache)) {
return &indexoflc($type, @supports_map_type_cache) >= 0;
}
+# split_props(text)
+# Converts multiple lines of text into LDAP attributes
+sub split_props
+{
+local ($text) = @_;
+local %pmap;
+foreach $p (split(/\t+/, $text)) {
+ if ($p =~ /^(\S+):\s*(.*)/) {
+ push(@{$pmap{$1}}, $2);
+ }
+ }
+local @rv;
+local $k;
+foreach $k (keys %pmap) {
+ local $v = $pmap{$k};
+ if (@$v == 1) {
+ push(@rv, $k, $v->[0]);
+ }
+ else {
+ push(@rv, $k, $v);
+ }
+ }
+return @rv;
+}
+
1;