--- amavisd~	Sat Mar 22 01:26:33 2003
+++ amavisd	Sat Mar 22 01:27:08 2003
@@ -182,7 +182,11 @@
 	    $viruses_that_fake_sender_re
 	    @lookup_sql_dsn $sql_key_fieldname
 	    $sql_select_policy $sql_select_white_black_list
-	    @lookup_ldap_host
+	    $enable_ldap $default_ldap $virus_lovers_ldap
+	    $banned_files_lovers_ldap $bypass_virus_checks_ldap
+	    $bypass_spam_checks_ldap $spam_tag_level_ldap
+	    $spam_tag2_level_ldap $spam_kill_level_ldap
+	    $spam_modifies_subj_ldap $local_domains_ldap
 	    %local_domains @local_domains_acl $local_domains_re
 	)],
 	'notifyconf' => [qw(
@@ -259,11 +263,7 @@
 # it makes no difference as file truncation will not be used.
 $can_truncate = 1;
 
-# Where to find LDAP server(s)?
-# A list of hashes: {hostname=>?, port=>?, timeout=>?, tls=>?,
-#                    base=>?, scope=>? filter=>?,
-#                    bind_dn=>?, bind_password=>?}.
-# Specify more than one for multiple (backup) LDAP servers.
+# Definitions of LDAP lookup queries.
 #
 # hostname      : The hostname of the LDAP server we connect to.
 #                 (Default = 'localhost')
@@ -274,24 +274,41 @@
 # base          : The DN that is the base object entry relative to
 #                 which the search is to be performed. (Default = undef)
 # scope         : Scope can be 'base', 'one' or 'sub'. (Default = 'sub')
-# filter        : The filter used to find the amavis account. The string
+# query_filter  : The filter used to find the amavis account. The string
 #                 must contain a '%m' token that will be replaced by the
 #                 actual e-mail address.
 #                 (Default = '(&(objectClass=amavisAccount)(mail=%m))')
+# res_attr      : (Default = undef)
+# res_filter    : (Default = %r)
 # bind_dn       : If binding is needed, this is where you specify the
 #                 DN to bind as. (Default = undef)
 # bind_password : Binding password. (Default = undef)
 #
-#@lookup_ldap_host = ( {hostname=>'ldap.domain', base=>'dc=example,dc=com'} );
-#
-# RELEASE NOTE: This release does not allow to change the attribute names
-# of the LDAP objects. They have to be 'amavisVirusLover',
-# 'amavisBannedFilesLover', 'amavisBypassVirusChecks', 'amavisBypassSpamChecks'
-# 'amavisSpamTagLevel', 'amavisSpamKillLevel'.
-# When white lists and black lists lookups will be implemented, the
-# corresponding attribute names will be 'amavisWhitelistSender' and
-# 'amavisBlacklistSender'.
-
+# $default_ldap = {
+#   hostname => 'ldap.example.com', tls => 1,
+#   base => 'dc=example,dc=com', scope => 'sub',
+#   query_filter => '(&(objectClass=amavisAccount)(mail=%m))'}
+# };
+#
+# $virus_lovers_ldap = {res_attr => 'amavisVirusLover'};
+# $banned_files_lovers_ldap = {res_attr => 'amavisBannedFilesLover'};
+# $bypass_virus_checks_ldap = {res_attr => 'amavisBypassVirusChecks'};
+# $bypass_spam_checks_ldap = {res_attr => 'amavisBypassSpamChecks'};
+# $spam_tag_level_ldap = {res_attr => 'amavisSpamTagLevel'};
+# $spam_kill_level_ldap = {res_attr => 'amavisSpamKillLevel'};
+#
+# $spam_whitelist_sender_ldap = {
+#   query_filter => '(&(objectClass=amavisAccount)(mail=%m)
+#                      (amavisWhitelistSender=%s))',
+#   res_filter => 'OK'};
+# $spam_blacklist_sender_ldap = {
+#   query_filter => '(&(objectClass=amavisAccount)(mail=%m)
+#                      (amavisBlacklistSender=%s))',
+#   res_filter => 'OK'};
+#
+# $local_domains_ldap = {
+#   query_filter => '(&(objectClass=mailDomain)(dc=%m))
+#   res_filter => 'OK'};
 #
 # Customizable notification messages, logging
 
@@ -4364,7 +4381,7 @@
 	    $spam_tag_level_sql $spam_tag2_level_sql $spam_kill_level_sql
 	    $spam_modifies_subj_sql $local_domains_sql $wb_listed_sql);
 
-use vars qw($user_id_ldap $virus_lovers_ldap $banned_files_lovers_ldap
+use vars qw($default_ldap $user_id_ldap $virus_lovers_ldap $banned_files_lovers_ldap
 	    $bypass_virus_checks_ldap $bypass_spam_checks_ldap
 	    $spam_tag_level_ldap $spam_tag2_level_ldap $spam_kill_level_ldap
 	    $spam_modifies_subj_ldap $local_domains_ldap $wb_listed_ldap);
@@ -4517,16 +4534,20 @@
 	    }
 	    undef @lookup_sql_dsn;   # destroy sensitive information
 	}
-	if ($extra_code_ldap && @lookup_ldap_host &&
-	    $child_invocation_count==1) {
-	    my($ldap) = Amavis::Lookup::LDAP::prepare_ldap_queries(
-		[ \$virus_lovers_ldap,        'amavisVirusLover'        ],
-		[ \$banned_files_lovers_ldap, 'amavisBannedFilesLover'  ],
-		[ \$bypass_virus_checks_ldap, 'amavisBypassVirusChecks' ],
-		[ \$bypass_spam_checks_ldap,  'amavisBypassSpamChecks'  ],
-		[ \$spam_tag_level_ldap,      'amavisSpamTagLevel'      ],
-	        [ \$spam_tag2_level_ldap,     'amavisSpamTag2Level'     ],
-		[ \$spam_kill_level_ldap,     'amavisSpamKillLevel'     ] );
+	if ($extra_code_ldap && $child_invocation_count==1) {
+	    # $ldap_wblist : TODO
+	    my $lf = sub {
+		Amavis::Lookup::LDAP->new($default_ldap, @_) if $_[0]
+	    }; #shorthand
+	    $virus_lovers_ldap        = $lf->($virus_lovers_ldap);
+	    $banned_files_lovers_ldap = $lf->($banned_files_lovers_ldap);
+	    $bypass_virus_checks_ldap = $lf->($bypass_virus_checks_ldap);
+	    $bypass_spam_checks_ldap  = $lf->($bypass_spam_checks_ldap);
+	    $spam_tag_level_ldap      = $lf->($spam_tag_level_ldap);
+	    $spam_tag2_level_ldap     = $lf->($spam_tag2_level_ldap);
+	    $spam_kill_level_ldap     = $lf->($spam_kill_level_ldap);
+	    $spam_modifies_subj_ldap  = $lf->($spam_modifies_subj_ldap);
+	    $local_domains_ldap       = $lf->($local_domains_ldap);
 	}
 
 	my($conn) = Amavis::In::Connection->new;
@@ -4890,8 +4911,8 @@
 		    my($ext) = @virusname ? $addr_extension_virus
 			     : @banned_filename ? $addr_extension_banned : '';
 		    if ($recipient_delimiter ne '' && $ext ne '' &&
-			lookup($r->recip_addr,
-			       $local_domains_sql, \%local_domains,
+			lookup($r->recip_addr, $local_domains_sql,
+			       $local_domains_ldap, \%local_domains,
 			       \@local_domains_acl, $local_domains_re)
 		    ) { # append appropriate address extensions
 			# to mailbox names if desired, but only to those
@@ -4941,8 +4962,8 @@
 		} else { #destiny==D_PASS (recip_destiny already set to D_PASS)
 		    if ($recipient_delimiter ne '' &&
 			$addr_extension_spam ne '' &&
-			lookup($r->recip_addr,
-			       $local_domains_sql, \%local_domains,
+			lookup($r->recip_addr, $local_domains_sql,
+			       $local_domains_ldap, \%local_domains,
 			       \@local_domains_acl, $local_domains_re)
 		    ) { # append appropriate address extensions
 			# to mailbox names if desired, but only to those
@@ -5141,7 +5162,8 @@
 	my($recip) = $r->recip_addr;
 	my($blacklisted) = $r->recip_blacklisted_sender;
 	my($is_local) =
-	    lookup($recip, $local_domains_sql, \%local_domains,
+	    lookup($recip, $local_domains_sql,
+	                   $local_domains_ldap, \%local_domains,
 		           \@local_domains_acl, $local_domains_re);
 	my($tag_level) =
 	    lookup($recip, $spam_tag_level_sql, $spam_tag_level_ldap,
@@ -5319,7 +5341,8 @@
 #	do_log(5,"do_virus: skip recipient notifications for unknown senders");
     } else {
 	my(@locals) = grep { $warn_offsite ||
-			     lookup($_, $local_domains_sql, \%local_domains,
+			     lookup($_, $local_domains_sql,
+			                $local_domains_ldap, \%local_domains,
 					\@local_domains_acl, $local_domains_re)
 			   } @{$msginfo->recips};
 	if (@locals) {
@@ -5624,7 +5647,7 @@
     eval $extra_code_sql or die "Problem in the Lookup::SQL code: $@";
     $extra_code_sql = 1;   # release memory occupied by the source code
 }
-if (!@lookup_ldap_host) { $extra_code_ldap = undef }
+if (!$enable_ldap) { $extra_code_ldap = undef }
 else {
     eval $extra_code_ldap or die "Problem in the Lookup::LDAP code: $@";
     $extra_code_ldap = 1;   # release memory occupied by the source code
@@ -5735,7 +5758,8 @@
 %builtins = (
     d => sub {rfc2822_timestamp()}, # provide RFC 2822 date-time (current time)
     h => $myhostname, # dns name of this host, or configurable name
-    l => sub {lookup($MSGINFO->sender, $local_domains_sql, \%local_domains,
+    l => sub {lookup($MSGINFO->sender, $local_domains_sql,
+				       $local_domains_ldap, \%local_domains,
 				       \@local_domains_acl, $local_domains_re)
 	      ? 1 : undef},     # sender is local
     s => sub {qquote_rfc2821_local($MSGINFO->sender)}, # original envelope sender in <>
@@ -6073,24 +6097,33 @@
 
 __DATA__
 #^L
-package Amavis::Lookup::LDAP; 
+package Amavis::Lookup::LDAP;
 # by Jacques Supcik, PhD
 # IP-Plus Internet Services - Swisscom Enterprise Solutions Ltd
 # Genfergasse 14, 3050 Bern, Switzerland (http://www.ip-plus.net/)
-#   2003-02
+# March 2003
 
 use strict;
 BEGIN {
   use Exporter ();
-  use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
+  use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION
+              $ldap_sys_default %ldap_cache);
   @ISA = qw(Exporter);
-}
+  $VERSION = '1.14';
 
-BEGIN {
   import Amavis::Util qw(do_log);
   import Amavis::Conf qw(:platform :confvars);
   import Amavis::Timing qw(section_time);
   import Amavis::rfc2821_2822_Tools qw(split_address split_localpart);
+
+  $ldap_sys_default = {
+    hostname => 'localhost', port => 389, timeout => 120, tls => 0,
+    base => undef, scope => 'sub',
+    query_filter => '(&(objectClass=amavisAccount)(mail=%m))',
+    res_attr => undef, res_filter => '%r',
+    bind_dn => undef, bind_password => undef
+  };
+  %ldap_cache = ();
 }
 
 sub trim {
@@ -6098,167 +6131,151 @@
   $str =~ s/\s+$//; $str =~ s/^\s+//;
   $str;
 }
- 
+
 sub new {
   my $proto = shift;
   my $class = ref($proto) || $proto;
-  my ($srv, $attribute) = @_;
-  my $log_prefix = __PACKAGE__ . "::new (LDAP, $attribute) -";
+  my ($default, $query) = @_;
   my ($self) = bless {}, $class;
-  $self->{srv} = $srv;
-  $self->{attribute} = $attribute;
-  my $schema = $srv->{_LDAP}->schema(); # Lookup schema
-  if ($schema) {
-    my $sa = $schema->attribute($attribute);
-    if ($sa and $sa->{equality} eq 'booleanMatch' and $sa->{'single-value'}) {
-      $self->{type} = "B" # Boolean
-    } elsif ($sa and $sa->{equality} eq 'integerMatch' and 
-      $sa->{'single-value'}) {
-      $self->{type} = "N" # Number
-    } elsif ($sa and not $sa->{'single-value'}) {
-      $self->{type} = "L" # List
-    } elsif ($sa) {
-      $self->{type} = "S" # String
-    } else {
-      do_log(1, "$log_prefix attribute not defined in schema");
-      $self->{type} = "S" # attribute not defined, default String
-    }
-  } else {
-    do_log(1, "$log_prefix unable to read LDAP schema");
-    $self->{type} = "S" # If no schema is defined, default String
+  my $llog = sub {
+    my $level = shift;
+    my $template = shift;
+    my $prefix = __PACKAGE__."::new (res_attr->".$query->{res_attr}.")";
+    do_log($level, sprintf("$prefix - $template", @_));
+  };
+  # Replace undefined attributes by defaults
+  foreach (qw(hostname port timeout tls base scope query_filter
+    res_attr res_filter bind_dn bind_password)) {
+    $query->{$_} = $default->{$_} unless (defined $query->{$_});
+    $query->{$_} = $ldap_sys_default->{$_} unless (defined $query->{$_});
   }
-  do_log(5, sprintf("$log_prefix type='%s'",$self->{type}));
-  return $self;
-}
-
-# Connect to a ldap server.  Take a list of ldap connection
-# parameters and try each until one succeeds. If one succeeds,
-# it returns a reference to hash that has to be passwd to the
-# constructor. Else, it returns undef.
-sub connect_to_ldap(@) {
-  my (@servers) = @_;  # a list of Hosts to try connecting to sequentially
-  my $log_prefix = __PACKAGE__ . "::connect_to_ldap -";
-  my $ldap = undef;
-  my $srv = undef;
-  for my $s (@servers) {
-    do_log(5, sprintf("$log_prefix trying '%s'", $s->{hostname}));
-    $ldap = Net::LDAP->new($s->{hostname},
-      port=>($s->{port} || 389),
-      timeout=>($s->{timeout} || 120),
-      onerror=>'undef'
-    );
+  my $ldap;
+  my $cache_key = join "\036", ($query->{hostname}, $query->{port},
+    $query->{timeout}, $query->{tls},
+    $query->{bind_dn}, $query->{bind_password});
+  if (exists $ldap_cache{$cache_key}) {
+    $llog->(5, "Fetching ldap connection from cache");
+    $ldap = $ldap_cache{$cache_key};
+  } else {
+    $llog->(5, "trying to connect to '%s'", $query->{hostname});
+    $ldap = Net::LDAP->new($query->{hostname}, port=>$query->{port},
+      timeout=>$query->{timeout}, onerror=>'undef');
     if ($ldap) {
-      do_log(5, sprintf ("$log_prefix connection to '%s' succeeded",
-	  $s->{hostname}));
+      $llog->(5, "connection to '%s' succeeded", $query->{hostname});
     } else {
-      do_log(1, sprintf ("$log_prefix unable to connect to host '%s'",
-	  $s->{hostname}));
-      next
+      $llog->(0, "unable to connect to host '%s'. LDAP lookups disabled.",
+        $query->{hostname});
+      return undef;
     }
-    if ($s->{tls}) { # TLS required
+    if ($query->{tls}) { # TLS required
       my $tlsVer = $ldap->start_tls(verify=>'none');
-      do_log(5, sprintf ("$log_prefix TLS version %s enabled", $tlsVer));
+      $llog->(5, "TLS version %s enabled", $tlsVer);
     }
-    if ($s->{bind_dn}) { # Binding required
-      if ($ldap->bind ($s->{bind_dn}, password => $s->{bind_password})) {
-        do_log(5, sprintf ("$log_prefix bind '%s' succeeded", $s->{bind_dn}));
-	$srv = $s; last;
+    if ($query->{bind_dn}) { # Binding required
+      if ($ldap->bind ($query->{bind_dn}, password => $query->{bind_password})) {
+        $llog->(5, "bind '%s' succeeded", $query->{bind_dn});
       } else {
-        do_log(1, sprintf ("$log_prefix unable to bind '%s'",$s->{bind_dn}));
-        $ldap = undef;
-        next;
+        $llog->(1, "unable to bind '%s'",$query->{bind_dn});
+        return undef;
       }
-    } else {
-      $srv = $s; last;
     }
+    $ldap_cache{$cache_key} = $ldap;
   }
-  if (@servers > 1 and not $srv) {
-    do_log(0, "$log_prefix unable to connect to any LDAP server at all!");
-    return undef;
-  } else {
-    $srv->{base} = '' unless defined $srv->{base};
-    $srv->{scope} = 'sub' unless defined $srv->{scope};
-    unless (defined $srv->{filter}) {
-      $srv->{filter} = '(&(objectClass=amavisAccount)(mail=%m))'
-    }
-    $srv->{_LDAP} = $ldap;
-    return $srv;
+  $self->{ldap} = $ldap;
+  foreach (qw(base scope query_filter res_attr res_filter)) {
+    $self->{$_} = $query->{$_};
   }
-}
-
-# connect to the LDAP server and prepare queries
-# (called at the beginning of child's life from process_request)
-sub prepare_ldap_queries {
-  my (@attributes) = @_;
-  my $log_prefix = __PACKAGE__ . "::prepare_ldap_queries -";
-  my $srv = connect_to_ldap(@lookup_ldap_host);
-  section_time('ldap-connect');
-  unless (defined($srv)) {
-    do_log(0, "$log_prefix Error connecting to LDAP, ldap lookups disabled: ");
+  if ($query->{res_attr} eq "dn") {
+    $self->{type} = "S" # String
   } else {
-    # prepare lookup objects with incorporated field names
-    for my $a (@attributes) {
-      my($var_ref, $attr_name) = @$a;
-      $$var_ref = Amavis::Lookup::LDAP->new($srv, $attr_name);
+    my $schema = $ldap->schema(); # Lookup schema
+    if ($schema) {
+      my $sa = $schema->attribute($query->{res_attr});
+      if ($sa and $sa->{equality} eq 'booleanMatch' and $sa->{'single-value'}) {
+        $self->{type} = "B" # Boolean
+      } elsif ($sa and $sa->{equality} eq 'integerMatch' and
+        $sa->{'single-value'}) {
+        $self->{type} = "N" # Number
+      } elsif ($sa and not $sa->{'single-value'}) {
+        $self->{type} = "L" # List
+      } elsif ($sa) {
+        $self->{type} = "S" # String
+      } else {
+        $llog->(1, "attribute not defined in schema");
+        $self->{type} = "S" # attribute not defined, default String
+      }
+    } else {
+      $llog->(1, "unable to read LDAP schema");
+      $self->{type} = "S" # If no schema is defined, default String
     }
-    section_time('ldap-prepare');
   }
-  undef @lookup_ldap_host; # destroy sensitive information
+  $llog->(5, "type='%s'",$self->{type});
+  return $self;
 }
 
 sub lookup_ldap_exact {
   my $self = shift;
   my ($addr) = @_;
-  my $log_prefix = __PACKAGE__ . "::lookup_ldap_exact($addr) -";
+  my $llog = sub {
+    my $level = shift;
+    my $template = shift;
+    my $prefix = __PACKAGE__."::lookup_ldap_exact ($addr)";
+    do_log($level, sprintf("$prefix - $template", @_));
+  };
   unless (defined $self) {
-    do_log(5, "$log_prefix object undefined, no match");
+    $llog->(5, "object undefined, no match");
     return undef;
   }
-  unless ((defined $self->{srv}) and (defined $self->{srv}->{_LDAP})) {
-    do_log(5, "$log_prefix - null ldap object, no match");
+  unless (defined $self->{ldap}) {
+    $llog->(5, "null ldap object, no match");
     return undef;
   }
-  my $base = $self->{srv}->{base};
-  my $scope = $self->{srv}->{scope};
-  my $filter = $self->{srv}->{filter};
+  my $filter = $self->{query_filter};
   $filter =~ s/%m/$addr/g;
-  my $attribute = $self->{attribute};
-  do_log(5, "$log_prefix searching attribute=$attribute, " .
-    "filter=$filter, base=$base, scope=$scope");
-  my $res = $self->{srv}->{_LDAP}->search (
-    base => $base,
-    scope => $scope,
-    filter => $filter
+  my $attribute = $self->{res_attr};
+  $llog->(5, "searching attribute=%s, filter=%s, base=\"%s\", scope=\"%s\"",
+    $self->{res_attr}, $filter, $self->{base}, $self->{scope});
+  my $res = $self->{ldap}->search (
+    base => $self->{base}, scope => $self->{scope}, filter => $filter
   );
   unless (defined $res) {
-    do_log(5, "$log_prefix result undefined, no match");
+    $llog->(5, "result undefined, no match");
     return undef;
   }
-  do_log(5, sprintf("$log_prefix result:%s", $res->code));
+  $llog->(5, "result:%s", $res->code);
   if (my $entry = $res->pop_entry) {
-    if ($entry->exists($attribute)) {
+    if ($self->{res_attr} eq "dn") {
+      my $x = trim($entry->dn);
+      my $f = $self->{res_filter}; $f =~ s/%r/$x/g;
+      $llog->(5, "dn match: %s (%s)", $x, $f);
+      return $f;
+    } elsif ($entry->exists($self->{res_attr})) {
       if ($self->{type} eq "B") {
-	my $x = (uc($entry->get_value($attribute)) eq "TRUE") ? 1 : 0;
-        do_log(5, "$log_prefix boolean match: $x");
-	return $x;
+	my $x = (uc($entry->get_value($self->{res_attr})) eq "TRUE") ? 1 : 0;
+  	my $f = $self->{res_filter}; $f =~ s/%r/$x/g;
+        $llog->(5, "boolean match: %s (%s)", $x, $f);
+	return $f;
       } elsif ($self->{type} eq "N") {
-	my $x = 0 + scalar $entry->get_value($attribute);
-        do_log(5, "$log_prefix numeric match: $x");
-	return $x;
+	my $x = 0 + scalar $entry->get_value($self->{res_attr});
+  	my $f = $self->{res_filter}; $f =~ s/%r/$x/g;
+        $llog->(5, "numeric match: %s (%s)", $x, $f);
+	return $f;
       } elsif ($self->{type} eq "S") {
-	my $x = trim(scalar $entry->get_value($attribute));
-        do_log(5, "$log_prefix string match: $x");
-	return $x
+	my $x = trim(scalar $entry->get_value($self->{res_attr}));
+  	my $f = $self->{res_filter}; $f =~ s/%r/$x/g;
+        $llog->(5, "string match: %s (%s)", $x, $f);
+	return $f;
       } else {
-	my @x = map { trim($_) } $entry->get_value($attribute);
-        do_log(5,"$log_prefix list match: (" . join (", ", @x) . ")");
-	return wantarray ? @x : \@x;
+	my @x = map { trim($_) } $entry->get_value($self->{res_attr});
+	my @f = map { my $f = $self->{res_filter}; $f =~ s/%r/$_/g; $f } @x;
+        $llog->(5, "list match: %s (%s)", join (", ", @x), join (", ", @f));
+	return wantarray ? @f : \@f;
       }
     } else {
-      do_log(5, "$log_prefix attribute does not exists, no match");
+      $llog->(5, "attribute does not exists, no match");
     }
   } else {
-    do_log(5, "$log_prefix address not found, no match");
+    $llog->(5, "address not found, no match");
   }
   return undef
 }
@@ -6266,6 +6283,12 @@
 sub lookup_ldap {
   my $self = shift;
   my ($addr) = @_;
+  my $llog = sub {
+    my $level = shift;
+    my $template = shift;
+    my $prefix = __PACKAGE__."::lookup_ldap ($addr)";
+    do_log($level, sprintf("$prefix - $template", @_));
+  };
   my $log_prefix = __PACKAGE__ . "::lookup_ldap($addr) -";
   my ($localpart, $domain) = split_address($addr);
   my $res;
@@ -6282,10 +6305,11 @@
     $res = $self->lookup_ldap_exact ($localpart.$recipient_delimiter.
       $extension.'@'.$domain);
     if (defined $res) { return $res }
-  } 
+  }
   $res = $self->lookup_ldap_exact($localpart.'@'.$domain);  # user@example.com
   if (defined $res) { return $res }
-  if (Amavis::Lookup::lookup($addr, \@local_domains, $local_domains_re)) {
+  if (Amavis::Lookup::lookup($addr, \%local_domains,
+				    \@local_domains_acl, $local_domains_re)) {
     if ($extension ne '') { # user+foo
       $res = $self->lookup_ldap_exact($localpart.$recipient_delimiter.
 	$extension);
@@ -6294,7 +6318,7 @@
     $res = $self->lookup_ldap_exact ($localpart); # user
     if (defined $res) { return $res }
   }
-  $res = $self->lookup_ldap_exact ('@'.$domain); # @example.co
+  $res = $self->lookup_ldap_exact ('@'.$domain); # @example.com
   if (defined $res) { return $res }
   $res = $self->lookup_ldap_exact ('@.'); # @. (catchall)
   return $res
