=head1 NAME per_user_config =head1 DESCRIPTION A simple approach for loading per-user or per-domain config data from subdirectories of the qpsmtpd/config directory. Three directory layouts are supported: 'domain/user' (default), 'domain', and 'user', specified as the single parameter to the per_user_config plugin. per_user_config returns DECLINED for excluded config files, if no user can be identified, or if no per-user config file can be found. To use per-user configs, simply load the per_user_config plugin early in the config/plugins file, and then in a subsequent sender or rcpt plugin pass a 'sender' or 'rcpt' argument in a second hashref argument to config() e.g. C<@config = $self->qp->config('myconfigfile', { rcpt => $rcpt });> B per_user_config currently requires a patched version of Qpsmtpd.pm to support this second hashref argument - the patch should be available with this plugin. ] By default per-user or -domain configs I more global ones. Sometimes merging earlier (more global) configs with later (more specific) ones makes sense - to enable this functionality pass an additional 'merge' argument to config() e.g. C<@config = $self->qp->config('myconfigfile', { rcpt => $rcpt, merge => 1 });> This simply concatenates config files together (more global first) - no checking for uniqueness or anything like that is done. =head1 CONFIG Per-user or per-domain config data is defined in standard qpsmtpd config files in subdirectories of qpsmtpd/config. Three directory layouts are supported, specified by the single parameter passed to the per_user_config plugin. Valid values are: domain/user | domain | user 'domain/user' configs (the default) allow individual recipient config files to be defined in a two-tier directory layout, having individual username subdirectories within domain subdirectories of qpsmtpd/config e.g. config/ spamassassin [0] openfusion.com.au/ spamassassin [1] gavin/ spamassassin [2] This allows the spamassassin config file [2] to be used for mail to gavin@openfusion.com.au, while all other openfusion.com.au users use the spamassassin config file [1]. If no other domain directories exists, all other domains will use the global spamassassin config file [0]. 'domain' configs do away with the user level and define configs within domain subdirectories of qpsmtpd/config e.g. config/ dnsbl_zones spamassassin openfusion.com.au/ dnsbl_zones spamassassin someotherdomain.com/ dnsbl_zones spamassassin whitelisthosts 'user' configs do away with the domain level and define configs directly within user subdirectories of qpsmtpd/config e.g. config/ spamassassin gavin/ spamassassin john/ spamassassin This is probably only of use in single-domain contexts, of course. For all layouts symlinks can be used for both directories and files to allow aliasing. =head1 NOTES per_user_config requires a 'sender' or 'rcpt' argument. Per-recipient configs are much more common (since qpsmtpd is usually used in 'inbound' settings), but sender configs also make sense, particularly in 'outbound' contexts. Per-recipient configs obviously work well in rcpt-hook plugins. If you want to use per-recipient configs with post_data plugins, you have to handle the possibility that the multiple recipients may not have identical configs. See the denysoft_multi_rcpt plugin for one approach to this. Plugins that want to support per-user configs typically have to be specially adapted (e.g. to defer most processing to rcpt hook time) and ideally should take a 'per_sender' or 'per_recipient' plugin argument to turn this functionality on. per_user_config records a couple of items for use by later plugins: a 'per_user_config_layout' connection note records the layout parameter passed in, and a 'per_rcpt_configdir' (or 'per_sender_configdir') transaction note records the most-specific config directory found for the most recent user. Note that username suffixes (the '-qpsmtpd' part in gavin-qpsmtpd@openfusion.com.au) are always removed from usernames before looking for user directories. This might sometimes surprise you (e.g. mailer-daemon -> mailer). No caching of results is done by default, since the number of config files involved might be huge. =head1 AUTHOR Written by Gavin Carr . =cut my %EXCLUDE = map { $_ => 1 } qw(me timeout per_user_config); my $VERSION = 0.05; sub register { my ($self, $qp, $layout) = @_; $self->register_hook("config", "per_user_config"); $layout = 'domain/user' unless $layout && $layout =~ m/^(domain|user)$/; $self->qp->connection->notes('per_user_config_layout', $layout); } sub per_user_config { my ($self, $transaction, $config, $arg) = @_; return DECLINED if $EXCLUDE{$config}; return DECLINED unless ref $arg eq 'HASH' && ($arg->{rcpt} || $arg->{sender}); $self->log(1, "cannot pass both sender and recipient - sender ignored") if $arg->{rcpt} && $arg->{sender}; # Setup my $hook = $arg->{rcpt} ? 'rcpt' : 'sender'; my $user = $arg->{$hook}; my $domain = lc $user->host; my $username = lc $user->user; # Remove username suffixes (e.g. gavin-qpsmtpd@openfusion.com.au) $username =~ s/\s*(\w+).*/$1/; # Find base configdir and configfile(s) to load my ($qphome) = ($0 =~ m!(.*?)/([^/]+)$!); $qphome =~ s!^\.!$ENV{PWD}!; my ($configdir, @configfile); my $layout = $self->qp->connection->notes('per_user_config_layout'); if ($layout eq 'user') { if (-d "$qphome/config/$username") { $configdir = "$qphome/config/$username"; if (-l $configdir) { my $l = readlink $configdir; $configdir = substr($l,0,1) eq '/' ? $l : "$qphome/config/$l"; } unshift @configfile, "$configdir/$config" if -f "$configdir/$config"; } } elsif (-d "$qphome/config/$domain") { $configdir = "$qphome/config/$domain"; if (-l $configdir) { my $l = readlink $configdir; $configdir = substr($l,0,1) eq '/' ? $l : "$qphome/config/$l"; } my $domaindir = $configdir; if ($layout ne 'domain' && -d "$domaindir/$username") { $configdir = "$domaindir/$username"; if (-l $configdir) { my $l = readlink $configdir; $configdir = substr($l,0,1) eq '/' ? $l : "$domaindir/$l"; } unshift @configfile, "$configdir/$config" if -f "$configdir/$config"; } unshift @configfile, "$domaindir/$config" if -f "$domaindir/$config" && (! @configfile || $arg->{merge}); } $self->log(6, "no '$config' configfile found for $username\@$domain, layout '$layout'") unless @configfile; $configdir ||= "$qphome/config"; unshift @configfile, "$qphome/config/$config" if -f "$qphome/config/$config" && $arg->{merge}; $configdir =~ s!//+!/!g; $transaction->notes("per_${hook}_configdir",$configdir); return DECLINED unless @configfile; # Read configfile(s) my @config; push @config, $self->qp->get_configfile($_, $arg) for @configfile; return (OK, @config); } # arch-tag: 0586409a-b249-448a-84b5-f3c1b87b5170