# Copyright(C) 2006 David Muir Sharnoff # # 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; }