home *** CD-ROM | disk | FTP | other *** search
/ ftp.f-secure.com / 2014.06.ftp.f-secure.com.tar / ftp.f-secure.com / support / hotfix / fsis / IS-SpamControl.fsfix / iufssc / lib / Mail / SpamAssassin / Client.pm < prev    next >
Text File  |  2006-11-29  |  10KB  |  500 lines

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