develooper Front page | perl.cvs.qpsmtpd | Postings from January 2007

[svn:qpsmtpd] r705 - contrib/hjp/spamassassin_rcpt

From:
hjp
Date:
January 11, 2007 13:13
Subject:
[svn:qpsmtpd] r705 - contrib/hjp/spamassassin_rcpt
Message ID:
20070111221312.C086BCBA1B@x12.develooper.com
Author: hjp
Date: Thu Jan 11 14:13:10 2007
New Revision: 705

Added:
   contrib/hjp/spamassassin_rcpt/Makefile
   contrib/hjp/spamassassin_rcpt/Makerules
   contrib/hjp/spamassassin_rcpt/qpsmtpd-plugin-spamassassin_rcpt.spec
   contrib/hjp/spamassassin_rcpt/spamassassin_rcpt

Log:
Added spamassassin_rcpt (spamassassin with per-rcpt config) plugin.


Added: contrib/hjp/spamassassin_rcpt/Makefile
==============================================================================
--- (empty file)
+++ contrib/hjp/spamassassin_rcpt/Makefile	Thu Jan 11 14:13:10 2007
@@ -0,0 +1,13 @@
+NAME=spamassassin_rcpt
+FILES = $(PKG).spec \
+    Makefile \
+    Makerules \
+    $(NAME) \
+
+
+all:
+
+clean:
+	rm -f $(PKG).tar.gz *.tmp
+
+include Makerules

Added: contrib/hjp/spamassassin_rcpt/Makerules
==============================================================================
--- (empty file)
+++ contrib/hjp/spamassassin_rcpt/Makerules	Thu Jan 11 14:13:10 2007
@@ -0,0 +1,15 @@
+PKG = qpsmtpd-plugin-$(NAME)
+CONTRIB_BASE=~/wrk/qpsmtpd/contrib/hjp
+CONTRIB_FILES=$(patsubst %, $(CONTRIB_BASE)/$(NAME)/%, $(FILES))
+
+contrib: $(CONTRIB_FILES)
+
+$(CONTRIB_BASE)/$(NAME)/%: %
+	cp -p $^ $@
+
+rpm: $(PKG).tar.gz
+	rpm -ta --clean --sign --rmsource $^
+
+$(PKG).tar.gz: $(FILES)
+	tar cfz $@ $^
+

Added: contrib/hjp/spamassassin_rcpt/qpsmtpd-plugin-spamassassin_rcpt.spec
==============================================================================
--- (empty file)
+++ contrib/hjp/spamassassin_rcpt/qpsmtpd-plugin-spamassassin_rcpt.spec	Thu Jan 11 14:13:10 2007
@@ -0,0 +1,59 @@
+Name: qpsmtpd-plugin-spamassassin_rcpt
+Version: 238
+Release: 1
+Packager: hjp@hjp.at
+Summary: SpamAssassin integration for qpsmtpd (with per-user thresholds)
+License: distributable
+Group: System Environment/Daemons
+URL: http://smtpd.develooper.com/
+BuildRoot: %{_tmppath}/%{name}-root
+Source0: %{name}.tar.gz
+BuildArch: noarch
+Requires: qpsmtpd-plugin-cf_wrapper >= 173
+Requires: qpsmtpd-plugin-address_notes
+
+%description
+Plugin that checks if the mail is spam by using the "spamd" daemon
+from the SpamAssassin package.  F<http://www.spamassassin.org>
+
+This plugin differs from the one in the qpsmtpd package in that it
+allows different configurations for different users. One message can be
+rejected for one user and accepted for another. To achieve this it
+relies on mechanisms implemented by the cf_wrapper and address_notes
+plugins.
+
+%prep
+%setup -q -c %{name}
+
+%build
+
+%clean
+rm -rf $RPM_BUILD_ROOT
+
+%install
+rm -rf $RPM_BUILD_ROOT
+mkdir -p $RPM_BUILD_ROOT/usr/share/qpsmtpd/plugins
+cp spamassassin_rcpt $RPM_BUILD_ROOT/usr/share/qpsmtpd/plugins
+
+[ -x /usr/lib/rpm/brp-compress ] && /usr/lib/rpm/brp-compress
+
+
+%files
+%defattr(-,root,root)
+/usr/share/qpsmtpd/plugins/spamassassin_rcpt
+
+%changelog
+* Fri Nov 24 2006 <hjp@hjp.at> 238-1
+- fixed suppression of multiple identical X-Spam-Status headers
+
+* Thu Nov 23 2006 <hjp@hjp.at> 237-1
+- made subject prefix configurable
+- suppress multiple identical X-Spam-Status headers
+
+* Sat Sep 02 2006 <hjp@hjp.at> 214-1
+- updated documentation
+- fixed bug where cf_wrapper_results wasn't set.
+
+* Sat Aug 05 2006 <hjp@hjp.at> 197-1
+- First RPM package.
+

Added: contrib/hjp/spamassassin_rcpt/spamassassin_rcpt
==============================================================================
--- (empty file)
+++ contrib/hjp/spamassassin_rcpt/spamassassin_rcpt	Thu Jan 11 14:13:10 2007
@@ -0,0 +1,367 @@
+#!/usr/bin/perl
+=head1 NAME
+
+spamassassin_rcpt - SpamAssassin integration for qpsmtpd with per-recipient config
+
+=head1 DESCRIPTION
+
+Plugin that checks if the mail is spam by using the "spamd" daemon
+from the SpamAssassin package.  F<http://www.spamassassin.org>
+Unlike the spamassassin plugin in the core distribution, this plugin allows
+per-recipient configuration.
+
+SpamAssassin 2.6 or newer is required.
+
+=head1 CONFIG
+
+Configured in the plugins file without any parameters, the
+spamassassin plugin will add relevant headers from the spamd
+(X-Spam-Status etc).
+
+The format goes like
+
+  spamassassin  option value  [option value]
+
+Options being those listed below and the values being parameters to
+the options.  Confused yet?  :-)
+
+=over 4
+
+=item reject_threshold [threshold]
+
+Set the threshold over which the plugin will reject the mail.  Some
+mail servers are so useless that they ignore 55x responses not coming
+after RCPT TO, so they might just keep retrying and retrying and
+retrying until the mail expires from their queue. 
+
+I like to configure this with 15 or 20 as the threshold.  
+
+The default is to never reject mail based on the SpamAssassin score.
+
+=item munge_subject_threshold [threshold]
+
+Set the threshold over which we will prefix the subject with
+'*****SPAM*****'.  A messed up subject is easier to filter on than the
+other headers for many people with not so clever mail clients.
+
+The default is to never munge the subject based on the SpamAssassin score.
+
+=item spamd_socket [/path/to/socket]
+
+Beginning with Mail::SpamAssassin 2.60, it is possible to use Unix 
+domain sockets for spamd.  This is faster and more secure than using
+a TCP connection.
+
+=item leave_old_headers [drop|rename|keep]
+
+Another mail server before might have checked this mail already and may have
+added X-Spam-Status, X-Spam-Flag and X-Spam-Check-By lines. Normally you can
+not trust such headers and should either rename them to X-Old-... (default,
+parameter 'rename') or have them removed (parameter 'drop'). If you know
+what you are doing, you can also leave them intact (parameter 'keep').
+
+=item subject_prefix [string]
+
+The string to insert into the subject if
+spamassassin_munge_subject_threshold is exceeded. This is currently a
+global option, because for multi-recipient mails the subject is
+rewritten at most once.
+
+=back
+
+With both of the first options the configuration line will look like the following
+
+ spamasssasin  reject_threshold 18  munge_subject_threshold 8
+
+=head2 Per-Recipient Configuration
+
+The following address notes are recognized by this plugin
+
+=over
+
+=item spamassassin_reject_threshold [threshold]
+
+Overrides the reject_threshold for this recipient. If there are several 
+recipients and the mail should be rejected for some and accepted for the
+others a temporary failure is generated and the cf_wrapper plugin is used
+to weed out the recipients at the next delivery attempt. Ths may cause 
+some mail to be delayed.
+
+=item spamassassin_munge_subject_threshold [threshold]
+
+Overrides the munge_subject_threshold for this recipient. If there are several
+recipients with different munge_subject_thresholds, the lowest one will be
+used. It is not possible to pass on a mail to several recipients with different
+munging. Either the subject is munged for all of them or none.
+
+=item spamassassin_user [username]
+
+The user to run as. This username will be passed to spamd and spamd will
+read the configuration files in the user's home directory. This allows 
+the user e.g., so set different scores for some rules and to use their 
+bayesian filter database. It does not allow the use of custom rules. 
+See the spamd documentation for details.
+
+=back
+
+=head1 DEPENDENCIES
+
+This plugin depends on several other plugins:
+
+=over
+
+=item cf_wrapper
+
+The cf_wrapper plugin implements a framework for filters which hook into 
+data_post to reject messages for only some recipients (which SMTP isn't
+designed for).
+
+=item address_notes
+
+The address_notes plugin extends the class Qpsmtpd::Address with a method 
+notes, which works similarly to connection and transaction notes. This
+allows passing around notes on recipients.
+
+=item address_notes_aliases
+
+Finally you need a plugin to set those address notes which spamassassin_rcpt queries.
+At the time of this writing the only one to do this is address_notes_aliases,
+which in turn needs the aliases_check plugin to parse a config file.
+Feel free to write one which gets the data from LDAP or an SQL database instead!
+
+=back
+
+In the plugins configuration file, the spamassassin_rcpt plugin needs to be after
+the address_notes_aliases (or equivalent) plugin but before the cf_wrapper plugin.
+
+=head1 TODO
+
+Make the "subject munge string" configurable
+
+Implement autolearning.
+
+=head1 COPYRIGHT AND LICENSE
+
+Copyright (c) 2006 Peter J. Holzer <hjp@hjp.at>. 
+
+This plugin is licensed under the same terms as the qpsmtpd package
+itself.
+Please see the LICENSE file included with qpsmtpd for details.
+
+=cut
+
+
+use Qpsmtpd::DSN;
+use Socket qw(:DEFAULT :crlf);
+use IO::Handle;
+
+sub register {
+  my ($self, $qp, @args) = @_;
+
+  $self->log(LOGERROR, "Bad parameters for the spamassassin plugin")
+    if @_ % 2;
+
+  %{$self->{_args}} = @args;
+
+}
+
+sub hook_data_post {
+  my ($self, $transaction) = @_;
+
+  $self->log(LOGDEBUG, "check_spam");
+  return (DECLINED) if $transaction->body_size > 500_000;
+
+
+  for ($transaction->recipients()) {
+    $self->set_result($transaction, $_, \&check_spam2);
+  }
+
+  return (DECLINED);
+}
+
+sub set_result {
+    my ($self, $transaction, $rcpt, $test) = @_;
+    my $results = $transaction->notes('cf_wrapper_results');
+    my $r = $rcpt->address;
+    my $rc = defined $results->{$r}
+	      ? (ref $results->{$r} eq "ARRAY"
+		 ? $results->{$r}[0] 
+		 : $results->{$r}
+		)
+	      : DECLINED;
+    if ($rc == DECLINED) {
+	my $msg;
+	($rc, $msg) = $self->$test($transaction, $rcpt);
+	$self->log(LOGINFO, "setting result for $r to $rc");
+	$results->{$r} = [$rc, $msg];
+    }
+    $transaction->notes('cf_wrapper_results', $results);
+}
+
+sub check_spam {
+  my ($self, $transaction, $username) = @_;
+
+  my $cache = $transaction->notes('spamassassin_cache');
+  unless ($cache) {
+    $cache = {};
+    $transaction->notes('spamassassin_cache', $cache);
+  }
+  $self->log(LOGDEBUG, "check_spam");
+  return @{ $cache->{$username} } if $cache->{$username};
+
+  my $remote  = 'localhost';
+  my $port    = 783;
+  if ($port =~ /\D/) { $port = getservbyname($port, 'tcp') }
+  die "No port" unless $port;
+  my $iaddr   = inet_aton($remote) or 
+    $self->log(LOGERROR, "Could not resolve host: $remote") and return ();
+  my $paddr   = sockaddr_in($port, $iaddr);
+
+  my $proto   = getprotobyname('tcp');
+  if ($self->{_args}->{spamd_socket} and
+      $self->{_args}->{spamd_socket} =~ /^([\w\/.-]+)$/ ) { # connect to Unix Domain Socket
+    my $spamd_socket = $1;
+    
+    socket(SPAMD, PF_UNIX, SOCK_STREAM, 0)
+      or $self->log(LOGERROR, "Could not open socket: $!") and return ();
+
+    $paddr = sockaddr_un($spamd_socket); 
+  }
+  else {
+    socket(SPAMD, PF_INET, SOCK_STREAM, $proto)
+      or $self->log(LOGERROR, "Could not open socket: $!") and return ();
+  }
+
+  connect(SPAMD, $paddr) 
+    or $self->log(LOGERROR, "Could not connect to spamassassin daemon: $!") and return ();
+  $self->log(LOGDEBUG, "check_spam: connected to spamd");
+
+  SPAMD->autoflush(1);
+  
+  $transaction->body_resetpos;
+
+  print SPAMD "SYMBOLS SPAMC/1.3" . CRLF;
+  print SPAMD "User: $username" . CRLF;
+       # Content-Length: 
+  print SPAMD  CRLF;
+  # or CHECK or REPORT or SYMBOLS
+
+  print SPAMD "X-Envelope-From: ", $transaction->sender->format, CRLF
+    or $self->log(LOGWARN, "Could not print to spamd: $!");
+
+  print SPAMD join CRLF, split /\n/, $transaction->header->as_string
+    or $self->log(LOGWARN, "Could not print to spamd: $!");
+
+  print SPAMD CRLF
+    or $self->log(LOGWARN, "Could not print to spamd: $!");
+
+  while (my $line = $transaction->body_getline) {
+    chomp $line;
+    print SPAMD $line, CRLF
+      or $self->log(LOGWARN, "Could not print to spamd: $!");
+  }
+
+  print SPAMD CRLF;
+  shutdown(SPAMD, 1);
+  $self->log(LOGDEBUG, "check_spam: finished sending to spamd");
+  my $line0 = <SPAMD>; # get the first protocol lines out
+  if ($line0) {
+    $self->log(LOGDEBUG, "check_spam: spamd: $line0");
+  }
+
+  my ($flag, $hits, $required);
+  while (<SPAMD>) {
+    $self->log(LOGDEBUG, "check_spam: spamd: $_");
+    last unless m/\S/;
+    if (m{Spam: (True|False) ; (-?\d+\.\d) / (-?\d+\.\d)}) {
+	($flag, $hits, $required) = ($1, $2, $3);
+    }
+
+  }
+  my $tests = <SPAMD>;
+  $tests =~ s/\015//;  # hack for outlook
+  $flag = $flag eq 'True' ? 'Yes' : 'No';
+  $self->log(LOGDEBUG, "check_spam: finished reading from spamd");
+
+  $self->log(LOGNOTICE, "check_spam: $flag, hits=$hits, required=$required, " .
+			     "tests=$tests");
+
+  $cache->{$username} = [ $flag, $hits, $required, $tests ];
+  return ($flag, $hits, $required, $tests);
+}
+
+
+sub check_spam2 {
+  my ($self, $transaction, $rcpt) = @_;
+  $self->log(LOGDEBUG, "check_spam2: rcpt = $rcpt. rcpt is a " . ref($rcpt));
+  my $username = $rcpt->notes('spamassassin_user') || getpwuid($>);
+  my ($flag, $hits, $required, $tests) = $self->check_spam($transaction, $username);
+  my $reject_threshold = $rcpt->notes('spamassassin_reject_threshold') || $self->{_args}->{reject_threshold} || 99;
+  $self->log(LOGDEBUG, "check_spam2: reject_threshold = $reject_threshold");
+  if ($hits > $reject_threshold) {
+    return Qpsmtpd::DSN->sec_sender_unauthorized("spamassassin score $hits/$reject_threshold");
+  } else {
+    my $munge_subject_threshold = $rcpt->notes('spamassassin_munge_subject_threshold') || $self->{_args}->{munge_subject_threshold} || 99;
+    $self->log(LOGDEBUG, "check_spam2: munge_subject_threshold = $munge_subject_threshold");
+    $self->munge_headers($transaction, $flag, $hits, $required, $tests, $munge_subject_threshold); # XXX
+    return (DECLINED, "spamassassin score $hits/$reject_threshold");
+  }
+
+}
+
+sub munge_headers {
+  my ($self, $transaction, $flag, $hits, $required, $tests, $munge_subject_threshold) = @_;
+  my $leave_old_headers = lc($self->{_args}->{leave_old_headers}) || 'rename';
+  unless ($transaction->notes('spamassassin_cleaned_headers')) {
+    if ( $leave_old_headers eq 'rename' )
+    {
+      foreach my $header ( $transaction->header->get('X-Spam-Check-By') )
+      {
+        $transaction->header->add('X-Old-Spam-Check-By', $header, 0);
+      }
+      foreach my $header ( $transaction->header->get('X-Spam-Flag') )
+      {
+        $transaction->header->add('X-Old-Spam-Flag', $header, 0);
+      }
+
+      foreach my $header ( $transaction->header->get('X-Spam-Status') )
+      {
+        $transaction->header->add('X-Old-Spam-Status', $header, 0);
+      }
+    }
+      
+    if ( $leave_old_headers eq 'drop' || $leave_old_headers eq 'rename' )
+    {
+      $transaction->header->delete('X-Spam-Check-By');
+      $transaction->header->delete('X-Spam-Flag');
+      $transaction->header->delete('X-Spam-Status');
+    }
+
+    $transaction->header->add("X-Spam-Check-By", $self->qp->config('me'), 0);
+
+    $transaction->notes('spamassassin_cleaned_headers', 1);
+  }
+
+  if ($flag eq 'Yes' && !$transaction->notes('spamassassin_flag_yes')) {
+    $transaction->header->add('X-Spam-Flag', 'YES', 0);
+    $transaction->notes('spamassassin_flag_yes', 1);
+  }
+  if ($hits >= $munge_subject_threshold && !$transaction->notes('spamassassin_munged_subject')) {
+    my $subject = $transaction->header->get('Subject') || '';
+    my $subject_prefix = $self->{_args}->{subject_prefix} || '*****SPAM*****';
+    $transaction->header->replace('Subject', "$subject_prefix $subject");
+
+    $transaction->notes('spamassassin_munged_subject', 1);
+  }
+  my $spam_status = "$flag, hits=$hits required=$required\n\ttests=$tests";
+  my $spam_status_seen = $transaction->notes('spamassassin_spam_status_seen');
+  unless($spam_status_seen) {
+    $spam_status_seen = {};
+    $transaction->notes('spamassassin_spam_status_seen', $spam_status_seen);
+  }
+  unless ($spam_status_seen->{$spam_status}) {
+    $transaction->header->add('X-Spam-Status', $spam_status, 0);
+    $spam_status_seen->{$spam_status} = 1;
+  }
+}
+# vim: sw=2 tw=0 expandtab



nntp.perl.org: Perl Programming lists via nntp and http.
Comments to Ask Bjørn Hansen at ask@perl.org | Group listing | About