Here is yet another module to determine which mail adresses are local.
It also expands them to several recipients if necessary and adds
per-recipient notes which can be used by other modules.
It is called aliases because I started with the format of sendmail's
/etc/aliases file, but it soon mutated into something different.

The module will reject any unknown addresses, and DECLINE those which
are known, i.e., it is designed to run first to weed out all
non-existent users, then other modules which can reject on a rcpt
address can run, and finally the rcpt_ok module should run to accept all
survivors. For example, our config/plugins file looks like this:

    # this plugin initializes the note "recipient_options" and therefore
    # has to run before all other "rcpt" plugins which access this note.
    aliases

    denysoft_greylist remote_ip 1 sender 1 recipient 1 black_timeout 900 grey_timeout 
32400 per_recipient 1

    # this plugin must run after all other "rcpt" plugins. It will simply
    # allow all recipients.
    rcpt_ok

The aliases module has been in use for about 2 months on an MX for about
200 users (and a few thousand dead adresses). In its current form it
doesn's scale well - I might add dbm or DBI support if there is
interest, but probably not LDAP.

        hp

-- 
   _  | Peter J. Holzer    | We have failed our own creation and given
|_|_) | Sysadmin WSR       | birth something truly awful. We're just too
| |   | [EMAIL PROTECTED]         | busy cooing over the pram to notice.
__/   | http://www.hjp.at/ |       -- http://www.internetisshit.org
Index: lib/Qpsmtpd/Transaction.pm
===================================================================
--- lib/Qpsmtpd/Transaction.pm  (revision 22)
+++ lib/Qpsmtpd/Transaction.pm  (working copy)
@@ -30,6 +30,11 @@
   ($self->{_recipients} ? @{$self->{_recipients}} : ());
 }
 
+sub clear_recipients {
+  my $self = shift;
+  $self->{_recipients} = [];
+}
+
 sub sender {
   my $self = shift;
   @_ and $self->{_sender} = shift;
@@ -163,6 +168,10 @@
 
 Each recipient returned is a C<Mail::Address> object.
 
+=head2 clear_recipients()
+
+Clear the list of recipients in the envelope.
+
 =head2 sender( [ ADDRESS ] )
 
 Get or set the sender (MAIL FROM) address in the envelope.

=head1 NAME

aliases - expand aliases

=head1 DESCRIPTION

This module looks up recipients (argument to the RCPT TO command) in an
alias file.
Recipients which are not found are immediately rejected.
After all recipients are known, the aliases are recursively expanded.

An alias can expand to one or more addresses,
a detail string (everything after '+' in the local part) is preserved in the expansion.

Duplicates are eliminated.

Unlike the sendmail aliases file, the aliases are complete email addresses, not just 
the local part.


=head1 CONFIGURATION

The aliases file is a simple text file, with one alias-pattern/expansion pair per 
line, separated by a colon.

The alias pattern consists of a list of local parts, an @ sign and a list of
domains, optionally followed by a parenthesized list of of options.

The expansion consists of a list of email-addresses.

Lists are comma-separated, whitespace is insignificant.

For example, consider the alias file:

    hjp,[EMAIL PROTECTED],wifo.at: [EMAIL PROTECTED] (denysoft_greylist, 
spamassassin_reject_threshold=10)
    postmaster@,wsr.ac.at,wifo.at: [EMAIL PROTECTED]
    [EMAIL PROTECTED]: [EMAIL PROTECTED],[EMAIL PROTECTED]

The addresses <postmaster>, <[EMAIL PROTECTED]> and <[EMAIL PROTECTED]> would 
all be expanded to <[EMAIL PROTECTED]>, which in turn would be expanded
to two adresses (<[EMAIL PROTECTED]>, <[EMAIL PROTECTED]>), of which the first
would again be expanded to <[EMAIL PROTECTED]>.

So if you send mail to <[EMAIL PROTECTED]>, it will be delivered
to <[EMAIL PROTECTED]> and <[EMAIL PROTECTED]>.

The options are stored in the transaction notes with key recipient_options and
can be accessed by other plugins. They are not recursively expanded, however, so 
in the above example, the greylisting plugin would only be active for the 
hjp and peter.holzer addresses, not for postmaster and sysadm.

The ability to specify patterns doesn't add any functionality: The first
line in the example above is exactly equivalent to:

    [EMAIL PROTECTED]: [EMAIL PROTECTED] (denysoft_greylist, 
spamassassin_reject_threshold=10)
    [EMAIL PROTECTED]: [EMAIL PROTECTED] (denysoft_greylist, 
spamassassin_reject_threshold=10)
    [EMAIL PROTECTED]: [EMAIL PROTECTED] (denysoft_greylist, 
spamassassin_reject_threshold=10)
    [EMAIL PROTECTED]: [EMAIL PROTECTED] (denysoft_greylist, 
spamassassin_reject_threshold=10)

But it should help to keep the expansions consistent.

The order of lines is not significant. If two lines for the same alias
exist, it is undefined which one is used. (In the current
implementation, later entries override earlier ones but this should not
be relied upon).

=head1 HOOKS

=cut

use strict;
use Time::HiRes qw(time);
use Data::Dumper;

my $al;
sub parse_al {
    my ($self) = @_;

    my $t0 = time();
    my ($QPHOME) = ($0 =~ m!(.*?)/([^/]+)$!);
    open(UL, "<$QPHOME/config/aliases");
    while (<UL>) {
        s/#.*//;
        s/\s+//gs;
        next if /^$/;
        my $options;
        if (/(.*)\((.*)\)/) {
            # options are parenthesized 
            $options = $2;
            $_ = $1;
        }
        my ($alias, $exp) = split(/:/);
        my ($a_local, $a_dom) = split(/\@/, $alias);
        my @locals = split(/,/, $a_local);
        my @domains = split(/,/, $a_dom);
        my @exp = split(/,/, $exp);
        for my $l (@locals) {
            for my $d (@domains) {
                #print STDERR "$$ aliases: [EMAIL PROTECTED]";
                $al->{"[EMAIL PROTECTED]"}{exp} = [EMAIL PROTECTED];
                if ($options) {
                    my @opt = split(/,/, $options);
                    for my $o (@opt) {
                        if ($o =~ m/(.*?)=(.*)/) {
                            $al->{"[EMAIL PROTECTED]"}{opt}{$1} = $2;
                            # $self->log(6, "aliases: parse_al: option <$1>=<$2>");
                        } else {
                            $al->{"[EMAIL PROTECTED]"}{opt}{$o} = 1;
                            # $self->log(6, "aliases: parse_al: option <$o>");
                        }
                    }
                }
            }
        }
    }
    my $t1 = time();

    print STDERR "$$ aliases: time to parse: ", $t1 - $t0, " seconds\n";
    #print STDERR "$$ aliases: aliases", Dumper $al, "\n";


}


sub register {
    my ($self, $qp) = @_;

    print STDERR "$$ aliases: in register\n";
    $self->parse_al();

    $self->register_hook("rcpt", "check_rcpt");
    $self->register_hook("data_post", "replace_rcpt");
    print STDERR "$$ aliases: finished register\n";
}

sub expand_alias {
    my ($alias, $detail, $null_ok) = @_;
    my $exp = undef;

    print STDERR "$$ aliases: expand_alias($alias, $detail, $null_ok\n";

    my $t0 = time();
    my $e = $al->{$alias}{exp};
    if ($e) {
        for (@$e) {
            my $e1 = expand_alias($_, $detail, 1);
            push @$exp, @$e1;
        }
    } else {
        if ($null_ok) {
            my ($mailbox, $server) = split(/@/, $alias);
            $exp = [ 
                    $mailbox . ($detail ? "+$detail" : "") .
                    '@' . $server
                   ];
        }
    }
    my $t1 = time();
    print STDERR "$$ aliases: $alias expanded to ",
                 ($exp ? scalar(@$exp) : 0), " recipients in : ",
                 $t1 - $t0, " seconds\n";
    return $exp;
}


sub alias_options {
    my ($self, $alias) = @_;

    return $al->{$alias}{opt};
}

=head2 rcpt: check_rcpt

The check_rcpt method plugs into the rcpt hook. It looks up the
recipient's email address in the aliases file, expands it, and stores
the result and per-address options (if any) in transaction notes.

If the address is not found, the request is DENYd, if it is found, the
request is DECLINED. This plugin should be run before any other plugin
which makes use of recipient_options. The last plugin to run must then
return OK for all recipients it doesn't DENY. (there is a rcpt_ok plugin 
which simply accepts all recipients which haven't yet been denied).

=cut

sub check_rcpt {
    my ($self, $transaction, $recipient) = @_;

    # get current list of recipients.
    my $exprcpt = $transaction->notes('expanded_recipients');
    $exprcpt = {} unless $exprcpt;

    # split recipient into local part, detail and domain
    # (local part and domain are case insensitive)
    #
    my $orig = $recipient->address;
    my $local_part = $recipient->user;
    my $detail;
    if ($local_part =~ m/([^+]+)\+(.*)/) {
        $local_part = $1;
        $detail = $2;
    }
    $local_part = lc $local_part;
    my $domain = lc $recipient->host;
    my $rcpt = "[EMAIL PROTECTED]";

    # look up alias
    my $e = expand_alias($rcpt, $detail, 0);
    return (DENY, "no such user <$rcpt>") unless ($e);

    $exprcpt->{$orig} = $e;
    $transaction->notes('expanded_recipients', $exprcpt);
    $transaction->notes('recipient_options', $self->alias_options($rcpt));
    return (DECLINED, "");
}

=head2 data_post: replace_rcpt

Replace all recipients with the list collected in note 'expanded_recipients'.

=cut

sub replace_rcpt {
    my ($self, $transaction) = @_;

    my $exprcpt = $transaction->notes('expanded_recipients');

    print STDERR "$$ aliases: exprcpt", Dumper $exprcpt, "\n";
    print STDERR "$$ aliases: clearing recipients\n";
    my @new_recipients = ();
    for ($transaction->recipients()) {
        my $e = $exprcpt->{$_->address()};
        push (@new_recipients, @$e) if ($e);
        $self->log(6, "replace_rcpt: recipient: ", $_->address(), " -> @$e\n");
    }
    return (DENY, "no recipients") unless @new_recipients;
    $transaction->clear_recipients();
    for (@new_recipients) {
        print STDERR "$$ aliases: adding $_\n";
        $transaction->add_recipient(Mail::Address->new('', $_));
    }
    print STDERR "$$ aliases: checking recipients\n";
    for ($transaction->recipients()) {
        print STDERR "$$ aliases: recipient: ", $_->address(), "\n";
    }
    print STDERR "$$ aliases: checking recipients done\n";
    
    return DECLINED;
}

=head1 BUGS

None known (yet).

=head1 TODO

Parsing a text file is fast enough for a few thousand aliases. For
larger user bases the text file should be replaced by a database with
proper indexes (*DBM, relational, LDAP, whatever).

=head1 AUTHOR

Peter J. Holzer <[EMAIL PROTECTED]>

=cut

=head1 NAME

rcpt_ok - allow all recipients

=head1 DESCRIPTION

This module simply returns OK for each rcpt request.
It is meant to be called after other plugins which return DECLINED for
addresses which are ok (e.g, the aliases plugin).

=head1 CONFIGURATION

Nothing to configure

=head1 HOOKS

=cut 

sub register {
  my ($self, $qp) = @_;
  $self->register_hook("rcpt", "rcpt_ok");
}

=head2 rcpt: rcpt_ok

Returns OK

=cut 

sub rcpt_ok {
  my ($self, $transaction, $recipient) = @_;

  $self->log(6, "rcpt_ok:");
  return OK;
}

=head1 BUGS

None known (yet).

=head1 TODO

Nothing (I hope).

=head1 AUTHOR

Peter J. Holzer <[EMAIL PROTECTED]>

=cut

Attachment: pgp00000.pgp
Description: PGP signature

Reply via email to