Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Fetching contributors…

Cannot retrieve contributors at this time

236 lines (177 sloc) 6.961 kb
=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;
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->qp->connection->notes('milter') || return DECLINED;
$milter->send_quit();
$self->qp->connection->notes('spam', undef);
$self->qp->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->qp->connection->notes(milter => $milter);
$self->qp->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->qp->connection->notes('spam', $@) if $@;
return DECLINED;
}
sub hook_helo {
my ($self, $transaction) = @_;
if (my $txt = $self->qp->connection->notes('spam')) {
return DENY, $txt;
}
my $milter = $self->qp->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->qp->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->qp->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->qp->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;
}
Jump to Line
Something went wrong with that request. Please try again.