#!perl -Tw =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 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->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; }