File Coverage

lib/Mail/SpamAssassin/Client.pm
Criterion Covered Total %
statement 138 212 65.0
branch 48 136 35.2
condition 10 38 26.3
subroutine 16 18 88.8
pod 8 8 100.0
total 220 412 53.4


line stmt bran cond sub pod time code
1             # <@LICENSE>
2             # Licensed to the Apache Software Foundation (ASF) under one or more
3             # contributor license agreements. See the NOTICE file distributed with
4             # this work for additional information regarding copyright ownership.
5             # The ASF licenses this file to you under the Apache License, Version 2.0
6             # (the "License"); you may not use this file except in compliance with
7             # the License. You may obtain a copy of the License at:
8             #
9             # http://www.apache.org/licenses/LICENSE-2.0
10             #
11             # Unless required by applicable law or agreed to in writing, software
12             # distributed under the License is distributed on an "AS IS" BASIS,
13             # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14             # See the License for the specific language governing permissions and
15             # limitations under the License.
16             # </@LICENSE>
17              
18             =head1 NAME
19              
20             Mail::SpamAssassin::Client - Client for spamd Protocol
21              
22             =head1 SYNOPSIS
23              
24             my $client = Mail::SpamAssassin::Client->new({
25             port => 783,
26             host => 'localhost',
27             username => 'someuser'});
28             or
29              
30             my $client = Mail::SpamAssassin::Client->new({
31             socketpath => '/path/to/socket',
32             username => 'someuser'});
33              
34             Optionally takes timeout, which is applied to IO::Socket for the
35             initial connection. If not supplied, it defaults to 30 seconds.
36              
37             if ($client->ping()) {
38             print "Ping is ok\n";
39             }
40              
41             my $result = $client->process($testmsg);
42              
43             if ($result->{isspam} eq 'True') {
44             do something with spam message here
45             }
46              
47             =head1 DESCRIPTION
48              
49             Mail::SpamAssassin::Client is a module which provides a perl implementation of
50             the spamd protocol.
51              
52             =cut
53              
54              
55             use strict;
56 1     1   7044054 use warnings;
  1         8  
  1         89  
57 1     1   23 use re 'taint';
  1         4  
  1         108  
58 1     1   18  
  1         2  
  1         104  
59             use IO::Socket;
60 1     1   10 use Errno qw(EBADF);
  1         7  
  1         52  
61 1     1   1621  
  1         2  
  1         496  
62             our($io_socket_module_name);
63             BEGIN {
64             if (eval { require IO::Socket::IP }) {
65 1 50   1   14 $io_socket_module_name = 'IO::Socket::IP';
  1 0       922  
    0          
66 1         9901 } elsif (eval { require IO::Socket::INET6 }) {
67 0         0 $io_socket_module_name = 'IO::Socket::INET6';
68 0         0 } elsif (eval { require IO::Socket::INET }) {
69 0         0 $io_socket_module_name = 'IO::Socket::INET';
70 0         0 }
71             }
72              
73             my $EOL = "\015\012";
74             my $BLANK = $EOL x 2;
75             my $PROTOVERSION = 'SPAMC/1.5';
76              
77             =head1 PUBLIC METHODS
78              
79             =head2 new
80              
81             public class (Mail::SpamAssassin::Client) new (\% $args)
82              
83             Description:
84             This method creates a new Mail::SpamAssassin::Client object.
85              
86             =cut
87              
88             my ($class, $args) = @_;
89              
90 3     3 1 16038922 $class = ref($class) || $class;
91              
92 3   33     49 my $self = {};
93              
94 3         10 # with a sockets_path set then it makes no sense to set host and port
95             if ($args->{socketpath}) {
96             $self->{socketpath} = $args->{socketpath};
97 3 100       14 }
98 1         24 else {
99             $self->{port} = $args->{port};
100             $self->{host} = $args->{host};
101 2         4 }
102 2         5  
103             if (defined $args->{username}) {
104             $self->{username} = $args->{username};
105 3 50       32 }
106 0         0  
107             if ($args->{timeout}) {
108             $self->{timeout} = $args->{timeout} || 30;
109 3 50       15 }
110 0   0     0  
111             bless($self, $class);
112              
113 3         15 $self;
114             }
115 3         14  
116             =head2 process
117              
118             public instance (\%) process (String $msg)
119              
120             Description:
121             This method calls the spamd server with the PROCESS command.
122              
123             The return value is a hash reference containing several pieces of information,
124             if available:
125              
126             content_length
127              
128             isspam
129              
130             score
131              
132             threshold
133              
134             message
135              
136             =cut
137              
138             my ($self, $msg, $is_check_p) = @_;
139              
140             my $command = 'PROCESS';
141 2     2 1 4260  
142             if ($is_check_p) {
143 2         13 warn "Passing in \$is_check_p is deprecated, just call the check method instead.\n";
144             $command = 'CHECK';
145 2 50       11 }
146 0         0  
147 0         0 return $self->_filter($msg, $command);
148             }
149              
150 2         11 =head2 check
151              
152             public instance (\%) check (String $msg)
153              
154             Description:
155             The method implements the check call.
156              
157             See the process method for the return value.
158              
159             =cut
160              
161             my ($self, $msg) = @_;
162              
163             return $self->_filter($msg, 'CHECK');
164             }
165 2     2 1 17  
166             =head2 headers
167 2         10  
168             public instance (\%) headers (String $msg)
169              
170             Description:
171             This method implements the headers call.
172              
173             See the process method for the return value.
174              
175             =cut
176              
177             my ($self, $msg) = @_;
178              
179             return $self->_filter($msg, 'HEADERS');
180             }
181              
182 1     1 1 3115 =head2 learn
183              
184 1         5 public instance (Boolean) learn (String $msg, Integer $learntype)
185              
186             Description:
187             This method implements the learn call. C<$learntype> should be
188             an integer, 0 for spam, 1 for ham and 2 for forget. The return
189             value is a boolean indicating if the message was learned or not.
190              
191             An undef return value indicates that there was an error and you
192             should check the resp_code/resp_msg values to determine what
193             the error was.
194              
195             =cut
196              
197             my ($self, $msg, $learntype) = @_;
198              
199             $self->_clear_errors();
200              
201             my $remote = $self->_create_connection();
202              
203 6     6 1 1696598 return unless $remote;
204              
205 6         57 my $msgsize = length($msg.$EOL);
206              
207 6         35 print $remote "TELL $PROTOVERSION$EOL";
208             print $remote "Content-length: $msgsize$EOL";
209 6 50       26 print $remote "User: $self->{username}$EOL" if defined $self->{username};
210              
211 6         62 if ($learntype == 0) {
212             print $remote "Message-class: spam$EOL";
213 6         212 print $remote "Set: local$EOL";
214 6         137 }
215 6 50       42 elsif ($learntype == 1) {
216             print $remote "Message-class: ham$EOL";
217 6 100       63 print $remote "Set: local$EOL";
    100          
    50          
218 2         33 }
219 2         85 elsif ($learntype == 2) {
220             print $remote "Remove: local$EOL";
221             }
222 2         45 else { # bad learntype
223 2         39 $self->{resp_code} = 00;
224             $self->{resp_msg} = 'do not know';
225             return;
226 2         41 }
227              
228             print $remote "$EOL";
229 0         0 print $remote $msg;
230 0         0 print $remote "$EOL";
231 0         0  
232             $! = 0; my $line = <$remote>;
233             # deal gracefully with a Perl I/O bug which may return status EBADF at eof
234 6         97 defined $line || $!==0 or
235 6         114 $!==EBADF ? dbg("error reading from spamd (1): $!")
236 6         96 : die "error reading from spamd (1): $!";
237             return unless defined $line;
238 6         24  
  6         218212  
239             my ($version, $resp_code, $resp_msg) = $self->_parse_response_line($line);
240 6 0 33     93  
    50          
241             $self->{resp_code} = $resp_code;
242             $self->{resp_msg} = $resp_msg;
243 6 50       27  
244             return unless $resp_code == 0;
245 6         49  
246             my $did_set = '';
247 6         55 my $did_remove = '';
248 6         22  
249             for ($!=0; defined($line=<$remote>); $!=0) {
250 6 50       33 local $1;
251             if ($line =~ /DidSet: (.*)/i) {
252 6         34 $did_set = $1;
253 6         28 }
254             elsif ($line =~ /DidRemove: (.*)/i) {
255 6         59 $did_remove = $1;
256 10         44 }
257 10 100       155 elsif ($line =~ /^${EOL}$/) {
    100          
    50          
258 2         15 last;
259             }
260             }
261 2         47 defined $line || $!==0 or
262             $!==EBADF ? dbg("error reading from spamd (2): $!")
263             : die "error reading from spamd (2): $!";
264 6         22 close $remote or die "error closing socket: $!";
265              
266             if ($learntype == 0 || $learntype == 1) {
267 6 0 33     33 return $did_set =~ /local/;
    50          
268             }
269             else { #safe since we've already checked the $learntype values
270 6 50       377 return $did_remove =~ /local/;
271             }
272 6 100 100     82 }
273 4         121  
274             =head2 report
275              
276 2         115 public instance (Boolean) report (String $msg)
277              
278             Description:
279             This method provides the report interface to spamd.
280              
281             =cut
282              
283             my ($self, $msg) = @_;
284              
285             $self->_clear_errors();
286              
287             my $remote = $self->_create_connection();
288              
289             return unless $remote;
290 0     0 1 0  
291             my $msgsize = length($msg.$EOL);
292 0         0  
293             print $remote "TELL $PROTOVERSION$EOL";
294 0         0 print $remote "Content-length: $msgsize$EOL";
295             print $remote "User: $self->{username}$EOL" if defined $self->{username};
296 0 0       0 print $remote "Message-class: spam$EOL";
297             print $remote "Set: local,remote$EOL";
298 0         0 print $remote "$EOL";
299             print $remote $msg;
300 0         0 print $remote "$EOL";
301 0         0  
302 0 0       0 $! = 0; my $line = <$remote>;
303 0         0 defined $line || $!==0 or
304 0         0 $!==EBADF ? dbg("error reading from spamd (3): $!")
305 0         0 : die "error reading from spamd (3): $!";
306 0         0 return unless defined $line;
307 0         0  
308             my ($version, $resp_code, $resp_msg) = $self->_parse_response_line($line);
309 0         0  
  0         0  
310 0 0 0     0 $self->{resp_code} = $resp_code;
    0          
311             $self->{resp_msg} = $resp_msg;
312              
313 0 0       0 return unless $resp_code == 0;
314              
315 0         0 my $reported_p = 0;
316              
317 0         0 for ($!=0; defined($line=<$remote>); $!=0) {
318 0         0 if ($line =~ /DidSet:\s+.*remote/i) {
319             $reported_p = 1;
320 0 0       0 last;
321             }
322 0         0 elsif ($line =~ /^${EOL}$/) {
323             last;
324 0         0 }
325 0 0       0 }
    0          
326 0         0 defined $line || $!==0 or
327 0         0 $!==EBADF ? dbg("error reading from spamd (4): $!")
328             : die "error reading from spamd (4): $!";
329             close $remote or die "error closing socket: $!";
330 0         0  
331             return $reported_p;
332             }
333 0 0 0     0  
    0          
334             =head2 revoke
335              
336 0 0       0 public instance (Boolean) revoke (String $msg)
337              
338 0         0 Description:
339             This method provides the revoke interface to spamd.
340              
341             =cut
342              
343             my ($self, $msg) = @_;
344              
345             $self->_clear_errors();
346              
347             my $remote = $self->_create_connection();
348              
349             return unless $remote;
350              
351 0     0 1 0 my $msgsize = length($msg.$EOL);
352              
353 0         0 print $remote "TELL $PROTOVERSION$EOL";
354             print $remote "Content-length: $msgsize$EOL";
355 0         0 print $remote "User: $self->{username}$EOL" if defined $self->{username};
356             print $remote "Message-class: ham$EOL";
357 0 0       0 print $remote "Set: local$EOL";
358             print $remote "Remove: remote$EOL";
359 0         0 print $remote "$EOL";
360             print $remote $msg;
361 0         0 print $remote "$EOL";
362 0         0  
363 0 0       0 $! = 0; my $line = <$remote>;
364 0         0 defined $line || $!==0 or
365 0         0 $!==EBADF ? dbg("error reading from spamd (5): $!")
366 0         0 : die "error reading from spamd (5): $!";
367 0         0 return unless defined $line;
368 0         0  
369 0         0 my ($version, $resp_code, $resp_msg) = $self->_parse_response_line($line);
370              
371 0         0 $self->{resp_code} = $resp_code;
  0         0  
372 0 0 0     0 $self->{resp_msg} = $resp_msg;
    0          
373              
374             return unless $resp_code == 0;
375 0 0       0  
376             my $revoked_p = 0;
377 0         0  
378             for ($!=0; defined($line=<$remote>); $!=0) {
379 0         0 if ($line =~ /DidRemove:\s+remote/i) {
380 0         0 $revoked_p = 1;
381             last;
382 0 0       0 }
383             elsif ($line =~ /^${EOL}$/) {
384 0         0 last;
385             }
386 0         0 }
387 0 0       0 defined $line || $!==0 or
    0          
388 0         0 $!==EBADF ? dbg("error reading from spamd (6): $!")
389 0         0 : die "error reading from spamd (6): $!";
390             close $remote or die "error closing socket: $!";
391              
392 0         0 return $revoked_p;
393             }
394              
395 0 0 0     0  
    0          
396             =head2 ping
397              
398 0 0       0 public instance (Boolean) ping ()
399              
400 0         0 Description:
401             This method performs a server ping and returns 0 or 1 depending on
402             if the server responded correctly.
403              
404             =cut
405              
406             my ($self) = @_;
407              
408             my $remote = $self->_create_connection();
409              
410             return 0 unless ($remote);
411              
412             print $remote "PING $PROTOVERSION$EOL";
413             print $remote "$EOL"; # bug 6187, bumps protocol version to 1.5
414              
415 2     2 1 1564 $! = 0; my $line = <$remote>;
416             defined $line || $!==0 or
417 2         10 $!==EBADF ? dbg("error reading from spamd (7): $!")
418             : die "error reading from spamd (7): $!";
419 2 50       5 close $remote or die "error closing socket: $!";
420             return unless defined $line;
421 2         78  
422 2         32 my ($version, $resp_code, $resp_msg) = $self->_parse_response_line($line);
423             return 0 unless ($resp_msg eq 'PONG');
424 2         12  
  2         10266  
425 2 0 33     38 return 1;
    50          
426             }
427              
428 2 50       69 =head1 PRIVATE METHODS
429 2 50       17  
430             =head2 _create_connection
431 2         21  
432 2 50       11 private instance (IO::Socket) _create_connection ()
433              
434 2         41 Description:
435             This method sets up a proper IO::Socket connection based on the arguments
436             used when creating the client object.
437              
438             On failure, it sets an internal error code and returns undef.
439              
440             =cut
441              
442             my ($self) = @_;
443              
444             my $remote;
445              
446             if ($self->{socketpath}) {
447             $remote = IO::Socket::UNIX->new( Peer => $self->{socketpath},
448             Type => SOCK_STREAM,
449             Timeout => $self->{timeout},
450             );
451             }
452 13     13   34 else {
453             my %params = ( Proto => "tcp",
454 13         36 PeerAddr => $self->{host},
455             PeerPort => $self->{port},
456 13 100       89 Timeout => $self->{timeout},
457             );
458             $remote = $io_socket_module_name->new(%params);
459             }
460 3         107  
461             unless ($remote) {
462             print "Failed to create connection to spamd daemon: $!\n";
463             return;
464             }
465              
466             $remote;
467 10         140 }
468 10         203  
469             =head2 _parse_response_line
470              
471 13 50       10691 private instance (@) _parse_response_line (String $line)
472 0         0  
473 0         0 Description:
474             This method parses the initial response line/header from the server
475             and returns its parts.
476 13         29  
477             We have this as a separate method in case we ever decide to get fancy
478             with the response line.
479              
480             =cut
481              
482             my ($self, $line) = @_;
483              
484             $line =~ s/\r?\n$//;
485             return split(/\s+/, $line, 3);
486             }
487              
488             =head2 _clear_errors
489              
490             private instance () _clear_errors ()
491              
492             Description:
493 13     13   170 This method clears out any current errors.
494              
495 13         229 =cut
496 13         266  
497             my ($self) = @_;
498              
499             $self->{resp_code} = undef;
500             $self->{resp_msg} = undef;
501             }
502              
503             =head2 _filter
504              
505             private instance (\%) _filter (String $msg, String $command)
506              
507             Description:
508             Makes the actual call to the spamd server for the various filter method
509 11     11   34 (ie PROCESS, CHECK, HEADERS, etc). The command that is passed in is
510             sent to the spamd server.
511 11         51  
512 11         49 The return value is a hash reference containing several pieces of information,
513             if available:
514              
515             content_length
516              
517             isspam
518              
519             score
520              
521             threshold
522              
523             message (if available)
524              
525             =cut
526              
527             my ($self, $msg, $command) = @_;
528              
529             my %data;
530              
531             $self->_clear_errors();
532              
533             my $remote = $self->_create_connection();
534              
535             return 0 unless ($remote);
536              
537             my $msgsize = length($msg.$EOL);
538              
539             print $remote "$command $PROTOVERSION$EOL";
540 5     5   19 print $remote "Content-length: $msgsize$EOL";
541             print $remote "User: $self->{username}$EOL" if defined $self->{username};
542 5         15 print $remote "$EOL";
543             print $remote $msg;
544 5         17 print $remote "$EOL";
545              
546 5         15 $! = 0; my $line = <$remote>;
547             defined $line || $!==0 or
548 5 50       27 $!==EBADF ? dbg("error reading from spamd (8): $!")
549             : die "error reading from spamd (8): $!";
550 5         35 return unless defined $line;
551              
552 5         197 my ($version, $resp_code, $resp_msg) = $self->_parse_response_line($line);
553 5         125
554 5 50       29 $self->{resp_code} = $resp_code;
555 5         82 $self->{resp_msg} = $resp_msg;
556 5         113  
557 5         78 return unless $resp_code == 0;
558              
559 5         34 for ($!=0; defined($line=<$remote>); $!=0) {
  5         156272  
560 5 0 33     63 local($1,$2,$3);
    50          
561             if ($line =~ /Content-length: (\d+)/) {
562             $data{content_length} = $1;
563 5 50       20 }
564             elsif ($line =~ m!Spam: (\S+) ; (\S+) / (\S+)!) {
565 5         42 $data{isspam} = $1;
566             $data{score} = $2 + 0;
567 5         66 $data{threshold} = $3 + 0;
568 5         27 }
569             elsif ($line =~ /^${EOL}$/) {
570 5 50       26 last;
571             }
572 5         45 }
573 13         80 defined $line || $!==0 or
574 13 100       197 $!==EBADF ? dbg("error reading from spamd (9): $!")
    100          
    50          
575 3         28 : die "error reading from spamd (9): $!";
576              
577             my $return_msg;
578 5         35 for ($!=0; defined($line=<$remote>); $!=0) {
579 5         41 $return_msg .= $line;
580 5         61 }
581             defined $line || $!==0 or
582             $!==EBADF ? dbg("error reading from spamd (10): $!")
583 5         28 : die "error reading from spamd (10): $!";
584              
585             $data{message} = $return_msg if ($return_msg);
586 5 0 33     54  
    50          
587             close $remote or die "error closing socket: $!";
588              
589             return \%data;
590 5         24 }
591 5         1304  
592 150         2308 1;
593