#!/usr/bin/perl -w # modules use strict; use warnings; use Getopt::Std; use Fcntl qw(:flock); use Sys::Hostname; use Sys::Syslog; use Net::LDAP; use Net::LDAP::Control::Paged; use Net::LDAP::Constant qw(LDAP_CONTROL_PAGED); # defaults our $BaseDirectory = "/var/CommuniGate"; our $LdapUrl = "localhost"; our $BindDn; # constructed from default domain if unset our $Password; # must have in configfile our $Facility = "LOG_MAIL"; our $MailAttribute = "mail"; our $Filter = "(objectclass=CommuniGateAccount)"; our $LockFile = "/var/run/aliassync.lock"; our $PageSize; # no paged results if undef or 0 # parse commandline $Getopt::Std::STANDARD_HELP_VERSION = 1; my %opt = ( c => "/usr/local/etc/aliassync.conf", n => 0, ); getopts("c:n", \%opt) || die "use --help\n"; # load config if (!do $opt{c}) { my $err = "not a valid Perl config file (did you forget '1;' " . "at the end of the file?)"; $err = $@ if $@; $err = $! if $!; die "$opt{c}: $err"; } die "$opt{c}: need BaseDirectory" if !$BaseDirectory; die "$opt{c}: need LdapUrl" if !$LdapUrl; die "$opt{c}: need Password" if !$Password; die "$opt{c}: need Facility" if !$Facility; die "$opt{c}: need MailAttribute" if !$MailAttribute; die "$opt{c}: need LockFile" if !$LockFile; if ($opt{n}) { print "loaded config $opt{c}\n"; } else { open(LOCK, ">$LockFile") || die "$LockFile: $!"; if (!flock(LOCK, LOCK_EX | LOCK_NB)) { die "already running" if $!{EWOULDBLOCK}; die "$LockFile: $!"; } } # get default domain my $mainSettings = "$BaseDirectory/Settings/Main.settings"; my $DomainName; if (-e $mainSettings) { open(FH, $mainSettings) || die "$mainSettings: $!"; while () { if (/DomainName = ([^;]+)/o) { $DomainName = $1; print "$mainSettings: DomainName is $DomainName\n" if $opt{n}; last; } } close(FH); } if (!$DomainName) { $DomainName = hostname; print "assuming hostname '$DomainName' as DomainName\n" if $opt{n}; } if (!$BindDn) { $BindDn = "postmaster\@$DomainName"; print "assuming '$BindDn' as BindDn\n" if $opt{n}; } # read aliases for default domain my %aliasesOnDisk = (); &parseAliasesFile(\%aliasesOnDisk, $DomainName, "Accounts"); # read aliases for other domains my $domainsDirectory = "$BaseDirectory/Domains"; opendir(DH, $domainsDirectory) || die "$domainsDirectory: $!"; foreach (readdir(DH)) { &parseAliasesFile(\%aliasesOnDisk, $_, "Domains/$_"); } closedir(DH); print &plural(\%aliasesOnDisk), " covered by aliases\n" if $opt{n}; # bind to LDAP my %aliasesInLdap = (); my $ldap = Net::LDAP->new($LdapUrl) || die "$@"; my $msg = $ldap->bind($BindDn, password => $Password); die $msg->error() if $msg->is_error(); # construct search parameters my @searchParams = ( filter => "(&($MailAttribute=*)$Filter)", attrs => [ $MailAttribute, ], ); my $pageControl; if ($PageSize) { $pageControl = Net::LDAP::Control::Paged->new(size => $PageSize); push @searchParams, [ control => [ $pageControl, ], ]; } # read mail addresses from LDAP for (;;) { $msg = $ldap->search(@searchParams); die $msg->error() if $msg->is_error(); for (my $i = $msg->count() - 1; $i >= 0; $i--) { my $entry = $msg->entry($i); $aliasesInLdap{$entry->dn()} = { map { $_ => undef } @{$entry->get_value($MailAttribute, asref => 1)} }; } last if !$PageSize; my ($control) = $msg->control(LDAP_CONTROL_PAGED) || last; my $cookie = $control->cookie() || last; $pageControl->cookie($cookie); } print "LDAP contains ", &plural(\%aliasesInLdap), "\n" if $opt{n}; # open syslog openlog("aliassync", "pid", $Facility); # compare alias database on disk to mail addresses in LDAP, # remember all mismatching entries my %dnsToUpdate = (); foreach my $dn(keys %aliasesOnDisk) { if (!defined $aliasesInLdap{$dn}) { warn "$dn has aliases, but is not in LDAP"; next; } foreach my $alias(keys %{$aliasesOnDisk{$dn}}) { if (!exists $aliasesInLdap{$dn}->{$alias}) { $dnsToUpdate{$dn} = undef; last; } } } # compare mail addresses in LDAP to alias database on disk foreach my $dn(keys %aliasesInLdap) { if (defined $aliasesOnDisk{$dn}) { foreach my $alias(keys %{$aliasesInLdap{$dn}}) { if (!exists $aliasesOnDisk{$dn}->{$alias}) { $dnsToUpdate{$dn} = undef; last; } } } } # update all mismatching entries foreach my $dn(keys %dnsToUpdate) { my @mailAddresses = keys %{$aliasesOnDisk{$dn}}; if ($opt{n}) { print "would update $dn with\n", join("", map { " $MailAttribute=$_\n" } @mailAddresses); next; } $msg = $ldap->modify($dn, changes => [ delete => [ $MailAttribute => [], ], add => [ $MailAttribute => \@mailAddresses, ], ]); die $msg->error() if $msg->is_error(); syslog("LOG_INFO", "updated $dn (" . join(", ", @mailAddresses) . ")\n"); } # byebye if (!$opt{n}) { close(LOCK); unlink($LockFile); } closelog(); $ldap->unbind(); sub parseAliasesFile() { my ($aliasesOnDisk, $domainName, $directory) = @_; my $aliasesFile = "$BaseDirectory/$directory/Settings/aliases.data"; return if !-e $aliasesFile; open(FH, $aliasesFile) || die "$aliasesFile: $!"; while () { next if !/^(.+) = \(("(.+)")?\);/o; my $mailbox = $1; my %aliases = ("$mailbox\@$domainName" => undef); foreach (split(/","/, $3 ? $3 : "")) { $aliases{"$_\@$domainName"} = undef; } $aliasesOnDisk->{"uid=$mailbox,cn=$domainName"} = \%aliases; } close(FH); print "parsed $aliasesFile\n" if $opt{n}; } sub plural() { my $cnt = keys %{$_[0]}; my $str = "$cnt mail account"; $str .= "s" if ($cnt != 1); return $str; } sub VERSION_MESSAGE() { print "aliassync V0.2\n"; } sub HELP_MESSAGE() { print <] [-n] -c use given configfile instead of /usr/local/etc/aliassync.conf -n dry run, test mode: don't modify LDAP directory, be verbose EOF }