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 / BayesStore / SQL.pm < prev   
Text File  |  2006-11-29  |  59KB  |  2,351 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::BayesStore::SQL - SQL Bayesian Storage Module Implementation
  19.  
  20. =head1 SYNOPSIS
  21.  
  22. =head1 DESCRIPTION
  23.  
  24. This module implementes a SQL based bayesian storage module.
  25.  
  26. =cut
  27.  
  28. package Mail::SpamAssassin::BayesStore::SQL;
  29.  
  30. use strict;
  31. use warnings;
  32. use bytes;
  33.  
  34. use Mail::SpamAssassin::BayesStore;
  35. use Mail::SpamAssassin::Logger;
  36. use Digest::SHA1 qw(sha1);
  37.  
  38. use vars qw( @ISA );
  39.  
  40. @ISA = qw( Mail::SpamAssassin::BayesStore );
  41.  
  42. use constant HAS_DBI => eval { require DBI; };
  43.  
  44. =head1 METHODS
  45.  
  46. =head2 new
  47.  
  48. public class (Mail::SpamAssassin::BayesStore::SQL) new (Mail::Spamassassin::Bayes $bayes)
  49.  
  50. Description:
  51. This methods creates a new instance of the Mail::SpamAssassin::BayesStore::SQL
  52. object.  It expects to be passed an instance of the Mail::SpamAssassin:Bayes
  53. object which is passed into the Mail::SpamAssassin::BayesStore parent object.
  54.  
  55. This method sets up the database connection and determines the username to
  56. use in queries.
  57.  
  58. =cut
  59.  
  60. sub new {
  61.   my $class = shift;
  62.   $class = ref($class) || $class;
  63.  
  64.   my $self = $class->SUPER::new(@_);
  65.  
  66.   $self->{supported_db_version} = 3;
  67.   $self->{db_writable_p} = 0;
  68.  
  69.   if (!$self->{bayes}->{conf}->{bayes_sql_dsn}) {
  70.     dbg("bayes: invalid config, must set bayes_sql_dsn config variable\n");
  71.     return undef;
  72.   }
  73.  
  74.   $self->{_dsn} = $self->{bayes}->{conf}->{bayes_sql_dsn};
  75.   $self->{_dbuser} = $self->{bayes}->{conf}->{bayes_sql_username};
  76.   $self->{_dbpass} = $self->{bayes}->{conf}->{bayes_sql_password};
  77.  
  78.   $self->{_dbh} = undef;
  79.  
  80.   unless (HAS_DBI) {
  81.     dbg("bayes: unable to connect to database: DBI module not available: $!");
  82.   }
  83.  
  84.   if ($self->{bayes}->{conf}->{bayes_sql_override_username}) {
  85.     $self->{_username} = $self->{bayes}->{conf}->{bayes_sql_override_username};
  86.   }
  87.   else {
  88.     $self->{_username} = $self->{bayes}->{main}->{username};
  89.  
  90.     # Need to make sure that a username is set, so just in case there is
  91.     # no username set in main, set one here.
  92.     unless ($self->{_username}) {
  93.       $self->{_username} = "GLOBALBAYES";
  94.     }
  95.   }
  96.   dbg("bayes: using username: ".$self->{_username});
  97.  
  98.   return $self;
  99. }
  100.  
  101. =head2 tie_db_readonly
  102.  
  103. public instance (Boolean) tie_db_readonly ();
  104.  
  105. Description:
  106. This method ensures that the database connection is properly setup
  107. and working.  If necessary it will initialize a user's bayes variables
  108. so that they can begin using the database immediately.
  109.  
  110. =cut
  111.  
  112. sub tie_db_readonly {
  113.   my ($self) = @_;
  114.  
  115.   return 0 unless (HAS_DBI);
  116.  
  117.   if ($self->{_dbh}) {
  118.     # already connected, but connection has now become readonly
  119.     $self->{db_writable_p} = 0;
  120.     return 1;
  121.   }
  122.  
  123.   my $main = $self->{bayes}->{main};
  124.  
  125.   $self->read_db_configs();
  126.  
  127.   return 0 unless ($self->_connect_db());
  128.  
  129.   my $db_ver = $self->_get_db_version();
  130.   $self->{db_version} = $db_ver;
  131.   dbg("bayes: found bayes db version ".$self->{db_version});
  132.  
  133.   if ( $db_ver != $self->DB_VERSION ) {
  134.     warn("bayes: database version $db_ver is different than we understand (".$self->DB_VERSION."), aborting!");
  135.     $self->untie_db();
  136.     return 0;
  137.   }
  138.  
  139.   unless ($self->_initialize_db(0)) {
  140.     dbg("bayes: unable to initialize database for ".$self->{_username}." user, aborting!");
  141.     $self->untie_db();
  142.     return 0;
  143.   }
  144.  
  145.   return 1;
  146. }
  147.  
  148. =head2 tie_db_writable
  149.  
  150. public instance (Boolean) tie_db_writable ()
  151.  
  152. Description:
  153. This method ensures that the database connetion is properly setup
  154. and working. If necessary it will initialize a users bayes variables
  155. so that they can begin using the database immediately.
  156.  
  157. =cut
  158.  
  159. sub tie_db_writable {
  160.   my ($self) = @_;
  161.  
  162.   return 0 unless (HAS_DBI);
  163.  
  164.   if ($self->{_dbh}) {
  165.     # already connected, but now it will be writable
  166.     $self->{db_writable_p} = 1;
  167.     return 1;
  168.   }
  169.  
  170.   my $main = $self->{bayes}->{main};
  171.  
  172.   $self->read_db_configs();
  173.  
  174.   return 0 unless ($self->_connect_db());
  175.  
  176.   my $db_ver = $self->_get_db_version();
  177.   $self->{db_version} = $db_ver;
  178.   dbg("bayes: found bayes db version ".$self->{db_version});
  179.  
  180.   if ( $db_ver != $self->DB_VERSION ) {
  181.     warn("bayes: database version $db_ver is different than we understand (".$self->DB_VERSION."), aborting!");
  182.     $self->untie_db();
  183.     return 0;
  184.   }
  185.  
  186.   unless ($self->_initialize_db(1)) {
  187.     dbg("bayes: unable to initialize database for ".$self->{_username}." user, aborting!");
  188.  
  189.     $self->untie_db();
  190.     return 0;
  191.   }
  192.  
  193.   $self->{db_writable_p} = 1;
  194.  
  195.   return 1;
  196. }
  197.  
  198.  
  199. =head2 untie_db
  200.  
  201. public instance () untie_db ()
  202.  
  203. Description:
  204. This method is unused for the SQL based implementation.
  205.  
  206. =cut
  207.  
  208. sub untie_db {
  209.   my ($self) = @_;
  210.  
  211.   return unless (defined($self->{_dbh}));
  212.  
  213.   $self->{db_writable_p} = 0;
  214.  
  215.   $self->{_dbh}->disconnect();
  216.   $self->{_dbh} = undef;
  217. }
  218.  
  219. =head2 calculate_expire_delta
  220.  
  221. public instance (%) calculate_expire_delta (Integer $newest_atime,
  222.                                              Integer $start,
  223.                                              Integer $max_expire_mult)
  224.  
  225. Description:
  226. This method performs a calculation on the data to determine the optimum
  227. atime for token expiration.
  228.  
  229. =cut
  230.  
  231. sub calculate_expire_delta {
  232.   my ($self, $newest_atime, $start, $max_expire_mult) = @_;
  233.  
  234.   my %delta = (); # use a hash since an array is going to be very sparse
  235.  
  236.   return %delta unless (defined($self->{_dbh}));
  237.  
  238.   my $sql = "SELECT count(*)
  239.                FROM bayes_token
  240.               WHERE id = ?
  241.                 AND (? - atime) > ?";
  242.  
  243.   my $sth = $self->{_dbh}->prepare_cached($sql);
  244.     
  245.   unless (defined($sth)) {
  246.     dbg("bayes: calculate_expire_delta: SQL Error: ".$self->{_dbh}->errstr());
  247.     return %delta;
  248.   }
  249.  
  250.   for (my $i = 1; $i <= $max_expire_mult; $i<<=1) {
  251.     my $rc = $sth->execute($self->{_userid}, $newest_atime, $start * $i);
  252.  
  253.     unless ($rc) {
  254.       dbg("bayes: calculate_expire_delta: SQL error: ".$self->{_dbh}->errstr());
  255.       return undef;
  256.     }
  257.  
  258.     my ($count) = $sth->fetchrow_array();
  259.  
  260.     $delta{$i} = $count;
  261.   }
  262.   $sth->finish();
  263.  
  264.   return %delta;
  265. }
  266.  
  267. =head2 token_expiration
  268.  
  269. public instance (Integer, Integer,
  270.                  Integer, Integer) token_expiration(\% $opts,
  271.                                                     Integer $newdelta,
  272.                                                     @ @vars)
  273.  
  274. Description:
  275. This method performs the database specific expiration of tokens based on
  276. the passed in C<$newdelta> and C<@vars>.
  277.  
  278. =cut
  279.  
  280. sub token_expiration {
  281.   my ($self, $opts, $newdelta, @vars) = @_;
  282.  
  283.   my $num_hapaxes;
  284.   my $num_lowfreq;
  285.   my $deleted;
  286.  
  287.   # Figure out how old is too old...
  288.   my $too_old = $vars[10] - $newdelta; # tooold = newest - delta
  289.  
  290.   # if token atime > newest, reset to newest ...
  291.   my $sql = "UPDATE bayes_token SET atime = ?
  292.               WHERE id  = ?
  293.                 AND atime > ?";
  294.  
  295.   my $rows = $self->{_dbh}->do($sql, undef, $vars[10], $self->{_userid}, $vars[10]);
  296.  
  297.   unless (defined($rows)) {
  298.     dbg("bayes: token_expiration: SQL error: ".$self->{_dbh}->errstr());
  299.     $deleted = 0;
  300.     goto token_expiration_final;
  301.   }
  302.  
  303.   # Check to make sure the expire won't remove too many tokens
  304.   $sql = "SELECT count(token) FROM bayes_token
  305.            WHERE id = ?
  306.              AND atime < ?";
  307.  
  308.   my $sth = $self->{_dbh}->prepare_cached($sql);
  309.  
  310.   unless (defined($sth)) {
  311.     dbg("bayes: token_expiration: SQL error: ".$self->{_dbh}->errstr());
  312.     $deleted = 0;
  313.     goto token_expiration_final;
  314.   }
  315.  
  316.   my $rc = $sth->execute($self->{_userid}, $too_old);
  317.   
  318.   unless ($rc) {
  319.     dbg("bayes: token_expiration: SQL error: ".$self->{_dbh}->errstr());
  320.     $deleted = 0;
  321.     goto token_expiration_final;
  322.   }
  323.  
  324.   my ($count) = $sth->fetchrow_array();
  325.  
  326.   $sth->finish();
  327.  
  328.   # Sanity check: if we expired too many tokens, abort!
  329.   if ($vars[3] - $count < 100000) {
  330.     dbg("bayes: token expiration would expire too many tokens, aborting");
  331.     # set these appropriately so the next expire pass does the first pass
  332.     $deleted = 0;
  333.     $newdelta = 0;
  334.   }
  335.   else {
  336.     # Do the expire
  337.     $sql = "DELETE from bayes_token
  338.              WHERE id = ?
  339.                AND atime < ?";
  340.  
  341.     $rows = $self->{_dbh}->do($sql, undef, $self->{_userid}, $too_old);
  342.  
  343.     unless (defined($rows)) {
  344.       dbg("bayes: token_expiration: SQL error: ".$self->{_dbh}->errstr());
  345.       $deleted = 0;
  346.       goto token_expiration_final;
  347.     }
  348.  
  349.     $deleted = $rows;
  350.   }
  351.  
  352.   # Update the magic tokens as appropriate
  353.   $sql = "UPDATE bayes_vars SET token_count = token_count - ?,
  354.                                 last_expire = ?,
  355.                                 last_atime_delta = ?,
  356.                                 last_expire_reduce = ?
  357.                 WHERE id = ?";
  358.  
  359.   $rows = $self->{_dbh}->do($sql, undef, $deleted, time(), $newdelta, $deleted, $self->{_userid});
  360.  
  361.   unless (defined($rows)) {
  362.     # Very bad, we actually deleted the tokens, but were unable to update
  363.     # bayes_vars with the new data.
  364.     dbg("bayes: token_expiration: SQL error: ".$self->{_dbh}->errstr());
  365.     dbg("bayes: bayes database now in inconsistent state, suggest a backup/restore");
  366.     goto token_expiration_final;
  367.   }
  368.  
  369.   # If we didn't remove any tokens, the oldest token age wouldn't have changed
  370.   if ($deleted) {
  371.     # Now lets update the oldest_token_age value, shouldn't need to worry about
  372.     # newest_token_age. There is a slight race condition here, but the chance is
  373.     # small that we'll insert a new token with such an old atime
  374.     my $oldest_token_age = $self->_get_oldest_token_age();
  375.  
  376.     $sql = "UPDATE bayes_vars SET oldest_token_age = ? WHERE id = ?";
  377.  
  378.     $rows = $self->{_dbh}->do($sql, undef, $oldest_token_age, $self->{_userid});
  379.  
  380.     unless (defined($rows)) {
  381.       # not much more we can do here, so just warn the user and bail out
  382.       dbg("bayes: token_expiration: SQL error: ".$self->{_dbh}->errstr());
  383.       # yeah I know it's the next thing anyway, but here in case someone adds
  384.       # additional code below this block
  385.       goto token_expiration_final; 
  386.     }
  387.   }
  388.  
  389. token_expiration_final:
  390.   my $kept = $vars[3] - $deleted;
  391.  
  392.   $num_hapaxes = $self->_get_num_hapaxes() if ($opts->{verbose});
  393.   $num_lowfreq = $self->_get_num_lowfreq() if ($opts->{verbose});
  394.  
  395.   # Call untie_db() first so we unlock correctly etc. first
  396.   $self->untie_db();
  397.  
  398.   return ($kept, $deleted, $num_hapaxes, $num_lowfreq);
  399. }
  400.  
  401. =head2 sync_due
  402.  
  403. public instance (Boolean) sync_due ()
  404.  
  405. Description:
  406. This method determines if a database sync is currently required.
  407.  
  408. Unused for SQL based implementation.
  409.  
  410. =cut
  411.  
  412. sub sync_due {
  413.   my ($self) = @_;
  414.  
  415.   return 0;
  416. }
  417.  
  418. =head2 seen_get
  419.  
  420. public instance (String) seen_get (string $msgid)
  421.  
  422. Description:
  423. This method retrieves the stored value, if any, for C<$msgid>.  The return value
  424. is the stored string ('s' for spam and 'h' for ham) or undef if C<$msgid> is not
  425. found.
  426.  
  427. =cut
  428.  
  429. sub seen_get {
  430.   my ($self, $msgid) = @_;
  431.  
  432.   return undef unless (defined($self->{_dbh}));
  433.  
  434.   my $sql = "SELECT flag FROM bayes_seen
  435.               WHERE id = ?
  436.                 AND msgid = ?";
  437.  
  438.   my $sth = $self->{_dbh}->prepare_cached($sql);
  439.  
  440.   unless (defined($sth)) {
  441.     dbg("bayes: seen_get: SQL Error: ".$self->{_dbh}->errstr());
  442.     return undef;
  443.   }
  444.  
  445.   my $rc = $sth->execute($self->{_userid}, $msgid);
  446.   
  447.   unless ($rc) {
  448.     dbg("bayes: seen_get: SQL error: ".$self->{_dbh}->errstr());
  449.     return undef;
  450.   }
  451.  
  452.   my ($flag) = $sth->fetchrow_array();
  453.  
  454.   $sth->finish();
  455.   
  456.   return $flag;
  457. }
  458.  
  459. =head2 seen_put
  460.  
  461. public (Boolean) seen_put (string $msgid, char $flag)
  462.  
  463. Description:
  464. This method records C<$msgid> as the type given by C<$flag>.  C<$flag> is one of
  465. two values 's' for spam and 'h' for ham.
  466.  
  467. =cut
  468.  
  469. sub seen_put {
  470.   my ($self, $msgid, $flag) = @_;
  471.  
  472.   return 0 if (!$msgid);
  473.   return 0 if (!$flag);
  474.   
  475.   return 0 unless (defined($self->{_dbh}));
  476.  
  477.   my $sql = "INSERT INTO bayes_seen (id, msgid, flag)
  478.              VALUES (?,?,?)";
  479.   
  480.   my $rows = $self->{_dbh}->do($sql,
  481.                    undef,
  482.                    $self->{_userid}, $msgid, $flag);
  483.   
  484.   unless (defined($rows)) {
  485.     dbg("bayes: seen_put: SQL error: ".$self->{_dbh}->errstr());
  486.     return 0;
  487.   }
  488.  
  489.   dbg("bayes: seen ($msgid) put");
  490.   return 1;
  491. }
  492.  
  493. =head2 seen_delete
  494.  
  495. public instance (Boolean) seen_delete (string $msgid)
  496.  
  497. Description:
  498. This method removes C<$msgid> from the database.
  499.  
  500. =cut
  501.  
  502. sub seen_delete {
  503.   my ($self, $msgid) = @_;
  504.  
  505.   return 0 if (!$msgid);
  506.  
  507.   return 0 unless (defined($self->{_dbh}));
  508.  
  509.   my $sql = "DELETE FROM bayes_seen
  510.               WHERE id = ?
  511.                 AND msgid = ?";
  512.   
  513.   my $rows = $self->{_dbh}->do($sql,
  514.                    undef,
  515.                    $self->{_userid}, $msgid);
  516.  
  517.   unless (defined($rows)) {
  518.     dbg("bayes: seen_delete: SQL error: ".$self->{_dbh}->errstr());
  519.     return 0;
  520.   }
  521.  
  522.   return 1;
  523. }
  524.  
  525. =head2 get_storage_variables
  526.  
  527. public instance (@) get_storage_variables ()
  528.  
  529. Description:
  530. This method retrieves the various administrative variables used by
  531. the Bayes process and database.
  532.  
  533. The values returned in the array are in the following order:
  534.  
  535. 0: scan count base
  536.  
  537. 1: number of spam
  538.  
  539. 2: number of ham
  540.  
  541. 3: number of tokens in db
  542.  
  543. 4: last expire atime
  544.  
  545. 5: oldest token in db atime
  546.  
  547. 6: db version value
  548.  
  549. 7: last journal sync
  550.  
  551. 8: last atime delta
  552.  
  553. 9: last expire reduction count
  554.  
  555. 10: newest token in db atime
  556.  
  557. =cut
  558.  
  559. sub get_storage_variables {
  560.   my ($self) = @_;
  561.   my @values;
  562.  
  563.   return (0,0,0,0,0,0,0,0,0,0,0) unless (defined($self->{_dbh}));
  564.  
  565.   my $sql = "SELECT spam_count, ham_count, token_count, last_expire,
  566.                     last_atime_delta, last_expire_reduce, oldest_token_age,
  567.                     newest_token_age
  568.                FROM bayes_vars
  569.               WHERE id = ?";
  570.  
  571.   my $sth = $self->{_dbh}->prepare_cached($sql);
  572.  
  573.   unless (defined($sth)) {
  574.     dbg("bayes: get_storage_variables: SQL error: ".$self->{_dbh}->errstr());
  575.     return (0,0,0,0,0,0,0,0,0,0,0);
  576.   }
  577.  
  578.   my $rc = $sth->execute($self->{_userid});
  579.  
  580.   unless ($rc) {
  581.     dbg("bayes: get_storage_variables: SQL error: ".$self->{_dbh}->errstr());
  582.     return (0,0,0,0,0,0,0,0,0,0,0);
  583.   }
  584.  
  585.   my ($spam_count, $ham_count, $token_count,
  586.       $last_expire, $last_atime_delta, $last_expire_reduce,
  587.       $oldest_token_age, $newest_token_age) = $sth->fetchrow_array();
  588.  
  589.   $sth->finish();
  590.  
  591.   my $db_ver = $self->DB_VERSION;
  592.  
  593.   @values = (
  594.              0,
  595.              $spam_count,
  596.              $ham_count,
  597.              $token_count,
  598.              $last_expire,
  599.              $oldest_token_age,
  600.              $db_ver,
  601.              0, # we do not do journal syncs
  602.              $last_atime_delta,
  603.              $last_expire_reduce,
  604.              $newest_token_age
  605.              );
  606.  
  607.   return @values;
  608. }
  609.  
  610. =head2 dump_db_toks
  611.  
  612. public instance () dump_db_toks (String $template, String $regex, Array @vars)
  613.  
  614. Description:
  615. This method loops over all tokens, computing the probability for the token and then
  616. printing it out according to the passed in token.
  617.  
  618. =cut
  619.  
  620. sub dump_db_toks {
  621.   my ($self, $template, $regex, @vars) = @_;
  622.  
  623.   return unless (defined($self->{_dbh}));
  624.  
  625.   # 0/0 tokens don't count, but in theory we shouldn't have any
  626.   my $token_select = $self->_token_select_string();
  627.  
  628.   my $sql = "SELECT $token_select, spam_count, ham_count, atime
  629.                FROM bayes_token
  630.               WHERE id = ?
  631.                 AND (spam_count > 0 OR ham_count > 0)";
  632.  
  633.   my $sth = $self->{_dbh}->prepare($sql);
  634.  
  635.   unless (defined($sth)) {
  636.     dbg("bayes: dump_db_toks: SQL error: ".$self->{_dbh}->errstr());
  637.     return;
  638.   }
  639.  
  640.   my $rc = $sth->execute($self->{_userid});
  641.  
  642.   unless ($rc) {
  643.     dbg("bayes: dump_db_toks: SQL error: ".$self->{_dbh}->errstr());
  644.     return;
  645.   }  
  646.  
  647.   while (my ($token, $spam_count, $ham_count, $atime) = $sth->fetchrow_array()) {
  648.     my $prob = $self->{bayes}->compute_prob_for_token($token, $vars[1], $vars[2],
  649.                               $spam_count, $ham_count);
  650.     $prob ||= 0.5;
  651.  
  652.     my $encoded_token = unpack("H*", $token);
  653.     
  654.     printf $template,$prob,$spam_count,$ham_count,$atime,$encoded_token;
  655.   }
  656.  
  657.   $sth->finish();
  658.  
  659.   return;
  660. }
  661.  
  662. =head2 set_last_expire
  663.  
  664. public instance (Boolean) set_last_expire (Integer $time)
  665.  
  666. Description:
  667. This method sets the last expire time.
  668.  
  669. =cut
  670.  
  671. sub set_last_expire {
  672.   my ($self, $time) = @_;
  673.  
  674.   return 0 unless (defined($time));
  675.  
  676.   return 0 unless (defined($self->{_dbh}));
  677.  
  678.   my $sql = "UPDATE bayes_vars SET last_expire = ? WHERE id = ?";
  679.  
  680.   my $rows = $self->{_dbh}->do($sql,
  681.                    undef,
  682.                    $time,
  683.                    $self->{_userid});
  684.  
  685.   unless (defined($rows)) {
  686.     dbg("bayes: set_last_expire: SQL error: ".$self->{_dbh}->errstr());
  687.     return 0;
  688.   }
  689.  
  690.   return 1;
  691. }
  692.  
  693. =head2 get_running_expire_tok
  694.  
  695. public instance (String $time) get_running_expire_tok ()
  696.  
  697. Description:
  698. This method determines if an expire is currently running and returns
  699. the last time set.
  700.  
  701. There can be multiple times, so we just pull the greatest (most recent)
  702. value.
  703.  
  704. =cut
  705.  
  706. sub get_running_expire_tok {
  707.   my ($self) = @_;
  708.  
  709.   return 0 unless (defined($self->{_dbh}));
  710.  
  711.   my $sql = "SELECT max(runtime) from bayes_expire WHERE id = ?";
  712.  
  713.   my $sth = $self->{_dbh}->prepare_cached($sql);
  714.  
  715.   unless (defined($sth)) {
  716.     dbg("bayes: get_running_expire_tok: SQL error: ".$self->{_dbh}->errstr());
  717.     return 0;
  718.   }
  719.  
  720.   my $rc = $sth->execute($self->{_userid});
  721.  
  722.   unless ($rc) {
  723.     dbg("bayes: get_running_expire_tok: SQL error: ".$self->{_dbh}->errstr());
  724.     return 0;
  725.   }
  726.  
  727.   my ($runtime) = $sth->fetchrow_array();
  728.  
  729.   $sth->finish();
  730.  
  731.   return $runtime;
  732. }
  733.  
  734. =head2 set_running_expire_tok
  735.  
  736. public instance (String $time) set_running_expire_tok ()
  737.  
  738. Description:
  739. This method sets the time that an expire starts running.
  740.  
  741. =cut
  742.  
  743. sub set_running_expire_tok {
  744.   my ($self) = @_;
  745.  
  746.   return 0 unless (defined($self->{_dbh}));
  747.  
  748.   my $sql = "INSERT INTO bayes_expire (id,runtime) VALUES (?,?)";
  749.  
  750.   my $time = time();
  751.  
  752.   my $rows = $self->{_dbh}->do($sql,
  753.                    undef,
  754.                    $self->{_userid}, $time);
  755.   unless (defined($rows)) {
  756.     dbg("bayes: set_running_expire_tok: SQL error: ".$self->{_dbh}->errstr());
  757.     return undef;
  758.   }
  759.  
  760.   return $time;
  761. }
  762.  
  763. =head2 remove_running_expire_tok
  764.  
  765. public instance (Boolean) remove_running_expire_tok ()
  766.  
  767. Description:
  768. This method removes the row in the database that indicates that
  769. and expire is currently running.
  770.  
  771. =cut
  772.  
  773. sub remove_running_expire_tok {
  774.   my ($self) = @_;
  775.  
  776.   return 0 unless (defined($self->{_dbh}));
  777.  
  778.   my $sql = "DELETE from bayes_expire
  779.               WHERE id = ?";
  780.  
  781.   my $rows = $self->{_dbh}->do($sql, undef, $self->{_userid});
  782.  
  783.   unless (defined($rows)) {
  784.     dbg("bayes: remove_running_expire_tok: SQL error: ".$self->{_dbh}->errstr());
  785.     return 0;
  786.   }
  787.  
  788.   return 1;
  789. }
  790.  
  791. =head2 tok_get
  792.  
  793. public instance (Integer, Integer, Integer) tok_get (String $token)
  794.  
  795. Description:
  796. This method retrieves a specificed token (C<$token>) from the database
  797. and returns it's spam_count, ham_count and last access time.
  798.  
  799. =cut
  800.  
  801. sub tok_get {
  802.   my ($self, $token) = @_;
  803.  
  804.   return (0,0,0) unless (defined($self->{_dbh}));
  805.  
  806.   my $sql = "SELECT spam_count, ham_count, atime
  807.                FROM bayes_token
  808.               WHERE id = ?
  809.                 AND token = ?";
  810.  
  811.   my $sth = $self->{_dbh}->prepare_cached($sql);
  812.  
  813.   unless (defined($sth)) {
  814.     dbg("bayes: tok_get: SQL error: ".$self->{_dbh}->errstr());
  815.     return (0,0,0);
  816.   }
  817.  
  818.   my $rc = $sth->execute($self->{_userid}, $token);
  819.  
  820.   unless ($rc) {
  821.     dbg("bayes: tok_get: SQL error: ".$self->{_dbh}->errstr());
  822.     return (0,0,0);
  823.   }
  824.  
  825.   my ($spam_count, $ham_count, $atime) = $sth->fetchrow_array();
  826.  
  827.   $sth->finish();
  828.  
  829.   $spam_count = 0 if (!$spam_count || $spam_count < 0);
  830.   $ham_count = 0 if (!$ham_count || $ham_count < 0);
  831.   $atime = 0 if (!$atime);
  832.  
  833.   return ($spam_count, $ham_count, $atime)
  834. }
  835.  
  836. =head2 tok_get_all
  837.  
  838. public instance (\@) tok_get (@ $tokens)
  839.  
  840. Description:
  841. This method retrieves the specified tokens (C<$tokens>) from storage and returns
  842. an array ref of arrays spam count, ham acount and last access time.
  843.  
  844. =cut
  845.  
  846. sub tok_get_all {
  847.   my ($self, @tokens) = @_;
  848.  
  849.   return [] unless (defined($self->{_dbh}));
  850.  
  851.   my $token_list_size = scalar(@tokens);
  852.   dbg("bayes: tok_get_all: token count: $token_list_size");
  853.   my @tok_results;
  854.  
  855.   my $search_index = 0;
  856.   my $results_index = 0;
  857.   my $bunch_end;
  858.  
  859.   my $token_select = $self->_token_select_string();
  860.  
  861.   my $multi_sql = "SELECT $token_select, spam_count, ham_count, atime
  862.                      FROM bayes_token
  863.                     WHERE id = ?
  864.                       AND token IN ";
  865.  
  866.   # fetch tokens in bunches of 100 until there are <= 100 left, then just fetch the rest
  867.   while ($token_list_size > $search_index) {
  868.     my $bunch_size;
  869.     if ($token_list_size - $search_index > 100) {
  870.       $bunch_size = 100;
  871.     }
  872.     else {
  873.       $bunch_size = $token_list_size - $search_index;
  874.     }
  875.     while ($token_list_size - $search_index >= $bunch_size) {
  876.       my @bindings;
  877.       my $bindcount;
  878.       my $in_str = '(';
  879.  
  880.       $bunch_end = $search_index + $bunch_size;
  881.       for ( ; $search_index < $bunch_end; $search_index++) {
  882.     $in_str .= '?,';
  883.     push(@bindings, $tokens[$search_index]);
  884.       }
  885.       chop $in_str;
  886.       $in_str .= ')';
  887.  
  888.       my $dynamic_sql = $multi_sql . $in_str;
  889.  
  890.       my $sth = $self->{_dbh}->prepare($dynamic_sql);
  891.  
  892.       unless (defined($sth)) {
  893.     dbg("bayes: tok_get_all: SQL error: ".$self->{_dbh}->errstr());
  894.     return [];
  895.       }
  896.  
  897.       my $rc = $sth->execute($self->{_userid}, @bindings);
  898.  
  899.       unless ($rc) {
  900.     dbg("bayes: tok_get_all: SQL error: ".$self->{_dbh}->errstr());
  901.     return [];
  902.       }
  903.  
  904.       my $results = $sth->fetchall_arrayref();
  905.  
  906.       $sth->finish();
  907.  
  908.       foreach my $result (@{$results}) {
  909.     # Make sure that spam_count and ham_count are not negative
  910.     $result->[1] = 0 if (!$result->[1] || $result->[1] < 0);
  911.     $result->[2] = 0 if (!$result->[2] || $result->[2] < 0);
  912.     # Make sure that atime has a value
  913.     $result->[3] = 0 if (!$result->[3]);
  914.     $tok_results[$results_index++] = $result;
  915.       }
  916.     }
  917.   }
  918.  
  919.   return \@tok_results;
  920. }
  921.  
  922. =head2 tok_count_change
  923.  
  924. public instance (Boolean) tok_count_change (Integer $spam_count,
  925.                         Integer $ham_count,
  926.                         String $token,
  927.                         String $atime)
  928.  
  929. Description:
  930. This method takes a C<$spam_count> and C<$ham_count> and adds it to
  931. C<$tok> along with updating C<$tok>s atime with C<$atime>.
  932.  
  933. =cut
  934.  
  935. sub tok_count_change {
  936.   my ($self, $spam_count, $ham_count, $token, $atime) = @_;
  937.  
  938.   $atime = 0 unless defined $atime;
  939.  
  940.   $self->_put_token($token, $spam_count, $ham_count, $atime);
  941. }
  942.  
  943. =head2 multi_tok_count_change
  944.  
  945. public instance (Boolean) multi_tok_count_change (Integer $spam_count,
  946.                                Integer $ham_count,
  947.                                \% $tokens,
  948.                               String $atime)
  949.  
  950. Description:
  951. This method takes a C<$spam_count> and C<$ham_count> and adds it to all
  952. of the tokens in the C<$tokens> hash ref along with updating each tokens
  953. atime with C<$atime>.
  954.  
  955. =cut
  956.  
  957. sub multi_tok_count_change {
  958.   my ($self, $spam_count, $ham_count, $tokens, $atime) = @_;
  959.  
  960.   $atime = 0 unless defined $atime;
  961.  
  962.   $self->_put_tokens($tokens, $spam_count, $ham_count, $atime);
  963. }
  964.  
  965. =head2 nspam_nham_get
  966.  
  967. public instance ($spam_count, $ham_count) nspam_nham_get ()
  968.  
  969. Description:
  970. This method retrieves the total number of spam and the total number of
  971. ham learned.
  972.  
  973. =cut
  974.  
  975. sub nspam_nham_get {
  976.   my ($self) = @_;
  977.  
  978.   return (0,0) unless (defined($self->{_dbh}));
  979.  
  980.   my @vars = $self->get_storage_variables();
  981.  
  982.   return ($vars[1] || 0, $vars[2] || 0);
  983. }
  984.  
  985. =head2 nspam_nham_change
  986.  
  987. public instance (Boolean) nspam_nham_change (Integer $num_spam,
  988.                                              Integer $num_ham)
  989.  
  990. Description:
  991. This method updates the number of spam and the number of ham in the database.
  992.  
  993. =cut
  994.  
  995. sub nspam_nham_change {
  996.   my ($self, $num_spam, $num_ham) = @_;
  997.  
  998.   return 0 unless (defined($self->{_dbh}));
  999.  
  1000.   my $sql;
  1001.   my @bindings;
  1002.  
  1003.   if ($num_spam != 0 && $num_ham != 0) {
  1004.     $sql = "UPDATE bayes_vars
  1005.                SET spam_count = spam_count + ?,
  1006.                    ham_count = ham_count + ?
  1007.              WHERE id = ?";
  1008.     @bindings = ($num_spam, $num_ham, $self->{_userid});
  1009.   }
  1010.   elsif ($num_spam != 0) {
  1011.     $sql = "UPDATE bayes_vars
  1012.               SET spam_count = spam_count + ?
  1013.              WHERE id = ?";
  1014.     @bindings = ($num_spam, $self->{_userid});
  1015.   }
  1016.   elsif ($num_ham != 0) {
  1017.     $sql = "UPDATE bayes_vars
  1018.                SET ham_count = ham_count + ?
  1019.             WHERE id = ?";
  1020.     @bindings = ($num_ham, $self->{_userid});
  1021.   }
  1022.   else {
  1023.     # For some reason called with no delta, it's ok though so just return
  1024.     dbg("bayes: nspam_nham_change: Called with no delta on spam or ham");
  1025.     return 1;
  1026.   }
  1027.  
  1028.   my $rows = $self->{_dbh}->do($sql,
  1029.                    undef,
  1030.                    @bindings);
  1031.  
  1032.   unless (defined($rows)) {
  1033.     dbg("bayes: nspam_nham_change: SQL error: ".$self->{_dbh}->errstr());
  1034.     return 0;
  1035.   }
  1036.  
  1037.   return 1;
  1038. }
  1039.  
  1040. =head2 tok_touch
  1041.  
  1042. public instance (Boolean) tok_touch (String $token,
  1043.                                      String $atime)
  1044.  
  1045. Description:
  1046. This method updates the given tokens (C<$token>) atime.
  1047.  
  1048. The assumption is that the token already exists in the database.
  1049.  
  1050. =cut
  1051.  
  1052. sub tok_touch {
  1053.   my ($self, $token, $atime) = @_;
  1054.  
  1055.   return 0 unless (defined($self->{_dbh}));
  1056.  
  1057.   # shortcut, will only update atime for the token if the atime is less than
  1058.   # what we are updating to
  1059.   my $sql = "UPDATE bayes_token
  1060.                 SET atime = ?
  1061.               WHERE id = ?
  1062.                 AND token = ?
  1063.                 AND atime < ?";
  1064.  
  1065.   my $rows = $self->{_dbh}->do($sql, undef, $atime, $self->{_userid},
  1066.                    $token, $atime);
  1067.  
  1068.   unless (defined($rows)) {
  1069.     dbg("bayes: tok_touch: SQL error: ".$self->{_dbh}->errstr());
  1070.     return 0;
  1071.   }
  1072.  
  1073.   # if we didn't update a row then no need to update newest_token_age
  1074.   return 1 if ($rows eq '0E0');
  1075.  
  1076.   # need to check newest_token_age
  1077.   # no need to check oldest_token_age since we would only update if the
  1078.   # atime was newer than what is in the database
  1079.   $sql = "UPDATE bayes_vars
  1080.              SET newest_token_age = ?
  1081.            WHERE id = ?
  1082.              AND newest_token_age < ?";
  1083.  
  1084.   $rows = $self->{_dbh}->do($sql, undef, $atime, $self->{_userid}, $atime);
  1085.  
  1086.   unless (defined($rows)) {
  1087.     dbg("bayes: tok_touch: SQL error: ".$self->{_dbh}->errstr());
  1088.     return 0;
  1089.   }
  1090.  
  1091.   return 1;
  1092. }
  1093.  
  1094. =head2 tok_touch_all
  1095.  
  1096. public instance (Boolean) tok_touch (\@ $tokens
  1097.                                      String $atime)
  1098.  
  1099. Description:
  1100. This method does a mass update of the given list of tokens C<$tokens>, if the existing token
  1101. atime is < C<$atime>.
  1102.  
  1103. The assumption is that the tokens already exist in the database.
  1104.  
  1105. We should never be touching more than N_SIGNIFICANT_TOKENS, so we can make
  1106. some assumptions about how to handle the data (ie no need to batch like we
  1107. do in tok_get_all)
  1108.  
  1109. =cut
  1110.  
  1111. sub tok_touch_all {
  1112.   my ($self, $tokens, $atime) = @_;
  1113.  
  1114.   return 0 unless (defined($self->{_dbh}));
  1115.  
  1116.   return 1 unless (scalar(@{$tokens}));
  1117.  
  1118.   my $sql = "UPDATE bayes_token SET atime = ? WHERE id = ? AND token IN (";
  1119.  
  1120.   my @bindings = ($atime, $self->{_userid});
  1121.   foreach my $token (@{$tokens}) {
  1122.     $sql .= "?,";
  1123.     push(@bindings, $token);
  1124.   }
  1125.   chop($sql); # get rid of trailing ,
  1126.  
  1127.   $sql .= ") AND atime < ?";
  1128.   push(@bindings, $atime);
  1129.  
  1130.   my $rows = $self->{_dbh}->do($sql, undef, @bindings);
  1131.  
  1132.   unless (defined($rows)) {
  1133.     dbg("bayes: tok_touch_all: SQL error: ".$self->{_dbh}->errstr());
  1134.     return 0;
  1135.   }
  1136.  
  1137.   # if we didn't update a row then no need to update newest_token_age
  1138.   return 1 if ($rows eq '0E0');
  1139.  
  1140.   # need to check newest_token_age
  1141.   # no need to check oldest_token_age since we would only update if the
  1142.   # atime was newer than what is in the database
  1143.   $sql = "UPDATE bayes_vars
  1144.              SET newest_token_age = ?
  1145.            WHERE id = ?
  1146.              AND newest_token_age < ?";
  1147.  
  1148.   $rows = $self->{_dbh}->do($sql, undef, $atime, $self->{_userid}, $atime);
  1149.  
  1150.   unless (defined($rows)) {
  1151.     dbg("bayes: tok_touch_all: SQL error: ".$self->{_dbh}->errstr());
  1152.     return 0;
  1153.   }
  1154.  
  1155.   return 1;
  1156. }
  1157.  
  1158. =head2 cleanup
  1159.  
  1160. public instance (Boolean) cleanup ()
  1161.  
  1162. Description:
  1163. This method peroms any cleanup necessary before moving onto the next
  1164. operation.
  1165.  
  1166. =cut
  1167.  
  1168. sub cleanup {
  1169.   my ($self) = @_;
  1170.  
  1171.  
  1172.   return 1 unless ($self->{needs_cleanup});
  1173.  
  1174.   # cleanup was needed, go ahead and clear the cleanup flag
  1175.   $self->{needs_cleanup} = 0;
  1176.  
  1177.   my $sql = "DELETE from bayes_token
  1178.               WHERE id = ?
  1179.                 AND spam_count = 0
  1180.                 AND ham_count = 0";
  1181.  
  1182.   my $toks_deleted = $self->{_dbh}->do($sql, undef, $self->{_userid});
  1183.  
  1184.   unless (defined($toks_deleted)) {
  1185.     dbg("bayes: cleanup: SQL error: ".$self->{_dbh}->errstr());
  1186.     return 0;
  1187.   }       
  1188.  
  1189.   # check to see if any tokens where deleted
  1190.   return 1 if ($toks_deleted eq '0E0');
  1191.  
  1192.   $sql = "UPDATE bayes_vars SET token_count = token_count - $toks_deleted
  1193.            WHERE id = ?";
  1194.  
  1195.   my $rows = $self->{_dbh}->do($sql, undef, $self->{_userid});
  1196.  
  1197.   unless (defined($rows)) {
  1198.     dbg("bayes: cleanup: SQL error: ".$self->{_dbh}->errstr());
  1199.     return 0;
  1200.   }       
  1201.  
  1202.   return 1;
  1203. }
  1204.  
  1205. =head2 get_magic_re
  1206.  
  1207. public instance get_magic_re (String)
  1208.  
  1209. Description:
  1210. This method returns a regexp which indicates a magic token.
  1211.  
  1212. Unused in SQL implementation.
  1213.  
  1214. =cut
  1215.  
  1216. sub get_magic_re {
  1217.   my ($self) = @_;
  1218.   undef;
  1219. }
  1220.  
  1221. =head2 sync
  1222.  
  1223. public instance (Boolean) sync (\% $opts)
  1224.  
  1225. Description:
  1226. This method performs a sync of the database
  1227.  
  1228. =cut
  1229.  
  1230. sub sync {
  1231.   my ($self, $opts) = @_;
  1232.  
  1233.   # Not used for this implementation
  1234.  
  1235.   return 1;
  1236. }
  1237.  
  1238. =head2 perform_upgrade
  1239.  
  1240. public instance (Boolean) perform_upgrade (\% $opts);
  1241.  
  1242. Description:
  1243. Performs an upgrade of the database from one version to another, not
  1244. currently used in this implementation.
  1245.  
  1246. =cut
  1247.  
  1248. sub perform_upgrade {
  1249.   my ($self) = @_;
  1250.  
  1251.   return 1;
  1252. }
  1253.  
  1254. =head2 clear_database
  1255.  
  1256. public instance (Boolean) clear_database ()
  1257.  
  1258. Description:
  1259. This method deletes all records for a particular user.
  1260.  
  1261. Callers should be aware that any errors returned by this method
  1262. could causes the database to be inconsistent for the given user.
  1263.  
  1264. =cut
  1265.  
  1266. sub clear_database {
  1267.   my ($self) = @_;
  1268.  
  1269.   # We want to open readonly first, because if they don't already have
  1270.   # a db entry, we want to avoid creating one, just to delete it in a few secs
  1271.   if ($self->tie_db_readonly()) {
  1272.     # Ok, they must have had a db entry, so now make the connection writable
  1273.     $self->tie_db_writable();
  1274.   }
  1275.   else {
  1276.     # If we were unable to create a readonly connection then they must
  1277.     # not have a db entry, so no need to clear.
  1278.     # But it should be considered a success.
  1279.     return 1;
  1280.   }
  1281.  
  1282.   return 0 unless (defined($self->{_dbh}));
  1283.  
  1284.   my $rows = $self->{_dbh}->do("DELETE FROM bayes_vars WHERE id = ?",
  1285.                    undef,
  1286.                    $self->{_userid});
  1287.   unless (defined($rows)) {
  1288.     dbg("bayes: SQL error removing user (bayes_vars) data: ".$self->{_dbh}->errstr());
  1289.     return 0;
  1290.   }
  1291.  
  1292.   $rows = $self->{_dbh}->do("DELETE FROM bayes_seen WHERE id = ?",
  1293.                 undef,
  1294.                 $self->{_userid});
  1295.   unless (defined($rows)) {
  1296.     dbg("bayes: SQL error removing seen data: ".$self->{_dbh}->errstr());
  1297.     return 0;
  1298.   }
  1299.  
  1300.   $rows = $self->{_dbh}->do("DELETE FROM bayes_token WHERE id = ?",
  1301.                 undef,
  1302.                 $self->{_userid});
  1303.   unless (defined($rows)) {
  1304.     dbg("bayes: SQL error removing token data: ".$self->{_dbh}->errstr());
  1305.     return 0;
  1306.   }
  1307.  
  1308.   return 1;
  1309. }
  1310.  
  1311. =head2 backup_database
  1312.  
  1313. public instance (Boolean) backup_database ()
  1314.  
  1315. Description:
  1316. This method will dump the users database in a marchine readable format.
  1317.  
  1318. =cut
  1319.  
  1320. sub backup_database {
  1321.   my ($self) = @_;
  1322.  
  1323.   return 0 unless ($self->tie_db_readonly());
  1324.  
  1325.   return 0 unless (defined($self->{_dbh}));
  1326.  
  1327.   my @vars = $self->get_storage_variables();
  1328.  
  1329.   my $num_spam = $vars[1] || 0;
  1330.   my $num_ham = $vars[2] || 0;
  1331.  
  1332.   print "v\t$vars[6]\tdb_version # this must be the first line!!!\n";
  1333.   print "v\t$num_spam\tnum_spam\n";
  1334.   print "v\t$num_ham\tnum_nonspam\n";
  1335.  
  1336.   my $token_select = $self->_token_select_string();
  1337.  
  1338.   my $token_sql = "SELECT spam_count, ham_count, atime, $token_select
  1339.                      FROM bayes_token
  1340.                     WHERE id = ?
  1341.                       AND (spam_count > 0 OR ham_count > 0)";
  1342.  
  1343.   my $seen_sql = "SELECT flag, msgid
  1344.                     FROM bayes_seen
  1345.                    WHERE id = ?";
  1346.  
  1347.   my $sth = $self->{_dbh}->prepare_cached($token_sql);
  1348.  
  1349.   unless (defined ($sth)) {
  1350.     dbg("bayes: backup_database: SQL error: ".$self->{_dbh}->errstr());
  1351.     return 0;
  1352.   }
  1353.  
  1354.   my $rc = $sth->execute($self->{_userid});
  1355.  
  1356.   unless ($rc) {
  1357.     dbg("bayes: backup_database: SQL error: ".$self->{_dbh}->errstr());
  1358.     return 0;
  1359.   }
  1360.  
  1361.   while (my @values = $sth->fetchrow_array()) {
  1362.     $values[3] = unpack("H*", $values[3]);
  1363.     print "t\t" . join("\t", @values) . "\n";
  1364.   }
  1365.  
  1366.   $sth->finish();
  1367.  
  1368.   $sth = $self->{_dbh}->prepare_cached($seen_sql);
  1369.  
  1370.   unless (defined ($sth)) {
  1371.     dbg("bayes: backup_database: SQL error: ".$self->{_dbh}->errstr());
  1372.     return 0;
  1373.   }
  1374.  
  1375.   $rc = $sth->execute($self->{_userid});
  1376.  
  1377.   unless ($rc) {
  1378.     dbg("bayes: backup_database: SQL error: ".$self->{_dbh}->errstr());
  1379.     return 0;
  1380.   }
  1381.  
  1382.   while (my @values = $sth->fetchrow_array()) {
  1383.     print "s\t" . join("\t",@values) . "\n";
  1384.   }
  1385.  
  1386.   $sth->finish();
  1387.  
  1388.   $self->untie_db();
  1389.  
  1390.   return 1;
  1391. }
  1392.  
  1393. =head2 restore_database
  1394.  
  1395. public instance (Boolean) restore_database (String $filename, Boolean $showdots)
  1396.  
  1397. Description:
  1398. This method restores a database from the given filename, C<$filename>.
  1399.  
  1400. Callers should be aware that any errors returned by this method
  1401. could causes the database to be inconsistent for the given user.
  1402.  
  1403. =cut
  1404.  
  1405. sub restore_database {
  1406.   my ($self, $filename, $showdots) = @_;
  1407.  
  1408.   if (!open(DUMPFILE, '<', $filename)) {
  1409.     dbg("bayes: unable to open backup file $filename: $!");
  1410.     return 0;
  1411.   }
  1412.  
  1413.   # This is the critical phase (moving sql around), so don't allow it
  1414.   # to be interrupted.
  1415.   local $SIG{'INT'} = 'IGNORE';
  1416.   local $SIG{'HUP'} = 'IGNORE' if (!Mail::SpamAssassin::Util::am_running_on_windows());
  1417.   local $SIG{'TERM'} = 'IGNORE';
  1418.  
  1419.   unless ($self->clear_database()) {
  1420.     return 0;
  1421.   }
  1422.  
  1423.   # we need to go ahead close the db connection so we can then open it up
  1424.   # in a fresh state after clearing
  1425.   $self->untie_db();
  1426.  
  1427.   unless ($self->tie_db_writable()) {
  1428.     return 0;
  1429.   }
  1430.  
  1431.   my $token_count = 0;
  1432.   my $db_version;
  1433.   my $num_spam;
  1434.   my $num_ham;
  1435.   my $error_p = 0;
  1436.   my $line_count = 0;
  1437.  
  1438.   my $line = <DUMPFILE>;
  1439.   $line_count++;
  1440.   # We require the database version line to be the first in the file so we can
  1441.   # figure out how to properly deal with the file.  If it is not the first
  1442.   # line then fail
  1443.   if ($line =~ m/^v\s+(\d+)\s+db_version/) {
  1444.     $db_version = $1;
  1445.   }
  1446.   else {
  1447.     dbg("bayes: database version must be the first line in the backup file, correct and re-run");
  1448.     return 0;
  1449.   }
  1450.  
  1451.   unless ($db_version == 2 || $db_version == 3) {
  1452.     warn("bayes: database version $db_version is unsupported, must be version 2 or 3");
  1453.     return 0;
  1454.   }
  1455.  
  1456.   my $token_error_count = 0;
  1457.   my $seen_error_count = 0;
  1458.  
  1459.   while (my $line = <DUMPFILE>) {
  1460.     chomp($line);
  1461.     $line_count++;
  1462.  
  1463.     if ($line_count % 1000 == 0) {
  1464.       print STDERR "." if ($showdots);
  1465.     }
  1466.  
  1467.     if ($line =~ /^v\s+/) { # variable line
  1468.       my @parsed_line = split(/\s+/, $line, 3);
  1469.       my $value = $parsed_line[1] + 0;
  1470.       if ($parsed_line[2] eq 'num_spam') {
  1471.     $num_spam = $value;
  1472.       }
  1473.       elsif ($parsed_line[2] eq 'num_nonspam') {
  1474.     $num_ham = $value;
  1475.       }
  1476.       else {
  1477.     dbg("bayes: restore_database: skipping unknown line: $line");
  1478.       }
  1479.     }
  1480.     elsif ($line =~ /^t\s+/) { # token line
  1481.       my @parsed_line = split(/\s+/, $line, 5);
  1482.       my $spam_count = $parsed_line[1] + 0;
  1483.       my $ham_count = $parsed_line[2] + 0;
  1484.       my $atime = $parsed_line[3] + 0;
  1485.       my $token = $parsed_line[4];
  1486.  
  1487.       my $token_warn_p = 0;
  1488.       my @warnings;
  1489.  
  1490.       if ($spam_count < 0) {
  1491.     $spam_count = 0;
  1492.     push(@warnings, 'spam count < 0, resetting');
  1493.     $token_warn_p = 1;
  1494.       }
  1495.       if ($ham_count < 0) {
  1496.     $ham_count = 0;
  1497.     push(@warnings, 'ham count < 0, resetting');
  1498.     $token_warn_p = 1;
  1499.       }
  1500.  
  1501.       if ($spam_count == 0 && $ham_count == 0) {
  1502.     dbg("bayes: token has zero spam and ham count, skipping");
  1503.     next;
  1504.       }
  1505.  
  1506.       if ($atime > time()) {
  1507.     $atime = time();
  1508.     push(@warnings, 'atime > current time, resetting');
  1509.     $token_warn_p = 1;
  1510.       }
  1511.  
  1512.       if ($token_warn_p) {
  1513.     dbg("bayes: token ($token) has the following warnings:\n".join("\n",@warnings));
  1514.       }
  1515.  
  1516.       if ($db_version < 3) {
  1517.     # versions < 3 use plain text tokens, so we need to convert to hash
  1518.     $token = substr(sha1($token), -5);
  1519.       }
  1520.       else {
  1521.     # turn unpacked binary token back into binary value
  1522.     $token = pack("H*",$token);
  1523.       }
  1524.  
  1525.       unless ($self->_put_token($token, $spam_count, $ham_count, $atime)) {
  1526.     dbg("bayes: error inserting token for line: $line");
  1527.     $token_error_count++;
  1528.       }
  1529.       $token_count++;
  1530.     }
  1531.     elsif ($line =~ /^s\s+/) { # seen line
  1532.       my @parsed_line = split(/\s+/, $line, 3);
  1533.       my $flag = $parsed_line[1];
  1534.       my $msgid = $parsed_line[2];
  1535.  
  1536.       unless ($flag eq 'h' || $flag eq 's') {
  1537.     dbg("bayes: unknown seen flag ($flag) for line: $line, skipping");
  1538.     next;
  1539.       }
  1540.  
  1541.       unless ($msgid) {
  1542.     dbg("bayes: blank msgid for line: $line, skipping");
  1543.     next;
  1544.       }
  1545.  
  1546.       unless ($self->seen_put($msgid, $flag)) {
  1547.     dbg("bayes: error inserting msgid in seen table for line: $line");
  1548.     $seen_error_count++;
  1549.       }
  1550.     }
  1551.     else {
  1552.       dbg("bayes: skipping unknown line: $line");
  1553.       next;
  1554.     }
  1555.  
  1556.     if ($token_error_count >= 20) {
  1557.       warn "bayes: encountered too many errors (20) while parsing token line, reverting to empty database and exiting\n";
  1558.       $self->clear_database();
  1559.       return 0;
  1560.     }
  1561.  
  1562.     if ($seen_error_count >= 20) {
  1563.       warn "bayes: encountered too many errors (20) while parsing seen lines, reverting to empty database and exiting\n";
  1564.       $self->clear_database();
  1565.       return 0;
  1566.     }
  1567.   }
  1568.   close(DUMPFILE);
  1569.  
  1570.   print STDERR "\n" if ($showdots);
  1571.  
  1572.   unless (defined($num_spam)) {
  1573.     dbg("bayes: unable to find num spam, please check file");
  1574.     $error_p = 1;
  1575.   }
  1576.  
  1577.   unless (defined($num_ham)) {
  1578.     dbg("bayes: unable to find num ham, please check file");
  1579.     $error_p = 1;
  1580.   }
  1581.  
  1582.   if ($error_p) {
  1583.     dbg("bayes: error(s) while attempting to load $filename, clearing database, correct and re-run");
  1584.     $self->clear_database();
  1585.     return 0;
  1586.   }
  1587.  
  1588.   if ($num_spam || $num_ham) {
  1589.     unless ($self->nspam_nham_change($num_spam, $num_ham)) {
  1590.       dbg("bayes: error updating num spam and num ham, clearing database");
  1591.       $self->clear_database();
  1592.       return 0;
  1593.     }
  1594.   }
  1595.  
  1596.   dbg("bayes: parsed $line_count lines");
  1597.   dbg("bayes: created database with $token_count tokens based on $num_spam spam messages and $num_ham ham messages");
  1598.  
  1599.   $self->untie_db();
  1600.  
  1601.   return 1;
  1602. }
  1603.  
  1604. =head2 db_readable
  1605.  
  1606. public instance (Boolean) db_readable()
  1607.  
  1608. Description:
  1609. This method returns a boolean value indicating if the database is in a
  1610. readable state.
  1611.  
  1612. =cut
  1613.  
  1614. sub db_readable {
  1615.   my ($self) = @_;
  1616.  
  1617.   # if there's a database handle, we can read...
  1618.   return defined $self->{_dbh};
  1619. }
  1620.  
  1621. =head2 db_writable
  1622.  
  1623. public instance (Boolean) db_writeable()
  1624.  
  1625. Description:
  1626. This method returns a boolean value indicating if the database is in a
  1627. writable state.
  1628.  
  1629. =cut
  1630.  
  1631. sub db_writable {
  1632.   my ($self) = @_;
  1633.  
  1634.   return (defined $self->{_dbh} && $self->{db_writable_p})
  1635. }
  1636.  
  1637. =head1 Private Methods
  1638.  
  1639. =head2 _connect_db
  1640.  
  1641. private instance (Boolean) _connect_db ()
  1642.  
  1643. Description:
  1644. This method connects to the SQL database.
  1645.  
  1646. =cut
  1647.  
  1648. sub _connect_db {
  1649.   my ($self) = @_;
  1650.  
  1651.   $self->{_dbh} = undef;
  1652.  
  1653.   # Turn off PrintError and explicitly set AutoCommit to off
  1654.   my $dbh = DBI->connect($self->{_dsn}, $self->{_dbuser}, $self->{_dbpass},
  1655.                         {'PrintError' => 0, 'AutoCommit' => 1});
  1656.  
  1657.   if (!$dbh) {
  1658.     dbg("bayes: unable to connect to database: ".DBI->errstr());
  1659.     return 0;
  1660.   }
  1661.   else {
  1662.     dbg("bayes: database connection established");
  1663.   }
  1664.  
  1665.   $self->{_dbh} = $dbh;
  1666.  
  1667.  return 1;
  1668. }
  1669.  
  1670. =head2 _get_db_version
  1671.  
  1672. private instance (Integer) _get_db_version ()
  1673.  
  1674. Description:
  1675. Gets the current version of the database from the special global vars
  1676. tables.
  1677.  
  1678. =cut
  1679.  
  1680. sub _get_db_version {
  1681.   my ($self) = @_;
  1682.  
  1683.   return 0 unless (defined($self->{_dbh}));
  1684.  
  1685.   return ($self->{_db_version_cache}) if (defined($self->{_db_version_cache}));
  1686.  
  1687.   my $sql = "SELECT value FROM bayes_global_vars WHERE variable = 'VERSION'";
  1688.  
  1689.   my $sth = $self->{_dbh}->prepare_cached($sql);
  1690.  
  1691.   unless (defined($sth)) {
  1692.     dbg("bayes: _get_db_version: SQL error: ".$self->{_dbh}->errstr());
  1693.     return 0;
  1694.   }
  1695.  
  1696.   my $rc = $sth->execute();
  1697.  
  1698.   unless ($rc) {
  1699.     dbg("bayes: _get_db_version: SQL error: ".$self->{_dbh}->errstr());
  1700.     return 0;
  1701.   }
  1702.  
  1703.   my ($version) = $sth->fetchrow_array();
  1704.  
  1705.   $sth->finish();
  1706.  
  1707.   $self->{_db_version_cache} = $version;
  1708.  
  1709.   return $version;
  1710. }
  1711.  
  1712. =head2 _initialize_db
  1713.  
  1714. private instance (Boolean) _initialize_db ()
  1715.  
  1716. Description:
  1717. This method will check to see if a user has had their bayes variables
  1718. initialized. If not then it will perform this initialization.
  1719.  
  1720. =cut
  1721.  
  1722. sub _initialize_db {
  1723.   my ($self, $create_entry_p) = @_;
  1724.  
  1725.   return 0 unless (defined($self->{_dbh}));
  1726.  
  1727.   return 0 if (!$self->{_username});
  1728.  
  1729.   # Check to see if we should call the services_authorized_for_username plugin
  1730.   # hook to see if this user is allowed/able to use bayes.  If not, do nothing
  1731.   # and return 0.
  1732.   if ($self->{bayes}->{conf}->{bayes_sql_username_authorized}) {
  1733.     my $services = { 'bayessql' => 0 };
  1734.     $self->{bayes}->{main}->call_plugins("services_allowed_for_username",
  1735.                      { services => $services,
  1736.                        username => $self->{_username},
  1737.                        conf => $self->{bayes}->{conf},
  1738.                      });
  1739.     
  1740.     unless ($services->{bayessql}) {
  1741.       dbg("bayes: username not allowed by services_allowed_for_username plugin call");
  1742.       return 0;
  1743.     }
  1744.   }
  1745.  
  1746.   my $sqlselect = "SELECT id FROM bayes_vars WHERE username = ?";
  1747.  
  1748.   my $sthselect = $self->{_dbh}->prepare_cached($sqlselect);
  1749.  
  1750.   unless (defined($sthselect)) {
  1751.     dbg("bayes: _initialize_db: SQL error: ".$self->{_dbh}->errstr());
  1752.     return 0;
  1753.   }
  1754.  
  1755.   my $rc = $sthselect->execute($self->{_username});
  1756.  
  1757.   unless ($rc) {
  1758.     dbg("bayes: _initialize_db: SQL error: ".$self->{_dbh}->errstr());
  1759.     return 0;
  1760.   }
  1761.  
  1762.   my ($id) = $sthselect->fetchrow_array();
  1763.  
  1764.   if ($id) {
  1765.     $self->{_userid} = $id;
  1766.     dbg("bayes: Using userid: ".$self->{_userid});
  1767.     $sthselect->finish();
  1768.     return 1;
  1769.   }
  1770.  
  1771.   # Do not create an entry for this user unless we were specifically asked to
  1772.   return 0 unless ($create_entry_p);
  1773.  
  1774.   # For now let the database setup the other variables as defaults
  1775.   my $sqlinsert = "INSERT INTO bayes_vars (username) VALUES (?)";
  1776.  
  1777.   my $rows = $self->{_dbh}->do($sqlinsert,
  1778.                    undef,
  1779.                    $self->{_username});
  1780.   unless (defined($rows)) {
  1781.     dbg("bayes: _initialize_db: SQL error: ".$self->{_dbh}->errstr());
  1782.     return 0;
  1783.   }
  1784.  
  1785.   # Now we need to figure out what id we inserted them at, in a perfect
  1786.   # world the database driver would handle this for us (ie mysql_insert_id)
  1787.   # but this is far from a perfect world, however since in theory we only
  1788.   # ever do this once it's ok to take the hit
  1789.   $rc = $sthselect->execute($self->{_username});
  1790.  
  1791.   unless ($rc) {
  1792.     dbg("bayes: _initialize_db: SQL error: ".$self->{_dbh}->errstr());
  1793.     return 0;
  1794.   }
  1795.  
  1796.   ($id) = $sthselect->fetchrow_array();
  1797.  
  1798.   $sthselect->finish();
  1799.  
  1800.   if ($id) {
  1801.     $self->{_userid} = $id;
  1802.     dbg("bayes: using userid: ".$self->{_userid});
  1803.     return 1;
  1804.   }
  1805.  
  1806.   return 1;
  1807. }
  1808.  
  1809. =head2 _put_token
  1810.  
  1811. private instance (Boolean) _put_token (string $token,
  1812.                                        integer $spam_count,
  1813.                                        integer $ham_count,
  1814.                        string $atime)
  1815.  
  1816. Description:
  1817. This method performs the work of either inserting or updating a token in
  1818. the database.
  1819.  
  1820. =cut
  1821.  
  1822. sub _put_token {
  1823.   my ($self, $token, $spam_count, $ham_count, $atime) = @_;
  1824.  
  1825.   return 0 unless (defined($self->{_dbh}));
  1826.  
  1827.   $spam_count ||= 0;
  1828.   $ham_count ||= 0;
  1829.  
  1830.   if ($spam_count == 0 && $ham_count == 0) {
  1831.     return 1;
  1832.   }
  1833.  
  1834.   my ($existing_spam_count,
  1835.       $existing_ham_count,
  1836.       $existing_atime) = $self->tok_get($token);
  1837.  
  1838.   if (!$existing_atime) {
  1839.  
  1840.     # You can't create a new entry for a token with a negative count, so just return
  1841.     # if we are unable to find an entry.
  1842.     return 1 if ($spam_count < 0 || $ham_count < 0);
  1843.  
  1844.     my $sql = "INSERT INTO bayes_token
  1845.                (id, token, spam_count, ham_count, atime)
  1846.                VALUES (?,?,?,?,?)";
  1847.  
  1848.     my $sth = $self->{_dbh}->prepare_cached($sql);
  1849.  
  1850.     unless (defined($sth)) {
  1851.       dbg("bayes: _put_token: SQL error: ".$self->{_dbh}->errstr());
  1852.       return 0;
  1853.     }
  1854.  
  1855.     my $rc = $sth->execute($self->{_userid},
  1856.                $token,
  1857.                $spam_count,
  1858.                $ham_count,
  1859.                $atime);
  1860.     
  1861.     unless ($rc) {
  1862.       dbg("bayes: _put_token: SQL error: ".$self->{_dbh}->errstr());
  1863.       return 0;
  1864.     }
  1865.  
  1866.     $sth->finish();
  1867.  
  1868.     $sql = "UPDATE bayes_vars SET token_count = token_count + 1
  1869.              WHERE id = ?";
  1870.  
  1871.     my $rows = $self->{_dbh}->do($sql, undef, $self->{_userid});
  1872.     
  1873.     unless (defined($rows)) {
  1874.       dbg("bayes: _put_token: SQL error: ".$self->{_dbh}->errstr());
  1875.       return 0;
  1876.     }
  1877.  
  1878.     $sql = "UPDATE bayes_vars SET newest_token_age = ?
  1879.              WHERE id = ? AND newest_token_age < ?";
  1880.  
  1881.     $rows = $self->{_dbh}->do($sql, undef, $atime, $self->{_userid}, $atime);
  1882.  
  1883.     unless (defined($rows)) {
  1884.       dbg("bayes: _put_token: SQL error: ".$self->{_dbh}->errstr());
  1885.       return 0;
  1886.     }
  1887.  
  1888.     if ($rows eq '0E0') {
  1889.       # no need to update oldest_token_age if we updated newest_token_age
  1890.       
  1891.       $sql = "UPDATE bayes_vars SET oldest_token_age = ?
  1892.                WHERE id = ? AND oldest_token_age > ?";
  1893.  
  1894.       $rows = $self->{_dbh}->do($sql, undef, $atime, $self->{_userid}, $atime);
  1895.       
  1896.       unless (defined($rows)) {
  1897.     dbg("bayes: _put_token: SQL error: ".$self->{_dbh}->errstr());
  1898.     return 0;
  1899.       }
  1900.     }
  1901.   }
  1902.   else {
  1903.  
  1904.     if ($spam_count < 0 || $ham_count < 0) {
  1905.       # we only need to cleanup when we subtract counts for a token and the
  1906.       # counts may have both reached 0
  1907.       # XXX - future optimization, since we have the existing spam/ham counts
  1908.       # we can make an educated guess on if the count would reach 0, for
  1909.       # instance, if we are decreasing spam_count but spam_count is currently
  1910.       # > 1000, then there is no possible why this update or any others that
  1911.       # might currently be happening could reduce that value to 0, so there
  1912.       # would be no need to set the needs_cleanup flag
  1913.       $self->{needs_cleanup} = 1;
  1914.     }
  1915.  
  1916.     my $update_atime_p = 1;
  1917.     my $updated_atime_p = 0;
  1918.  
  1919.     # if the existing atime is already >= the one we are going to set, then
  1920.     # don't bother
  1921.     $update_atime_p = 0 if ($existing_atime >= $atime);
  1922.  
  1923.     # These SQL statements include as part of the WHERE clause something like
  1924.     # "AND spam_count + ? >= 0" or "AND ham_count + ? >= 0".  This is to keep
  1925.     # the count from going negative.
  1926.  
  1927.     if ($spam_count) {
  1928.       my $sql;
  1929.       my @args;
  1930.       if ($update_atime_p) {
  1931.     $sql = "UPDATE bayes_token
  1932.                    SET spam_count = spam_count + ?,
  1933.                        atime = ?
  1934.                  WHERE id = ?
  1935.                    AND token = ?
  1936.                    AND spam_count + ? >= 0";
  1937.     @args = ($spam_count, $atime, $self->{_userid}, $token, $spam_count);
  1938.     $updated_atime_p = 1; # note the fact that we did do it
  1939.       }
  1940.       else {
  1941.     $sql = "UPDATE bayes_token
  1942.                    SET spam_count = spam_count + ?
  1943.                  WHERE id = ?
  1944.                    AND token = ?
  1945.                    AND spam_count + ? >= 0";
  1946.     @args = ($spam_count, $self->{_userid}, $token, $spam_count);
  1947.       }
  1948.  
  1949.       my $rows = $self->{_dbh}->do($sql, undef, @args);
  1950.  
  1951.       unless (defined($rows)) {
  1952.     dbg("bayes: _put_token: SQL error: ".$self->{_dbh}->errstr());
  1953.     return 0;
  1954.       }
  1955.     }
  1956.  
  1957.     if ($ham_count) {
  1958.       my $sql;
  1959.       my @args;
  1960.       if ($update_atime_p && !$updated_atime_p) {
  1961.     $sql = "UPDATE bayes_token
  1962.                    SET ham_count = ham_count + ?,
  1963.                        atime = ?
  1964.                  WHERE id = ?
  1965.                    AND token = ?
  1966.                    AND ham_count + ? >= 0";
  1967.     @args = ($ham_count, $atime, $self->{_userid}, $token, $ham_count);
  1968.     $updated_atime_p = 1; # note the fact that we did do it
  1969.       }
  1970.       else {
  1971.     $sql = "UPDATE bayes_token
  1972.                    SET ham_count = ham_count + ?
  1973.                  WHERE id = ?
  1974.                    AND token = ?
  1975.                    AND ham_count + ? >= 0";
  1976.     @args = ($ham_count, $self->{_userid}, $token, $ham_count);
  1977.       }
  1978.  
  1979.       my $rows = $self->{_dbh}->do($sql, undef, @args);
  1980.  
  1981.       unless (defined($rows)) {
  1982.     dbg("bayes: _put_token: SQL error: ".$self->{_dbh}->errstr());
  1983.     return 0;
  1984.       }
  1985.     }
  1986.  
  1987.     if ($updated_atime_p) {
  1988.       # we updated the atime, so we need to check and update bayes_vars
  1989.       # we only need to worry about newest_token_age since we would have
  1990.       # only updated the atime if it was > the previous value
  1991.       my $sql = "UPDATE bayes_vars SET newest_token_age = ?
  1992.                   WHERE id = ? AND newest_token_age < ?";
  1993.  
  1994.       my $rows = $self->{_dbh}->do($sql, undef, $atime, $self->{_userid}, $atime);
  1995.  
  1996.       unless (defined($rows)) {
  1997.     dbg("bayes: _put_token: SQL error: ".$self->{_dbh}->errstr());
  1998.     return 0;
  1999.       }
  2000.     }
  2001.   }
  2002.  
  2003.   return 1;
  2004. }
  2005.  
  2006. =head2 _put_tokens
  2007.  
  2008. private instance (Boolean) _put_tokens (\% $tokens,
  2009.                                         integer $spam_count,
  2010.                                         integer $ham_count,
  2011.                          string $atime)
  2012.  
  2013. Description:
  2014. This method performs the work of either inserting or updating tokens in
  2015. the database.
  2016.  
  2017. =cut
  2018.  
  2019. sub _put_tokens {
  2020.   my ($self, $tokens, $spam_count, $ham_count, $atime) = @_;
  2021.  
  2022.   return 0 unless (defined($self->{_dbh}));
  2023.  
  2024.   $spam_count ||= 0;
  2025.   $ham_count ||= 0;
  2026.  
  2027.   if ($spam_count == 0 && $ham_count == 0) {
  2028.     return 1;
  2029.   }
  2030.  
  2031.   my $atime_updated_p = 0;
  2032.   my $atime_inserted_p = 0;
  2033.   my $new_tokens = 0;
  2034.  
  2035.   my $insertsql = "INSERT INTO bayes_token
  2036.                    (id, token, spam_count, ham_count, atime)
  2037.                    VALUES (?,?,?,?,?)";
  2038.  
  2039.   my $insertsth = $self->{_dbh}->prepare_cached($insertsql);
  2040.  
  2041.   unless (defined($insertsth)) {
  2042.     dbg("bayes: _put_token: SQL error: ".$self->{_dbh}->errstr());
  2043.     return 0;
  2044.   }
  2045.  
  2046.   foreach my $token (keys %{$tokens}) {
  2047.     my ($existing_spam_count,
  2048.     $existing_ham_count,
  2049.     $existing_atime) = $self->tok_get($token);
  2050.  
  2051.     if (!$existing_atime) {
  2052.  
  2053.       # You can't create a new entry for a token with a negative count, so
  2054.       # just skip to the next one if we are unable to find an entry.
  2055.       next if ($spam_count < 0 || $ham_count < 0);
  2056.  
  2057.  
  2058.       my $rc = $insertsth->execute($self->{_userid},
  2059.                    $token,
  2060.                    $spam_count,
  2061.                    $ham_count,
  2062.                    $atime);
  2063.     
  2064.       unless ($rc) {
  2065.     dbg("bayes: _put_token: SQL error: ".$self->{_dbh}->errstr());
  2066.     next;
  2067.       }
  2068.  
  2069.       $insertsth->finish();
  2070.  
  2071.       $atime_inserted_p = 1;
  2072.       $new_tokens++;
  2073.     }
  2074.     else {
  2075.  
  2076.       if ($spam_count < 0 || $ham_count < 0) {
  2077.     # we only need to cleanup when we subtract counts for a token and the
  2078.     # counts may have both reached 0
  2079.     # XXX - future optimization, since we have the existing spam/ham counts
  2080.     # we can make an educated guess on if the count would reach 0, for
  2081.     # instance, if we are decreasing spam_count but spam_count is currently
  2082.     # > 1000, then there is no possible why this update or any others that
  2083.     # might currently be happening could reduce that value to 0, so there
  2084.     # would be no need to set the needs_cleanup flag
  2085.     $self->{needs_cleanup} = 1;
  2086.       }
  2087.  
  2088.       my $update_atime_p = 1;
  2089.  
  2090.       # if the existing atime is already >= the one we are going to set, then
  2091.       # don't bother
  2092.       $update_atime_p = 0 if ($existing_atime >= $atime);
  2093.       
  2094.       # These SQL statements include as part of the WHERE clause something like
  2095.       # "AND spam_count + ? >= 0" or "AND ham_count + ? >= 0".  This is to keep
  2096.       # the count from going negative.
  2097.       
  2098.       if ($spam_count) {
  2099.     my $sql;
  2100.     my @args;
  2101.     if ($update_atime_p) {
  2102.       $sql = "UPDATE bayes_token
  2103.                      SET spam_count = spam_count + ?,
  2104.                          atime = ?
  2105.                    WHERE id = ?
  2106.                      AND token = ?
  2107.                      AND spam_count + ? >= 0";
  2108.       @args = ($spam_count, $atime, $self->{_userid}, $token, $spam_count);
  2109.       $atime_updated_p = 1;
  2110.     }
  2111.     else {
  2112.       $sql = "UPDATE bayes_token
  2113.                      SET spam_count = spam_count + ?
  2114.                    WHERE id = ?
  2115.                      AND token = ?
  2116.                      AND spam_count + ? >= 0";
  2117.       @args = ($spam_count, $self->{_userid}, $token, $spam_count);
  2118.     }
  2119.  
  2120.     my $rows = $self->{_dbh}->do($sql, undef, @args);
  2121.  
  2122.     unless (defined($rows)) {
  2123.       dbg("bayes: _put_token: SQL error: ".$self->{_dbh}->errstr());
  2124.     }
  2125.       }
  2126.  
  2127.       if ($ham_count) {
  2128.     my $sql;
  2129.     my @args;
  2130.     # if $spam_count then we already updated the atime
  2131.     if ($update_atime_p && !$spam_count) { 
  2132.       $sql = "UPDATE bayes_token
  2133.                      SET ham_count = ham_count + ?,
  2134.                          atime = ?
  2135.                    WHERE id = ?
  2136.                      AND token = ?
  2137.                      AND ham_count + ? >= 0";
  2138.       @args = ($ham_count, $atime, $self->{_userid}, $token, $ham_count);
  2139.       $atime_updated_p = 1;
  2140.     }
  2141.     else {
  2142.       $sql = "UPDATE bayes_token
  2143.                      SET ham_count = ham_count + ?
  2144.                    WHERE id = ?
  2145.                      AND token = ?
  2146.                      AND ham_count + ? >= 0";
  2147.       @args = ($ham_count, $self->{_userid}, $token, $ham_count);
  2148.     }
  2149.     
  2150.     my $rows = $self->{_dbh}->do($sql, undef, @args);
  2151.  
  2152.     unless (defined($rows)) {
  2153.       dbg("bayes: _put_token: SQL error: ".$self->{_dbh}->errstr());
  2154.     }
  2155.       }
  2156.     }
  2157.   }
  2158.  
  2159.   if ($new_tokens) {
  2160.     my $sql = "UPDATE bayes_vars SET token_count = token_count + ?
  2161.                 WHERE id = ?";
  2162.  
  2163.     my $rows = $self->{_dbh}->do($sql, undef, $new_tokens, $self->{_userid});
  2164.  
  2165.     unless (defined($rows)) {
  2166.       dbg("bayes: _put_token: SQL error: ".$self->{_dbh}->errstr());
  2167.     }
  2168.   }
  2169.  
  2170.   if ($atime_updated_p || $atime_inserted_p) {
  2171.     # we updated the atime, so we need to check and update bayes_vars
  2172.     # we only need to worry about newest_token_age since we would have
  2173.     # only updated the atime if it was > the previous value
  2174.     my $sql = "UPDATE bayes_vars SET newest_token_age = ?
  2175.                 WHERE id = ? AND newest_token_age < ?";
  2176.  
  2177.     my $rows = $self->{_dbh}->do($sql, undef, $atime, $self->{_userid}, $atime);
  2178.  
  2179.     unless (defined($rows)) {
  2180.       dbg("bayes: _put_token: SQL error: ".$self->{_dbh}->errstr());
  2181.     }
  2182.   }
  2183.  
  2184.  
  2185.   # If we inserted then we might need to update oldest_token_age
  2186.   # but if we already updated newest_token_age then there is no need
  2187.  
  2188.   if ($atime_inserted_p) {
  2189.     my $sql = "UPDATE bayes_vars SET oldest_token_age = ?
  2190.                 WHERE id = ? AND oldest_token_age > ?";
  2191.  
  2192.     my $rows = $self->{_dbh}->do($sql, undef, $atime, $self->{_userid}, $atime);
  2193.  
  2194.     unless (defined($rows)) {
  2195.       dbg("bayes: _put_token: SQL error: ".$self->{_dbh}->errstr());
  2196.     }
  2197.   }
  2198.  
  2199.   return 1;
  2200. }
  2201.  
  2202. =head2 _get_oldest_token_age
  2203.  
  2204. private instance (Integer) _get_oldest_token_age ()
  2205.  
  2206. Description:
  2207. This method finds the atime of the oldest token in the database.
  2208.  
  2209. The use of min(atime) in the SQL is ugly and but really the most efficient
  2210. way of getting the oldest_token_age after we've done a mass expire.  It should
  2211. only be called at expire time.
  2212.  
  2213. =cut
  2214.  
  2215. sub _get_oldest_token_age {
  2216.   my ($self) = @_;
  2217.  
  2218.   return 0 unless (defined($self->{_dbh}));
  2219.  
  2220.   my $sql = "SELECT min(atime) FROM bayes_token
  2221.               WHERE id = ?";
  2222.  
  2223.   my $sth = $self->{_dbh}->prepare_cached($sql);
  2224.  
  2225.   unless (defined($sth)) {
  2226.     dbg("bayes: _get_oldest_token_age: SQL error: ".$self->{_dbh}->errstr());
  2227.     return 0;
  2228.   }
  2229.  
  2230.   my $rc = $sth->execute($self->{_userid});
  2231.  
  2232.   unless ($rc) {
  2233.     dbg("bayes: _get_oldest_token_age: SQL error: ".$self->{_dbh}->errstr());
  2234.     return 0;
  2235.   }
  2236.  
  2237.   my ($atime) = $sth->fetchrow_array();
  2238.  
  2239.   $sth->finish();
  2240.  
  2241.   return $atime;
  2242. }
  2243.  
  2244.  
  2245. =head2 _get_num_hapaxes
  2246.  
  2247. private instance (Integer) _get_num_hapaxes ()
  2248.  
  2249. Description:
  2250. This method gets the total number of hapaxes (spam_count + ham_count == 1) in
  2251. the token database for a user.
  2252.  
  2253. =cut
  2254.  
  2255. sub _get_num_hapaxes {
  2256.   my ($self) = @_;
  2257.  
  2258.   return 0 unless (defined($self->{_dbh}));
  2259.  
  2260.   my $sql = "SELECT count(*)
  2261.                FROM bayes_token
  2262.               WHERE id = ?
  2263.                 AND spam_count + ham_count = 1";
  2264.  
  2265.   my $sth = $self->{_dbh}->prepare_cached($sql);
  2266.  
  2267.   unless (defined($sth)) {
  2268.     dbg("bayes: _get_num_hapaxes: SQL error: ".$self->{_dbh}->errstr());
  2269.     return 0;
  2270.   }
  2271.  
  2272.   my $rc = $sth->execute($self->{_userid});
  2273.  
  2274.   unless ($rc) {
  2275.     dbg("bayes: _get_num_hapaxes: SQL error: ".$self->{_dbh}->errstr());
  2276.     return 0;
  2277.   }
  2278.  
  2279.   
  2280.   my ($num_hapaxes) = $sth->fetchrow_array();
  2281.  
  2282.   $sth->finish();
  2283.  
  2284.   return $num_hapaxes;
  2285. }
  2286.  
  2287. =head2 _get_num_lowfreq
  2288.  
  2289. private instance (Integer) _get_num_lowfreq ()
  2290.  
  2291. Description:
  2292. This method gets the total number of lowfreq tokens (spam_count < 8 and
  2293. ham_count < 8) in the token database for a user
  2294.  
  2295. =cut
  2296.  
  2297. sub _get_num_lowfreq {
  2298.   my ($self) = @_;
  2299.  
  2300.   return 0 unless (defined($self->{_dbh}));
  2301.  
  2302.   my $sql = "SELECT count(*)
  2303.                FROM bayes_token
  2304.               WHERE id = ?
  2305.                 AND (spam_count >= 0 AND spam_count < 8)
  2306.                 AND (ham_count >= 0 AND ham_count < 8)
  2307.                 AND spam_count + ham_count != 1";
  2308.  
  2309.   my $sth = $self->{_dbh}->prepare_cached($sql);
  2310.  
  2311.   unless (defined($sth)) {
  2312.     dbg("bayes: _get_num_lowfreq: SQL error: ".$self->{_dbh}->errstr());
  2313.     return 0;
  2314.   }
  2315.  
  2316.   my $rc = $sth->execute($self->{_userid});
  2317.  
  2318.   unless ($rc) {
  2319.     dbg("bayes: _get_num_lowfreq: SQL error: ".$self->{_dbh}->errstr());
  2320.     return 0;
  2321.   }
  2322.  
  2323.   my ($num_lowfreq) = $sth->fetchrow_array();
  2324.  
  2325.   $sth->finish();
  2326.  
  2327.   return $num_lowfreq;
  2328. }
  2329.  
  2330. =head2 _token_select_string
  2331.  
  2332. private instance (String) _token_select_string
  2333.  
  2334. Description:
  2335. This method returns the string to be used in SELECT statements to represent
  2336. the token column.
  2337.  
  2338. The default is to use the RPAD function to pad the token out to 5 characters.
  2339.  
  2340. =cut
  2341.  
  2342. sub _token_select_string {
  2343.   return "RPAD(token, 5, ' ')";
  2344. }
  2345.  
  2346. sub sa_die { Mail::SpamAssassin::sa_die(@_); }
  2347.  
  2348. 1;
  2349.