qpsmtpd/plugins/milter

267 lines
7.1 KiB
Perl

#!perl -w
=head1 NAME
milter
=head1 DESCRIPTION
This plugin allows you to attach to milter filters (yes, those written for
sendmail) as though they were qpsmtpd plugins.
In order to do this you need the C<Net::Milter> module from CPAN.
=head1 CONFIG
It takes two required parameters - a milter name (for logging) and the port
to connect to on the localhost. This can also contain a hostname if
the filter is on another machine:
milter Brightmail 5513
or
milter Brightmail bmcluster:5513
This plugin has so far only been tested with Brightmail's milter module.
=cut
use Net::Milter;
use Qpsmtpd::Constants;
no warnings;
sub register {
my ($self, $qp, @args) = @_;
die "Invalid milter setup args: '@args'" unless @args > 1;
my ($name, $port) = @args;
my $host = '127.0.0.1';
if ($port =~ s/^(.*)://) {
$host = $1;
}
$self->{name} = $name;
$self->{host} = $host;
$self->{port} = $port;
}
sub hook_disconnect {
my ($self) = @_;
my $milter = $self->connection->notes('milter') || return DECLINED;
$milter->send_quit();
$self->connection->notes('spam', undef);
$self->connection->notes('milter', undef);
return DECLINED;
}
sub check_results {
my ($self, $transaction, $where, @results) = @_;
foreach my $result (@results) {
next if $result->{action} eq 'continue';
$self->log(LOGINFO,
"milter $self->{name} result action: $result->{action}");
if ($result->{action} eq 'reject') {
die(
"Rejected at $where by $self->{name} milter ($result->{explanation})");
}
elsif ($result->{action} eq 'add') {
if ($result->{header} eq 'body') {
$transaction->body_write($result->{value});
}
else {
push @{$transaction->notes('milter_header_changes')->{add}},
[$result->{header}, $result->{value}];
}
}
elsif ($result->{action} eq 'delete') {
push @{$transaction->notes('milter_header_changes')->{delete}},
$result->{header};
}
elsif ($result->{action} eq 'accept') {
# TODO - figure out what this is used for
}
elsif ($result->{action} eq 'replace') {
push @{$transaction->notes('milter_header_changes')->{replace}},
[$result->{header}, $result->{value}];
}
}
}
sub hook_connect {
my ($self, $transaction) = @_;
$self->log(LOGDEBUG,
"milter $self->{name} opening connection to milter backend");
my $milter = Net::Milter->new();
$milter->open($self->{host}, $self->{port}, 'tcp');
$milter->protocol_negotiation();
$self->connection->notes(milter => $milter);
$self->connection->notes(
milter_header_changes => {add => [], delete => [], replace => [],});
my $remote_ip = $self->qp->connection->remote_ip;
my $remote_host = $self->qp->connection->remote_host;
$self->log(LOGDEBUG,
"milter $self->{name} checking connect from $remote_host\[$remote_ip\]"
);
eval {
$self->check_results(
$transaction,
"connection",
$milter->send_connect(
$remote_host, 'tcp4', 0, $remote_ip
)
);
};
$self->connection->notes('spam', $@) if $@;
return DECLINED;
}
sub hook_helo {
my ($self, $transaction) = @_;
if (my $txt = $self->connection->notes('spam')) {
return DENY, $txt;
}
my $milter = $self->connection->notes('milter');
my $helo = $self->qp->connection->hello;
my $host = $self->qp->connection->hello_host;
$self->log(LOGDEBUG, "milter $self->{name} checking HELO $host");
eval {
$self->check_results($transaction, "HELO", $milter->send_helo($host));
};
return DENY, $@ if $@;
return DECLINED;
}
sub hook_mail {
my ($self, $transaction, $address, %param) = @_;
my $milter = $self->connection->notes('milter');
$self->log(LOGDEBUG,
"milter $self->{name} checking MAIL FROM " . $address->format);
eval {
$self->check_results($transaction, "MAIL FROM",
$milter->send_mail_from($address->format));
};
return DENY, $@ if $@;
return DECLINED;
}
sub hook_rcpt {
my ($self, $transaction, $address, %param) = @_;
my $milter = $self->connection->notes('milter');
$self->log(LOGDEBUG,
"milter $self->{name} checking RCPT TO " . $address->format);
eval {
$self->check_results($transaction, "RCPT TO",
$milter->send_rcpt_to($address->format));
};
return DENY, $@ if $@;
return DECLINED;
}
sub hook_data_post {
my ($self, $transaction) = @_;
my $milter = $self->connection->notes('milter');
$self->log(LOGDEBUG, "milter $self->{name} checking headers");
my $headers = $transaction->header(); # Mail::Header object
foreach my $h ($headers->tags) {
# munge these headers because milters prefer them this way
$h =~ s/\b(\w)/\U$1/g;
$h =~ s/\bid\b/ID/g;
foreach my $val ($headers->get($h)) {
# $self->log(LOGDEBUG, "milter $self->{name} checking header: $h: $val");
eval {
$self->check_results($transaction, "header $h",
$milter->send_header($h, $val));
};
return DENY, $@ if $@;
}
}
eval {
$self->check_results($transaction, "end headers",
$milter->send_end_headers());
};
return DENY, $@ if $@;
$transaction->body_resetpos;
# skip past headers
while (my $line = $transaction->body_getline) {
$line =~ s/\r?\n//;
$line =~ s/\s*$//;
last unless length($line);
}
$self->log(LOGDEBUG, "milter $self->{name} checking body");
my $data = '';
while (my $line = $transaction->body_getline) {
$data .= $line;
if (length($data) > 60000) {
eval {
$self->check_results($transaction, "body",
$milter->send_body($data));
};
return DENY, $@ if $@;
$data = '';
}
}
if (length($data)) {
eval {
$self->check_results($transaction, "body",
$milter->send_body($data));
};
return DENY, $@ if $@;
$data = '';
}
eval {
$self->check_results($transaction, "end of DATA",
$milter->send_end_body());
};
return DENY, $@ if $@;
my $milter_header_changes = $transaction->notes('milter_header_changes');
foreach my $add (@{$milter_header_changes->{add}}) {
$headers->add($add->[0], $add->[1]);
}
foreach my $del (@{$milter_header_changes->{'delete'}}) {
$headers->delete($del);
}
foreach my $repl (@{$milter_header_changes->{replace}}) {
$headers->replace($repl->[0], $repl->[1]);
}
return DECLINED;
}