yay, plugin support works! :-D

git-svn-id: https://svn.perl.org/qpsmtpd/branches/v010@34 958fd67b-6ff1-0310-b445-bb7760255be9
This commit is contained in:
Ask Bjørn Hansen 2002-07-08 02:30:11 +00:00
parent a032ced541
commit 2fe35f1b8d
14 changed files with 335 additions and 103 deletions

87
README.plugins Normal file
View File

@ -0,0 +1,87 @@
#
# read this with 'perldoc README.plugins' ...
#
=head1 qpsmtpd plugin system; developer documentation
See the examples in plugins/ and ask questions on the qpsmtpd
mailinglist; subscribe by sending mail to qpsmtpd-subscribe@perl.org.
=head1 General return codes
Each plugin must return an allowed constant for the hook and (usually)
optionally a "message".
Generally all plugins for a hook are processed until one returns
something other than "DECLINED".
Plugins are run in the order they are listed in the "plugins"
configuration.
=over 4
=item OK
Action allowed
=item DENY
Action denied
=item DENYSOFT
Action denied; return a temporary rejection code (say 450 instead of 550).
=item DECLINED
Plugin declined work; proceed as usual. This return code is always
allowed unless noted otherwise.
=item DONE
Finishing processing of the request. Usually used when the plugin
sent the response to the client.
=back
See more detailed description for each hook below.
=head1 Hooks
=head2 mail
Called right after the envelope sender address is passed. The plugin
gets passed a Mail::Address object. Default is to allow the
recipient.
Allowed return codes
OK - sender allowed
DENY - Return a hard failure code
DENYSOFT - Return a soft failure code
DONE - skip further processing
=head2 rcpt
Hook for the "rcpt" command. Defaults to deny the mail with a soft
error code.
Allowed return codes
OK - recipient allowed
DENY - Return a hard failure code
DENYSOFT - Return a soft failure code
DONE - skip further processing
=head2 quit
Called on the "quit" command.
Allowed return codes:
DONE
All other codes will qpsmtpd do the default response.

27
STATUS
View File

@ -2,29 +2,40 @@
things to do for v0.10 things to do for v0.10
---------------------- ----------------------
transaction should probably be a part of the connection object instead transaction should maybe be a part of the connection object instead
of off the main object of off the main object
get timeouts to work in "tcpserver" mode (or generally...) get timeouts to work in "tcpserver" mode (or generally; not sure where
it fits best)
plugin support; plugin support;
load plugins in a funny namespace support plugins for the rest of the commands.
let them register the "hooks" they want to run in
specify a priority in register_hook. ("LAST", "FIRST", "MIDDLE", or
maybe a number)
proper access to the message body through the transaction
data command data command
how to spool the file? how to spool message to a file when it grows large and still give
reasonable easy access to the data from plugins?
... ...
TRACE in Constants.pm is not actually being used. Should it?
TRACE in Constants.pm is not actually being used.
Plugin Documentation! Future Ideas
============
Methods to create a bounce message easily; partly so we can accept a
mail for one user but bounce it right away for another RCPT'er.
David Carraway has some thoughts for "user filters"
http://nntp.perl.org/group/perl.qpsmtpd/2

View File

@ -1,4 +1,4 @@
0 64.81.84.165
# the first line of this file is being used as the IP # the first line of this file is being used as the IP
# address tcpserver will bind to. Use 0 to bind to all # address tcpserver will bind to. Use 0 to bind to all
# interfaces. # interfaces.

View File

@ -1,3 +1,7 @@
quit_fortune quit_fortune
require_resolvable_fromhost
rhsbl
# dnsbl # dnsbl
# this plugin needs to run after all other "rcpt" plugins
check_relay

View File

@ -2,3 +2,4 @@ dsn.rfc-ignorant.org does not accept bounces. This violates RFC 821/2505/2821 ht

View File

@ -72,8 +72,6 @@ sub dispatch {
my $self = shift; my $self = shift;
my ($cmd) = lc shift; my ($cmd) = lc shift;
warn "command: $cmd";
#$self->respond(553, $state{dnsbl_blocked}), return 1 #$self->respond(553, $state{dnsbl_blocked}), return 1
# if $state{dnsbl_blocked} and ($cmd eq "rcpt"); # if $state{dnsbl_blocked} and ($cmd eq "rcpt");
@ -164,59 +162,61 @@ sub mail {
} }
return $self->respond(501, "could not parse your mail from command") unless $from; return $self->respond(501, "could not parse your mail from command") unless $from;
# this needs to be moved to a plugin --- FIXME my ($rc, $msg) = $self->run_hooks("mail", $from);
0 and $from->format ne "<>" if ($rc == DONE) {
and $self->config("require_resolvable_fromhost") return 1;
and !check_dns($from->host) }
and return $self->respond(450, $from->host ? "Could not resolve ". $from->host : "FQDN required in the envelope sender"); elsif ($rc == DENY) {
$msg ||= $from->format . ', denied';
$self->log(2, "deny mail from " . $from->format . " ($msg)");
$self->respond(550, $msg);
}
elsif ($rc == DENYSOFT) {
$msg ||= $from->format . ', temporarily denied';
$self->log(2, "denysoft mail from " . $from->format . " ($msg)");
$self->respond(450, $msg);
}
else { # includes OK
$self->log(2, "getting mail from ".$from->format); $self->log(2, "getting mail from ".$from->format);
$self->respond(250, $from->format . ", sender OK - how exciting to get mail from you!"); $self->respond(250, $from->format . ", sender OK - how exciting to get mail from you!");
$self->transaction->sender($from); $self->transaction->sender($from);
} }
} }
}
sub rcpt { sub rcpt {
my $self = shift; my $self = shift;
return $self->respond(501, "syntax error in parameters") unless $_[0] =~ m/^to:/i; return $self->respond(501, "syntax error in parameters") unless $_[0] =~ m/^to:/i;
return(503, "Use MAIL before RCPT") unless $self->transaction->sender; return(503, "Use MAIL before RCPT") unless $self->transaction->sender;
my $from = $self->transaction->sender;
# Move to a plugin -- FIXME
if (0 and $from->format ne "<>" and $self->config('rhsbl_zones')) {
my %rhsbl_zones = map { (split /\s+/, $_, 2)[0,1] } $self->config('rhsbl_zones');
my $host = $from->host;
for my $rhsbl (keys %rhsbl_zones) {
$self->respond("550", "Mail from $host rejected because it $rhsbl_zones{$rhsbl}"), return 1
if check_rhsbl($rhsbl, $host);
}
}
my ($rcpt) = ($_[0] =~ m/to:(.*)/i)[0]; my ($rcpt) = ($_[0] =~ m/to:(.*)/i)[0];
$rcpt = $_[1] unless $rcpt; $rcpt = $_[1] unless $rcpt;
$rcpt = (Mail::Address->parse($rcpt))[0]; $rcpt = (Mail::Address->parse($rcpt))[0];
return $self->respond(501, "could not parse recipient") unless $rcpt; return $self->respond(501, "could not parse recipient") unless $rcpt;
return $self->respond(550, "will not relay for ". $rcpt->host) unless $self->check_relay($rcpt->host);
$self->transaction->add_recipient($rcpt); my ($rc, $msg) = $self->run_hooks("rcpt", $rcpt);
$self->respond(250, $rcpt->format . ", recipient ok"); if ($rc == DONE) {
return 1;
} }
elsif ($rc == DENY) {
$msg ||= 'relaying denied';
sub check_relay { $self->respond(550, $msg);
my $self = shift; }
my $host = lc shift; elsif ($rc == DENYSOFT) {
my @rcpt_hosts = $self->config("rcpthosts"); $msg ||= 'relaying denied';
return 1 if exists $ENV{RELAYCLIENT}; return $self->respond(550, $msg);
for my $allowed (@rcpt_hosts) { }
$allowed =~ s/^\s*(\S+)/$1/; elsif ($rc == OK) {
return 1 if $host eq lc $allowed; $self->respond(250, $rcpt->format . ", recipient ok");
return 1 if substr($allowed,0,1) eq "." and $host =~ m/\Q$allowed\E$/i; return $self->transaction->add_recipient($rcpt);
}
else {
return $self->respond(450, "Could not determine of relaying is allowed");
} }
return 0; return 0;
} }
sub get_qmail_config { sub get_qmail_config {
my ($self, $config) = (shift, shift); my ($self, $config) = (shift, shift);
$self->log(5, "trying to get config for $config"); $self->log(5, "trying to get config for $config");
@ -269,9 +269,10 @@ sub rset {
sub quit { sub quit {
my $self = shift; my $self = shift;
my @fortune = `/usr/games/fortune -s`; my ($rc, $msg) = $self->run_hooks("quit");
@fortune = map { chop; s/^/ \/ /; $_ } @fortune; if ($rc != DONE) {
$self->respond(221, $self->config('me') . " closing connection. Have a wonderful day.", @fortune); $self->respond(221, $self->config('me') . " closing connection. Have a wonderful day.");
}
exit; exit;
} }
@ -449,37 +450,57 @@ sub load_plugins {
my $eval = join( my $eval = join(
"\n", "\n",
"package $package;", "package $package;",
'use Qpsmtpd::Constants;',
"require Qpsmtpd::Plugin;", "require Qpsmtpd::Plugin;",
'use vars qw(@ISA);', 'use vars qw(@ISA);',
'@ISA = qw(Qpsmtpd::Plugin);', '@ISA = qw(Qpsmtpd::Plugin);',
# $line, $line,
$sub, $sub,
"\n", # last line comment without newline? "\n", # last line comment without newline?
); );
warn "eval: $eval"; warn "eval: $eval";
$eval =~ m/(.*)/; $eval =~ m/(.*)/s;
$eval = $1; $eval = $1;
eval $eval; eval $eval;
warn "EVAL: $@"; warn "EVAL: $@";
die "eval $@" if $@; die "eval $@" if $@;
#my $package_path = $package; my $plug = $package->new(qpsmtpd => $self);
#$package_path =~ s!::!/!g; $plug->register($self);
#$package_path .= ".pm";
#$INC{$package_path} = "$dir/$plugin";
#use Data::Dumper;
#warn Data::Dumper->Dump([\%INC, \@INC], [qw(INCh INCa)]);
my $plug = $package->new();
$plug->register();
} }
} }
sub run_hooks {
my ($self, $hook) = (shift, shift);
if ($self->{_hooks}->{$hook}) {
my @r;
for my $code (@{$self->{_hooks}->{$hook}}) {
(@r) = &{$code}($self->transaction, @_);
last unless $r[0] == DECLINED;
}
return @r;
}
warn "Did not run any hooks ...";
return (0, '');
}
sub _register_hook {
my $self = shift;
my ($hook, $code) = @_;
#my $plugin = shift; # see comment in Plugin.pm:register_hook
$self->{_hooks} ||= {};
my $hooks = $self->{_hooks};
push @{$hooks->{$hook}}, $code;
}
1; 1;

View File

@ -1,7 +1,21 @@
package Qpsmtpd::Constants; package Qpsmtpd::Constants;
use strict; use strict;
require Exporter;
my (@common) = qw(OK DECLINED DONE DENY DENYSOFT TRACE);
use vars qw($VERSION @ISA @EXPORT);
@ISA = qw(Exporter);
@EXPORT = @common;
use constant TRACE => 10; use constant TRACE => 10;
use constant OK => 900;
use constant DENY => 901;
use constant DENYSOFT => 902;
use constant DECLINED => 909;
use constant DONE => 910;
1; 1;

View File

@ -4,14 +4,28 @@ use strict;
sub new { sub new {
my $proto = shift; my $proto = shift;
my $class = ref($proto) || $proto; my $class = ref($proto) || $proto;
bless ({}, $class); my %args = @_;
bless ({ _qp => $args{qpsmtpd} }, $class);
} }
sub register_hook { sub register_hook {
warn "REGISTER HOOK!"; my ($plugin, $hook, $method) = @_;
# I can't quite decide if it's better to parse this code ref or if
# we should pass the plugin object and method name ... hmn.
$plugin->qp->_register_hook($hook, sub { $plugin->$method(@_) });
} }
sub qp {
shift->{_qp};
}
sub log {
shift->qp->log(@_);
}
sub transaction {
# not sure if this will work in a non-forking or a threaded daemon
shift->qp->transaction;
}
1; 1;

View File

@ -45,6 +45,12 @@ sub blocked {
$self->{_blocked}; $self->{_blocked};
} }
sub notes {
my $self = shift;
my $key = shift;
@_ and $self->{_notes}->{$key} = shift;
$self->{_notes}->{$key};
}
#sub add_header_line { #sub add_header_line {
#} #}

23
plugins/check_relay Normal file
View File

@ -0,0 +1,23 @@
# this plugin checks the standard rcpthosts config and
# $ENV{RELAYCLIENT} to see if relaying is allowed.
#
# It should be configured to be run _LAST_!
#
sub register {
my ($self, $qp) = @_;
$self->register_hook("rcpt", "check_relay");
}
sub check_relay {
my ($self, $transaction, $recipient) = @_;
my $host = lc $recipient->host;
my @rcpt_hosts = $self->qp->config("rcpthosts");
return (OK) if exists $ENV{RELAYCLIENT};
for my $allowed (@rcpt_hosts) {
$allowed =~ s/^\s*(\S+)/$1/;
return (OK) if $host eq lc $allowed;
return (OK) if substr($allowed,0,1) eq "." and $host =~ m/\Q$allowed\E$/i;
}
return (DENY);
}

View File

@ -1,14 +1,12 @@
sub new {}
sub register { sub register {
my ($self, $qp) = @_; shift->register_hook("quit", "quit_handler");
$qp->register_hook("quit", "quit_handler");
} }
sub quit_handler { sub quit_handler {
my ($self, $qp) = @_; my $qp = shift->qp;
my @fortune = `/usr/games/fortune -s`;
@fortune = map { chop; s/^/ \/ /; $_ } @fortune;
$qp->respond(221, $qp->config('me') . " closing connection.", @fortune);
return DONE;
} }

View File

@ -0,0 +1,46 @@
use Net::DNS qw(mx);
sub register {
my ($self, $qp) = @_;
$self->register_hook("mail", "mail_handler");
}
sub mail_handler {
my ($self, $transaction, $sender) = @_;
$sender->format ne "<>"
and $self->qp->config("require_resolvable_fromhost")
and !check_dns($sender->host)
and return (DENYSOFT,
($sender->host
? "Could not resolve ". $sender->host
: "FQDN required in the envelope sender"));
return DECLINED;
}
sub check_dns {
my $host = shift;
# for stuff where we can't even parse a hostname out of the address
return 0 unless $host;
return 1 if $host =~ m/^\[(\d{1,3}\.){3}\d{1,3}\]$/;
my $res = new Net::DNS::Resolver;
return 1 if mx($res, $host);
my $query = $res->search($host);
if ($query) {
foreach my $rr ($query->answer) {
return 1 if $rr->type eq "A" or $rr->type eq "MX";
}
}
else {
warn "$$ query for $host failed: ", $res->errorstring, "\n"
unless $res->errorstring eq "NXDOMAIN";
}
return 0;
}

37
plugins/rhsbl Normal file
View File

@ -0,0 +1,37 @@
sub register {
my ($self, $qp) = @_;
$self->register_hook("mail", "mail_handler");
$self->register_hook("rcpt", "rcpt_handler");
}
sub mail_handler {
my ($self, $transaction, $sender) = @_;
# lookup the address here; but always just return DECLINED
if ($sender->format ne "<>" and $self->qp->config('rhsbl_zones')) {
my %rhsbl_zones = map { (split /\s+/, $_, 2)[0,1] } $self->qp->config('rhsbl_zones');
my $host = $sender->host;
for my $rhsbl (keys %rhsbl_zones) {
$transaction->notes('rhsbl', "Mail from $host rejected because it $rhsbl_zones{$rhsbl}")
if check_rhsbl($self, $rhsbl, $host);
}
}
}
sub rcpt_handler {
my ($self, $transaction, $rcpt) = @_;
my $note = $transaction->notes('rhsbl');
return (DENY, $note) if $note;
return DECLINED;
}
sub check_rhsbl {
my ($self, $rhsbl, $host) = @_;
return 0 unless $host;
$self->log(2, "checking $host in $rhsbl");
return 1 if ((gethostbyname("$host.$rhsbl"))[4]);
return 0;
}

30
qpsmtpd
View File

@ -26,36 +26,6 @@ $qpsmtpd->run();
__END__ __END__
sub check_rhsbl {
my ($rhsbl, $host) = @_;
return 0 unless $host;
warn "$$ checking $host in $rhsbl\n" if $TRACE > 2;
return 1 if ((gethostbyname("$host.$rhsbl"))[4]);
return 0;
}
sub check_dns {
my $host = shift;
# for stuff where we can't even parse a hostname out of the address
return 0 unless $host;
return 1 if $host =~ m/^\[(\d{1,3}\.){3}\d{1,3}\]$/;
my $res = new Net::DNS::Resolver;
return 1 if mx($res, $host);
my $query = $res->search($host);
if ($query) {
foreach my $rr ($query->answer) {
return 1 if $rr->type eq "A" or $rr->type eq "MX";
}
}
else {
warn "$$ query for $host failed: ", $res->errorstring, "\n"
unless $res->errorstring eq "NXDOMAIN";
}
return 0;
}
1; 1;