File Coverage

blib/lib/SpamMonkey.pm
Criterion Covered Total %
statement 18 129 13.9
branch 0 42 0.0
condition 0 12 0.0
subroutine 6 23 26.0
pod 2 16 12.5
total 26 222 11.7


line stmt bran cond sub pod time code
1             package SpamMonkey;
2 1     1   14430 use UNIVERSAL::require;
  1         3767  
  1         12  
3 1     1   1482 use File::Path::Expand qw(expand_filename);
  1         23339  
  1         66  
4 1     1   1315 use URI::Find::Schemeless;
  1         100107  
  1         182  
5 1     1   44 use 5.006;
  1         5  
  1         47  
6 1     1   7 use strict;
  1         2  
  1         45  
7 1     1   6 use warnings;
  1         1  
  1         1940  
8             our $VERSION = '0.03';
9              
10             =head1 NAME
11              
12             SpamMonkey - Like SpamAssassin, only not.
13              
14             =head1 SYNOPSIS
15              
16             use SpamMonkey;
17             my $monkey = SpamMonkey->new();
18             $monkey->ready;
19              
20             for (@things) {
21             my $result = $monkey->test($_);
22             if ($result->is_spam) { $result->rewrite }
23             }
24              
25             =head1 DESCRIPTION
26              
27             SpamMonkey is a general purpose spam detection suite. It borrows heavily
28             from SpamAssassin, but it is designed to be used for plain text as well
29             as email.
30              
31             =cut
32              
33 0     0 0   sub rulesets { qw(body full rawbody uri header); }
34              
35 0     0 0   sub default_rule_dir { "/etc/mail/spamassassin/" }
36              
37             =head1 CONSTRUCTOR
38              
39             SpamMonkey->new(
40             rule_dir => "/etc/mail/spamassassin/"
41             );
42              
43             SpamMonkey by default loads up rules from F
44             and then F<~/.spammonkey/user_prefs>. To override the rule directory,
45             specify C in the constructor.
46              
47             =cut
48              
49 0     0 0   sub new { my $class = shift; bless { @_ }, $class; }
  0            
50              
51 0     0 0   sub config_class { "SpamMonkey::Config" }
52 0     0 0   sub result_class { "SpamMonkey::Result" } # Subclassers like these
53              
54             =head1 METHODS
55              
56             =head2 ready
57              
58             This loads up the ruleset and then prunes out rules which have no score
59             attached to them. You must call C before doing a test, else
60             you'll have no rules to test with.
61              
62             =cut
63              
64             sub ready {
65 0     0 1   my $self = shift;
66 0           $self->config_class->require;
67 0           $self->{conf} = $self->config_class->new();
68              
69             # Read rules
70 0   0       for (glob(($self->{rule_dir} || $self->default_rule_dir)."/*.cf")) {
71 0           $self->{conf}->read($_);
72             }
73              
74 0           my $file = expand_filename("~/.spammonkey/user_prefs");
75 0 0         if (-e $file) { $self->{conf}->read($file); }
  0            
76              
77             # Delete rules with no score(!)
78 0           for ($self->rulesets) {
79 0           my $set = $self->{conf}{rules}{$_};
80 0           for (keys %$set) {
81 0 0 0       if (!exists $self->{conf}{score}{$_}
82             or !$self->{conf}{score}{$_}[0]) {
83 0           delete $set->{$_};# warn "Killing boring rule $_";
84             }
85             }
86             }
87             }
88              
89             =head2 test
90              
91             $self->test(Email::MIME $mime);
92             $self->test($text);
93              
94             This tests an email or a piece of text using the ruleset loaded by
95             C and returns a C object.
96              
97             =cut
98              
99             sub test {
100 0     0 1   my ($self, $text) = @_;
101 0           $self->{result} = {};
102 0           $self->{per_message} = {};
103 0 0         if (not ref $text) {
    0          
104 0           $self->{text} = $text;
105 0           $self->test_text()
106             } elsif (UNIVERSAL::isa($text, "Email::MIME")) {
107 0           $self->{email} = $text;
108 0           $self->test_email();
109             }
110 0           $self->result_class->require;
111 0           bless $self->{result}, $self->result_class;
112 0           delete $self->{per_message}; # Ready it to go again
113             # Invert
114 0           $self->{result}{monkey} = $self;
115 0           delete $self->{result};
116             }
117              
118             sub test_text {
119 0     0 0   my ($self) = @_;
120 0           my $text_r = \do{$self->{text}};
  0            
121 0           $self->test_bodylike("rawbody",$text_r,0);
122             # Munge text here
123 0           $self->test_bodylike("body",$text_r,0);
124 0           $self->test_uris($text_r,0);
125             }
126              
127             sub test_email {
128 0     0 0   my ($self) = @_;
129 0           $self->test_headers($self->{email}, 0);
130 0           $self->{text} = $self->{email}->body_raw;
131 0           $self->test_bodylike("rawbody",\do{$self->{text}},0);
  0            
132 0           my $body_r = \do{$self->{email}->body};
  0            
133 0 0         if ($$body_r) {
134 0           $self->test_bodylike("body",$body_r, 0);
135             } else {
136 0           $self->test_bodylike("body",\do{$self->{text}}, 0);
  0            
137             }
138 0           $self->test_uris(\do{$self->{text}}, 0);
  0            
139             }
140              
141             sub test_headers {
142 0     0 0   my ($self, $email, $scoretype) = @_;
143 0           my $set = $self->{conf}{rules}{header};
144 0           for my $test (keys %$set) {
145 0           my $rule = $set->{$test};
146 0 0         if ($rule->{op} eq "eval") {
147             $self->match($test, $scoretype)
148 0           for $self->do_code_rule($rule); # Maybe more than one
149             next
150 0           }
151 0           my $text = join "\n", $email->header($rule->{header});
152 0 0         if ($rule->{header} eq "ALL") {
153 0           $text = $email->_headers_as_string; # Urgh
154             }
155              
156 0 0 0       if ($rule->{unset} and not $text) { $text = $rule->{unset} }
  0            
157             #warn "$rule->{header}: $text $rule->{op} $rule->{re}";
158 0 0         next unless $text;
159 0 0         if ($rule->{op} eq "exists") {
    0          
160 0 0         $self->match($test, $scoretype) if $text;
161             } elsif ($rule->{op} eq "!~") {
162 0 0         if($text !~ $rule->{re}) { $self->match($test, $scoretype); }
  0            
163             } else {
164 0 0         if($text =~ $rule->{re}) { $self->match($test, $scoretype); }
  0            
165             }
166             }
167             }
168              
169             sub test_bodylike {
170 0     0 0   my ($self, $ruleset, $text, $scoretype) = @_;
171 0           my $set = $self->{conf}{rules}{$ruleset};
172 0           for my $test (keys %$set) {
173 0           my $rule = $set->{$test};
174 0 0         if (ref $rule eq "HASH") {
175             $self->match($test, $scoretype)
176 0           for $self->do_code_rule($rule, $text);
177             } else {
178 0 0         if($$text =~ $rule) { $self->match($test, $scoretype); }
  0            
179             }
180             }
181             }
182              
183             sub get_uris {
184 0     0 0   my ($self, $text_r) = @_;
185 0 0         return if $self->{per_message}{uris};
186 0           $self->{per_message}{uris}= [];
187             my $finder = URI::Find::Schemeless->new( sub {
188 0     0     push @{$self->{per_message}{uris}}, $_[0];
  0            
189 0           });
190 0           $finder->find($text_r);
191             }
192              
193             sub uris {
194 0     0 0   my $self = shift;
195 0           $self->get_uris($self->{text});
196 0           return @{$self->{per_message}{uris}};
  0            
197             }
198              
199             sub test_uris {
200 0     0 0   my ($self, $text_r, $scoretype) = @_;
201 0           my $set = $self->{conf}{rules}{uri};
202 0           $self->get_uris($text_r);
203 0           for my $uri (@{$self->{per_message}{uris}}) {
  0            
204 0           for my $test (keys %$set) {
205 0           my $rule = $set->{$test};
206 0 0         if ($uri =~ $rule) { $self->match($test, $scoretype) };
  0            
207             }
208             }
209             }
210              
211             sub match {
212 0     0 0   my ($self, $test, $scoretype) = @_;
213 0           push @{$self->{result}{matched}}, $test;
  0            
214 0           $self->{result}{score} += $self->{conf}{score}{$test}[$scoretype];
215             }
216              
217             sub do_code_rule {
218 0     0 0   my ($self, $rule,$text_r) = @_;
219 0 0         my ($pack, $args) = $rule->{code} =~ /(\S+)\((.*)\)$/ or die("Urgh? $rule->{code}");
220 0           my @args = ($args =~ m/['"](.*?)['"]\s*(?:,\s*|$)/g);
221 0           $pack = "SpamMonkey::Test::".$pack;
222 0 0         $pack->require or return;
223 0 0 0       if (!$self->{init}{$pack} and $pack->can("init")) {
224 0           $pack->init($self->{conf});
225 0           $self->{init}{$pack}++;
226             }
227 0           $pack->test($self, $text_r, @args);
228             }
229              
230             =head1 AUTHOR
231              
232             simon, Esimon@E (please don't contact me about this module,
233             unless you wish to take over its maintainance, in which case upload your
234             own version.)
235              
236             =head1 COPYRIGHT AND LICENSE
237              
238             Copyright (C) 2005 by simon
239              
240             This library is free software; you can redistribute it and/or modify
241             it under the same terms as Perl itself, either Perl version 5.8.7 or,
242             at your option, any later version of Perl 5 you may have available.
243              
244              
245             =cut