| # NOTE: This interface is alpha at best, and almost guaranteed to change |
| # <@LICENSE> |
| # Licensed to the Apache Software Foundation (ASF) under one or more |
| # contributor license agreements. See the NOTICE file distributed with |
| # this work for additional information regarding copyright ownership. |
| # The ASF licenses this file to you under the Apache License, Version 2.0 |
| # (the "License"); you may not use this file except in compliance with |
| # the License. You may obtain a copy of the License at: |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| # </@LICENSE> |
| |
| =head1 NAME |
| |
| Mail::SpamAssassin::Client - Client for spamd Protocol |
| |
| NOTE: This interface is alpha at best, and almost guaranteed to change |
| |
| =head1 SYNOPSIS |
| |
| my $client = new Mail::SpamAssassin::Client({port => 783, |
| host => 'localhost', |
| username => 'someuser'}); |
| |
| if ($client->ping()) { |
| print "Ping is ok\n"; |
| } |
| |
| my $result = $client->process($testmsg); |
| |
| if ($result->{isspam} eq 'True') { |
| do something with spam message here |
| } |
| |
| =head1 DESCRIPTION |
| |
| Mail::SpamAssassin::Client is a module that provides a perl implementation for |
| the spamd protocol. |
| |
| =cut |
| |
| package Mail::SpamAssassin::Client; |
| |
| use IO::Socket; |
| |
| my $EOL = "\015\012"; |
| my $BLANK = $EOL x 2; |
| my $PROTOVERSION = 'SPAMC/1.3'; |
| |
| =head1 PUBLIC METHODS |
| |
| =head2 new |
| |
| public class (Mail::SpamAssassin::Client) new (\% $args) |
| |
| Description: |
| This method creates a new Mail::SpamAssassin::Client object. |
| |
| =cut |
| |
| sub new { |
| my ($class, $args) = @_; |
| |
| $class = ref($class) || $class; |
| |
| my $self = {}; |
| |
| # with a sockets_path set then it makes no sense to set host and port |
| if ($args->{socketpath}) { |
| $self->{socketpath} = $args->{socketpath}; |
| } |
| else { |
| $self->{port} = $args->{port}; |
| $self->{host} = $args->{host}; |
| } |
| |
| if ($args->{username}) { |
| $self->{username} = $args->{username}; |
| } |
| |
| bless($self, $class); |
| |
| $self; |
| } |
| |
| =head2 process |
| |
| public instance (\%) process (String $msg, Boolean $is_check_p) |
| |
| Description: |
| This method makes a call to the spamd server and depending on the value of |
| C<$is_check_p> either calls PROCESS or CHECK. |
| |
| The return value is a hash reference containing several pieces of information, |
| if available: |
| |
| content_length |
| |
| isspam |
| |
| score |
| |
| threshold |
| |
| message |
| |
| =cut |
| |
| sub process { |
| my ($self, $msg, $is_check_p) = @_; |
| |
| my %data; |
| |
| my $command = $is_check_p ? 'CHECK' : 'PROCESS'; |
| |
| $self->_clear_errors(); |
| |
| my $remote = $self->_create_connection(); |
| |
| return 0 unless ($remote); |
| |
| my $msgsize = length($msg.$EOL); |
| |
| print $remote "$command $PROTOVERSION$EOL"; |
| print $remote "Content-length: $msgsize$EOL"; |
| print $remote "User: $self->{username}$EOL" if ($self->{username}); |
| print $remote "$EOL"; |
| print $remote $msg; |
| print $remote "$EOL"; |
| |
| my $line = <$remote>; |
| return undef unless (defined $line); |
| |
| my ($version, $resp_code, $resp_msg) = $self->_parse_response_line($line); |
| |
| $self->{resp_code} = $resp_code; |
| $self->{resp_msg} = $resp_msg; |
| |
| return undef unless ($resp_code == 0); |
| |
| while ($line = <$remote>) { |
| if ($line =~ /Content-length: (\d+)/) { |
| $data{content_length} = $1; |
| } |
| elsif ($line =~ m!Spam: (\S+) ; (\S+) / (\S+)!) { |
| $data{isspam} = $1; |
| $data{score} = $2 + 0; |
| $data{threshold} = $3 + 0; |
| } |
| elsif ($line =~ /^${EOL}$/) { |
| last; |
| } |
| } |
| |
| my $return_msg; |
| while(<$remote>) { |
| $return_msg .= $_; |
| } |
| |
| $data{message} = $return_msg if ($return_msg); |
| |
| close $remote; |
| |
| return \%data; |
| } |
| |
| =head2 check |
| |
| public instance (\%) check (String $msg) |
| |
| Description: |
| The method implements the check call. |
| |
| Since check and process are so similar, we simply pass this |
| call along to the process method with a flag to indicate |
| to actually make the CHECK call. |
| |
| See the process method for the return value. |
| |
| =cut |
| |
| sub check { |
| my ($self, $msg) = @_; |
| |
| return $self->process($msg, 1); |
| } |
| |
| =head2 learn |
| |
| public instance (Boolean) learn (String $msg, Integer $learntype) |
| |
| Description: |
| This method implements the learn call. C<$learntype> should be |
| an integer, 0 for spam, 1 for ham and 2 for forget. The return |
| value is a boolean indicating if the message was learned or not. |
| |
| An undef return value indicates that there was an error and you |
| should check the resp_code/resp_msg values to determine what |
| the error was. |
| |
| =cut |
| |
| sub learn { |
| my ($self, $msg, $learntype) = @_; |
| |
| $self->_clear_errors(); |
| |
| my $remote = $self->_create_connection(); |
| |
| return undef unless ($remote); |
| |
| my $msgsize = length($msg.$EOL); |
| |
| print $remote "TELL $PROTOVERSION$EOL"; |
| print $remote "Content-length: $msgsize$EOL"; |
| print $remote "User: $self->{username}$EOL" if ($self->{username}); |
| |
| if ($learntype == 0) { |
| print $remote "Message-class: spam$EOL"; |
| print $remote "Set: local$EOL"; |
| } |
| elsif ($learntype == 1) { |
| print $remote "Message-class: ham$EOL"; |
| print $remote "Set: local$EOL"; |
| } |
| elsif ($learntype == 2) { |
| print $remote "Remove: local$EOL"; |
| } |
| else { # bad learntype |
| $self->{resp_code} = 00; |
| $self->{resp_msg} = 'do not know'; |
| return undef; |
| } |
| |
| print $remote "$EOL"; |
| print $remote $msg; |
| print $remote "$EOL"; |
| |
| my $line = <$remote>; |
| return undef unless (defined $line); |
| |
| my ($version, $resp_code, $resp_msg) = $self->_parse_response_line($line); |
| |
| $self->{resp_code} = $resp_code; |
| $self->{resp_msg} = $resp_msg; |
| |
| return undef unless ($resp_code == 0); |
| |
| my $did_set; |
| my $did_remove; |
| |
| while ($line = <$remote>) { |
| if ($line =~ /DidSet: (.*)/i) { |
| $did_set = $1; |
| } |
| elsif ($line =~ /DidRemove: (.*)/i) { |
| $did_remove = $1; |
| } |
| elsif ($line =~ /^${EOL}$/) { |
| last; |
| } |
| } |
| |
| close $remote; |
| |
| if ($learntype == 0 || $learntype == 1) { |
| return $did_set =~ /local/; |
| } |
| else { #safe since we've already checked the $learntype values |
| return $did_remove =~ /local/; |
| } |
| } |
| |
| =head2 report |
| |
| public instance (Boolean) report (String $msg) |
| |
| Description: |
| This method provides the report interface to spamd. |
| |
| =cut |
| |
| sub report { |
| my ($self, $msg) = @_; |
| |
| $self->_clear_errors(); |
| |
| my $remote = $self->_create_connection(); |
| |
| return undef unless ($remote); |
| |
| my $msgsize = length($msg.$EOL); |
| |
| print $remote "TELL $PROTOVERSION$EOL"; |
| print $remote "Content-length: $msgsize$EOL"; |
| print $remote "User: $self->{username}$EOL" if ($self->{username}); |
| print $remote "Message-class: spam$EOL"; |
| print $remote "Set: local,remote$EOL"; |
| print $remote "$EOL"; |
| print $remote $msg; |
| print $remote "$EOL"; |
| |
| my $line = <$remote>; |
| return undef unless (defined $line); |
| |
| my ($version, $resp_code, $resp_msg) = $self->_parse_response_line($line); |
| |
| $self->{resp_code} = $resp_code; |
| $self->{resp_msg} = $resp_msg; |
| |
| return undef unless ($resp_code == 0); |
| |
| my $reported_p = 0; |
| |
| while (($line = <$remote>)) { |
| if ($line =~ /DidSet:\s+.*remote/i) { |
| $reported_p = 1; |
| last; |
| } |
| elsif ($line =~ /^${EOL}$/) { |
| last; |
| } |
| } |
| |
| close $remote; |
| |
| return $reported_p; |
| } |
| |
| =head2 revoke |
| |
| public instance (Boolean) revoke (String $msg) |
| |
| Description: |
| This method provides the revoke interface to spamd. |
| |
| =cut |
| |
| sub revoke { |
| my ($self, $msg) = @_; |
| |
| $self->_clear_errors(); |
| |
| my $remote = $self->_create_connection(); |
| |
| return undef unless ($remote); |
| |
| my $msgsize = length($msg.$EOL); |
| |
| print $remote "TELL $PROTOVERSION$EOL"; |
| print $remote "Content-length: $msgsize$EOL"; |
| print $remote "User: $self->{username}$EOL" if ($self->{username}); |
| print $remote "Message-class: ham$EOL"; |
| print $remote "Set: local$EOL"; |
| print $remote "Remove: remote$EOL"; |
| print $remote "$EOL"; |
| print $remote $msg; |
| print $remote "$EOL"; |
| |
| my $line = <$remote>; |
| return undef unless (defined $line); |
| |
| my ($version, $resp_code, $resp_msg) = $self->_parse_response_line($line); |
| |
| $self->{resp_code} = $resp_code; |
| $self->{resp_msg} = $resp_msg; |
| |
| return undef unless ($resp_code == 0); |
| |
| my $revoked_p = 0; |
| |
| while (!$revoked_p && ($line = <$remote>)) { |
| if ($line =~ /DidRemove:\s+remote/i) { |
| $revoked_p = 1; |
| last; |
| } |
| elsif ($line =~ /^${EOL}$/) { |
| last; |
| } |
| } |
| |
| close $remote; |
| |
| return $revoked_p; |
| } |
| |
| |
| =head2 ping |
| |
| public instance (Boolean) ping () |
| |
| Description: |
| This method performs a server ping and returns 0 or 1 depending on |
| if the server responded correctly. |
| |
| =cut |
| |
| sub ping { |
| my ($self) = @_; |
| |
| my $remote = $self->_create_connection(); |
| |
| return 0 unless ($remote); |
| |
| print $remote "PING $PROTOVERSION$EOL"; |
| print $remote "$EOL"; |
| |
| my $line = <$remote>; |
| close $remote; |
| return undef unless (defined $line); |
| |
| my ($version, $resp_code, $resp_msg) = $self->_parse_response_line($line); |
| return 0 unless ($resp_msg eq 'PONG'); |
| |
| return 1; |
| } |
| |
| =head1 PRIVATE METHODS |
| |
| =head2 _create_connection |
| |
| private instance (IO::Socket) _create_connection () |
| |
| Description: |
| This method sets up a proper IO::Socket connection based on the arguments |
| used when greating the client object. |
| |
| On failure, it sets an internal error code and returns undef. |
| |
| =cut |
| |
| sub _create_connection { |
| my ($self) = @_; |
| |
| my $remote; |
| |
| if ($self->{socketpath}) { |
| $remote = IO::Socket::UNIX->new( Peer => $self->{socketpath}, |
| Type => SOCK_STREAM, |
| ); |
| } |
| else { |
| $remote = IO::Socket::INET->new( Proto => "tcp", |
| PeerAddr => $self->{host}, |
| PeerPort => $self->{port}, |
| ); |
| } |
| |
| unless ($remote) { |
| print "Failed to create connection to spamd daemon: $!\n"; |
| return undef; |
| } |
| |
| $remote; |
| } |
| |
| =head2 _parse_response_line |
| |
| private instance (@) _parse_response_line (String $line) |
| |
| Description: |
| This method parses the initial response line/header from the server |
| and returns its parts. |
| |
| We have this as a seperate method in case we ever decide to get fancy |
| with the response line. |
| |
| =cut |
| |
| sub _parse_response_line { |
| my ($self, $line) = @_; |
| |
| $line =~ s/\r?\n$//; |
| return split(/\s+/, $line, 3); |
| } |
| |
| =head2 _clear_errors |
| |
| private instance () _clear_errors () |
| |
| Description: |
| This method clears out any current errors. |
| |
| =cut |
| |
| sub _clear_errors { |
| my ($self) = @_; |
| |
| $self->{resp_code} = undef; |
| $self->{resp_msg} = undef; |
| } |
| |
| 1; |
| |