#!/usr/bin/perl # # /usr/local/sbin/localmkpostfixvirtual # Copyright 2001-2006 Jonas Smedegaard # # $Id: localmkpostfixvirtual,v 1.34 2006-10-30 13:36:49 jonas Exp $ # # Generate virtual file for postfix # # This script provides a poor-man's email ISP tool using the standard # Unix users/groups database (with generic getent lookups, supporting # smooth upgrades to LDAP or other enterprise systems supporting the # libc Name Service Switch. # # # Email domains # ------------- # # A system group is needed to locate email domains: # # # addgroup --system maildomains # # # Each domain (or group of syncronously maintained domains) needs a # virtual user, added to that central group: # # # adduser --system --no-create-home --group --disabled-password abcde # # # adduser abcde maildomains # # In the "Other" field of the GECOS info, the virtual user accounts has # hint(s) on the email domains tied to that domain: # # # usermod -c ",,,,@abc.com @cde.org" abcde # # The GECOS info has limited space. A domain group can span multiple # virtual accounts by mentioning the secondary account names in the # group field of the main virtual account. # "Office" or "roomnumber" field of the primary mailgroup (include the # primary mailgroup itself too, if using this field at all!). # # # Email addresses # --------------- # # In the "Other" field of the GECOS info, real user accounts has hint(s) # on the email userparts tied to that user: # # # usermod -c "db@ dbowie@abcde +@xyz" david # # # Suggestion: Add postmaster-specific hints to the root account # (default: "postmaster@"): # # # usermod -c "postmaster@ abuse@ support@abcde" root # # # Postfix lookup tables # --------------------- # # Lookup table for postfix is generated/update with this command: # # # localmaildomainupdate # # # The above example entries should generate the following in # /etc/postfix/virtual: # # abc.com VIRTUAL # postmaster@abc.com root # abuse@abc.com root # support@abc.com root # # db@abc.com david # dbowie@abc.com david # # cde.org VIRTUAL # postmaster@cde.org root # abuse@cde.org root # support@cde.org root # # db@cde.org david # dbowie@cde.org david # # If another virtual domain account "xyz" is added with @xyz.net # in comment field, the following will be included as well: # # xyz.net VIRTUAL # postmaster@xyz.net # abuse@xyz.net # # db@xyz.net # @xyz.net # # (notice how support is only tied to the abcde domains, and the # specific dbowie userpart is substituted with a wildcard) use strict; use warnings; use User::pwent; use User::grent; my (%username, %fullname, %office, %workphone, %homephone, %other); my (%username_by_gid, %addresshints, %domains); while (my $pw = getpwent()) { $username_by_gid{$pw->gid} = $pw->name; ($fullname{$pw->name}, $office{$pw->name}, $workphone{$pw->name}, $homephone{$pw->name}, $other{$pw->name}) = split /\s*,\s*/, $pw->gecos; if (defined($other{$pw->name})) { @{$addresshints{$pw->name}} = grep {/^([\.[:alnum:]_-]+|\+)?@([\.[:alnum:]_-]+)?$/} split /\s+/, $other{$pw->name}; } } my (%owner, %members, %groups); while (my $gr = getgrent()) { $owner{$gr->name} = $username_by_gid{$gr->gid}; $members{$gr->name} = $gr->members; foreach my $member (@{$members{$gr->name}}) { push @{$groups{$member}}, $gr->name unless (defined($owner{$gr->name}) and $member eq $owner{$gr->name}); } } my (%warned); sub warnonce($$$) { my ($test, $id, $warning) = @_; if ($test) { return 1; } if ($warned{$id}) { return ''; } print STDERR 'W: ' . $warning . "\n"; $warned{$id} = 1; return ''; } my (%username_by_localpart_by_maildomain); sub print_accounts($$$$) { my ($username, $mailgroup, $maildomain, $pre_text, $post_fallback_text) = @_; ($pre_text) && print $pre_text . "\n"; my $joker_seen; if (&warnonce(@{$addresshints{$username}}, "addresshints_$username", "Skipping non-hinted username \"$username\".")) { my @localparthints = @{$addresshints{$username}}; my @localparts = grep {s/(.+)@($mailgroup|$maildomain)?$/$1/} @localparthints; foreach my $localpart (@localparts) { for ($localpart) { # FIXME: the below doesn't work as intended?!? # if (&warnonce(! @{$username_by_localpart_by_maildomain{$_}}, "address_$_", "Skipping duplicate address \"$_\" for \"$username\").")) { # next; # } if (/^\+$/) { if (!$joker_seen) { print "\@$maildomain $username\n"; $joker_seen = $username; } else { print "#WARNING: Catch-all for $maildomain already set to $joker_seen!"; } } else { print "$localpart\@$maildomain $username\n"; } } } } else { print $post_fallback_text . "\n" if ($post_fallback_text); } print "\n"; } sub usercomment($) { my $user = shift; my @s = (); if (&warnonce(defined($fullname{$user}), "fullname_$user", "User \"$user\" lacks fullname.")) { push @s, $fullname{$user}; } if (&warnonce(@{$groups{$user}}, "groups_$user", "User \"$user\" belongs to no (secondary) group.")) { my @groups_sorted = sort @{$groups{$user}}; push @s, '(' . join(' ', @groups_sorted) . ')'; } if (@s) { unshift @s, '#'; } my $string = join(' ', @s); return "$string"; } my $loop; my @mailgroups = @ARGV ? @ARGV : @{$members{'maildomains'}}; foreach my $mailgroup (@mailgroups) { if (not &warnonce(@{$addresshints{$mailgroup}}, "addresshints_$mailgroup", "Skipping empty mailgroup \"$mailgroup\".")) { next; } my @maildomainhints = @{$addresshints{$mailgroup}}; my @maildomains = grep {s/^@(.+)/$1/} @maildomainhints; foreach my $maildomain (@maildomains) { my (@mailgroupowners, @mailusers); my @mailgroupgroups = split / +/, $office{$mailgroup}; push @mailgroupgroups, $mailgroup unless (@mailgroupgroups); foreach my $mailgroupgroup (@mailgroupgroups) { push @mailgroupowners, $owner{$mailgroupgroup} if ($owner{$mailgroupgroup}); push @mailusers, @{$members{$mailgroupgroup}}; } my @mailgroupowners_sorted = sort @mailgroupowners; my @mailusers_sorted = sort @mailusers; if ($loop) { print "\n##################################################################\n\n"; } $loop++; &print_accounts('root', $mailgroup, $maildomain, "$maildomain VIRTUAL", "postmaster\@$maildomain root"); # Do mailgroup owners (and don't warn if there's no addresses attached) foreach my $mailgroupowner (@mailgroupowners_sorted) { &print_accounts($mailgroupowner, $mailgroup, $maildomain, &usercomment($mailgroupowner)); } # Do secondary mailgroup members foreach my $mailuser (@mailusers_sorted) { &print_accounts($mailuser, $mailgroup, $maildomain, &usercomment($mailuser), "#WARNING: No addresses for $mailuser"); } } }