Extraire des données Active Directory pour Postfix

Ou comment extraire les mails valides d’un domaine Active Directory pour les fournir à Postfix.

“Pourquoi faire ?” Me direz-vous. Et bien c’est simple, refuser un mail envoyé à une adresse mail qui n’existe pas, avant de la relayer à votre infrastructure Exchange, c’est chouette. Un peu comme un retour de la poste “N’habite plus à l’adresse indiquée” en somme.

1. Configuration de Postfix

Pour demander à Postfix d’y regarder à deux fois avant d’accepter un mail, il faut lui dire dans quelle liste il va devoir chercher une occurrence. Pour ça on ajoute la ligne suivante dans main.cf:

relay_recipient_maps = hash:/usr/local/etc/postfix/relay_recipients

Ce qui à pour effet de demander à Postfix de regarder dans ce fichier à chaque fin de commande RCPT To: qu’il reçoit et de répondre par une erreur s’il ne trouve rien qui corresponde.

Formidable n’est-ce pas ? Mais maintenant il faut le remplir ce fichier (sous la forme mailaddress@example.com OK pour un fichier de type “hash”). Donc soit on embauche quelqu’un pour réactualiser ce fichier à chaque nouvelle adresse-mail (sans oublier le “postmap /usr/local/etc/postfix/relay_recipients” après chaque modif), soit on demande (gentiment) à son serveur de le faire à notre place :)

Mais juste avant ça, regardons les différents options (non exhaustives, cf “man 5 postconf” pour les curieux) qui s’offrent à nous pour stocker nos belles adresses e-mails valides dans notre infrasctructure Exchange.

  • Soit on les met tel quels dans notre fichier plat avec un “postmap <relay_recipients>” qui va bien pour en faire un “<relay_recipients.db>” au format BerkeleyDB.
  • Soit on met nos jolies adresses dans une base de donnée type MySQL ou PostgreSQL.
  • Soit on est un bourrin et on fait directement des requêtes LDAP dans Active Directory pour chaque vérification d’adresse (et à plusieurs par seconde sur un serveur mail d’entreprise moyen, vos serveurs Active Directory vont faire une drôle de tronche).

Etant de nature mi-figue mi-raisin, et puisque mon serveur dispose déjà du moteur de base de donnée MySQL, j’opte pour la deuxième option.

2. Ca va scripter chérie !

Résumons ! Nous voulons demander des infos à ActiveDirectory donc LDAP, les mettre sous la forme “mailaddress@example.com” et les écrire dans une base MySQL (qu’on appellera postfix). Et pour ça le moteur de script Perl c’est super ! Pourquoi ? Parcequ’il existe une foultitude de modules Perl pour moult moteurs de base de données, protocoles réseaux, serveurs, j’en passe et des meilleures. Ce qui fait qu’en un script, on peut faire tout ce qu’il nous faut et même plus (par contre pour faire café, voyez plutôt avec Java).

2.1 Avant de “coder”

Avant de vous lancer à corps perdu dans le copier-col^WWécriture de ce script, veuillez noter les pré requis dont vous aurez besoin en matière de modules Perl (attention ce sont les noms des ports FreeBSD):

  • perl 5.8.x (Le moteur de script mondialement connu)
  • p5-DBD-mysql50 (Le module pour parler à sa base de donnée)
  • p5-perl-ldap (Le module pour parler LDAP dans le texte)
  • p5-DBI (Le module dont dépend p5-DBD pour fonctionner)
  • p5-Storable (Le module dont dépend p5-DBI pour fonctionner)

Assurez-vous aussi de créer une table dans votre base de donnée  avec un index pour accueillir vos adresses e-mails. Ce n’est pas l’objet de ce (long) billet mais voici comment qu’on fait (on part du principe d’être déjà connecté à MySQL):

mysql> CREATE DATABASE postfix;
mysql> CREATE TABLE valid_emails (mail_addresses VARCHAR(128) NOT NULL) type=MyISAM;
mysql> CREATE UNIQUE INDEX id_valid_emails_01 ON valid_emails(mail_addresses);

2.2 Le script

Evidement (quoique…) le script va se partager en trois actions, la récolte d’infos, leur mise en forme et leur mise à jours dans la base de donnée. Et comme un exemple vaut mieux qu’un long discours:

#!/usr/bin/perl
# On déclare ci-dessus que c'est un script perl

# On charge nos modules Perl. Notez le "Paged" du module LDAP, il va nous permettre
# d'extraire les données d'Active Directory en contournant la limite de 100 résultats
# par requêtes de celui-ci
use DBI();
use Net::LDAP;
use Net::LDAP::Control::Paged;
use Net::LDAP::Constant ( "LDAP_CONTROL_PAGED" );
# On (est un con, je sais) déclare les constantes pour nos contrôleurs de domaine
$dc1="nameserver1";
$dc2="nameserver2";
$hqbase="dc=example,dc=com"; # Votre nom de domain complet

# Les login et mot de passe d'un compte ayant les droits en lecture (et seulement en lecture)
# dans Active Directory.
$ldapuser="user";
$ldappasswd="passwd";
# Le nom d'hôte abritant MySQL, le type de base de donnée, le nom de la base et
# de la table à remplir et un login/mot de passe d'un utilisateur MySQL ayant
# les droits INSERT et DELETE sur votre table et seulement celle-ci, histoire
# d'éviter les bêtises.
$dbhost="localhost";
$dbtype="mysql";
$dbname="postfix";
$dbtable="valid_emails";
$dbcolumn="mail_addresses";
$dbuser="user";
$dbpasswd="passwd";
# On se connecte à Active Directory avec une gestion d'erreur en cas de problème
$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 => $ldapuser,
                     password =>$ldappasswd);
if ( $mesg->code()) {
    die ("error:", $mesg->code(),"\n","error name: ",$mesg->error_name(),
        "\n", "error text: ",$mesg->error_text(),"\n");
}

# On se connecte au moteur de base de donnée MySQL
my $dbh = DBI->connect("DBI:$dbtype:database=$dbname;host=$dbhost","$dbuser","$dbpasswd");

# On prépare notre requête LDAP en prenant bien soin de ne pas dépasser une taille de 1000
# réponses par mise en cache de la requête.
$page = Net::LDAP::Control::Paged->new( size => 990 );

# On applique un filtrage des résultats LDAP pour ne garder que ce qui nous intéresse
# (voir aussi RCF 4515, LDAP:String Representation of Search Filters)
# Et on recherche l'attribut proxyAddresses qui contient les adresses e-mails sous forme
# multivaluée

@args = ( base     => $hqbase,
          filter => "(& (mailnickname=*)(| (&(objectCategory=person)
                     (objectClass=user)(!(homeMDB=*))(!(msExchHomeServerName=*)))
                     (&(objectCategory=person)(objectClass=user)(|(homeMDB=*)
                     (msExchHomeServerName=*)))
                     (objectCategory=group)(objectCategory=publicFolder)))",
          control  => [ $page ],
          attrs  => "proxyAddresses",
);

# On commence la recherche pour de vrai
my $cookie;
while(1) {
  my $mesg = $ldap->search( @args );

# Et on filtre les résulats
  foreach my $entry ( $mesg->entries ) {

    # En ne gardant que les lignes contenant "proxyAddresses".
     foreach my $mail ( $entry->get_value( "proxyAddresses" ) ) {
     # On dégage les vilains "smtp:" et "SMTP:" qui se trouvent devant nos adresses e-mails
      if ( $mail =~ s/^(smtp|SMTP)://gs ) {
	 # On passe tout en minuscule pour Postfix
	 $mail = lc($mail);
         # On écrit le tout dans notre base de donnée
         # Notez que REPLACE ici est très pratique car il permet à la fois de mettre à jour
         # les données lorsqu'elles existent, grâce à l'index, et de les INSERT-er si elles
         # n'existent pas encore. Voyez aussi le (" . $dbh->quote("$mail") . ") qui permet
         # d'insérer nos donnéesau format caractère.
	 $dbh->do("REPLACE INTO $dbtable ($dbcolumn) VALUES (" . $dbh->quote("$mail") . ")");
        }
     }
  }

  # J'avoue le morceau qui suit n'est pas de moi, je laisse donc les commentaires d'origine
  # En gros on s'assure que les requêtes LDAP mises en cache se passent bien
  # 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");
}

Rendez votre script exécutable avec chmod +x et voilà ! Evidemment, faites un petit SELECT sur votre table afin de vérifier que les mails sont bien dans la table valid_emails, et que ces valeurs ne se retouvent pas en double quand on lance deux fois le script coup sur coup.

3. Postfix, le retour

Tout ça c’est bien gentil, mais à la base nous voulons permettre à Postfix de faire une requête dans notre table toute chaude, afin d’y vérifier que les destinataires existent bien.

Pour ça il faut modifier un chouilla notre ligne relay_recipient_maps dans main.cf:

relay_recipient_maps = mysql:/usr/local/etc/postfix/relay_recipients

Puis modifier le contenu de ce fichier pour qu’il ressemble à ça:

# Autant dans le script il nous fallait un utilisateur MySQL ayant des droits d'écriture sur
# la table "valid_emails", autant ici un utilisateur MySQL en lecture seule fera l'affaire.
user = dbuser
password = dbpasswd
dbname = postfix
query = SELECT mail_addresses FROM valid_emails WHERE mail_addresses='%s'

N’oubliez pas de recharger la conf de Postfix:

# /usr/local/etc/rc.d/postfix reload

Le tout suivi d’un:

% postmap /usr/local/etc/postfix/relay_recipients

Puis on teste notre requête via postmap:

% postmap -q “mailaddress@example.com” mysql:/usr/local/etc/postfix/relay_recipients

Si tout fonctionne, l’email demandé est affiché sur la sortie standard, sinon rien n’est affiché et il faut tout vous retaper du début ;) .

Voilà ! Postfix rejette maintenant tous les mails envoyés à des adresses mails non valides sur notre domaine Active Directory, c’est déjà ça en moins à traiter pour votre serveur mail adoré.

Sep09

Leave a Reply

You must be logged in to post a comment.