You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@spamassassin.apache.org by qu...@apache.org on 2004/07/23 05:33:38 UTC

svn commit: rev 23171 - in spamassassin/trunk: . lib/Mail lib/Mail/SpamAssassin

Author: quinlan
Date: Thu Jul 22 20:33:38 2004
New Revision: 23171

Modified:
   spamassassin/trunk/INSTALL
   spamassassin/trunk/lib/Mail/SpamAssassin.pm
   spamassassin/trunk/lib/Mail/SpamAssassin/Conf.pm
   spamassassin/trunk/lib/Mail/SpamAssassin/Reporter.pm
   spamassassin/trunk/lib/Mail/SpamAssassin/Util.pm
   spamassassin/trunk/spamassassin.raw
Log:
bug 3621: add SpamCop reporting feature


Modified: spamassassin/trunk/INSTALL
==============================================================================
--- spamassassin/trunk/INSTALL	(original)
+++ spamassassin/trunk/INSTALL	Thu Jul 22 20:33:38 2004
@@ -230,8 +230,14 @@
 
   - Net::DNS        (from CPAN)
 
-    Used to check the RBL, RSS, DUL etc. and perform MX checks.
-    Recommended.
+    Used for all DNS-based tests (SBL, XBL, SpamCop, DSBL, etc.),
+    perform MX checks, and is also used when manually reporting spam to
+    SpamCop.  Recommended.
+
+
+  - Net::SMTP        (from CPAN)
+
+    Used when manually reporting spam to SpamCop.
 
 
   - Mail::SPF::Query     (from CPAN)

Modified: spamassassin/trunk/lib/Mail/SpamAssassin.pm
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin.pm	(original)
+++ spamassassin/trunk/lib/Mail/SpamAssassin.pm	Thu Jul 22 20:33:38 2004
@@ -615,9 +615,10 @@
 
 =item $f->report_as_spam ($mail, $options)
 
-Report a mail, encapsulated in a C<Mail::SpamAssassin::Message> object, as human-verified spam.
-This will submit the mail message to live, collaborative, spam-blocker
-databases, allowing other users to block this message.
+Report a mail, encapsulated in a C<Mail::SpamAssassin::Message> object, as
+human-verified spam.  This will submit the mail message to live,
+collaborative, spam-blocker databases, allowing other users to block this
+message.
 
 It will also submit the mail to SpamAssassin's Bayesian learner.
 
@@ -626,20 +627,21 @@
 
 =over 4
 
-=item dont_report_to_razor
-
-Inhibits reporting of the spam to Razor; useful if you know it's already
-been listed there.
-
 =item dont_report_to_dcc
 
-Inhibits reporting of the spam to DCC; useful if you know it's already
-been listed there.
+Inhibits reporting of the spam to DCC.
 
 =item dont_report_to_pyzor
 
-Inhibits reporting of the spam to Pyzor; useful if you know it's already
-been listed there.
+Inhibits reporting of the spam to Pyzor.
+
+=item dont_report_to_razor
+
+Inhibits reporting of the spam to Razor.
+
+=item dont_report_to_spamcop
+
+Inhibits reporting of the spam to SpamCop.
 
 =back
 
@@ -665,9 +667,10 @@
 
 =item $f->revoke_as_spam ($mail, $options)
 
-Revoke a mail, encapsulated in a C<Mail::SpamAssassin::Message> object, as human-verified ham
-(non-spam).  This will revoke the mail message from live, collaborative,
-spam-blocker databases, allowing other users to block this message.
+Revoke a mail, encapsulated in a C<Mail::SpamAssassin::Message> object, as
+human-verified ham (non-spam).  This will revoke the mail message from live,
+collaborative, spam-blocker databases, allowing other users to block this
+message.
 
 It will also submit the mail to SpamAssassin's Bayesian learner as nonspam.
 

Modified: spamassassin/trunk/lib/Mail/SpamAssassin/Conf.pm
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/Conf.pm	(original)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Conf.pm	Thu Jul 22 20:33:38 2004
@@ -1067,6 +1067,48 @@
     }
   });
 
+=item spamcop_from_address add@ress.com   (default: none)
+
+This address is used during manual reports to SpamCop as the From:
+address.  You can use your normal email address.  If this is not set, a
+guess will be used as the From: address in SpamCop reports.
+
+=cut
+
+  push (@cmds, {
+    setting => 'spamcop_from_address',
+    default => '',
+    type => $CONF_TYPE_STRING,
+    code => sub {
+      my ($self, $key, $value, $line) = @_;
+      if ($value =~ /([^<\s]+\@[^>\s]+)/) {
+        $self->{spamcop_from_address} = $1;
+      }
+    },
+  });
+
+=item spamcop_to_address add@ress.com   (default: generic reporting address)
+
+Your customized SpamCop report submission address.  You need to obtain
+this address by registering at C<http://www.spamcop.net/>.  If this is
+not set, SpamCop reports will go to a generic reporting address for
+SpamAssassin users and your reports will probably have less weight in
+the SpamCop system.
+
+=cut
+
+  push (@cmds, {
+    setting => 'spamcop_to_address',
+    default => 'spamassassin-submit@spam.spamcop.net',
+    type => $CONF_TYPE_STRING,
+    code => sub {
+      my ($self, $key, $value, $line) = @_;
+      if ($value =~ /([^<\s]+\@[^>\s]+)/) {
+        $self->{spamcop_to_address} = $1;
+      }
+    },
+  });
+
 =item trusted_networks ip.add.re.ss[/mask] ...   (default: none)
 
 What networks or hosts are 'trusted' in your setup.   B<Trusted> in this case

Modified: spamassassin/trunk/lib/Mail/SpamAssassin/Reporter.pm
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/Reporter.pm	(original)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Reporter.pm	Thu Jul 22 20:33:38 2004
@@ -22,6 +22,8 @@
 use bytes;
 use Carp;
 use POSIX ":sys_wait_h";
+use constant HAS_NET_DNS => eval { require Net::DNS; };
+use constant HAS_NET_SMTP => eval { require Net::SMTP; };
 
 use vars qw{
   @ISA $VERSION
@@ -58,16 +60,6 @@
 
   my $text = $self->{main}->remove_spamassassin_markup ($self->{msg});
 
-  if (!$self->{options}->{dont_report_to_razor} && $self->is_razor_available()) {
-    if ($self->razor_report($text)) {
-      $available = 1;
-      dbg ("SpamAssassin: spam reported to Razor.");
-      $return = 0;
-    }
-    else {
-      dbg ("SpamAssassin: could not report spam to Razor.");
-    }
-  }
   if (!$self->{options}->{dont_report_to_dcc} && $self->is_dcc_available()) {
     if ($self->dcc_report($text)) {
       $available = 1;
@@ -88,6 +80,26 @@
       dbg ("SpamAssassin: could not report spam to Pyzor.");
     }
   }
+  if (!$self->{options}->{dont_report_to_razor} && $self->is_razor_available()) {
+    if ($self->razor_report($text)) {
+      $available = 1;
+      dbg ("SpamAssassin: spam reported to Razor.");
+      $return = 0;
+    }
+    else {
+      dbg ("SpamAssassin: could not report spam to Razor.");
+    }
+  }
+  if (!$self->{options}->{dont_report_to_spamcop} && $self->is_spamcop_available()) {
+    if ($self->spamcop_report($text)) {
+      $available = 1;
+      dbg ("SpamAssassin: spam reported to SpamCop.");
+      $return = 0;
+    }
+    else {
+      dbg ("SpamAssassin: could not report spam to SpamCop.");
+    }
+  }
 
   $self->delete_fulltext_tmpfile();
 
@@ -364,6 +376,122 @@
 
   return 1;
 }
+
+sub smtp_dbg {
+  my ($command, $smtp) = @_;
+
+  dbg("SpamCop -> sent $command");
+  my $code = $smtp->code();
+  my $message = $smtp->message();
+  my $debug;
+  $debug .= $code if $code;
+  $debug .= ($code ? " " : "") . $message if $message;
+  chomp $debug;
+  dbg("SpamCop -> received $debug");
+  return 1;
+}
+
+sub spamcop_report {
+  my ($self, $original) = @_;
+
+  # check date
+  my $header = $original;
+  $header =~ s/\r?\n\r?\n.*//s;
+  my $date = Mail::SpamAssassin::Util::receive_date($header);
+  if ($date && $date < time - 3*86400) {
+    warn ("SpamCop -> message older than 3 days, not reporting\n");
+    return 0;
+  }
+
+  # message variables
+  my $boundary = "----------=_" . sprintf("%08X.%08X",time,int(rand(2**32)));
+  while ($original =~ /^\Q${boundary}\E$/m) {
+    $boundary .= "/".sprintf("%08X",int(rand(2**32)));
+  }
+  my $description = "spam report via " . Mail::SpamAssassin::Version();
+  my $trusted = $self->{msg}->{metadata}->{relays_trusted_str};
+  my $untrusted = $self->{msg}->{metadata}->{relays_untrusted_str};
+  my $user = $self->{main}->{'username'} || 'unknown';
+  my $host = Mail::SpamAssassin::Util::fq_hostname() || 'unknown';
+  my $from = $self->{conf}->{spamcop_from_address} || "$user\@$host";
+  my $name = (Mail::SpamAssassin::Util::portable_getpwuid($>))[6] || "Unknown";
+
+  # message data
+  my %head = (
+	      'To' => $self->{conf}->{spamcop_to_address},
+	      'From' => "\"$name\" <$from>",
+	      'Subject' => 'report spam',
+	      'Date' => Mail::SpamAssassin::Util::time_to_rfc822_date(),
+	      'Message-Id' =>
+		sprintf("<%08X.%08X@%s>",time,int(rand(2**32)),$host),
+	      'MIME-Version' => '1.0',
+	      'Content-Type' => "multipart/mixed; boundary=\"$boundary\"",
+	      );
+
+  # truncate message
+  if (length($original) > 64*1024) {
+    substr($original,(64*1024)) = "\n[truncated by SpamAssassin]\n";
+  }
+
+  my $body = <<"EOM";
+This is a multi-part message in MIME format.
+
+--$boundary
+Content-Type: message/rfc822; x-spam-type=report
+Content-Description: $description
+Content-Disposition: attachment
+Content-Transfer-Encoding: 8bit
+X-Spam-Relays-Trusted: $trusted
+X-Spam-Relays-Untrusted: $untrusted
+
+$original
+--$boundary--
+
+EOM
+
+  # compose message
+  my $message;
+  while (my ($k, $v) = each %head) {
+    $message .= "$k: $v\n";
+  }
+  $message .= "\n" . $body;
+
+  # send message
+  my $failure;
+  my $mx = $head{To};
+  my $hello = Mail::SpamAssassin::Util::fq_hostname() || $from;
+  $mx =~ s/.*\@//;
+  $hello =~ s/.*\@//;
+  for my $rr (Net::DNS::mx($mx)) {
+    my $exchange = Mail::SpamAssassin::Util::untaint_hostname($rr->exchange);
+    next unless $exchange;
+    my $smtp;
+    if ($smtp = Net::SMTP->new($exchange,
+			       Hello => $hello,
+			       Port => 25, # change to 587 before 3.0.0-final
+			       Timeout => 10))
+    {
+      if ($smtp->mail($from) && smtp_dbg("FROM $from", $smtp) &&
+	  $smtp->recipient($head{To}) && smtp_dbg("TO $head{To}", $smtp) &&
+	  $smtp->data($message) && smtp_dbg("DATA", $smtp) &&
+	  $smtp->quit() && smtp_dbg("QUIT", $smtp))
+      {
+	# tell user we succeeded after first attempt if we previously failed
+	warn("SpamCop -> report to $exchange succeeded\n") if defined $failure;
+	return 1;
+      }
+      my $code = $smtp->code();
+      my $text = $smtp->message();
+      $failure = "$code $text" if ($code && $text);
+    }
+    $failure ||= "Net::SMTP error";
+    chomp $failure;
+    warn("SpamCop -> report to $exchange failed: $failure\n");
+  }
+
+  return 0;
+}
+
 ###########################################################################
 
 sub dbg { Mail::SpamAssassin::dbg (@_); }
@@ -371,10 +499,20 @@
 sub delete_fulltext_tmpfile { Mail::SpamAssassin::PerMsgStatus::delete_fulltext_tmpfile(@_) }
 
 # Use the Dns versions ...  At least something only needs 1 copy of code ...
-sub is_pyzor_available { Mail::SpamAssassin::PerMsgStatus::is_pyzor_available(@_); }
-sub is_dcc_available { Mail::SpamAssassin::PerMsgStatus::is_dcc_available(@_); }
+sub is_dcc_available {
+  Mail::SpamAssassin::PerMsgStatus::is_dcc_available(@_);
+}
+sub is_pyzor_available {
+  Mail::SpamAssassin::PerMsgStatus::is_pyzor_available(@_);
+}
 sub is_razor_available {
   Mail::SpamAssassin::PerMsgStatus::is_razor2_available(@_);
+}
+sub is_spamcop_available {
+  my ($self) = @_;
+  return (HAS_NET_DNS &&
+	  HAS_NET_SMTP &&
+	  $self->{conf}{scores}{'RCVD_IN_BL_SPAMCOP_NET'});
 }
 
 sub enter_helper_run_mode { Mail::SpamAssassin::PerMsgStatus::enter_helper_run_mode(@_); }

Modified: spamassassin/trunk/lib/Mail/SpamAssassin/Util.pm
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/Util.pm	(original)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Util.pm	Thu Jul 22 20:33:38 2004
@@ -194,6 +194,25 @@
   }
 }
 
+sub untaint_hostname {
+  my ($host) = @_;
+
+  return unless defined($host);
+  return '' if ($host eq '');
+
+  # from RFC 1035, but allowing domains starting with numbers
+  my $label = q/[A-Za-z\d](?:[A-Za-z\d-]{0,61}[A-Za-z\d])?/;
+  my $domain = qq<$label(?:\.$label)*>;
+
+  if (length($host) <= 255 && $host =~ /^($domain)$/) {
+    return $1;
+  }
+  else {
+    warn "security: cannot untaint hostname: \"$host\"\n";
+    return $host;
+  }
+}
+
 # This sub takes a scalar or a reference to an array, hash, scalar or another
 # reference and recursively untaints all its values (and keys if it's a
 # reference to a hash). It should be used with caution as blindly untainting
@@ -438,6 +457,20 @@
 
   s/\=\r?\n//gs;
   s/\=([0-9a-fA-F]{2})/chr(hex($1))/ge;
+  return $_;
+}
+
+sub base64_encode {
+  local $_ = shift;
+
+  if (HAS_MIME_BASE64) {
+    return MIME::Base64::encode_base64($_);
+  }
+
+  $_ = pack("u57", $_);
+  s/^.//mg;
+  tr| -_`|A-Za-z0-9+/A|;
+  s/(A+)$/'=' x length $1/e;
   return $_;
 }
 

Modified: spamassassin/trunk/spamassassin.raw
==============================================================================
--- spamassassin/trunk/spamassassin.raw	(original)
+++ spamassassin/trunk/spamassassin.raw	Thu Jul 22 20:33:38 2004
@@ -515,39 +515,42 @@
 
 =item B<-r>, B<--report>
 
-Report this message as verified spam.  This will submit the mail message
-read from STDIN to various spam-blocker databases.  Currently, these are
-Vipul's Razor ( http://razor.sourceforge.net/ ), the Distributed Checksum
-Clearinghouse ( http://www.rhyolite.com/anti-spam/dcc/ ), and Pyzor (
-http://pyzor.sourceforge.net/ ).
-
-If the message contains SpamAssassin markup, this will be stripped out
-automatically before submission.  The support modules for DCC, Razor
-and/or Pyzor must be installed for spam to be reported to each service.
+Report this message as manually-verified spam.  This will submit the mail
+message read from STDIN to various spam-blocker databases.  Currently,
+these are the Distributed Checksum Clearinghouse
+C<http://www.rhyolite.com/anti-spam/dcc/>, Pyzor
+C<http://pyzor.sourceforge.net/>, Vipul's Razor
+C<http://razor.sourceforge.net/>, and SpamCop C<http://www.spamcop.net/>.
+
+If the message contains SpamAssassin markup, the markup will be stripped
+out automatically before submission.  The support modules for DCC, Pyzor,
+and Razor must be installed for spam to be reported to each service.
+SpamCop reports will have greater effect if you register and set the
+C<spamcop_submission_address> option.
 
 The message will also be submitted to SpamAssassin's learning systems;
-currently this is the internal Bayesian statistical-filtering system (the BAYES
-rules).   (Note that if you I<only> want to perform statistical learning, and
-do not want to report mail to a third-party server, you should use the
-C<sa-learn> command directly instead.)
+currently this is the internal Bayesian statistical-filtering system (the
+BAYES rules).  (Note that if you I<only> want to perform statistical
+learning, and do not want to report mail to third-parties, you should use
+the C<sa-learn> command directly instead.)
 
 =item B<-k>, B<--revoke>
 
 Revoke this message.  This will revoke the mail message read from STDIN from
 various spam-blocker databases.  Currently, these are Vipul's Razor.
 
-Revocation support for the Distributed Checksum Clearinghouse, and Pyzor
-is not currently available.
+Revocation support for the Distributed Checksum Clearinghouse, Pyzor, and
+SpamCop is not currently available.
 
-If the message contains SpamAssassin markup, this will be stripped out
-automatically before submission.  The support modules for Razor must be
-installed for spam to be revoked from the service.
+If the message contains SpamAssassin markup, the markup will be stripped
+out automatically before submission.  The support modules for Razor must
+be installed for spam to be revoked from the service.
 
 The message will also be submitted as 'ham' (non-spam) to SpamAssassin's
-learning systems; currently this is the internal Bayesian statistical-filtering
-system (the BAYES rules).   (Note that if you I<only> want to perform
-statistical learning, and do not want to report mail to a third-party server,
-you should use the C<sa-learn> command directly instead.)
+learning systems; currently this is the internal Bayesian
+statistical-filtering system (the BAYES rules).  (Note that if you I<only>
+want to perform statistical learning, and do not want to report mail to
+third-parties, you should use the C<sa-learn> command directly instead.)
 
 =item B<--lint>