You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

snf4sa.pm 7.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. #
  2. # SpamAssassin SNF4SA Plugin for SNFServer.
  3. #
  4. # This plugin implements a SpamAssassin rule to use SNFServer to test
  5. # whether an email is spam.
  6. #
  7. # Copyright (C) 2009 ARM Research Labs, LLC.
  8. #
  9. # snf4sa.pm
  10. #
  11. # The plugin implements a single evaluation rule, which passes the
  12. # email message through SNFServer. The communication with SNFServer
  13. # is through XCI and a temporary file on disk which contains the email
  14. # message truncated to the frist 64K bytes.
  15. #
  16. package Snf4sa;
  17. use strict;
  18. use Mail::SpamAssassin;
  19. use Mail::SpamAssassin::Plugin;
  20. use Mail::SpamAssassin::PerMsgStatus;
  21. use IO::Socket;
  22. use IO::File;
  23. use File::Temp qw/ tempfile tempdir /;
  24. our @ISA = qw(Mail::SpamAssassin::Plugin);
  25. # Convenience variables and pseudo-constants
  26. my $CRLF = "\x0d\x0a";
  27. my $DefaultMaxTempFileSize = 64 * 1024;
  28. # translation table for SNF rule codes
  29. my $rule_code_xlat = {
  30. 0 => 'Standard White Rules',
  31. 20 => 'GBUdb Truncate (superblack)',
  32. 40 => 'GBUdb Caution (suspicious)',
  33. 47 => 'Travel',
  34. 48 => 'Insurance',
  35. 49 => 'Antivirus Push',
  36. 50 => 'Media Theft',
  37. 51 => 'Spamware',
  38. 52 => 'Snake Oil',
  39. 53 => 'Scam Patterns',
  40. 54 => 'Porn/Adult',
  41. 55 => 'Malware & Scumware Greetings',
  42. 56 => 'Ink & Toner',
  43. 57 => 'Get Rich',
  44. 58 => 'Debt & Credit',
  45. 59 => 'Casinos & Gambling',
  46. 60 => 'Ungrouped Black Rules',
  47. 61 => 'Experimental Abstract',
  48. 62 => 'Obfuscation Techniques',
  49. 63 => 'Experimental Received [ip]',
  50. };
  51. sub new {
  52. my ($class, $mailsa) = @_;
  53. $class = ref($class) || $class;
  54. my $self = $class->SUPER::new($mailsa);
  55. bless ($self, $class);
  56. # Name of evaluation rule.
  57. $self->register_eval_rule ("snf4sa_sacheck");
  58. # Use localhost.
  59. $self->{SNF_Host} = "localhost";
  60. # Use default port.
  61. $self->{SNF_Port} = 9001;
  62. # Timeout.
  63. $self->{SNF_Timeout} = 1;
  64. # Directory for files containing emails read by SNFServer.
  65. $self->{Temp_Dir} = '/tmp/snf4sa';
  66. # Maximum email message size (including headers).
  67. $self->{SNF_MaxTempFileSize} = $DefaultMaxTempFileSize;
  68. return $self;
  69. }
  70. sub snf4sa_sacheck {
  71. my ($self, $permsgstatus, $fulltext) = @_;
  72. my $testscore = 0;
  73. my $response ='';
  74. my $exitvalue;
  75. # Make sure we have a temp dir
  76. unless(-d $self->{Temp_Dir}) {
  77. mkdir($self->{Temp_Dir});
  78. chmod(0777, $self->{Temp_Dir});
  79. };
  80. # Truncate the message.
  81. my $mailtext = substr( ${$fulltext}, 0, $self->{SNF_MaxTempFileSize});
  82. # create our temp file, $filename will contain the full path
  83. my ($fh, $filename) = tempfile( DIR => $self->{Temp_Dir} );
  84. # spew our mail into the temp file
  85. my $SNF_fh = IO::File->new( $filename, "w" ) ||
  86. die(__PACKAGE__ . ": Unable to create temporary file '" . $filename . "'");
  87. $SNF_fh->print($mailtext) ||
  88. $self->cleanup_die($filename,
  89. __PACKAGE__ . ": Unable to write to temporary file '" .
  90. $filename . "'");
  91. $SNF_fh->close ||
  92. $self->cleanup_die($filename,
  93. __PACKAGE__ . ": Unable to close temporary file '" .
  94. $filename . "'");
  95. # Change permissions.
  96. my $cnt = chmod(0666, $filename) ||
  97. $self->cleanup_die($filename, __PACKAGE__ .
  98. ": Unable to change permissions of temporary file '" .
  99. $filename . "'");
  100. # xci_scan connects to SNFServer with XCI to scan the message
  101. my $SNF_XCI_Return = $self->xci_scan( $filename );
  102. # Remove the temp file, we are done with it.
  103. unlink($filename);
  104. # Check response from SNFServer.
  105. if (! $SNF_XCI_Return ) {
  106. die(__PACKAGE__ . ": Internal error");
  107. }
  108. # Check for success.
  109. if (! $SNF_XCI_Return->{"success"}) {
  110. die(__PACKAGE__ . ": Error from SNFServer: " .
  111. $SNF_XCI_Return->{"message"});
  112. }
  113. # get the return code and translation
  114. my ( $rc, $rcx ) = ( $SNF_XCI_Return->{"code"},
  115. $rule_code_xlat->{ $SNF_XCI_Return->{"code"} } );
  116. $rc = -1 unless defined $rc; # default values
  117. $rcx = 'Unknown' unless $rcx;
  118. my $rch = $SNF_XCI_Return->{"header"}; # the SNF header(s)
  119. # Result code of 0 indicates non-spam. Any other value indicates
  120. # spam.
  121. if ($rc >= 1) {
  122. $testscore=1;
  123. } else {
  124. $testscore=0;
  125. }
  126. # Add the header.
  127. $permsgstatus->set_tag("SNFRESULTTAG", "$rc ($rcx)");
  128. #all SA cares about is whether a 0 or 1 comes back. 0 = good 1=spam.
  129. return $testscore;
  130. }
  131. sub abort
  132. {
  133. my ( $self, $message ) = @_;
  134. }
  135. # xci_scan( $file )
  136. # returns hashref:
  137. # success : true/false
  138. # code : response code from SNF
  139. # message : scalar message (if any)
  140. sub xci_scan
  141. {
  142. my ( $self, $file ) = @_;
  143. return undef unless $self and $file;
  144. my $ret_hash = {
  145. success => undef,
  146. code => undef,
  147. message => undef,
  148. header => undef,
  149. xml => undef
  150. };
  151. my $xci = $self->connect_socket( $self->{SNF_Host}, $self->{SNF_Port} )
  152. or return $self->err_hash("cannot connect to socket ($!)");
  153. $xci->print("<snf><xci><scanner><scan file='$file' xhdr='yes' /></scanner></xci></snf>\n");
  154. my $rc = $ret_hash->{xml} = $self->socket_response($xci, $file);
  155. $xci->close;
  156. if ( $rc =~ /^<snf><xci><scanner><result code='(\d*)'>/ ) {
  157. $ret_hash->{success} = 1;
  158. $ret_hash->{code} = $1;
  159. $rc =~ /<xhdr>(.*)<\/xhdr>/s and $ret_hash->{header} = $1;
  160. } elsif ( $rc =~ /^<snf><xci><error message='(.*)'/ ) {
  161. $ret_hash->{message} = $1;
  162. } else {
  163. $ret_hash->{message} = "unknown XCI response: $rc";
  164. }
  165. return $ret_hash;
  166. }
  167. # connect_socket( $host, $port )
  168. # returns IO::Socket handle
  169. sub connect_socket
  170. {
  171. my ( $self, $host, $port ) = @_;
  172. return undef unless $self and $host and $port;
  173. my $protoname = 'tcp'; # Proto should default to tcp but it's not expensive to specify
  174. $self->{XCI_Socket} = IO::Socket::INET->new(
  175. PeerAddr => $host,
  176. PeerPort => $port,
  177. Proto => $protoname,
  178. Timeout => $self->{SNF_Timeout} ) or return undef;
  179. $self->{XCI_Socket}->autoflush(1); # make sure autoflush is on -- legacy
  180. return $self->{XCI_Socket}; # return the socket handle
  181. }
  182. # socket_response( $socket_handle )
  183. # returns scalar string
  184. sub socket_response
  185. {
  186. my ( $self, $rs, $file ) = @_;
  187. my $buf = ''; # buffer for response
  188. # blocking timeout for servers who accept but don't answer
  189. eval {
  190. local $SIG{ALRM} = sub { die "timeout\n" }; # set up the interrupt
  191. alarm $self->{SNF_Timeout}; # set up the alarm
  192. while (<$rs>) { # read the socket
  193. $buf .= $_;
  194. }
  195. alarm 0; # reset the alarm
  196. };
  197. # report a blocking timeout
  198. if ( $@ eq "timeout\n" ) {
  199. $self->cleanup_die($file,
  200. __PACKAGE__ . ": Timeout waiting for response from SNFServer");
  201. } elsif ( $@ =~ /alarm.*unimplemented/ ) { # no signals on Win32
  202. while (<$rs>) { # get whatever's left
  203. # in the socket.
  204. $buf .= $_;
  205. }
  206. }
  207. return $buf;
  208. }
  209. # return an error message for xci_scan
  210. sub err_hash
  211. {
  212. my ( $self, $message ) = @_;
  213. return {
  214. success => undef,
  215. code => undef,
  216. message => $message
  217. };
  218. }
  219. sub cleanup_die
  220. {
  221. my ( $self, $file, $message ) = @_;
  222. unlink($file);
  223. die($message);
  224. }
  225. 1;