#!/usr/bin/perl -w # # mkvalidrcptto # John Simpson 2005-04-20 # # reads qmail control files and builds a list of all valid email addresses # on the system. # # 2005-05-04 jms1 - cleaned up the code a little bit. holding the output in # memory until the whole thing is done so that partial output doesn't # become an issue. it also makes it possible to create a cdb file directly, # but i don't think i'm going to do that- i like the "chaining" approach # better, piping this script's output through "cdbmake-12" to produce the # cdb file. # # 2005-05-09 jms1 - changing the vpopmail user info process. instead of # reading vpasswd.cdb, using "vuserinfo -D {domain}" and parsing that # output. this should include mysql user information for people who are # using vpopmail with mysql. Thanks to Roman Volf on the qmailrocks # mailing list for pointing this out. # # 2005-06-09 jms1 - adding support for "fastforward" aliases. # # 2005-06-18 jms1 - finishing support for .qmail-* files in local user home # directories... i don't ever use local users for mailboxes myself, so it # wasn't an issue for me, but somebody out there may be doing it, so... # # 2005-06-23 jms1 - changed the search pattern when reading "vuserinfo -D" # to get a vpopmail domain's mailbox list, instead of recognizing \w+ # it now uses \S+ which should allow "." in mailbox names # # 2005-06-29 jms1 - translating ":" to "." in .qmail-* filenames... i forgot # that qmail-local does this. somebody emailed me to remind me about this, # but i can't find the email so i don't know who to thank for reminding me # about this... # # 2005-06-30 jms1 - when vpopmail stores everything in a mysql database, # aliases are apparently not represented by .qmail files at all. it looks # like we have no choice but to use the vpopmail command-line tools to # get the lists of mailboxes and aliases in the domain. thanks to Rob Pitt # for telling me that this wasn't working (i don't use vpopmail with mysql # so i had no way to know that this would be a problem.) # # 2005-07-17 jms1 - domains listed in smtproutes are currently listed as # just "@domain", meaning the entire domain is accepted without any checks # done for individual userid's. adding code so that you can create a # directory full of files named for the domains, containing userid's # which exist in that domain. thanks to roman volf for the suggestion. # # 2005-08-03 jms1 - turns out if vpopmail is compiled with support for mysql, # the "valias -s" command doesn't list aliases which exist by virtue of # .qmail-blah files... which rather sucks, because this is how ezmlm sets # up the aliases it needs, by creating .qmail-blah files. thanks again to # Roman Volf for pointing this out. # # 2005-10-24 jms1 - adding a "-n" switch to generate a list without the # system accounts (i.e. no "locals" or "me" domains will be printed.) # note that "fastforward" aliases are considered local, since they are # processed through the local delivery mechanism. # # 2005-11-29 jms1 - now treats missing vpopmail directory as a warning # rather than a fatal error. # # 2005-12-07 jms1 - after reviewing qmail-send.c and controls.c, it # turns out that the "me" becoming part of "locals" only happens if # the "control/locals" file does not exist... otherwise an empty # "control/locals" file means that there are no locals. updating the # script to duplicate this logic. # also replacing ":" with "." in .qmail filenames. # thanks to jeff hedlund for pointing me towards the "me" problem, # and for pointing out my oversight with the ":" thing. # # 2005-12-29 jms1 - adding an array of numeric uid's (empty by default) # which will be ignored when system uid's are scanned. thanks to # roman volf for the suggestion. # # 2006-01-08 jms1 - fixed a typo, thanks to "marlowe" for pointing it out. # # 2006-01-11 jms1 - vpopmail has a "--enable-qmail-ext" option which # changes how mailbox names are handled. if vpopmail is running WITH # this option, every vpopmail mailbox has an implied "-default" alias # whether there's a .qmail-user-default file there or not. thanks to # robin bowes for pointing this out. # # 2006-01-12 jms1 - making the script work correctly in the unlikely # case that the user-ext separator character was changed, either by # changing conf-break before compiling qmail, or specifying a custom # value in the users/cdb file. # # 2006-02-05 jms1 - adding an "exclude" list, for addresses which you # may not want to include in the output (i.e. private mailing list # aliases and things like that.) also fixed a bug in the code which # handles the user-ext separator character. # # 2006-03-26 jms1 - fixed a minor typo in the text of an error message # (which does not affect how the script works.) Thanks to Robin Bowes # for pointing it out. # # 2006-11-29 jms1 - adding logic to work around the case where users/cdb # just plain doesn't exist (which can happen on systems which don't use # vpopmail etc.) thanks to "Eric d'Alibut" on djb's qmail mailing list # for pointing it out. # ############################################################################### # # Copyright (C) 2005-2006 John Simpson. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License, version 2, as # published by the Free Software Foundation. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # or visit http://www.gnu.org/licenses/gpl.txt # ############################################################################### require 5.003 ; use strict ; use CDB_File ; ############################################################################### # # configuration my $vq = "/var/qmail" ; my $vuser = "vpopmail" ; # vpopmail userid # any numeric uid's on the system which are lower than this will be ignored # this way we don't create entries for root, bin, daemon, lp, news, uucp, # and other non-used system users. my $uid_min = 500 ; # ignore uid's lower than this my $uid_max = 65000 ; # ignore uid's higher than this my @uid_ignore = qw ( ) ; # ignore any uid's listed in this array # any entries listed in this array will NOT be included in the output my @exclude = qw ( sample1@domain.xyz sample2@domain.xyz ) ; # if you have text files containing lists of valid mailboxes for smtproutes # domains, put them all into a single directory and put that directory in # the variable below. # # each line of each files should contain a mailbox userid- anything on the # line which comes after "#" or "@" is deleted, so you can use "#" for # comments if you like, and you can use a symlink (or hard link) to cause # one file to be effective for multiple domains. # # note that these files are only consulted for domains listed in the # smtproutes file. if this variable is blank, or it points to a directory # which doesn't exist, all smtproutes domains will be printed as "@domain", # which tells qmail-smtpd to accept any mailbox userid in that domain. my $smtpr_dir = "" ; ############################################################################### # # global variables my ( %alldom , %ldom , %vdom , %sdom , %adom , %lusr , %ausr , %home , %MRH , %UCDB , @output , $ffl , %ACDB ) ; my $err = 0 ; my $lfound = 0 ; my $afound = 0 ; my $vfound = 0 ; my $showlocal = 1 ; my $need_untie = 0 ; my $vhome = "" ; my $vbin = "" ; my $vinc = "" ; my $vqext = "?" ; my $dash = "" ; my $gdash = "" ; ############################################################################### # # debugging function my $show_debug = 0 ; sub debug($) { $show_debug && print $_[0] ; } ############################################################################### # # function to read /var/qmail/alias/.qmail-default sub find_ffl($) { my $file = shift ; my $flagdeliver = 1 ; my $flagpassthrough = 0 ; my $flagdefault = 0 ; if ( open ( DQD , "<$file" ) ) { while ( my $line = ) { chomp $line ; next unless ( $line =~ /^\|.*fastforward/ ) ; $line =~ s/^.*fastforward\s+// ; my @dw = split ( /\s+/ , $line ) ; while ( my $zz = shift @dw ) { next if ( $zz =~ /^\-/ ) ; $ffl = $zz ; last ; } last if ( $ffl ) ; } close DQD ; } } ############################################################################### ############################################################################### ############################################################################### my $arg = ( shift || "" ) ; if ( "-n" eq $arg ) { $showlocal = 0 ; } ############################################################################### # # only interested in domains for which we legitimately receive mail open ( I , "<$vq/control/rcpthosts" ) or die "$vq/control/rcpthosts: $!\n" ; while ( my $line = ) { chomp $line ; $alldom{$line} = 1 ; } close I ; open ( I , "<$vq/control/morercpthosts" ) or die "$vq/control/morercpthosts: $!\n" ; while ( my $line = ) { chomp $line ; $alldom{$line} = 1 ; } close I ; if ( -f "$vq/control/morercpthosts.cdb" ) { tie ( %MRH , "CDB_File" , "$vq/control/morercpthosts.cdb" ) or die "$vq/control/morercpthosts: $!\n" ; map { $alldom{$_} = 1 } keys %MRH ; untie %MRH ; } ############################################################################### # # classify each one as local, virtual, or pass-thru # # note that if the control/locals file does not exist, the name listed # in the control/me file is used as if control/locals contained the # data. if ( -f "$vq/control/locals" ) { open ( I , "<$vq/control/locals" ) or die "$vq/control/locals: $!\n" ; while ( my $line = ) { chomp $line ; ######################################## # ignore any that we don't actually receive mail for next unless ( exists $alldom{$line} ) ; delete $alldom{$line} ; ######################################## # mark this one as local $ldom{$line} = 1 ; $lfound ++ ; $afound ++ ; } close I ; } elsif ( -f "$vq/control/me" ) { open ( I , "<$vq/control/me" ) or die "$vq/control/me: $!\n" ; while ( my $line = ) { chomp $line ; ######################################## # ignore any that we don't actually receive mail for next unless ( exists $alldom{$line} ) ; delete $alldom{$line} ; ######################################## # mark this one as local $ldom{$line} = 1 ; $lfound ++ ; $afound ++ ; } close I ; } if ( -f "$vq/control/virtualdomains" ) { open ( I , "<$vq/control/virtualdomains" ) or die "$vq/control/virtualdomains: $!\n" ; while ( my $line = ) { chomp $line ; ######################################## # extract the domain name my ( $dom , $zu ) = split ( /\s*\:\s*/ , $line ) ; $dom || die "error in $vq/control/virtualdomains\n$line\n" ; ######################################## # ignore any that we don't actually receive mail for next unless ( exists $alldom{$dom} ) ; delete $alldom{$dom} ; ######################################## # check the userid if ( $zu eq "alias" ) { ######################################## # if the domain is handled by the qmail "alias" # user, then it needs alias processing $adom{$dom} = 1 ; $afound ++ ; } else { ######################################## # mark this one as a virtual domain # and remember the full line, we will need it later $vdom{$dom} = $line ; $vfound ++ ; } } close I ; } if ( -f "$vq/control/smtproutes" ) { open ( I , "<$vq/control/smtproutes" ) or die "$vq/control/smtproutes: $!\n" ; while ( my $line = ) { chomp $line ; ######################################## # extract the domain name $line =~ s/\:.*// ; ######################################## # ignore lines with no domain (default instruction) next unless $line ; ######################################## # ignore any that we don't actually receive mail for next unless ( exists $alldom{$line} ) ; delete $alldom{$line} ; ######################################## # mark this one as an smtproutes domain $sdom{$line} = 1 ; } close I ; } ############################################################################### # # catch leftovers - domains which come into the machine but are not handled for my $d ( sort keys %alldom ) { print "ERROR: $d is listed in rcpthosts/morercpthosts.cdb" . " but is not handled by the server.\n" ; $err ++ ; } $err && die "Cannot continue.\n" ; ############################################################################### # # start generating output. # # smtproutes domains - if a directory was specified, and it exists, and a # file for the domain exists, read userid's from the file and generate # "userid@domain" lines... otherwise just generate a single "@domain" line. for my $d ( sort keys %sdom ) { if ( $smtpr_dir && ( -d $smtpr_dir ) && ( -f "$smtpr_dir/$d" ) ) { open ( I , "<$smtpr_dir/$d" ) or die "Can\'t read $smtpr_dir/$d: $!\n" ; while ( my $line = ) { chomp $line ; $line =~ s/#.*// ; $line =~ s/\@.*// ; $line =~ s/^\s+// ; $line =~ s/\s+$// ; next unless ( $line ) ; push ( @output , "$line\@$d" ) ; } close I ; } else { push ( @output, "\@$d" ) ; } } ############################################################################### # # local domains - all system accounts and aliases, in each local domain if ( $lfound || $afound ) { ######################################## # need the global "dash" character unless ( $gdash ) { open ( GD , "$vq/bin/qmail-showctl |" ) or die "Can\'t run qmail-showctl: $!\n" ; while ( my $gdline = ) { if ( $gdline =~ /user\-ext delimiter\: (.)/ ) { $gdash = $1 ; last ; } } close GD ; } } if ( $lfound ) { ######################################## # turn array into hash for speed my %ig = () ; map { $ig{$_} = "" } @uid_ignore ; ######################################## # grab a list of system accounts while ( my @pw = getpwent() ) { next if ( $pw[2] < $uid_min ) ; # ignore system accounts next if ( $pw[2] > $uid_max ) ; # ignore "nobody" accounts next if ( exists $ig{$pw[2]} ) ; # ignore special accounts next unless ( $pw[2] ) ; # no deliveries to root $lusr{$pw[0]} = 1 ; if ( opendir ( D , $pw[7] ) ) { while ( my $f = readdir D ) { next unless ( $f =~ /^\.qmail${gdash}(.+)/ ) ; my $zu = $1 ; $zu =~ s/\:/./g ; $lusr{"$pw[0]${gdash}$zu"} = 1 ; } closedir D ; } } } if ( $showlocal && ( $lfound || $afound ) ) { ######################################## # grab a list of aliases opendir ( D , "$vq/alias" ) or die "$vq/alias: $!\n" ; while ( my $f = readdir ( D ) ) { next unless ( $f =~ /^\.qmail${gdash}(.*)/ ) ; my $u = $1 ; if ( $u eq "default" ) { find_ffl ( "$vq/alias/.qmail${gdash}default" ) ; } else { $u =~ s/\:/./g ; $ausr{$u} = 1 ; } } closedir D ; ######################################## # if we found a fastforward file, grab those aliases as well if ( $ffl ) { tie ( %ACDB , "CDB_File" , $ffl ) or die "$ffl: $!\n" ; for my $k ( keys %ACDB ) { next unless ( $k =~ /^\:(.*)\@(.*)$/ ) ; my ( $au , $ad ) = ( $1 , $2 ) ; if ( $ad ) { next unless ( exists ( $adom{$ad} ) || exists ( $ldom{$ad} ) ) ; push ( @output , "$au\@$ad" ) ; } else { $ausr{$au} = 1 ; } } untie %ACDB ; } ######################################## # generate output. # local domains get every system user AND every alias user for my $dd ( sort keys %ldom ) { map { push ( @output , "$_\@$dd" ) } sort keys %lusr ; map { push ( @output , "$_\@$dd" ) } sort keys %ausr ; } ######################################## # alias domains get every alias user for my $dd ( sort keys %adom ) { map { push ( @output , "$_\@$dd" ) } sort keys %ausr ; } } ############################################################################### # # virtual domains - a little more complicated. if ( $vfound ) { ####################################################################### # # the virtualdomains file contains a mapping from the domain name to a # userid, which may be a system account and may be a virtual userid # defined in the $vq/users/assign file. # # vpopmail normally uses the domain name as the virtual userid for # this purpose, but we want to be flexible enough to handle other # cases as well. # # in order to deal with this extra layer of indirection, we need to # read the users/cdb file. and because it's a cdb, we don't even need # to read the whole thing- we just need to open it so that we can # search it. if ( -f "$vq/users/cdb" ) { tie ( %UCDB , "CDB_File" , "$vq/users/cdb" ) or die "$vq/users/cdb: $!\n" ; $need_untie = 1 ; } else { %UCDB = () ; } my $wc = ( $UCDB{""} || "" ) ; ####################################################################### # # now we have the list of users/assign virtual users (if any), we need # to identify the home directory, real or virutal, for the user. for my $dom ( sort keys %vdom ) { $vdom{$dom} =~ /\:(.*)$/ ; my $usr = $1 ; my %vusr = () ; my $dir = "" ; my $vpopmail = 0 ; ######################################## # note that in cases where a given "userid" exists as both a # system userid and a virtual userid, the virtual userid takes # precedence (according to the qmail-lspawn man page.) # this is why we saved the home directories above. if ( exists $UCDB{"!$usr$wc"} ) { my @w = split ( /\0/ , $UCDB{"!$usr$wc"} ) ; $dir = ( $w[3] || die "mis-formed users/cdb data:" . " $usr\n" ) ; $dash = ( $w[4] || "" ) ; } else { if ( my @pw = getpwnam ( $usr ) ) { $dir = $pw[7] ; } } die "ERROR: virtual user \"$usr\" not found" . " (for virtualdomain \"$dom\")\n" unless ( $dir ) ; ######################################## # now we know which directory to look in. check for a # ".qmail-default" file. if it contains "vdelievermail", we # know that vpopmail is in control here... and if the # vdelivermail line also has "bounce-no-mailbox", we need # the list of individual users. otherwise we can just # blindly accept the entire domain. unless ( -r $dir ) { print STDERR "Can\'t read directory $dir" . " (for vpopmail domain \"$dom\")\n" ; next ; } if ( -f "$dir/.qmail${dash}default" ) { open ( V , "<$dir/.qmail${dash}default" ) or die "$dir/.qmail${dash}default: $!\n" ; while ( my $line = ) { if ( $line =~ /vdelivermail.*(bounce\-no\-mailbox|delete)/ ) { $vpopmail = 1 ; } } close V ; } ######################################## # if we need the users... if ( $vpopmail ) { ######################################## # if we don't already know where it is, # find the vpopmail user's "/bin" directory. unless ( $vhome ) { my @pw = getpwnam ( $vuser ) or die "getpwnam($vuser): $!\n" ; $vhome = $pw[7] ; $vbin = "$vhome/bin" ; $vinc = "$vhome/include" ; die "Can\'t find $vbin/vuserinfo: $!\n" unless ( -e "$vbin/vuserinfo" ) ; } ######################################## # if we don't already know, find out if # vpopmail was built with --enable-qmail-ext if ( $vqext eq "?" ) { $vqext = "no" ; open ( VCH , "<$vinc/vpopmail_config.h" ) or die ( "Can\'t read " . "$vinc/vpopmail_config.h: $!\n" ) ; while ( my $vcl = ) { next unless ( $vcl =~ /^#define QMAIL_EXT 1/ ) ; $vqext = "yes" ; last ; } close VCH ; debug "vqext=$vqext\n" ; } ######################################## # run "vuserinfo -D {domain}" to get a list of # actual mailboxes within the domain. debug "/----- $vbin/vuserinfo -D $dom\n" ; open ( VP , "$vbin/vuserinfo -D $dom |" ) or die "Can\'t execute $vbin/vuserinfo: $!\n" ; while ( my $line = ) { debug $line ; next unless ( $line =~ /^name\:\s+(\S+)/ ) ; my $u = $1 ; $vusr{$u} = $u ; debug "\t[$u]" ; if ( $vqext eq "yes" ) { $vusr{"$u${dash}default"} = "$u${dash}default" ; debug " [$u${dash}default]" ; } debug "\n" ; } close VP ; debug "\\-----\n" ; ######################################## # run "valias -s {domain}" to get a list of # aliases within the domain. debug "/----- $vbin/valias -s $dom\n" ; open ( VP , "$vbin/valias -s $dom |" ) or die "Can\'t execute $vbin/valias: $!\n" ; while ( my $line = ) { debug $line ; next unless ( $line =~ /^(.+?)\@/ ) ; my $u = $1 ; $vusr{$u} = $u ; debug "\t[$u]\n" ; } close VP ; debug "\\-----\n" ; ######################################## # read the directory itself. any .qmail-___ files are # also valid aliases within the domain, even if # "valias" doesn't seem to know about them. opendir ( D , $dir ) or die "$dir: $!\n" ; while ( my $f = readdir ( D ) ) { if ( $f =~ /^\.qmail${dash}(.*)/ ) { my $u = $1 ; next if ( $u eq "default" ) ; $u =~ s/\:/./g ; $vusr{$u} = $u ; } } closedir D ; ######################################## # now %vusr contains a list of all valid email # addresses within the domain. map { push ( @output , "$_\@$dom" ) ; debug "{$_\@$dom}\n" } sort keys %vusr ; } else { ######################################## # virtual domain, but either it's not handled by # vpopmail, or there is something going on with # itother than "bounce-no-mailbox", which means # we don't need the full list of mailboxes. push ( @output , "\@$dom" ) ; } } if ( $need_untie ) { untie %UCDB ; $need_untie = 0 ; } } ############################################################################### # # if we make it this far, we have no errors and can print the list. # we need to filter out any "exclude" entries my %ex = () ; map { $ex{lc $_} = "" } @exclude ; for my $k ( @output ) { $k = lc $k ; unless ( exists $ex{$k} ) { print "$k\n" ; } }