#!/usr/bin/perl
#
# /usr/local/sbin/localmkpostfixvirtual
# Copyright 2001-2006 Jonas Smedegaard <dr@jones.dk>
#
# $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");
		}
	}
}