=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<NB:> 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<replace> 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 <gavin@openfusion.com.au>. 

=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

