From 7bfad42ac9c07c2981e44a7ad891015a5bf75757 Mon Sep 17 00:00:00 2001 From: Hanno Hecker Date: Fri, 6 Mar 2009 00:27:10 +0800 Subject: new plugin rcpt_map Check recipients from a postfix style map. The valid return codes are of course qpsmtpd constants. By storing the addresses in a %hash, this is much faster for fixed addresses than using the rcpt_regexp plugin just with fixed strings. This plugin handles only one domain per plugin instance. Use the :N suffix for the plugin if you need several domains mapped. --- plugins/rcpt_map | 187 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 files changed, 187 insertions(+), 0 deletions(-) create mode 100644 plugins/rcpt_map diff --git a/plugins/rcpt_map b/plugins/rcpt_map new file mode 100644 index 0000000..727ae7d --- /dev/null +++ b/plugins/rcpt_map @@ -0,0 +1,187 @@ + +=head1 NAME + +rcpt_map - check recipients against recipient map + +=head1 DESCRIPTION + +B reads a list of adresses, return codes and comments +from the supplied config file. Adresses are compared with I. +The recipient addresses are checked against this list, and if the first +matches, the return code from that line and the comment are returned to +qpsmtpd. Return code can be any valid plugin return code from +L. Matching is always done case insenstive. + +=head1 ARGUMENTS + +The C and C arguments are required. The default value of +the C argument is C (see below why C<_>). + +=over 4 + +=item domain NAME + +If the recipient address does not match this domain name NAME, this plugin will +return C + +=item file MAP + +Use the config file as map file, format as explained below + +=item default CODE[=MSG] + +Use CODE as default return code (and return MSG as message) if a recipient +was B found in the map. Since we can't use spaces in MSG, every C<_> +is replaced by a space, i.e. use C if you want a deny +message C. + +=back + +=head1 CONFIG FILE + +The config file contains lines with an address, a return code and a comment, +which will be returned to the sender, if the code is not OK or DECLINED. +Example: + + # example_org_map - config for rcpt_map plugin + me@example.org OK + you@example.org OK + info@example.org DENY User not found. + +=head1 NOTES + +We're currently running this plugin like shown in the following example. + +Excerpt from the C config file: + + ## list of valid users, config in /srv/qpsmtpd/config/rcpt_regexp + ## ... except for "*@example.org": + rcpt_regexp + ## only for "@example.org": + rcpt_map domain example.org file /srv/qpsmtpd/config/map_example_org + +And the C config file: + + ### "example.org" addresses are checked later by the rcpt_map + ### plugin, return DECLINED here: + /^.*\@example\.org$/ DECLINED + ### all other domains just check for valid users, the validity + ### of the domain is checked by the rcpt_ok plugin => never use + ### something else than "DENY" or "DECLINED" here! + /^(abuse|postmaster)\@/ DECLINED + /^(me|you)\@/ DECLINED + /^.*$/ DENY No such user. + +=head1 COPYRIGHT AND LICENSE + +Copyright (c) 2009 Hanno Hecker + +This plugin is licensed under the same terms as the qpsmtpd package itself. +Please see the LICENSE file included with qpsmtpd for details. + +=cut + +use Qpsmtpd::Constants; + +our %map; + +sub register { + my ($self, $qp, %args) = @_; + foreach my $arg (qw(domain file default)) { + next unless exists $args{$arg}; + if ($arg eq "default") { + my ($code, $msg) = split /=/, $args{$arg}; + $code = Qpsmtpd::Constants::return_code($code); + unless (defined $code) { + $self->log(LOGERROR, "Not a valid constant for 'default' arg"); + die "Not a valid constant for 'default' arg"; + } + + if ($msg) { + $msg =~ s/_/ /g; + } + else { + $msg = "No such user."; + } + + $self->{_default} = [$code, $msg]; + } + else { + $self->{"_$arg"} = $args{$arg}; + } + } + + $self->{_default} + or $self->{_default} = [DENY, "No such user"]; + + $self->{_file} + or die "No map file given..."; + + $self->log(LOGDEBUG, "Using file ".$self->{_file}); + %map = $self->read_map(1); + die "Empty map file" + unless keys %map; +} + +sub hook_pre_connection { + my $self = shift; + my ($time) = (stat($self->{_file}))[9] || 0; + if ($time > $self->{_time}) { + my %temp = $self->read_map(); + keys %temp + or return DECLINED; + %map = %temp; + } + return DECLINED; +} + +sub read_map { + my $self = shift; + my %hash = (); + open F, $self->{_file} + or do { $_[0] ? die "ERROR opening: $!" : return (); }; + + ($self->{_time}) = (stat(F))[9] || 0; + + my $line = 0; + while () { + ++$line; + s/^\s*//; + next if /^#/; + next unless $_; + my ($addr, $code, $msg) = split ' ', $_, 3; + next unless $addr; + + unless ($code) { + $self->log(LOGERROR, + "No constant in line $line in ".$self->{_file}); + next; + } + $code = Qpsmtpd::Constants::return_code($code); + unless (defined $code) { + $self->log(LOGERROR, + "Not a valid constant in line $line in ".$self->{_file}); + next; + } + $msg or $msg = "No such user."; + $hash{$addr} = [$code, $msg]; + } + return %hash; +} + +sub hook_rcpt { + my ($self, $transaction, $recipient) = @_; + return (DECLINED) + unless $recipient->host && $recipient->user; + + return (DECLINED) + unless lc($recipient->host) eq $self->{_domain}; + + my $rcpt = lc $recipient->user . '@' . lc $recipient->host; + return (@{$self->{_default}}) + unless exists $map{$rcpt}; + + return @{$map{$rcpt}}; +} + +# vim: ts=4 sw=4 expandtab syn=perl -- 1.7.2.2