From 3f3e2eebe343a82e43b3b8c3dfd83fe2a9d6b8a5 Mon Sep 17 00:00:00 2001 From: Jamie Cameron Date: Sun, 28 Oct 2007 00:07:57 +0000 Subject: [PATCH] Pretty much completed LDAP map support --- postfix/config | 1 + postfix/config-freebsd | 1 + postfix/config-mandrake-linux | 1 + postfix/config-msc-linux | 1 + postfix/config-netbsd | 1 + postfix/config.info | 6 + postfix/lang/en | 11 + postfix/postfix-lib.pl | 399 ++++++++++++++++++++++++++++------ 8 files changed, 351 insertions(+), 70 deletions(-) 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;