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
pgp00000.pgp
Description: PGP signature
