Thursday, July 1, 2010

Automatically Update Recipient Maps From Active Directory

The following directions are meant for people using Microsoft Exchange 2000 or Microsoft Exchange 2003.
This page describes how to configure your mail gateway to periodically get a list of valid recipient email addresses from your Exchange system. By doing this, you can configure your server to automatically reject any email addressed to invalid addresses. This will reduce the load on your exchange server, since it no longer has to process non-delivery reports, and it will reduce the load on your postfix server since it won't have to perform spam and virus scanning on the message.
BTW, the directions and script are courtesy Chris Covington - malth@umich.edu.

Install the perl module Net::LDAP:
perl -MCPAN -e shell
install Net::LDAP
You do not need to install the SSL optional module (I had issues installing it, but I went ahead and installed all the other optional modules when asked).

Next you will want to create and edit the script:
vi /usr/local/sbin/getadsmtp.pl
Copy and paste the code below into this new file.
#!/usr/bin/perl -T -w
 
# This script will pull all users' SMTP addresses from your Active Directory
# (including primary and secondary email addresses) and list them in the
# format "user@example.com OK" which Postfix uses with relay_recipient_maps.
# Be sure to double-check the path to perl above.
 
# This requires Net::LDAP to be installed.  To install Net::LDAP, at a shell
# type "perl -MCPAN -e shell" and then "install Net::LDAP"
 
use Net::LDAP;
use Net::LDAP::Control::Paged;
use Net::LDAP::Constant ( "LDAP_CONTROL_PAGED" );
 
# Enter the path/file for the output
$VALID = "/etc/postfix/relay_recipients";
open VALID, ">$VALID" or die "CANNOT OPEN $VALID $!";
 
# Enter the FQDN of your Active Directory domain controllers below
$dc1="domaincontroller1.example.com";
$dc2="domaincontroller2.example.com";
 
# Enter the LDAP container for your userbase.
# The syntax is CN=Users,dc=example,dc=com
# This can be found by installing the Windows 2000 Support Tools
# then running ADSI Edit.
# In ADSI Edit, expand the "Domain NC [domaincontroller1.example.com]" &
# you will see, for example, DC=example,DC=com (this is your base).
# The Users Container will be specified in the right pane as
# CN=Users depending on your schema (this is your container).
# You can double-check this by clicking "Properties" of your user
# folder in ADSI Edit and examining the "Path" value, such as:
# LDAP://domaincontroller1.example.com/CN=Users,DC=example,DC=com
# which would be $hqbase="cn=Users,dc=example,dc=com"
# Note:  You can also use just $hqbase="dc=example,dc=com"
$hqbase="cn=Users,dc=example,dc=com";
 
# Enter the username & password for a valid user in your Active Directory
# with username in the form cn=username,cn=Users,dc=example,dc=com
# Make sure the user's password does not expire.  Note that this user
# does not require any special privileges.
# You can double-check this by clicking "Properties" of your user in
# ADSI Edit and examining the "Path" value, such as:
# LDAP://domaincontroller1.example.com/CN=user,CN=Users,DC=example,DC=com
# which would be $user="cn=user,cn=Users,dc=example,dc=com"
# Note: You can also use the UPN login: "user\@example.com"
$user="cn=user,cn=Users,dc=example,dc=com";
$passwd="password";
 
# Connecting to Active Directory domain controllers
$noldapserver=0;
$ldap = Net::LDAP->new($dc1) or
   $noldapserver=1;
if ($noldapserver == 1)  {
   $ldap = Net::LDAP->new($dc2) or
      die "Error connecting to specified domain controllers $@ \n";
}
 
$mesg = $ldap->bind ( dn => $user,
                      password =>$passwd);
if ( $mesg->code()) {
    die ("error:", $mesg->error_text((),"\n"));
}
 
# How many LDAP query results to grab for each paged round
# Set to under 1000 for Active Directory
$page = Net::LDAP::Control::Paged->new( size => 990 );
 
@args = ( base     => $hqbase,
# Play around with this to grab objects such as Contacts, Public Folders, etc.
# A minimal filter for just users with email would be:
# filter => "(&(sAMAccountName=*)(mail=*))"
         filter => "(& (mailnickname=*) (| (&(objectCategory=person)
                    (objectClass=user)(!(homeMDB=*))(!(msExchHomeServerName=*)))
                    (&(objectCategory=person)(objectClass=user)(|(homeMDB=*)
                    (msExchHomeServerName=*)))(&(objectCategory=person)(objectClass=contact))
                    (objectCategory=group)(objectCategory=publicFolder) ))",
          control  => [ $page ],
          attrs  => "proxyAddresses",
);
 
my $cookie;
while(1) {
  # Perform search
  my $mesg = $ldap->search( @args );
 
# Filtering results for proxyAddresses attributes  
  foreach my $entry ( $mesg->entries ) {
    my $name = $entry->get_value( "cn" );
    # LDAP Attributes are multi-valued, so we have to print each one.
    foreach my $mail ( $entry->get_value( "proxyAddresses" ) ) {
     # Test if the Line starts with one of the following lines:
     # proxyAddresses: [smtp|SMTP]:
     # and also discard this starting string, so that $mail is only the
     # address without any other characters...
     if ( $mail =~ s/^smtp://igs ) {
       print VALID lc($mail)." OK\n"; 
     }
    }
  }
 
  # Only continue on LDAP_SUCCESS
  $mesg->code and last;
 
  # Get cookie from paged control
  my($resp)  = $mesg->control( LDAP_CONTROL_PAGED ) or last;
  $cookie    = $resp->cookie or last;
 
  # Set cookie in paged control
  $page->cookie($cookie);
}
 
if ($cookie) {
  # We had an abnormal exit, so let the server know we do not want any more
  $page->cookie($cookie);
  $page->size(0);
  $ldap->search( @args );
  # Also would be a good idea to die unhappily and inform OP at this point
     die("LDAP query unsuccessful");
}
 
$ldap->unbind;
 
# Add additional restrictions, users, etc. to the output file below.
#print VALID "user\@domain1.com OK\n";
#print VALID "user\@domain2.com 550 User unknown.\n";
#print VALID "domain3.com 550 User does not exist.\n";
 
close VALID;

Next set the permissions on the file to allow it to be executed:
chmod 500 /usr/local/sbin/getadsmtp.pl

Edit the file to customize it for your specific domain. Since the file is read only, you will need to use :w! to save the file in vi.
  1. Set $dc1 and $dc2 to the fully qualified domain names or IP addresses of 2 of your domain controllers.
  2. Set $hqbase equal to the LDAP path to the container or organizational unit which holds the email accounts for which you wish to get the email addresses.
  3. Set $user and $passwd to indicate which user account should be used to access this information. This account only needs to be a member of the domain, so it would be a good idea to setup an account specifically for this.

Try running the script. If it works correctly, it will create /etc/postfix/relay_recipients Note that if your postfix server is separated from your active directory controllers by a firewall, you will need to open TCP port 389 from the postfix server to the ADCs. At this point, you can update your /etc/postfix/main.cf to relay_recipient_maps and uncomment show_user_unknown_table_name (See Installing and Configuring Postfix).

Finally, you will want to setup a cron job to periodically update and build the /etc/postfix/relay_recipients.db file. In my case, I setup a script called /usr/local/sbin/update-relay-recipients.sh:
#!/bin/sh
 
/usr/local/sbin/getadsmtp.pl
cd /etc/postfix
postmap relay_recipients 
Run crontab to add this script to the scheduled jobs:
crontab -e
Now add the following lines to the bottom of the file. Note that this cron job will run every day at 2:30 AM to update the database file. You may want to run yours more frequently depending on how often you add new email users to your system.
# syncronize relay_recipients with Active Directory addresses
30 2 * * * /usr/local/sbin/update-relay-recipients.sh


5 comments:

  1. So, I'm trying to implement this with Microsoft Active Directory. I've confirmed my Base DN and filter with another tool and receive the correct results. Yet with this script, I got no errors and a blank relay_recipients file. Any ideas on where to look?

    ReplyDelete
    Replies
    1. I use this script too, and something in recent Perl versions has broken it. Haven't figured out what it is yet though.

      Delete
    2. Firing up Data::Dump gives us errorMessage => "Can't use string (\"proxyAddresses\") as an ARRAY ref while \"strict refs\" in use at /usr/share/perl5/Convert/ASN1/_encode.pm line 269, line 558.\n" which is a good start. Ok, so add square brackets around "proxyAddresses" after the filter:

      attrs => ["proxyAddresses"],

      Delete
  2. Adding the brackets worked.

    Thanks!

    ReplyDelete