/usr/local/CPAN/Qpsmtpd-Plugin-Quarantine/Qpsmtpd/Plugin/Quarantine.pm
# Copyright(C) 2006 David Muir Sharnoff <muir@idiom.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
#
# This software is available without the GPL: please write if you need
# a non-GPL license. All submissions of patches must come with a
# copyright grant so that David Sharnoff remains able to change the
# license at will.
package Qpsmtpd::Plugin::Quarantine;
use Qpsmtpd::Constants;
use Qpsmtpd::DSN;
use OOPS;
use Digest::MD5 qw(md5_hex);
use File::Slurp;
use Time::HiRes qw();
use Net::Netmask;
use CGI;
use Template;
use Sys::Hostname;
use Mail::SPF::Query;
use Mail::Field;
use Net::SMTP;
use Mail::Address;
use DB_File;
use Qpsmtpd::Plugin::Quarantine::Spam;
use Qpsmtpd::Plugin::Quarantine::Common;
use Qpsmtpd::Plugin::Quarantine::Sendmail;
require Mail::Field::Received;
require Exporter;
@ISA = qw(Exporter);
@EXPORT = qw(hook_data_post register);
@EXPORT_OK = qw($hostname);
use strict;
use warnings;
our $VERSION = 1.02;
my $debug = 1;
our $myhostname = hostname();
my $fmes1 = "Your message is quarantined because we think it is probably spam, if it is not spam, click";
my $fmes2 = "to release your message from quarantine or to choose to have the spam you send silently deleted instead of bounced";
my $fmes3 = "None of the recipients of your email wish to receive mail that is likely to be spam";
# -----------------------------------------------------------------------
my $qa = {}; # state hash
my $fmes1rx = $defaults{senderbounce1};
$fmes1rx =~ s/ /\\s+/g;
$fmes1rx = qr/$fmes1rx/;
my $fmes2rx = $defaults{senderbounce2};
$fmes2rx =~ s/ /\\s+/g;
$fmes2rx = qr/$fmes2rx/;
my $fmes3rx = $defaults{senderbounce3};
$fmes3rx =~ s/ /\\s+/g;
$fmes3rx = qr/$fmes3rx/;
# -----------------------------------------------------------------------
sub register
{
my ($qp, undef, @args) = @_;
my %args;
if (@args % 2 == 0) {
%args = @args;
} else {
warn "Malformed arguments to syslog plugin";
return DECLINED;
}
$qa->{justkidding} = "no templates directory"
unless -d $defaults{templates};
$qa->{start_time} = Time::HiRes::time();
$qa->{our_domains} = { map { $_ => 1 } $qp->qp->config('our_domains') };
$qa->{filter_domains} = { map { $_ => 1 } $qp->qp->config('filter_domains') };
recompute_defaults;
if (0 && $debug) {
for my $i (sort keys %{$qa->{our_domains}}) {
$qp->log(LOGDEBUG, "Our domain: $i");
}
for my $i (sort keys %{$qa->{filter_domains}}) {
$qp->log(LOGDEBUG, "Filter domain: $i");
}
}
$qa->{our_networks} = {};
for my $net ($qp->qp->config('our_networks')) {
my $block = new2 Net::Netmask $net;
$qp->log(LOGDEBUG, "Our network: $net");
if ($block) {
$block->storeNetblock($qa->{our_networks});
} else {
warn "Cannot parse network block '$net': $Net::Netmask::error"
}
}
$qa->{ignore_networks} = {};
for my $net ($qp->qp->config('ignore_networks'), @{$defaults{ignore_networks}}) {
my $block = new2 Net::Netmask $net;
$qp->log(LOGDEBUG, "Ingore network: $net");
if ($block) {
$block->storeNetblock($qa->{ignore_networks});
} else {
warn "Cannot parse network block '$net': $Net::Netmask::error"
}
}
$qa->{template} = Template->new({
INCLUDE_PATH => $args{templates},
INTERPOLATE => 1,
POST_CHOMP => 0,
EVAL_PERL => 1,
RECURSION => 1,
}) || die Template->error();
srand(time ^ ($$ << 5));
if ($defaults{notify_recipient_only} && -e $defaults{notify_recipient_only}) {
my $db = tie my %h, 'DB_File', $defaults{notify_recipient_only}
or die "dbopen $defaults{notify_recipient_only}: $!";
$db->filter_fetch_key ( sub { s/\0$// } ) ;
$db->filter_store_key ( sub { $_ .= "\0" } ) ;
$db->filter_fetch_value( sub { s/\0$// } ) ;
$db->filter_store_value( sub { $_ .= "\0" } ) ;
$qa->{notify_recipient_only} = \%h;
} else {
$qa->{notify_recipient_only} = {};
}
if ($defaults{special_sender_db} && -e $defaults{special_sender_db}) {
my $db = tie my %h, 'DB_File', $defaults{special_sender_db}
or die "dbmopen $defaults{special_sender_db}: $!";
$db->filter_fetch_key ( sub { s/\0$// } ) ;
$db->filter_store_key ( sub { $_ .= "\0" } ) ;
$db->filter_fetch_value( sub { s/\0$// } ) ;
$db->filter_store_value( sub { $_ .= "\0" } ) ;
$qa->{special_sender_db} = \%h;
} else {
$qa->{sepcial_sender_db} = {};
}
spam_init();
return OK;
}
sub hook_data_post
{
my ($qp, $transaction) = @_;
$qp->log(LOGDEBUG, "----------------------------------------------------------------------------------");
get_message_info($qp, $transaction);
if ($transaction->notes('filtered_recipient_count')) {
$qp->log(LOGDEBUG, "Checking message to a filtered destination");
} elsif ($defaults{check_from_our_ip} && $transaction->notes('address_type') eq 'internal') {
$qp->log(LOGDEBUG, "Checking message from our IP addresses");
} elsif ($defaults{check_not_from_our_ip} && $transaction->notes('address_type') eq 'external') {
$qp->log(LOGDEBUG, "Checking message not from our IP addresses");
} elsif ($defaults{check_from_our_domain} && $transaction->notes('from_our_domain')) {
$qp->log(LOGDEBUG, "Checking message from our domain");
} elsif ($defaults{check_not_our_domain} && ! $transaction->notes('from_our_domain')) {
$qp->log(LOGDEBUG, "Checking message not from our domain");
} elsif ($defaults{check_all_recipients}) {
$qp->log(LOGDEBUG, "Checking message because we check 'em all");
} elsif ($defaults{special_sender_db} && $qa->{special_sender_db}{$transaction->sender->address()}) {
$qp->log(LOGDEBUG, "Checking message from a special sender");
} elsif ($defaults{randomly_check_messages} && rand(100) < $defaults{randomly_check_messages}) {
$qp->log(LOGDEBUG, "Checking message randomly");
} else {
$qp->log(LOGDEBUG, "QRESULT: Not checking for spam");
return DECLINED;
}
my $spammy = check_message_for_spam($qp, $transaction);
unless ($spammy) {
$qp->log(LOGDEBUG, "QRESULT: Not spam");
return DECLINED;
}
return filterit($qp, $transaction, $spammy);
}
sub filterit
{
my ($qp, $transaction, $spam_reason) = @_;
my @retcode = DECLINED;
die unless $qa->{our_networks};
my $qt = $transaction->notes('quarantine');
if ($qa->{justkidding}) {
$qp->log(LOGDEBUG, "skipping... $qa->{justkidding}");
return DECLINED;
}
if ($qt->{already_filtered}++) {
$qp->log(LOGDEBUG, "skipping... already filtered");
return DECLINED;
}
my $body = $transaction->body_as_string();
my $body_checksum = md5_hex($body);
my $headers = $transaction->header();
my $header = $headers->as_string();
my $suser = $transaction->sender->user();
my $sdomain = $transaction->sender->host();
my $sender_address = "$suser\@$sdomain";
my @recipients = map { $_->address() } $transaction->recipients();
my $ip = $transaction->notes('external_ip') || $transaction->notes('internal_ip');
transaction(sub {
my $oops = get_oops();
my $time = time;
my $qd = $oops->{quarantine} || initialize($qp, $oops, $transaction);
my $psender = $qd->{senders}{$sender_address};
my $sender_token = $psender
? $psender->{token}
: md5_hex($$ . Time::HiRes::time() . $sender_address);
my $header_checksum = md5_hex($header . $qd->{random_token} . $sender_token);
#
# Let our own bounces through (up to a point)
#
$qp->log(LOGDEBUG, "RX: $fmes1rx\\s+\Q$defaults{baseurl}\E/message/(\\S+)\\s+$fmes2rx");
if ($body =~ m{$fmes1rx\s+\Q$defaults{baseurl}\E/message/(\S+)\s+$fmes2rx}s && exists $qd->{headers}{$1}) {
# This is an email about something we have held in quarantine.
my $pheader = $qd->{headers}{$1};
if ($pheader->{bounce_seen}++ < $defaults{max_bounces_per_header}) {
$oops->commit();
$qp->log(LOGINFO, "QRESULT: Message with our own bounce forwarded");
@retcode = DECLINED;
return;
} else {
$qp->log(LOGINFO, "No free pass for our own bounce, this is number $pheader->{bounce_seen} for this message");
}
}
$qp->log(LOGDEBUG, "RX MATCHED ($1)") if $1;
#
# Check to see if any of the recipients have set options to control
# their mail.
#
my $recipients_modified = 0;
my (@nr);
my $counter = 0;
my %remap_from;
for my $r (@recipients) {
my $rd;
if (($rd = $qd->{recipients}{$r}) && $rd->{action}) {
if ($rd->{action} eq 'drop') {
$qp->log(LOGDEBUG, "Recipient '$r': drop");
# skip this one...
} elsif ($rd->{action} eq 'forward' && $counter < 10) {
my ($nap) = Mail::Address->parse($rd->{new_address});
if ($nap && $nap->address) {
$qp->log(LOGDEBUG, "Recipient '$r': redirect to ".$nap->address);
$remap_from{$nap->address} = $r;
$r = $nap->address;
$counter++;
redo;
} else {
$qp->log(LOGWARN, "Could not parse forward address for $r: '$rd->{new_address}'");
push(@nr, $r);
}
} else {
$qp->log(LOGWARN, "Bogus action for $r: '$rd->{action}'");
push(@nr, $r);
}
} else {
$qp->log(LOGDEBUG, "No special action for recipient '$r'");
push(@nr, $r);
}
$counter = 0;
}
@recipients = @nr;
#
# Let's figure out which recipients get notified, which get protected,
# and which get a message.
#
my @passthrough_recipients; # not quarantined
my @quarantine_recipients; # quarantined and they know it
my @filter_recipients; # quarantined but they don't know it
my @queued_messages;
for my $r (@recipients) {
my $rd = $qd->{recipients}{$r};
my ($ra) = Mail::Address->parse($r);
die unless ref($ra);
my $rdomain = $ra->host;
if (! $rd && ! match_domain($qp, $rdomain, $qa->{filter_domains}, 'filter_domains')) {
push(@passthrough_recipients, $r);
next;
}
$rd = new_recipient($oops, $r)
unless $rd;
$rd->{mcount} += 1;
$rd->{total_count} += 1;
my $do_qr = 0;
if ($rd->{total_count} >= $defaults{notify_recipients}) {
$do_qr = "Recipient $r has is over the threshold ($rd->{total_count}): let the recipient choose";
} else {
$qp->log(LOGDEBUG, "Recipient $r is under the threshold ($rd->{total_count})");
}
if (! $do_qr and $qa->{notify_recipient_only}{lc($r)}) {
$do_qr = "Recipient is in the special list: let the recipient choose";
} else {
$qp->log(LOGDEBUG, "Lookup of notify_recipient_only{$r} = nada");
}
if ($do_qr) {
$qp->log(LOGDEBUG, $do_qr);
if ($time - $rd->{last_timestamp} > 86400 * $defaults{renotify_recipient_days}) {
push(@queued_messages, send_recipient_notification($qp, $transaction, $r, $rd, $qd))
} else {
$qp->log(LOGDEBUG, "Not yet time to bug recipient $r");
}
push(@quarantine_recipients, $r);
} else {
push(@filter_recipients, $r);
}
$rd->{last_timestamp} = $time;
}
if (@passthrough_recipients == 1 && @quarantine_recipients == 0 && @filter_recipients == 0 && $remap_from{$passthrough_recipients[0]}) {
$headers->replace('X-Mail-Redirected-From', "$remap_from{$passthrough_recipients[0]} on $myhostname");
$transaction->header($headers);
}
my $notify_recipients_only = @quarantine_recipients && ! @filter_recipients;
#
# Basic sender tracking
#
$psender = new_sender($oops, $sender_address, $transaction->sender->address(), $sender_token)
unless $psender;
my $sender_ip_last_used = $psender->{send_ip_used}{$ip} || 0;
#
# What are we doing?
#
my $reply = '';
my $noteOK = '';
my $noteDENY = '';
my $sender_okay =
(
spf($qp, $transaction) eq 'pass'
&&
($noteOK = 'sender-SPF-passed')
)
||
(
(
(
$transaction->notes('origin_type') eq 'internal'
&&
($noteOK = 'from-one-of-our-ip-addresses')
)
||
(
$transaction->notes('from_our_domain')
&&
($noteOK = 'from-one-of-our-domains')
)
)
&&
! (
spf($qp, $transaction) eq 'fail'
&&
($noteDENY = 'sender-SPF-failed')
)
)
||
(
(
$defaults{notify_other_senders}
&&
($noteOK = 'notify-external-senders')
)
&&
! (
spf($qp, $transaction) eq 'fail'
&&
($noteDENY = 'sender-SPF-failed')
)
)
;
my $no_recipient_bounce_body =
! @recipients
&&
$body =~ m{$fmes3rx}
&&
($reply = 'message will be discarded as useless bounce')
;
my $sender_bounce = $sender_okay;
$sender_bounce = 0
if
(
$transaction->sender() eq "<>"
&&
($reply = 'no bounces for MAILER-DAEMON')
)
||
(
$psender->{action}
&&
$psender->{action} eq 'discard'
&&
($reply = "sender doesn't want to know")
)
||
(
$notify_recipients_only
&&
($reply = 'recipients will be notified instead')
)
||
$no_recipient_bounce_body
;
my $sender_quarantine = $sender_bounce;
$sender_quarantine = 0
if
(
$psender->{action}
&&
$psender->{action} eq 'bounce'
&&
($reply = "Bounced due to your request at $defaults{baseurl}")
)
||
(
! @recipients
&&
($reply = 'No Recipients Want Spammy Messages')
)
;
my $do_quarantine =
(
(
$sender_quarantine
&&
@filter_recipients
)
||
@quarantine_recipients
)
;
$sender_bounce = 0
if
(
$sender_bounce
&&
$time - $sender_ip_last_used < 86400 * ($psender->{renotify_days} || $defaults{renotify_sender_ip})
&&
($reply = 'not yet time for another bounce')
)
;
#
# Sender tracking. Track how many spams are sent and save one every now and then.
#
if ($sender_bounce) {
$psender->{send_ip_used}{$ip} = $time;
}
my $today = $time / 86400;
my $spams_sent = 0;
$psender->{spams_sent_perday}{$today} += 1;
for my $spamday ($psender->{spams_sent_perday}) {
next if $today - $spamday > $defaults{sender_history_to_keep};
$spams_sent += $psender->{spams_sent_perday};
}
if ($defaults{keep_every_nth_message} && ($spams_sent % $defaults{keep_every_nth_message}) == 0) {
$do_quarantine = 1;
}
$psender->{last_message} = $time;
#
# We only need to save the message if it's interesting.
#
if ($do_quarantine) {
my $pbody = $qd->{bodies}{$body_checksum};
unless ($pbody) {
$pbody = $qd->{bodies}{$body_checksum} = bless {
body => $body,
cksum => $body_checksum,
size => length($body),
}, 'Quarantine::Body';
$qd->{diskused}{$$ % $defaults{size_storage_array_size}}
+= length($body) + $defaults{message_size_overhead};
}
my @recip = $sender_quarantine
? @recipients
: (@filter_recipients, @quarantine_recipients);
my $pheader = $qd->{headers}{$header_checksum} = bless {
from => $headers->get('From'),
to => $headers->get('To'),
subject => $headers->get('Subject'),
date => $headers->get('Date'),
time => $time,
sender => $psender,
recipients => (bless [ @recip ], 'Quarantine::RecipientList'),
header => $header,
body => $pbody,
cksum => $header_checksum,
}, 'Quarantine::Header';
$pbody->{last_reference} = $pheader;
$psender->{headers}{$header_checksum} = $pheader;
unless ($qd->{buckets3}) {
$qd->{buckets3} = bless {}, 'Quarantine::Buckets';
}
$qd->{buckets3}{int($time / 86400)}{int(($time % 86400) / 3600)}{$header_checksum} = $pheader;
$oops->virtual_object($qd->{buckets3}{int($time / 86400)}, 1);
$oops->virtual_object($qd->{buckets3}{int($time / 86400)}{int(($time % 86400) / 3600)}, 1);
for my $r (@quarantine_recipients, @filter_recipients) {
my $rd = $qd->{recipients}{$r};
$rd->{headers}{$header_checksum} = $pheader;
}
}
#
# Some debugging
#
$qp->log(LOGDEBUG, "recipients = @recipients");
$qp->log(LOGDEBUG, "filter_recipients = @filter_recipients");
$qp->log(LOGDEBUG, "quarantine_recipients = @quarantine_recipients");
$qp->log(LOGDEBUG, "passthrough_recipients = @passthrough_recipients");
$qp->log(LOGDEBUG, "notify_recipients_only = $notify_recipients_only");
$qp->log(LOGDEBUG, "reply = $reply");
$qp->log(LOGDEBUG, "noteOK = $noteOK");
$qp->log(LOGDEBUG, "noteDENY = $noteDENY");
$qp->log(LOGDEBUG, "sender_okay = $sender_okay");
$qp->log(LOGDEBUG, "no_recipient_bounce_body = $no_recipient_bounce_body");
$qp->log(LOGDEBUG, "sender_bounce = $sender_bounce");
$qp->log(LOGDEBUG, "sender_quarantine = $sender_quarantine");
$qp->log(LOGDEBUG, "do_quarantine = $do_quarantine");
#
# Do it.
#
$oops->commit();
my $messageid = $headers->get('Message-ID');
$messageid =~ s/[\r\n]+\z//;
my (@new_recip);
for my $r (@passthrough_recipients) {
push(@new_recip, Qpsmtpd::Address->new($r));
}
@new_recip = Qpsmtpd::Address->new($defaults{nobody_address})
unless @new_recip;
if ($do_quarantine && $sender_quarantine) {
my $and_recipients = @queued_messages ? ", Recipients Notified" : "";
$qp->log(LOGINFO, "QRESULT: Message quarantined, sender notified$and_recipients - $noteOK - $messageid");
@retcode = Qpsmtpd::DSN->mbox_disabled(DENY, "$fmes1 $defaults{baseurl}/message/$header_checksum $fmes2");
} elsif ($sender_bounce && ! @recipients) {
$qp->log(LOGINFO, "QRESULT: Message bounced - no recipients - $noteOK - $messageid");
@retcode = Qpsmtpd::DSN->mbox_disabled(DENY, $defaults{senderbounce3});
} elsif ($do_quarantine && @quarantine_recipients && @passthrough_recipients) {
$qp->log(LOGINFO, "QRESULT: Message quarantined silently; some recipients notified, some passthrough - $noteOK $noteDENY - $messageid");
$transaction->recipients(@new_recip);
@retcode = DECLINED;
} elsif ($do_quarantine && @quarantine_recipients) {
$qp->log(LOGINFO, "QRESULT: Message quarantined silently, recipients notified - $noteOK $noteDENY - $messageid");
$transaction->recipients(@new_recip);
@retcode = DECLINED;
} elsif ($do_quarantine && @passthrough_recipients) {
# is this possible?
$qp->log(LOGINFO, "QRESULT: Message quarantined silently, some recipients passthrough $noteOK $noteDENY - $messageid");
$transaction->recipients(@new_recip);
@retcode = DECLINED;
} elsif ($do_quarantine) {
# is this possible?
$qp->log(LOGINFO, "QRESULT: Message quarantined silently $noteOK $noteDENY - $messageid");
$transaction->recipients(@new_recip);
@retcode = DECLINED;
} elsif (@passthrough_recipients && ! @filter_recipients && ! @quarantine_recipients) {
$qp->log(LOGINFO, "QRESULT: Messages allowed $noteOK $noteDENY - $messageid");
$transaction->recipients(@new_recip);
@retcode = DECLINED;
} elsif ($sender_bounce) {
#
# it doesn't matter if there were some passthrough recipients, we're
# bouncing it back to the sender 'cause it looked spammy.
#
$qp->log(LOGINFO, "QRESULT: Message bounced $reply - $noteOK - $messageid");
@retcode = Qpsmtpd::DSN->mbox_disabled(DENY, $reply || "Spammy message rejected. See $defaults{baseurl} for options");
} elsif (@passthrough_recipients) {
$qp->log(LOGINFO, "QRESULT: Message passed through $noteOK $noteDENY - $messageid");
$transaction->recipients(@new_recip);
@retcode = DECLINED;
} else {
$qp->log(LOGINFO, "QRESULT: Message discarded silently $reply $noteOK $noteDENY - $messageid");
$transaction->recipients(Qpsmtpd::Address->new($defaults{nobody_address}));
@retcode = DECLINED;
}
if ($do_quarantine) {
for my $qm (@queued_messages) {
$qp->log(LOGDEBUG, "Sending message to $qm->{recipient}:\n$qm->{message}");
sendmail_or_queue(
from => $defaults{send_from},
to => $qm->{recipient},
message => $qm->{message},
debuglogger => sub { $qp->log(LOGDEBUG, @_) },
errorlogger => sub { $qp->log(LOGINFO, @_) },
);
}
}
return 1;
}) or return DECLINED;
$qp->log(LOGDEBUG, "quarantine returning: @retcode");
return @retcode;
}
sub initialize
{
my ($qp, $oops, $transaction) = @_;
require Data::Dumper;
# my $qa = $qp->{_quarantine};
die unless $qa->{our_networks};
my $qd = $oops->{quarantine} = bless {
senders => (bless {}, 'Quarantine::Senders'),
headers => (bless {}, 'Quarantine::Headers'),
bodies => (bless {}, 'Quarantine::Bodies'),
buckets3 => (bless {}, 'Quarantine::Buckets'),
recipients => (bless {}, 'Quarantine::Recipients'),
mqueue => (bless {}, 'Quarantine::MailQueue'),
diskused => (bless {}, 'Quarantine::DiskUsage'),
version => $VERSION,
}, 'Quarantine::Top';
$oops->virtual_object($qd->{headers}, 1);
$oops->virtual_object($qd->{bodies}, 1);
$oops->virtual_object($qd->{senders}, 1);
$oops->virtual_object($qd->{recipients}, 1);
$oops->virtual_object($qd->{mqueue}, 1);
$oops->virtual_object($qd->{diskused}, 1);
# make a pseudo-random token
my $header_checksum = md5_hex($transaction->header()->as_string());
my $time = Time::HiRes::time();
my $stuff1 = "$$.$header_checksum.$time.";
my $stuff2 = join('?',values(%$qa));
my $stuff3;
open(RANDOM, "/dev/random") || next;
read(RANDOM, $stuff3, 32, 0);
close(RANDOM);
$qd->{random_token} = md5_hex($stuff1).md5_hex($stuff2).md5_hex($stuff3);
$qp->log(LOGWARN, "Quarantine data structures initialized");
return $qd;
}
sub send_recipient_notification
{
my ($qp, $transaction, $r, $rd, $qd) = @_;
my $headers = $transaction->header();
my $buf;
my (undef, $domain) = split('@', $r);
$qa->{template}->process('recipient-notification.mail', {
config => \%defaults,
recipient => $r,
mcount => $rd->{mcount},
domain => $domain,
headers => $headers,
sender => $transaction->sender(),
recipient_url => $rd->url($qd),
now => scalar(localtime(time)),
}, \$buf);
$buf =~ s/\A\s*//s;
return {
recipient => $r,
message => $buf,
};
}
sub get_message_info
{
my ($qp, $transaction) = @_;
# my $qa = $qp->{_quarantine} = {};
die unless $qa->{our_networks};
my $headers = $transaction->header();
if (0 && $debug) {
$qp->log(LOGDEBUG, "Our stuff...");
for my $i (sort keys %{$qa->{our_domains}}) {
$qp->log(LOGDEBUG, "Our domain: $i");
}
for my $i (sort keys %{$qa->{filter_domains}}) {
$qp->log(LOGDEBUG, "Filter domain: $i");
}
for my $i (dumpNetworkTable($qa->{our_networks})) {
$qp->log(LOGDEBUG, "Our netblock: $i");
}
for my $i (dumpNetworkTable($qa->{ignore_networks})) {
$qp->log(LOGDEBUG, "Ignore netblock: $i");
}
}
unless ($transaction->notes('origin_type')) {
my $external_ip;
my $internal_ip;
for my $received_line ($headers->get('Received')) {
# $qp->log(LOGDEBUG, "Processing Received line: $received_line");
my $received = Mail::Field->new('Received', $received_line);
if ($received->parsed_ok()) {
my $pt = $received->parse_tree();
my $ip = $pt->{from}{address};
my $our_block = findNetblock($ip, $qa->{our_networks});
if ($our_block) {
$internal_ip = $ip;
$qp->log(LOGDEBUG, "Found an internal address: $ip, will see if there's more...");
next;
}
my $ignore_block = findNetblock($ip, $qa->{ignore_networks});
if ($ignore_block) {
$qp->log(LOGDEBUG, "IP address should be ignored ($ip)");
next;
}
unless ($ip) {
$qp->log(LOGDEBUG, "No IP address found in: $received_line");
next;
}
$qp->log(LOGDEBUG, "Looked for $ip, but isn't one of ours");
$external_ip = $ip;
# $qp->log(LOGDEBUG, "sender host:", $transaction->sender->host());
# $qp->log(LOGDEBUG, "sender user:", $transaction->sender->user());
$qp->log(LOGDEBUG, "sender address:", $transaction->sender->address());
$transaction->notes(
entry_ip => $ip,
entry_helo => $pt->{from}{HELO},
entry_domain => $pt->{by}{domain},
);
last;
} else {
$qp->log(LOGWARN, "Mail::Field::Received failed: ", $received->diagnostics());
last;
}
}
if ($external_ip) {
$transaction->notes(external_ip => $external_ip);
$transaction->notes(origin_type => 'external');
$qp->log(LOGDEBUG, "External IP origin: $external_ip");
} else {
$transaction->notes(origin_type => 'internal');
$qp->log(LOGDEBUG, "Internal IP origin: $internal_ip");
}
}
unless (defined $transaction->notes('from_our_domain')) {
my $sdomain = $transaction->sender->host();
my $from_our_domain = match_domain($qp, $sdomain, $qa->{our_domains}, 'our_domains');
$transaction->notes(from_our_domain => $from_our_domain);
$qp->log(LOGDEBUG, "From our domain: $from_our_domain ($sdomain)");
}
unless (defined $transaction->notes('filtered_recipient_count')) {
my @rdomains = map { $_->host() } $transaction->recipients();
my $filter_count = 0;
for my $rd (@rdomains) {
$filter_count++ if match_domain($qp, $rd, $qa->{filter_domains}, 'filter_domains');
}
$transaction->notes(filtered_recipient_count => $filter_count);
$qp->log(LOGDEBUG, "Filtered Recipient Count: $filter_count");
}
}
sub spf
{
my ($qp, $transaction) = @_;
my $done = $transaction->notes('spf_result');
return $done if $done;
get_message_info($qp, $transaction)
unless $transaction->notes('origin_type');
return ''
unless $transaction->notes('origin_type') eq 'external';
my $ip = $transaction->notes('entry_ip');
my $sender = $transaction->sender->address();
my $entry_helo = $transaction->notes('entry_helo');
$qp->log(LOGDEBUG, "SPF Query with ip=$ip, sender=$sender, helo='$entry_helo'");
my $spf = new Mail::SPF::Query (
ip => $ip,
sender => $sender,
helo => $entry_helo,
myhostname => ($transaction->notes('entry_domain') || $myhostname),
debug => 0,
debuglog => sub { $qp->log(LOGDEBUG, @_) },
trusted => 1,
guess => 1,
sanitize => 0,
);
my $result = ($spf->result())[0];
$qp->log(LOGDEBUG, "SPF results: $result");
$transaction->notes(spf_result => $result);
return $result;
}
sub match_domain
{
my ($qp, $domain, $hashref, $what) = @_;
while ($domain) {
$qp->log(LOGDEBUG, "Trying to match $domain for $what");
if ($hashref->{$domain}) {
return 1;
}
$domain =~ s/^[^\.]+// or last;
$domain =~ s/^\.//;
}
return 0;
}