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 / Bayes.pm < prev    next >
Text File  |  2006-11-29  |  44KB  |  1,418 lines

  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. #     http://www.apache.org/licenses/LICENSE-2.0
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. # </@LICENSE>
  15.  
  16. =head1 NAME
  17.  
  18. Mail::SpamAssassin::Bayes - determine spammishness using a Bayesian classifier
  19.  
  20. =head1 DESCRIPTION
  21.  
  22. This is a Bayesian-like form of probability-analysis classification, using an
  23. algorithm based on the one detailed in Paul Graham's I<A Plan For Spam> paper
  24. at:
  25.  
  26.   http://www.paulgraham.com/spam.html
  27.  
  28. It also incorporates some other aspects taken from Graham Robinson's webpage
  29. on the subject at:
  30.  
  31.   http://radio.weblogs.com/0101454/stories/2002/09/16/spamDetection.html
  32.  
  33. And the chi-square probability combiner as described here:
  34.  
  35.   http://www.linuxjournal.com/print.php?sid=6467
  36.  
  37. The results are incorporated into SpamAssassin as the BAYES_* rules.
  38.  
  39. =head1 METHODS
  40.  
  41. =over 4
  42.  
  43. =cut
  44.  
  45. package Mail::SpamAssassin::Bayes;
  46.  
  47. use strict;
  48. use warnings;
  49. use bytes;
  50.  
  51. use Mail::SpamAssassin;
  52. use Mail::SpamAssassin::PerMsgStatus;
  53. use Mail::SpamAssassin::Logger;
  54.  
  55. # pick ONLY ONE of these combining implementations.
  56. use Mail::SpamAssassin::Bayes::CombineChi;
  57. # use Mail::SpamAssassin::Bayes::CombineNaiveBayes;
  58.  
  59. use Digest::SHA1 qw(sha1 sha1_hex);
  60.  
  61. use vars qw{
  62.   @ISA
  63.   $IGNORED_HDRS
  64.   $MARK_PRESENCE_ONLY_HDRS
  65.   %HEADER_NAME_COMPRESSION
  66.   $OPPORTUNISTIC_LOCK_VALID
  67. };
  68.  
  69. @ISA = qw();
  70.  
  71. # Which headers should we scan for tokens?  Don't use all of them, as it's easy
  72. # to pick up spurious clues from some.  What we now do is use all of them
  73. # *less* these well-known headers; that way we can pick up spammers' tracking
  74. # headers (which are obviously not well-known in advance!).
  75.  
  76. # Received is handled specially
  77. $IGNORED_HDRS = qr{(?: (?:X-)?Sender    # misc noise
  78.   |Delivered-To |Delivery-Date
  79.   |(?:X-)?Envelope-To
  80.   |X-MIME-Auto[Cc]onverted |X-Converted-To-Plain-Text
  81.  
  82.   |Subject      # not worth a tiny gain vs. to db size increase
  83.  
  84.   # Date: can provide invalid cues if your spam corpus is
  85.   # older/newer than ham
  86.   |Date
  87.  
  88.   # List headers: ignore. a spamfiltering mailing list will
  89.   # become a nonspam sign.
  90.   |X-List|(?:X-)?Mailing-List
  91.   |(?:X-)?List-(?:Archive|Help|Id|Owner|Post|Subscribe
  92.     |Unsubscribe|Host|Id|Manager|Admin|Comment
  93.     |Name|Url)
  94.   |X-Unsub(?:scribe)?
  95.   |X-Mailman-Version |X-Been[Tt]here |X-Loop
  96.   |Mail-Followup-To
  97.   |X-eGroups-(?:Return|From)
  98.   |X-MDMailing-List
  99.   |X-XEmacs-List
  100.  
  101.   # gatewayed through mailing list (thanks to Allen Smith)
  102.   |(?:X-)?Resent-(?:From|To|Date)
  103.   |(?:X-)?Original-(?:From|To|Date)
  104.  
  105.   # Spamfilter/virus-scanner headers: too easy to chain from
  106.   # these
  107.   |X-MailScanner(?:-SpamCheck)?
  108.   |X-Spam(?:-(?:Status|Level|Flag|Report|Hits|Score|Checker-Version))?
  109.   |X-Antispam |X-RBL-Warning |X-Mailscanner
  110.   |X-MDaemon-Deliver-To |X-Virus-Scanned
  111.   |X-Mass-Check-Id
  112.   |X-Pyzor |X-DCC-\S{2,25}-Metrics
  113.   |X-Filtered-B[Yy] |X-Scanned-By |X-Scanner
  114.   |X-AP-Spam-(?:Score|Status) |X-RIPE-Spam-Status
  115.   |X-SpamCop-[^:]+
  116.   |X-SMTPD |(?:X-)?Spam-Apparently-To
  117.   |SPAM |X-Perlmx-Spam
  118.   |X-Bogosity
  119.  
  120.   # some noisy Outlook headers that add no good clues:
  121.   |Content-Class |Thread-(?:Index|Topic)
  122.   |X-Original[Aa]rrival[Tt]ime
  123.  
  124.   # Annotations from IMAP, POP, and MH:
  125.   |(?:X-)?Status |X-Flags |Replied |Forwarded
  126.   |Lines |Content-Length
  127.   |X-UIDL? |X-IMAPbase
  128.  
  129.   # Annotations from Bugzilla
  130.   |X-Bugzilla-[^:]+
  131.  
  132.   # Annotations from VM: (thanks to Allen Smith)
  133.   |X-VM-(?:Bookmark|(?:POP|IMAP)-Retrieved|Labels|Last-Modified
  134.     |Summary-Format|VHeader|v\d-Data|Message-Order)
  135.  
  136.   # Annotations from Gnus:
  137.   | X-Gnus-Mail-Source
  138.   | Xref
  139.  
  140. )}x;
  141.  
  142. # Note only the presence of these headers, in order to reduce the
  143. # hapaxen they generate.
  144. $MARK_PRESENCE_ONLY_HDRS = qr{(?: X-Face
  145.   |X-(?:Gnu-?PG|PGP|GPG)(?:-Key)?-Fingerprint
  146. )}ix;
  147.  
  148. # tweaks tested as of Nov 18 2002 by jm: see SpamAssassin-devel list archives
  149. # for results.  The winners are now the default settings.
  150. use constant IGNORE_TITLE_CASE => 1;
  151. use constant TOKENIZE_LONG_8BIT_SEQS_AS_TUPLES => 1;
  152. use constant TOKENIZE_LONG_TOKENS_AS_SKIPS => 1;
  153.  
  154. # tweaks of May 12 2003, see SpamAssassin-devel archives again.
  155. use constant PRE_CHEW_ADDR_HEADERS => 1;
  156. use constant CHEW_BODY_URIS => 1;
  157. use constant CHEW_BODY_MAILADDRS => 1;
  158. use constant HDRS_TOKENIZE_LONG_TOKENS_AS_SKIPS => 1;
  159. use constant BODY_TOKENIZE_LONG_TOKENS_AS_SKIPS => 1;
  160. use constant URIS_TOKENIZE_LONG_TOKENS_AS_SKIPS => 0;
  161. use constant IGNORE_MSGID_TOKENS => 0;
  162.  
  163. # tweaks of 12 March 2004, see bug 2129.
  164. use constant DECOMPOSE_BODY_TOKENS => 1;
  165. use constant MAP_HEADERS_MID => 1;
  166. use constant MAP_HEADERS_FROMTOCC => 1;
  167. use constant MAP_HEADERS_USERAGENT => 1;
  168.  
  169. # tweaks, see http://issues.apache.org/SpamAssassin/show_bug.cgi?id=3173#c26
  170. use constant ADD_INVIZ_TOKENS_I_PREFIX => 1;
  171. use constant ADD_INVIZ_TOKENS_NO_PREFIX => 0;
  172.  
  173. # We store header-mined tokens in the db with a "HHeaderName:val" format.
  174. # some headers may contain lots of gibberish tokens, so allow a little basic
  175. # compression by mapping the header name at least here.  these are the headers
  176. # which appear with the most frequency in my db.  note: this doesn't have to
  177. # be 2-way (ie. LHSes that map to the same RHS are not a problem), but mixing
  178. # tokens from multiple different headers may impact accuracy, so might as well
  179. # avoid this if possible. These are the top ones from my corpus, BTW (jm).
  180. %HEADER_NAME_COMPRESSION = (
  181.   'Message-Id'        => '*m',
  182.   'Message-ID'        => '*M',
  183.   'Received'        => '*r',
  184.   'User-Agent'        => '*u',
  185.   'References'        => '*f',
  186.   'In-Reply-To'        => '*i',
  187.   'From'        => '*F',
  188.   'Reply-To'        => '*R',
  189.   'Return-Path'        => '*p',
  190.   'Return-path'        => '*rp',
  191.   'X-Mailer'        => '*x',
  192.   'X-Authentication-Warning' => '*a',
  193.   'Organization'    => '*o',
  194.   'Organisation'        => '*o',
  195.   'Content-Type'    => '*c',
  196.   'X-Spam-Relays-Trusted' => '*RT',
  197.   'X-Spam-Relays-Untrusted' => '*RU',
  198. );
  199.  
  200. # How many seconds should the opportunistic_expire lock be valid?
  201. $OPPORTUNISTIC_LOCK_VALID = 300;
  202.  
  203. # Should we use the Robinson f(w) equation from
  204. # http://radio.weblogs.com/0101454/stories/2002/09/16/spamDetection.html ?
  205. # It gives better results, in that scores are more likely to distribute
  206. # into the <0.5 range for nonspam and >0.5 for spam.
  207. use constant USE_ROBINSON_FX_EQUATION_FOR_LOW_FREQS => 1;
  208.  
  209. # How many of the most significant tokens should we use for the p(w)
  210. # calculation?
  211. use constant N_SIGNIFICANT_TOKENS => 150;
  212.  
  213. # How many significant tokens are required for a classifier score to
  214. # be considered usable?
  215. #use constant REQUIRE_SIGNIFICANT_TOKENS_TO_SCORE => -1;
  216. use constant REQUIRE_SIGNIFICANT_TOKENS_TO_SCORE => 50;
  217.  
  218. # How long a token should we hold onto?  (note: German speakers typically
  219. # will require a longer token than English ones.)
  220. use constant MAX_TOKEN_LENGTH => 15;
  221.  
  222. ###########################################################################
  223.  
  224. sub new {
  225.   my $class = shift;
  226.   $class = ref($class) || $class;
  227.  
  228.   my ($main) = @_;
  229.   my $self = {
  230.     'main'              => $main,
  231.     'conf'        => $main->{conf},
  232.     'log_raw_counts'    => 0,
  233.     'use_ignores'       => 1,
  234.     'tz'        => Mail::SpamAssassin::Util::local_tz(),
  235.   };
  236.   bless ($self, $class);
  237.  
  238.   if ($self->{conf}->{bayes_store_module}) {
  239.     my $module = $self->{conf}->{bayes_store_module};
  240.     my $store;
  241.  
  242.     eval '
  243.       require '.$module.';
  244.       $store = '.$module.'->new($self);
  245.     ';
  246.     if ($@) { die $@; }
  247.     $self->{store} = $store;
  248.   }
  249.   else {
  250.     require Mail::SpamAssassin::BayesStore::DBM;
  251.     $self->{store} = Mail::SpamAssassin::BayesStore::DBM->new($self);
  252.   }
  253.  
  254.   $self;
  255. }
  256.  
  257. sub finish {
  258.   my $self = shift;
  259.   #if (!$self->{conf}->{use_bayes}) { return; }
  260.  
  261.   # if we're untying too much, uncomment this...
  262.   # use Carp qw(cluck); cluck "stack trace at untie";
  263.  
  264.   $self->{store}->untie_db();
  265.   delete $self->{store};
  266. }
  267.  
  268. sub sa_die { Mail::SpamAssassin::sa_die(@_); }
  269.  
  270. ###########################################################################
  271.  
  272. sub sanity_check_is_untied {
  273.   my $self = shift;
  274.   my $quiet = shift;
  275.  
  276.   # do a sanity check here.  Wierd things happen if we remain tied
  277.   # after compiling; for example, spamd will never see that the
  278.   # number of messages has reached the bayes-scanning threshold.
  279.   if ($self->{store}->db_readable()) {
  280.     warn "bayes: oops! still tied to bayes DBs, untying\n" unless $quiet;
  281.     $self->{store}->untie_db();
  282.   }
  283. }
  284.  
  285. ###########################################################################
  286.  
  287. # read configuration items to control bayes behaviour.  Called by
  288. # BayesStore::read_db_configs().
  289. sub read_db_configs {
  290.   my ($self) = @_;
  291.  
  292.   # use of hapaxes.  Set on bayes object, since it controls prob
  293.   # computation.
  294.   $self->{use_hapaxes} = $self->{conf}->{bayes_use_hapaxes};
  295. }
  296.  
  297. ###########################################################################
  298.  
  299. # The calling functions expect a uniq'ed array of tokens ...
  300. sub tokenize {
  301.   my ($self, $msg, $msgdata) = @_;
  302.  
  303.   # the body
  304.   my @tokens = map { $self->tokenize_line ($_, '', 1) }
  305.                                     @{$msgdata->{bayes_token_body}};
  306.  
  307.   # the URI list
  308.   push (@tokens, map { $self->tokenize_line ($_, '', 2) }
  309.                                     @{$msgdata->{bayes_token_uris}});
  310.  
  311.   # add invisible tokens
  312.   if (ADD_INVIZ_TOKENS_I_PREFIX) {
  313.     push (@tokens, map { $self->tokenize_line ($_, "I*:", 1) }
  314.                                     @{$msgdata->{bayes_token_inviz}});
  315.   }
  316.   if (ADD_INVIZ_TOKENS_NO_PREFIX) {
  317.     push (@tokens, map { $self->tokenize_line ($_, "", 1) }
  318.                                     @{$msgdata->{bayes_token_inviz}});
  319.   }
  320.  
  321.   # Tokenize the headers
  322.   my %hdrs = $self->tokenize_headers ($msg);
  323.   while( my($prefix, $value) = each %hdrs ) {
  324.     push(@tokens, $self->tokenize_line ($value, "H$prefix:", 0));
  325.   }
  326.  
  327.   # Go ahead and uniq the array, skip null tokens (can happen sometimes)
  328.   # generate an SHA1 hash and take the lower 40 bits as our token
  329.   my %tokens;
  330.   foreach my $token (@tokens) {
  331.     next unless length($token); # skip 0 length tokens
  332.     $tokens{substr(sha1($token), -5)} = $token;
  333.   }
  334.  
  335.   # return the keys == tokens ...
  336.   return \%tokens;
  337. }
  338.  
  339. sub tokenize_line {
  340.   my $self = $_[0];
  341.   my $tokprefix = $_[2];
  342.   my $region = $_[3];
  343.   local ($_) = $_[1];
  344.  
  345.   my @rettokens = ();
  346.  
  347.   # include quotes, .'s and -'s for URIs, and [$,]'s for Nigerian-scam strings,
  348.   # and ISO-8859-15 alphas.  Do not split on @'s; better results keeping it.
  349.   # Some useful tokens: "$31,000,000" "www.clock-speed.net" "f*ck" "Hits!"
  350.   tr/-A-Za-z0-9,\@\*\!_'"\$.\241-\377 / /cs;
  351.  
  352.   # DO split on "..." or "--" or "---"; common formatting error resulting in
  353.   # hapaxes.  Keep the separator itself as a token, though, as long ones can
  354.   # be good spamsigns.
  355.   s/(\w)(\.{3,6})(\w)/$1 $2 $3/gs;
  356.   s/(\w)(\-{2,6})(\w)/$1 $2 $3/gs;
  357.  
  358.   if (IGNORE_TITLE_CASE) {
  359.     if ($region == 1 || $region == 2) {
  360.       # lower-case Title Case at start of a full-stop-delimited line (as would
  361.       # be seen in a Western language).
  362.       s/(?:^|\.\s+)([A-Z])([^A-Z]+)(?:\s|$)/ ' '. (lc $1) . $2 . ' ' /ge;
  363.     }
  364.   }
  365.  
  366.   my $magic_re = $self->{store}->get_magic_re();
  367.  
  368.   foreach my $token (split) {
  369.     $token =~ s/^[-'"\.,]+//;        # trim non-alphanum chars at start or end
  370.     $token =~ s/[-'"\.,]+$//;        # so we don't get loads of '"foo' tokens
  371.  
  372.     # Skip false magic tokens
  373.     # TVD: we need to do a defined() check since SQL doesn't have magic
  374.     # tokens, so the SQL BayesStore returns undef.  I really want a way
  375.     # of optimizing that out, but I haven't come up with anything yet.
  376.     #
  377.     next if ( defined $magic_re && /$magic_re/ );
  378.  
  379.     # *do* keep 3-byte tokens; there's some solid signs in there
  380.     my $len = length($token);
  381.  
  382.     # but extend the stop-list. These are squarely in the gray
  383.     # area, and it just slows us down to record them.
  384.     next if $len < 3 ||
  385.        # SDBM workaround: Don't do tokens longer than 1000 characters
  386.        $len > 1000 ||
  387.     ($token =~ /^(?:a(?:nd|ny|ble|ll|re)|
  388.         m(?:uch|ost|ade|ore|ail|ake|ailing|any|ailto)|
  389.         t(?:his|he|ime|hrough|hat)|
  390.         w(?:hy|here|ork|orld|ith|ithout|eb)|
  391.         f(?:rom|or|ew)| e(?:ach|ven|mail)|
  392.         o(?:ne|ff|nly|wn|ut)| n(?:ow|ot|eed)|
  393.         s(?:uch|ame)| l(?:ook|ike|ong)|
  394.         y(?:ou|our|ou're)|
  395.         The|has|have|into|using|http|see|It's|it's|
  396.         number|just|both|come|years|right|know|already|
  397.         people|place|first|because|
  398.         And|give|year|information|can)$/x);
  399.  
  400.     # are we in the body?  If so, apply some body-specific breakouts
  401.     if ($region == 1 || $region == 2) {
  402.       if (CHEW_BODY_MAILADDRS && $token =~ /\S\@\S/i) {
  403.     push (@rettokens, $self->tokenize_mail_addrs ($token));
  404.       }
  405.       elsif (CHEW_BODY_URIS && $token =~ /\S\.[a-z]/i) {
  406.     push (@rettokens, "UD:".$token); # the full token
  407.     my $bit = $token; while ($bit =~ s/^[^\.]+\.(.+)$/$1/gs) {
  408.       push (@rettokens, "UD:".$1); # UD = URL domain
  409.     }
  410.       }
  411.     }
  412.  
  413.     # note: do not trim down overlong tokens if they contain '*'.  This is
  414.     # used as part of split tokens such as "HTo:D*net" indicating that 
  415.     # the domain ".net" appeared in the To header.
  416.     #
  417.     if ($len > MAX_TOKEN_LENGTH && $token !~ /\*/) {
  418.       if (TOKENIZE_LONG_8BIT_SEQS_AS_TUPLES && $token =~ /[\xa0-\xff]{2}/) {
  419.     # Matt sez: "Could be asian? Autrijus suggested doing character ngrams,
  420.     # but I'm doing tuples to keep the dbs small(er)."  Sounds like a plan
  421.     # to me! (jm)
  422.     while ($token =~ s/^(..?)//) {
  423.       push (@rettokens, "8:$1");
  424.     }
  425.     next;
  426.       }
  427.  
  428.       if (($region == 0 && HDRS_TOKENIZE_LONG_TOKENS_AS_SKIPS)
  429.             || ($region == 1 && BODY_TOKENIZE_LONG_TOKENS_AS_SKIPS)
  430.             || ($region == 2 && URIS_TOKENIZE_LONG_TOKENS_AS_SKIPS))
  431.       {
  432.     # if (TOKENIZE_LONG_TOKENS_AS_SKIPS)
  433.     # Spambayes trick via Matt: Just retain 7 chars.  Do not retain
  434.     # the length, it does not help; see my mail to -devel of Nov 20 2002.
  435.     # "sk:" stands for "skip".
  436.     $token = "sk:".substr($token, 0, 7);
  437.       }
  438.     }
  439.  
  440.     # decompose tokens?  do this after shortening long tokens
  441.     if ($region == 1 || $region == 2) {
  442.       if (DECOMPOSE_BODY_TOKENS) {
  443.         if ($token =~ /[^\w:\*]/) {
  444.           my $decompd = $token;                        # "Foo!"
  445.           $decompd =~ s/[^\w:\*]//gs;
  446.           push (@rettokens, $tokprefix.$decompd);      # "Foo"
  447.         }
  448.  
  449.         if ($token =~ /[A-Z]/) {
  450.           my $decompd = $token; $decompd = lc $decompd;
  451.           push (@rettokens, $tokprefix.$decompd);      # "foo!"
  452.  
  453.           if ($token =~ /[^\w:\*]/) {
  454.             $decompd =~ s/[^\w:\*]//gs;
  455.             push (@rettokens, $tokprefix.$decompd);    # "foo"
  456.           }
  457.         }
  458.       }
  459.     }
  460.  
  461.     push (@rettokens, $tokprefix.$token);
  462.   }
  463.  
  464.   return @rettokens;
  465. }
  466.  
  467. sub tokenize_headers {
  468.   my ($self, $msg) = @_;
  469.  
  470.   my %parsed = ();
  471.  
  472.   my %user_ignore;
  473.   $user_ignore{lc $_} = 1 for @{$self->{main}->{conf}->{bayes_ignore_headers}};
  474.  
  475.   # get headers in array context
  476.   my @hdrs;
  477.   my @rcvdlines;
  478.   for ($msg->get_all_headers()) {
  479.     # first, keep a copy of Received headers, so we can strip down to last 2
  480.     if (/^Received:/i) {
  481.       push(@rcvdlines, $_);
  482.       next;
  483.     }
  484.     # and now skip lines for headers we don't want (including all Received)
  485.     next if /^${IGNORED_HDRS}:/i;
  486.     next if IGNORE_MSGID_TOKENS && /^Message-ID:/i;
  487.     push(@hdrs, $_);
  488.   }
  489.   push(@hdrs, $msg->get_all_metadata());
  490.  
  491.   # and re-add the last 2 received lines: usually a good source of
  492.   # spamware tokens and HELO names.
  493.   if ($#rcvdlines >= 0) { push(@hdrs, $rcvdlines[$#rcvdlines]); }
  494.   if ($#rcvdlines >= 1) { push(@hdrs, $rcvdlines[$#rcvdlines-1]); }
  495.  
  496.   for (@hdrs) {
  497.     next unless /\S/;
  498.     my ($hdr, $val) = split(/:/, $_, 2);
  499.  
  500.     # remove user-specified headers here, after Received, in case they
  501.     # want to ignore that too
  502.     next if exists $user_ignore{lc $hdr};
  503.  
  504.     # Prep the header value
  505.     $val ||= '';
  506.     chomp($val);
  507.  
  508.     # special tokenization for some headers:
  509.     if ($hdr =~ /^(?:|X-|Resent-)Message-Id$/i) {
  510.       $val = $self->pre_chew_message_id ($val);
  511.     }
  512.     elsif (PRE_CHEW_ADDR_HEADERS && $hdr =~ /^(?:|X-|Resent-)
  513.     (?:Return-Path|From|To|Cc|Reply-To|Errors-To|Mail-Followup-To|Sender)$/ix)
  514.     {
  515.       $val = $self->pre_chew_addr_header ($val);
  516.     }
  517.     elsif ($hdr eq 'Received') {
  518.       $val = $self->pre_chew_received ($val);
  519.     }
  520.     elsif ($hdr eq 'Content-Type') {
  521.       $val = $self->pre_chew_content_type ($val);
  522.     }
  523.     elsif ($hdr eq 'MIME-Version') {
  524.       $val =~ s/1\.0//;        # totally innocuous
  525.     }
  526.     elsif ($hdr =~ /^${MARK_PRESENCE_ONLY_HDRS}$/i) {
  527.       $val = "1"; # just mark the presence, they create lots of hapaxen
  528.     }
  529.  
  530.     if (MAP_HEADERS_MID) {
  531.       if ($hdr =~ /^(?:In-Reply-To|References|Message-ID)$/i) {
  532.         $parsed{"*MI"} = $val;
  533.       }
  534.     }
  535.     if (MAP_HEADERS_FROMTOCC) {
  536.       if ($hdr =~ /^(?:From|To|Cc)$/i) {
  537.         $parsed{"*Ad"} = $val;
  538.       }
  539.     }
  540.     if (MAP_HEADERS_USERAGENT) {
  541.       if ($hdr =~ /^(?:X-Mailer|User-Agent)$/i) {
  542.         $parsed{"*UA"} = $val;
  543.       }
  544.     }
  545.  
  546.     # replace hdr name with "compressed" version if possible
  547.     if (defined $HEADER_NAME_COMPRESSION{$hdr}) {
  548.       $hdr = $HEADER_NAME_COMPRESSION{$hdr};
  549.     }
  550.  
  551.     if (exists $parsed{$hdr}) {
  552.       $parsed{$hdr} .= " ".$val;
  553.     } else {
  554.       $parsed{$hdr} = $val;
  555.     }
  556.     if (would_log('dbg', 'bayes') > 1) {
  557.       dbg("bayes: header tokens for $hdr = \"$parsed{$hdr}\"");
  558.     }
  559.   }
  560.  
  561.   return %parsed;
  562. }
  563.  
  564. sub pre_chew_content_type {
  565.   my ($self, $val) = @_;
  566.  
  567.   # hopefully this will retain good bits without too many hapaxen
  568.   if ($val =~ s/boundary=[\"\'](.*?)[\"\']/ /ig) {
  569.     my $boundary = $1;
  570.     $boundary =~ s/[a-fA-F0-9]/H/gs;
  571.     # break up blocks of separator chars so they become their own tokens
  572.     $boundary =~ s/([-_\.=]+)/ $1 /gs;
  573.     $val .= $boundary;
  574.   }
  575.  
  576.   # stop-list words for Content-Type header: these wind up totally gray
  577.   $val =~ s/\b(?:text|charset)\b//;
  578.  
  579.   $val;
  580. }
  581.  
  582. sub pre_chew_message_id {
  583.   my ($self, $val) = @_;
  584.   # we can (a) get rid of a lot of hapaxen and (b) increase the token
  585.   # specificity by pre-parsing some common formats.
  586.  
  587.   # Outlook Express format:
  588.   $val =~ s/<([0-9a-f]{4})[0-9a-f]{4}[0-9a-f]{4}\$
  589.            ([0-9a-f]{4})[0-9a-f]{4}\$
  590.            ([0-9a-f]{8})\@(\S+)>/ OEA$1 OEB$2 OEC$3 $4 /gx;
  591.  
  592.   # Exim:
  593.   $val =~ s/<[A-Za-z0-9]{7}-[A-Za-z0-9]{6}-0[A-Za-z0-9]\@//;
  594.  
  595.   # Sendmail:
  596.   $val =~ s/<20\d\d[01]\d[0123]\d[012]\d[012345]\d[012345]\d\.
  597.            [A-F0-9]{10,12}\@//gx;
  598.  
  599.   # try to split Message-ID segments on probable ID boundaries. Note that
  600.   # Outlook message-ids seem to contain a server identifier ID in the last
  601.   # 8 bytes before the @.  Make sure this becomes its own token, it's a
  602.   # great spam-sign for a learning system!  Be sure to split on ".".
  603.   $val =~ s/[^_A-Za-z0-9]/ /g;
  604.   $val;
  605. }
  606.  
  607. sub pre_chew_received {
  608.   my ($self, $val) = @_;
  609.  
  610.   # Thanks to Dan for these.  Trim out "useless" tokens; sendmail-ish IDs
  611.   # and valid-format RFC-822/2822 dates
  612.  
  613.   $val =~ s/\swith\sSMTP\sid\sg[\dA-Z]{10,12}\s/ /gs;  # Sendmail
  614.   $val =~ s/\swith\sESMTP\sid\s[\dA-F]{10,12}\s/ /gs;  # Sendmail
  615.   $val =~ s/\bid\s[a-zA-Z0-9]{7,20}\b/ /gs;    # Sendmail
  616.   $val =~ s/\bid\s[A-Za-z0-9]{7}-[A-Za-z0-9]{6}-0[A-Za-z0-9]/ /gs; # exim
  617.  
  618.   $val =~ s/(?:(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun),\s)?
  619.            [0-3\s]?[0-9]\s
  620.            (?:Jan|Feb|Ma[ry]|Apr|Ju[nl]|Aug|Sep|Oct|Nov|Dec)\s
  621.            (?:19|20)?[0-9]{2}\s
  622.            [0-2][0-9](?:\:[0-5][0-9]){1,2}\s
  623.            (?:\s*\(|\)|\s*(?:[+-][0-9]{4})|\s*(?:UT|[A-Z]{2,3}T))*
  624.            //gx;
  625.  
  626.   # IPs: break down to nearest /24, to reduce hapaxes -- EXCEPT for
  627.   # IPs in the 10 and 192.168 ranges, they gets lots of significant tokens
  628.   # (on both sides)
  629.   # also make a dup with the full IP, as fodder for
  630.   # bayes_dump_to_trusted_networks: "H*r:ip*aaa.bbb.ccc.ddd"
  631.   $val =~ s{\b(\d{1,3}\.)(\d{1,3}\.)(\d{1,3})(\.\d{1,3})\b}{
  632.            if ($2 eq '10' || ($2 eq '192' && $3 eq '168')) {
  633.              $1.$2.$3.$4.
  634.         " ip*".$1.$2.$3.$4." ";
  635.            } else {
  636.              $1.$2.$3.
  637.         " ip*".$1.$2.$3.$4." ";
  638.            }
  639.          }gex;
  640.  
  641.   # trim these: they turn out as the most common tokens, but with a
  642.   # prob of about .5.  waste of space!
  643.   $val =~ s/\b(?:with|from|for|SMTP|ESMTP)\b/ /g;
  644.  
  645.   $val;
  646. }
  647.  
  648. sub pre_chew_addr_header {
  649.   my ($self, $val) = @_;
  650.   local ($_);
  651.  
  652.   my @addrs = $self->{main}->find_all_addrs_in_line ($val);
  653.   my @toks = ();
  654.   foreach (@addrs) {
  655.     push (@toks, $self->tokenize_mail_addrs ($_));
  656.   }
  657.   return join (' ', @toks);
  658. }
  659.  
  660. sub tokenize_mail_addrs {
  661.   my ($self, $addr) = @_;
  662.  
  663.   ($addr =~ /(.+)\@(.+)$/) or return ();
  664.   my @toks = ();
  665.   push(@toks, "U*".$1, "D*".$2);
  666.   $_ = $2; while (s/^[^\.]+\.(.+)$/$1/gs) { push(@toks, "D*".$1); }
  667.   return @toks;
  668. }
  669.  
  670. ###########################################################################
  671.  
  672. sub ignore_message {
  673.   my ($self,$PMS) = @_;
  674.  
  675.   return 0 unless $self->{use_ignores};
  676.  
  677.   my $ignore = $PMS->check_from_in_list('bayes_ignore_from')
  678.             || $PMS->check_to_in_list('bayes_ignore_to');
  679.  
  680.   dbg("bayes: not using bayes, bayes_ignore_from or _to rule") if $ignore;
  681.  
  682.   return $ignore;
  683. }
  684.  
  685. ###########################################################################
  686.  
  687. sub learn {
  688.   my ($self, $isspam, $msg, $id) = @_;
  689.  
  690.   if (!$self->{conf}->{use_bayes}) { return; }
  691.   if (!defined $msg) { return; }
  692.  
  693.   if( $self->{use_ignores} )  # Remove test when PerMsgStatus available.
  694.   {
  695.     # DMK, koppel@ece.lsu.edu:  Hoping that the ultimate fix to bug 2263 will
  696.     # make it unnecessary to construct a PerMsgStatus here.
  697.     my $PMS = new Mail::SpamAssassin::PerMsgStatus $self->{main}, $msg;
  698.     my $ignore = $self->ignore_message($PMS);
  699.     $PMS->finish();
  700.     return if $ignore;
  701.   }
  702.  
  703.   my $msgdata = $self->get_body_from_msg ($msg);
  704.   my $ret;
  705.  
  706.   eval {
  707.     local $SIG{'__DIE__'};    # do not run user die() traps in here
  708.  
  709.     my $ok;
  710.     if ($self->{main}->{learn_to_journal}) {
  711.       # If we're going to learn to journal, we'll try going r/o first...
  712.       # If that fails for some reason, let's try going r/w.  This happens
  713.       # if the DB doesn't exist yet.
  714.       $ok = $self->{store}->tie_db_readonly() || $self->{store}->tie_db_writable();
  715.     } else {
  716.       $ok = $self->{store}->tie_db_writable();
  717.     }
  718.  
  719.     if ($ok) {
  720.       $ret = $self->learn_trapped ($isspam, $msg, $msgdata, $id);
  721.  
  722.       if (!$self->{main}->{learn_caller_will_untie}) {
  723.         $self->{store}->untie_db();
  724.       }
  725.     }
  726.   };
  727.  
  728.   if ($@) {        # if we died, untie the dbs.
  729.     my $failure = $@;
  730.     $self->{store}->untie_db();
  731.     die "bayes: $failure";
  732.   }
  733.  
  734.   return $ret;
  735. }
  736.  
  737. # this function is trapped by the wrapper above
  738. sub learn_trapped {
  739.   my ($self, $isspam, $msg, $msgdata, $msgid) = @_;
  740.   my @msgid = ( $msgid );
  741.  
  742.   if (!defined $msgid) {
  743.     @msgid = $self->get_msgid($msg);
  744.   }
  745.  
  746.   foreach $msgid ( @msgid ) {
  747.     my $seen = $self->{store}->seen_get ($msgid);
  748.  
  749.     if (defined ($seen)) {
  750.       if (($seen eq 's' && $isspam) || ($seen eq 'h' && !$isspam)) {
  751.         dbg("bayes: $msgid already learnt correctly, not learning twice");
  752.         return 0;
  753.       } elsif ($seen !~ /^[hs]$/) {
  754.         warn("bayes: db_seen corrupt: value='$seen' for $msgid, ignored");
  755.       } else {
  756.         # bug 3704: If the message was already learned, don't try learning it again.
  757.         # this prevents, for instance, manually learning as spam, then autolearning
  758.         # as ham, or visa versa.
  759.         if ($self->{main}->{learn_no_relearn}) {
  760.       dbg("bayes: $msgid already learnt as opposite, not re-learning");
  761.       return 0;
  762.     }
  763.  
  764.         dbg("bayes: $msgid already learnt as opposite, forgetting first");
  765.  
  766.         # kluge so that forget() won't untie the db on us ...
  767.         my $orig = $self->{main}->{learn_caller_will_untie};
  768.         $self->{main}->{learn_caller_will_untie} = 1;
  769.  
  770.         my $fatal = !defined $self->forget ($msg);
  771.  
  772.         # reset the value post-forget() ...
  773.         $self->{main}->{learn_caller_will_untie} = $orig;
  774.     
  775.         # forget() gave us a fatal error, so propagate that up
  776.         if ($fatal) {
  777.           dbg("bayes: forget() returned a fatal error, so learn() will too");
  778.       return;
  779.         }
  780.       }
  781.  
  782.       # we're only going to have seen this once, so stop if it's been
  783.       # seen already
  784.       last;
  785.     }
  786.   }
  787.  
  788.   # Now that we're sure we haven't seen this message before ...
  789.   $msgid = $msgid[0];
  790.  
  791.   if ($isspam) {
  792.     $self->{store}->nspam_nham_change (1, 0);
  793.   } else {
  794.     $self->{store}->nspam_nham_change (0, 1);
  795.   }
  796.  
  797.   my $msgatime = $msg->receive_date();
  798.  
  799.   # If the message atime comes back as being more than 1 day in the
  800.   # future, something's messed up and we should revert to current time as
  801.   # a safety measure.
  802.   #
  803.   $msgatime = time if ( $msgatime - time > 86400 );
  804.  
  805.   my $tokens = $self->tokenize($msg, $msgdata);
  806.  
  807.   if ($isspam) {
  808.     $self->{store}->multi_tok_count_change(1, 0, $tokens, $msgatime);
  809.   } else {
  810.     $self->{store}->multi_tok_count_change(0, 1, $tokens, $msgatime);
  811.   }
  812.  
  813.   $self->{store}->seen_put ($msgid, ($isspam ? 's' : 'h'));
  814.   $self->{store}->cleanup();
  815.  
  816.   $self->{main}->call_plugins("bayes_learn", { toksref => $tokens,
  817.                            isspam => $isspam,
  818.                            msgid => $msgid,
  819.                            msgatime => $msgatime,
  820.                          });
  821.  
  822.   dbg("bayes: learned '$msgid', atime: $msgatime");
  823.  
  824.   1;
  825. }
  826.  
  827. ###########################################################################
  828.  
  829. sub forget {
  830.   my ($self, $msg, $id) = @_;
  831.  
  832.   if (!$self->{conf}->{use_bayes}) { return; }
  833.   if (!defined $msg) { return; }
  834.  
  835.   my $msgdata = $self->get_body_from_msg ($msg);
  836.   my $ret;
  837.  
  838.   # we still tie for writing here, since we write to the seen db
  839.   # synchronously
  840.   eval {
  841.     local $SIG{'__DIE__'};    # do not run user die() traps in here
  842.  
  843.     my $ok;
  844.     if ($self->{main}->{learn_to_journal}) {
  845.       # If we're going to learn to journal, we'll try going r/o first...
  846.       # If that fails for some reason, let's try going r/w.  This happens
  847.       # if the DB doesn't exist yet.
  848.       $ok = $self->{store}->tie_db_readonly() || $self->{store}->tie_db_writable();
  849.     } else {
  850.       $ok = $self->{store}->tie_db_writable();
  851.     }
  852.  
  853.     if ($ok) {
  854.       $ret = $self->forget_trapped ($msg, $msgdata, $id);
  855.  
  856.       if (!$self->{main}->{learn_caller_will_untie}) {
  857.         $self->{store}->untie_db();
  858.       }
  859.     }
  860.   };
  861.  
  862.   if ($@) {        # if we died, untie the dbs.
  863.     my $failure = $@;
  864.     $self->{store}->untie_db();
  865.     die "bayes: $failure";
  866.   }
  867.  
  868.   return $ret;
  869. }
  870.  
  871. # this function is trapped by the wrapper above
  872. sub forget_trapped {
  873.   my ($self, $msg, $msgdata, $msgid) = @_;
  874.   my @msgid = ( $msgid );
  875.   my $isspam;
  876.  
  877.   if (!defined $msgid) {
  878.     @msgid = $self->get_msgid($msg);
  879.   }
  880.  
  881.   while( $msgid = shift @msgid ) {
  882.     my $seen = $self->{store}->seen_get ($msgid);
  883.  
  884.     if (defined ($seen)) {
  885.       if ($seen eq 's') {
  886.         $isspam = 1;
  887.       } elsif ($seen eq 'h') {
  888.         $isspam = 0;
  889.       } else {
  890.         dbg("bayes: forget: msgid $msgid seen entry is neither ham nor spam, ignored");
  891.         return 0;
  892.       }
  893.  
  894.       # messages should only be learned once, so stop if we find a msgid
  895.       # which was seen before
  896.       last;
  897.     }
  898.     else {
  899.       dbg("bayes: forget: msgid $msgid not learnt, ignored");
  900.     }
  901.   }
  902.  
  903.   # This message wasn't learnt before, so return
  904.   if (!defined $isspam) {
  905.     dbg("bayes: forget: no msgid from this message has been learnt, skipping message");
  906.     return 0;
  907.   }
  908.   elsif ($isspam) {
  909.     $self->{store}->nspam_nham_change (-1, 0);
  910.   }
  911.   else {
  912.     $self->{store}->nspam_nham_change (0, -1);
  913.   }
  914.  
  915.   my $tokens = $self->tokenize($msg, $msgdata);
  916.  
  917.   if ($isspam) {
  918.     $self->{store}->multi_tok_count_change (-1, 0, $tokens);
  919.   } else {
  920.     $self->{store}->multi_tok_count_change (0, -1, $tokens);
  921.   }
  922.  
  923.   $self->{store}->seen_delete ($msgid);
  924.   $self->{store}->cleanup();
  925.  
  926.   $self->{main}->call_plugins("bayes_forget", { toksref => $tokens,
  927.                         isspam => $isspam,
  928.                         msgid => $msgid,
  929.                           });
  930.  
  931.   1;
  932. }
  933.  
  934. ###########################################################################
  935.  
  936. sub get_msgid {
  937.   my ($self, $msg) = @_;
  938.  
  939.   my @msgid = ();
  940.  
  941.   my $msgid = $msg->get_header("Message-Id");
  942.   if (defined $msgid && $msgid ne '' && $msgid !~ /^\s*<\s*(?:\@sa_generated)?>.*$/) {
  943.     # remove \r and < and > prefix/suffixes
  944.     chomp $msgid;
  945.     $msgid =~ s/^<//; $msgid =~ s/>.*$//g;
  946.     push(@msgid, $msgid);
  947.   }
  948.  
  949.   # Use sha1_hex(Date:, last received: and top N bytes of body)
  950.   # where N is MIN(1024 bytes, 1/2 of body length)
  951.   #
  952.   my $date = $msg->get_header("Date");
  953.   $date = "None" if (!defined $date || $date eq ''); # No Date?
  954.  
  955.   my @rcvd = $msg->get_header("Received");
  956.   my $rcvd = $rcvd[$#rcvd];
  957.   $rcvd = "None" if (!defined $rcvd || $rcvd eq ''); # No Received?
  958.  
  959.   # Make a copy since pristine_body is a reference ...
  960.   my $body = join('', $msg->get_pristine_body());
  961.   if (length($body) > 64) { # Small Body?
  962.     my $keep = ( length $body > 2048 ? 1024 : int(length($body) / 2) );
  963.     substr($body, $keep) = '';
  964.   }
  965.  
  966.   unshift(@msgid, sha1_hex($date."\000".$rcvd."\000".$body).'@sa_generated');
  967.  
  968.   return wantarray ? @msgid : $msgid[0];
  969. }
  970.  
  971. sub get_body_from_msg {
  972.   my ($self, $msg) = @_;
  973.  
  974.   if (!ref $msg) {
  975.     # I have no idea why this seems to happen. TODO
  976.     warn "bayes: msg not a ref: '$msg'";
  977.     return { };
  978.   }
  979.  
  980.   $msg->extract_message_metadata ($self->{main});
  981.   my $permsgstatus =
  982.         Mail::SpamAssassin::PerMsgStatus->new($self->{main}, $msg);
  983.   my $msgdata = $self->get_msgdata_from_permsgstatus ($permsgstatus);
  984.   $permsgstatus->finish();
  985.  
  986.   if (!defined $msgdata) {
  987.     # why?!
  988.     warn "bayes: failed to get body for ".scalar($self->get_msgid($self->{msg}))."\n";
  989.     return { };
  990.   }
  991.  
  992.   return $msgdata;
  993. }
  994.  
  995. sub get_msgdata_from_permsgstatus {
  996.   my ($self, $msg) = @_;
  997.  
  998.   my $msgdata = { };
  999.   $msgdata->{bayes_token_body} = $msg->{msg}->get_visible_rendered_body_text_array();
  1000.   $msgdata->{bayes_token_inviz} = $msg->{msg}->get_invisible_rendered_body_text_array();
  1001.   @{$msgdata->{bayes_token_uris}} = $msg->get_uri_list();
  1002.   return $msgdata;
  1003. }
  1004.  
  1005. ###########################################################################
  1006.  
  1007. sub sync {
  1008.   my ($self, $sync, $expire, $opts) = @_;
  1009.   if (!$self->{conf}->{use_bayes}) { return 0; }
  1010.  
  1011.   if ($sync) {
  1012.     dbg("bayes: bayes journal sync starting");
  1013.     $self->{store}->sync($opts);
  1014.     dbg("bayes: bayes journal sync completed");
  1015.   }
  1016.   if ($expire) {
  1017.     dbg("bayes: expiry starting");
  1018.     $self->{store}->expire_old_tokens($opts);
  1019.     dbg("bayes: expiry completed");
  1020.   }
  1021.  
  1022.   return 0;
  1023. }
  1024.  
  1025. ###########################################################################
  1026.  
  1027. # compute the probability that that token is spammish
  1028. sub compute_prob_for_token {
  1029.   my ($self, $token, $ns, $nn, $s, $n) = @_;
  1030.  
  1031.   # we allow the caller to give us the token information, just
  1032.   # to save a potentially expensive lookup
  1033.   if (!defined($s) || !defined($n)) {
  1034.     ($s, $n, undef) = $self->{store}->tok_get ($token);
  1035.   }
  1036.  
  1037.   return if ($s == 0 && $n == 0);
  1038.  
  1039.   if (!USE_ROBINSON_FX_EQUATION_FOR_LOW_FREQS) {
  1040.     return if ($s + $n < 10);      # ignore low-freq tokens
  1041.   }
  1042.  
  1043.   if (!$self->{use_hapaxes}) {
  1044.     return if ($s + $n < 2);
  1045.   }
  1046.  
  1047.   return if ( $ns == 0 || $nn == 0 );
  1048.  
  1049.   my $ratios = ($s / $ns);
  1050.   my $ration = ($n / $nn);
  1051.  
  1052.   my $prob;
  1053.  
  1054.   if ($ratios == 0 && $ration == 0) {
  1055.     warn "bayes: oops? ratios == ration == 0";
  1056.     return;
  1057.   } else {
  1058.     $prob = ($ratios) / ($ration + $ratios);
  1059.   }
  1060.  
  1061.   if (USE_ROBINSON_FX_EQUATION_FOR_LOW_FREQS) {
  1062.     # use Robinson's f(x) equation for low-n tokens, instead of just
  1063.     # ignoring them
  1064.     my $robn = $s+$n;
  1065.     $prob = ($Mail::SpamAssassin::Bayes::Combine::FW_S_DOT_X + ($robn * $prob))
  1066.                              /
  1067.             ($Mail::SpamAssassin::Bayes::Combine::FW_S_CONSTANT + $robn);
  1068.   }
  1069.  
  1070.   if ($self->{log_raw_counts}) {
  1071.     $self->{raw_counts} .= " s=$s,n=$n ";
  1072.   }
  1073.  
  1074.   return $prob;
  1075. }
  1076.  
  1077. ###########################################################################
  1078. # If a token is neither hammy nor spammy, return 0.
  1079. # For a spammy token, return the minimum number of additional ham messages
  1080. # it would have had to appear in to no longer be spammy.  Hammy tokens
  1081. # are handled similarly.  That's what the function does (at the time
  1082. # of this writing, 31 July 2003, 16:02:55 CDT).  It would be slightly
  1083. # more useful if it returned the number of /additional/ ham messages
  1084. # a spammy token would have to appear in to no longer be spammy but I
  1085. # fear that might require the solution to a cubic equation, and I
  1086. # just don't have the time for that now.
  1087.  
  1088. sub compute_declassification_distance {
  1089.   my ($self, $Ns, $Nn, $ns, $nn, $prob) = @_;
  1090.  
  1091.   return 0 if $ns == 0 && $nn == 0;
  1092.  
  1093.   if (!USE_ROBINSON_FX_EQUATION_FOR_LOW_FREQS) {return 0 if ($ns + $nn < 10);}
  1094.   if (!$self->{use_hapaxes}) {return 0 if ($ns + $nn < 2);}
  1095.  
  1096.   return 0 if $Ns == 0 || $Nn == 0;
  1097.   return 0 if abs( $prob - 0.5 ) <
  1098.                 $Mail::SpamAssassin::Bayes::Combine::MIN_PROB_STRENGTH;
  1099.  
  1100.   my ($Na,$na,$Nb,$nb) = $prob > 0.5 ? ($Nn,$nn,$Ns,$ns) : ($Ns,$ns,$Nn,$nn);
  1101.   my $p = 0.5 - $Mail::SpamAssassin::Bayes::Combine::MIN_PROB_STRENGTH;
  1102.  
  1103.   return int( 1.0 - 1e-6 + $nb * $Na * $p / ($Nb * ( 1 - $p )) ) - $na
  1104.     unless USE_ROBINSON_FX_EQUATION_FOR_LOW_FREQS;
  1105.  
  1106.   my $s = $Mail::SpamAssassin::Bayes::Combine::FW_S_CONSTANT;
  1107.   my $sx = $Mail::SpamAssassin::Bayes::Combine::FW_S_DOT_X;
  1108.   my $a = $Nb * ( 1 - $p );
  1109.   my $b = $Nb * ( $sx + $nb * ( 1 - $p ) - $p * $s ) - $p * $Na * $nb;
  1110.   my $c = $Na * $nb * ( $sx - $p * ( $s + $nb ) );
  1111.   my $discrim = $b * $b - 4 * $a * $c;
  1112.   my $disc_max_0 = $discrim < 0 ? 0 : $discrim;
  1113.   my $dd_exact = ( 1.0 - 1e-6 + ( -$b + sqrt( $disc_max_0 ) ) / ( 2*$a ) ) - $na;
  1114.  
  1115.   # This shouldn't be necessary.  Should not be < 1
  1116.   return $dd_exact < 1 ? 1 : int($dd_exact);
  1117. }
  1118.  
  1119.  
  1120. # Check to make sure we can tie() the DB, and we have enough entries to do a scan
  1121. # if we're told the caller will untie(), go ahead and leave the db tied.
  1122. sub is_scan_available {
  1123.   my $self = shift;
  1124.  
  1125.   return 0 unless $self->{conf}->{use_bayes};
  1126.   return 0 unless $self->{store}->tie_db_readonly();
  1127.  
  1128.   # We need the DB to stay tied, so if the journal sync occurs, don't untie!
  1129.   my $caller_untie = $self->{main}->{learn_caller_will_untie};
  1130.   $self->{main}->{learn_caller_will_untie} = 1;
  1131.  
  1132.   # Do a journal sync if necessary.  Do this before the nspam_nham_get()
  1133.   # call since the sync may cause an update in the number of messages
  1134.   # learnt.
  1135.   $self->opportunistic_calls(1);
  1136.  
  1137.   # Reset the variable appropriately
  1138.   $self->{main}->{learn_caller_will_untie} = $caller_untie;
  1139.  
  1140.   my ($ns, $nn) = $self->{store}->nspam_nham_get();
  1141.  
  1142.   if ($ns < $self->{conf}->{bayes_min_spam_num}) {
  1143.     dbg("bayes: not available for scanning, only $ns spam(s) in bayes DB < ".$self->{conf}->{bayes_min_spam_num});
  1144.     if (!$self->{main}->{learn_caller_will_untie}) {
  1145.       $self->{store}->untie_db();
  1146.     }
  1147.     return 0;
  1148.   }
  1149.   if ($nn < $self->{conf}->{bayes_min_ham_num}) {
  1150.     dbg("bayes: not available for scanning, only $nn ham(s) in bayes DB < ".$self->{conf}->{bayes_min_ham_num});
  1151.     if (!$self->{main}->{learn_caller_will_untie}) {
  1152.       $self->{store}->untie_db();
  1153.     }
  1154.     return 0;
  1155.   }
  1156.  
  1157.   return 1;
  1158. }
  1159.  
  1160. ###########################################################################
  1161. # Finally, the scoring function for testing mail.
  1162.  
  1163. sub scan {
  1164.   my ($self, $permsgstatus, $msg) = @_;
  1165.   my $score;
  1166.  
  1167.   # When we're doing a scan, we'll guarantee that we'll do the untie,
  1168.   # so override the global setting until we're done.
  1169.   my $caller_untie = $self->{main}->{learn_caller_will_untie};
  1170.   $self->{main}->{learn_caller_will_untie} = 1;
  1171.  
  1172.   goto skip if ($self->ignore_message($permsgstatus));
  1173.  
  1174.   goto skip unless $self->is_scan_available();
  1175.  
  1176.   my ($ns, $nn) = $self->{store}->nspam_nham_get();
  1177.  
  1178.   if ($self->{log_raw_counts}) {
  1179.     $self->{raw_counts} = " ns=$ns nn=$nn ";
  1180.   }
  1181.  
  1182.   dbg("bayes: corpus size: nspam = $ns, nham = $nn");
  1183.  
  1184.   my $msgdata = $self->get_msgdata_from_permsgstatus ($permsgstatus);
  1185.  
  1186.   my $msgtokens = $self->tokenize($msg, $msgdata);
  1187.  
  1188.   my $tokensdata = $self->{store}->tok_get_all(keys %{$msgtokens});
  1189.  
  1190.   my %pw;
  1191.  
  1192.   foreach my $tokendata (@{$tokensdata}) {
  1193.     my ($token, $tok_spam, $tok_ham, $atime) = @{$tokendata};
  1194.     my $prob = $self->compute_prob_for_token($token, $ns, $nn, $tok_spam, $tok_ham);
  1195.     if (defined($prob)) {
  1196.       $pw{$token}->{prob} = $prob;
  1197.       $pw{$token}->{spam_count} = $tok_spam;
  1198.       $pw{$token}->{ham_count} = $tok_ham;
  1199.       $pw{$token}->{atime} = $atime;
  1200.     }
  1201.   }
  1202.  
  1203.   # If none of the tokens were found in the DB, we're going to skip
  1204.   # this message...
  1205.   if (!keys %pw) {
  1206.     dbg("bayes: cannot use bayes on this message; none of the tokens were found in the database");
  1207.     goto skip;
  1208.   }
  1209.  
  1210.   my $tcount_total = keys %{$msgtokens};
  1211.   my $tcount_learned = keys %pw;
  1212.  
  1213.   # Figure out the message receive time (used as atime below)
  1214.   # If the message atime comes back as being in the future, something's
  1215.   # messed up and we should revert to current time as a safety measure.
  1216.   #
  1217.   my $msgatime = $msg->receive_date();
  1218.   my $now = time;
  1219.   $msgatime = $now if ( $msgatime > $now );
  1220.  
  1221.   # now take the $count most significant tokens and calculate probs using
  1222.   # Robinson's formula.
  1223.   my $count = N_SIGNIFICANT_TOKENS;
  1224.   my @sorted = ();
  1225.  
  1226.   my ($tcount_spammy,$tcount_hammy) = (0,0);
  1227.   my $tinfo_spammy = $permsgstatus->{bayes_token_info_spammy} = [];
  1228.   my $tinfo_hammy = $permsgstatus->{bayes_token_info_hammy} = [];
  1229.  
  1230.   my @touch_tokens;
  1231.  
  1232.   for (sort {
  1233.               abs($pw{$b}->{prob} - 0.5) <=> abs($pw{$a}->{prob} - 0.5)
  1234.             } keys %pw)
  1235.   {
  1236.     if ($count-- < 0) { last; }
  1237.     my $pw = $pw{$_}->{prob};
  1238.     next if (abs($pw - 0.5) < 
  1239.                 $Mail::SpamAssassin::Bayes::Combine::MIN_PROB_STRENGTH);
  1240.  
  1241.     # What's more expensive, scanning headers for HAMMYTOKENS and
  1242.     # SPAMMYTOKENS tags that aren't there or collecting data that
  1243.     # won't be used?  Just collecting the data is certainly simpler.
  1244.     #
  1245.     my $raw_token = $msgtokens->{$_} || "(unknown)";
  1246.     my $s = $pw{$_}->{spam_count};
  1247.     my $n = $pw{$_}->{ham_count};
  1248.     my $a = $pw{$_}->{atime};
  1249.     push @$tinfo_spammy, [$raw_token,$pw,$s,$n,$a] if $pw >= 0.5 && ++$tcount_spammy;
  1250.     push @$tinfo_hammy,  [$raw_token,$pw,$s,$n,$a] if $pw <  0.5 && ++$tcount_hammy;
  1251.  
  1252.     push (@sorted, $pw);
  1253.  
  1254.     # update the atime on this token, it proved useful
  1255.     push(@touch_tokens, $_);
  1256.  
  1257.     if (would_log('dbg', 'bayes') > 1) {
  1258.       dbg("bayes: token '$raw_token' => $pw");
  1259.     }
  1260.   }
  1261.  
  1262.   if (!@sorted || (REQUIRE_SIGNIFICANT_TOKENS_TO_SCORE > 0 && 
  1263.     $#sorted <= REQUIRE_SIGNIFICANT_TOKENS_TO_SCORE))
  1264.   {
  1265.     dbg("bayes: cannot use bayes on this message; not enough usable tokens found");
  1266.     goto skip;
  1267.   }
  1268.  
  1269.   $score = Mail::SpamAssassin::Bayes::Combine::combine($ns, $nn, \@sorted);
  1270.  
  1271.   # Couldn't come up with a probability?
  1272.   goto skip unless defined $score;
  1273.  
  1274.   dbg("bayes: score = $score");
  1275.  
  1276.   # no need to call tok_touch_all unless there were significant
  1277.   # tokens and a score was returned
  1278.   # we don't really care about the return value here
  1279.   $self->{store}->tok_touch_all(\@touch_tokens, $msgatime);
  1280.  
  1281.   $permsgstatus->{bayes_nspam} = $ns;
  1282.   $permsgstatus->{bayes_nham} = $nn;
  1283.  
  1284.   if ($self->{log_raw_counts}) {
  1285.     print "#Bayes-Raw-Counts: $self->{raw_counts}\n";
  1286.   }
  1287.  
  1288.   $self->{main}->call_plugins("bayes_scan", { toksref => $msgtokens,
  1289.                           probsref => \%pw,
  1290.                           score => $score,
  1291.                           msgatime => $msgatime,
  1292.                           significant_tokens => \@touch_tokens,
  1293.                         });
  1294.  
  1295. skip:
  1296.   if (!defined $score) {
  1297.     dbg("bayes: not scoring message, returning undef");
  1298.   }
  1299.  
  1300.   # Take any opportunistic actions we can take
  1301.   if ($self->{main}->{opportunistic_expire_check_only}) {
  1302.     # we're supposed to report on expiry only -- so do the
  1303.     # opportunistic_calls() run for the journal only.
  1304.     $self->opportunistic_calls(1);
  1305.     $permsgstatus->{bayes_expiry_due} = $self->{store}->expiry_due();
  1306.   }
  1307.   else {
  1308.     $self->opportunistic_calls();
  1309.   }
  1310.  
  1311.   # Do any cleanup we need to do
  1312.   $self->{store}->cleanup();
  1313.  
  1314.   # Reset the value accordingly
  1315.   $self->{main}->{learn_caller_will_untie} = $caller_untie;
  1316.  
  1317.   # If our caller won't untie the db, we need to do it.
  1318.   if (!$caller_untie) {
  1319.     $self->{store}->untie_db();
  1320.   }
  1321.  
  1322.   $permsgstatus->{tag_data}{BAYESTCHAMMY} = $tcount_hammy;
  1323.   $permsgstatus->{tag_data}{BAYESTCSPAMMY} = $tcount_spammy;
  1324.   $permsgstatus->{tag_data}{BAYESTCLEARNED} = $tcount_learned;
  1325.   $permsgstatus->{tag_data}{BAYESTC} = $tcount_total;
  1326.  
  1327.   return $score;
  1328. }
  1329.  
  1330. sub opportunistic_calls {
  1331.   my($self, $journal_only) = @_;
  1332.  
  1333.   # If we're not already tied, abort.
  1334.   if (!$self->{store}->db_readable()) {
  1335.     dbg("bayes: opportunistic call attempt failed, DB not readable");
  1336.     return;
  1337.   }
  1338.  
  1339.   # Is an expire or sync running?
  1340.   my $running_expire = $self->{store}->get_running_expire_tok();
  1341.   if ( defined $running_expire && $running_expire+$OPPORTUNISTIC_LOCK_VALID > time() ) {
  1342.     dbg("bayes: opportunistic call attempt skipped, found fresh running expire magic token");
  1343.     return;
  1344.   }
  1345.  
  1346.   # handle expiry and syncing
  1347.   if (!$journal_only && $self->{store}->expiry_due()) {
  1348.     dbg("bayes: opportunistic call found expiry due");
  1349.  
  1350.     # sync will bring the DB R/W as necessary, and the expire will remove
  1351.     # the running_expire token, may untie as well.
  1352.     $self->sync(1,1);
  1353.   }
  1354.   elsif ( $self->{store}->sync_due() ) {
  1355.     dbg("bayes: opportunistic call found journal sync due");
  1356.  
  1357.     # sync will bring the DB R/W as necessary, may untie as well
  1358.     $self->sync(1,0);
  1359.  
  1360.     # We can only remove the running_expire token if we're doing R/W
  1361.     if ($self->{store}->db_writable()) {
  1362.       $self->{store}->remove_running_expire_tok();
  1363.     }
  1364.   }
  1365.  
  1366.   return;
  1367. }
  1368.  
  1369. ###########################################################################
  1370.  
  1371. sub dump_bayes_db {
  1372.   my($self, $magic, $toks, $regex) = @_;
  1373.  
  1374.   # allow dump to occur even if use_bayes disables everything else ...
  1375.   #return 0 unless $self->{conf}->{use_bayes};
  1376.   return 0 unless $self->{store}->tie_db_readonly();
  1377.   
  1378.   my @vars = $self->{store}->get_storage_variables();
  1379.  
  1380.   my($sb,$ns,$nh,$nt,$le,$oa,$bv,$js,$ad,$er,$na) = @vars;
  1381.  
  1382.   my $template = '%3.3f %10u %10u %10u  %s'."\n";
  1383.  
  1384.   if ( $magic ) {
  1385.     printf ($template, 0.0, 0, $bv, 0, 'non-token data: bayes db version');
  1386.     printf ($template, 0.0, 0, $ns, 0, 'non-token data: nspam');
  1387.     printf ($template, 0.0, 0, $nh, 0, 'non-token data: nham');
  1388.     printf ($template, 0.0, 0, $nt, 0, 'non-token data: ntokens');
  1389.     printf ($template, 0.0, 0, $oa, 0, 'non-token data: oldest atime');
  1390.     printf ($template, 0.0, 0, $na, 0, 'non-token data: newest atime') if ( $bv >= 2 );
  1391.     printf ($template, 0.0, 0, $sb, 0, 'non-token data: current scan-count') if ( $bv < 2 );
  1392.     printf ($template, 0.0, 0, $js, 0, 'non-token data: last journal sync atime') if ( $bv >= 2 );
  1393.     printf ($template, 0.0, 0, $le, 0, 'non-token data: last expiry atime');
  1394.     if ( $bv >= 2 ) {
  1395.       printf ($template, 0.0, 0, $ad, 0, 'non-token data: last expire atime delta');
  1396.       printf ($template, 0.0, 0, $er, 0, 'non-token data: last expire reduction count');
  1397.     }
  1398.   }
  1399.  
  1400.   if ( $toks ) {
  1401.     # let the store sort out the db_toks
  1402.     $self->{store}->dump_db_toks($template, $regex, @vars);
  1403.   }
  1404.  
  1405.   if (!$self->{main}->{learn_caller_will_untie}) {
  1406.     $self->{store}->untie_db();
  1407.   }
  1408.   return 1;
  1409. }
  1410.  
  1411. 1;
  1412.  
  1413. =back
  1414.  
  1415. =cut
  1416.